mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-11 03:00:54 +02:00
Compare commits
2 Commits
ci/fix_cov
...
feat/dap_t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc246cf92 | ||
|
|
550753078b |
28
.github/workflows/check_pr.yml
vendored
28
.github/workflows/check_pr.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Check PR status for branch
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
branch-pr:
|
||||
description: The PR number if the branch is in one
|
||||
value: ${{ jobs.pr.outputs.branch-pr }}
|
||||
|
||||
jobs:
|
||||
pr:
|
||||
runs-on: "ubuntu-latest"
|
||||
outputs:
|
||||
branch-pr: ${{ steps.script.outputs.result }}
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
id: script
|
||||
if: github.event_name == 'push'
|
||||
with:
|
||||
script: |
|
||||
const prs = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: context.repo.owner + ':${{ github.ref_name }}'
|
||||
})
|
||||
if (prs.data.length) {
|
||||
console.log(`::notice ::Skipping CI on branch push as it is already run in PR #${prs.data[0]["number"]}`)
|
||||
return prs.data[0]["number"]
|
||||
}
|
||||
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: Full CI
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check_pr_status:
|
||||
uses: ./.github/workflows/check_pr.yml
|
||||
|
||||
formatter:
|
||||
needs: check_pr_status
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: ./.github/workflows/formatter.yml
|
||||
|
||||
unit-test:
|
||||
needs: [check_pr_status, formatter]
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: ./.github/workflows/pytest.yml
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
unit-test-matrix:
|
||||
needs: [check_pr_status, formatter]
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: ./.github/workflows/pytest-matrix.yml
|
||||
|
||||
generate-cli-test:
|
||||
needs: [check_pr_status, formatter]
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: ./.github/workflows/generate-cli-check.yml
|
||||
|
||||
end2end-test:
|
||||
needs: [check_pr_status, formatter]
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: ./.github/workflows/end2end-conda.yml
|
||||
48
.github/workflows/end2end-conda.yml
vendored
48
.github/workflows/end2end-conda.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Run Pytest with Coverage
|
||||
on: [workflow_call]
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -el {0}
|
||||
|
||||
env:
|
||||
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
BEC_CORE_BRANCH: main # Set the branch you want for bec
|
||||
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
PROJECT_PATH: ${{ github.repository }}
|
||||
QTWEBENGINE_DISABLE_SANDBOX: 1
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Conda
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
auto-update-conda: true
|
||||
auto-activate-base: true
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Conda install and run pytest
|
||||
run: |
|
||||
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
git clone --branch $BEC_CORE_BRANCH https://github.com/bec-project/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://github.com/bec-project/ophyd_devices.git
|
||||
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
cd ./bec
|
||||
conda create -q -n test-environment python=3.11
|
||||
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
|
||||
61
.github/workflows/formatter.yml
vendored
61
.github/workflows/formatter.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Formatter and Pylint jobs
|
||||
on: [workflow_call]
|
||||
jobs:
|
||||
|
||||
Formatter:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Run black and isort
|
||||
run: |
|
||||
pip install black isort
|
||||
pip install -e .[dev]
|
||||
black --check --diff --color .
|
||||
isort --check --diff ./
|
||||
Pylint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pylint pylint-exit anybadge
|
||||
|
||||
- name: Run Pylint
|
||||
run: |
|
||||
mkdir -p ./pylint
|
||||
set +e
|
||||
pylint ./${{ github.event.repository.name }} --output-format=text > ./pylint/pylint.log
|
||||
pylint-exit $?
|
||||
set -e
|
||||
|
||||
- name: Extract Pylint Score
|
||||
id: score
|
||||
run: |
|
||||
SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
|
||||
echo "score=$SCORE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Badge
|
||||
run: |
|
||||
anybadge --label=Pylint --file=./pylint/pylint.svg --value="${{ steps.score.outputs.score }}" 2=red 4=orange 8=yellow 10=green
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pylint-artifacts
|
||||
path: |
|
||||
# ./pylint/pylint.log # not sure why this isn't working
|
||||
./pylint/pylint.svg
|
||||
49
.github/workflows/generate-cli-check.yml
vendored
49
.github/workflows/generate-cli-check.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: Run bw-generate-cli
|
||||
on: [workflow_call]
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -el {0}
|
||||
|
||||
env:
|
||||
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
BEC_CORE_BRANCH: main # Set the branch you want for bec
|
||||
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
PROJECT_PATH: ${{ github.repository }}
|
||||
QTWEBENGINE_DISABLE_SANDBOX: 1
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install os dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Clone and install dependencies
|
||||
run: |
|
||||
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
git clone --branch $BEC_CORE_BRANCH https://github.com/bec-project/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://github.com/bec-project/ophyd_devices.git
|
||||
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
pip install -e ./ophyd_devices
|
||||
pip install -e ./bec/bec_lib[dev]
|
||||
pip install -e ./bec/bec_ipython_client
|
||||
pip install -e .[dev,pyside6]
|
||||
|
||||
- name: Run bw-generate-cli
|
||||
run: |
|
||||
bw-generate-cli --target bec_widgets
|
||||
git diff --exit-code
|
||||
|
||||
48
.github/workflows/pytest-matrix.yml
vendored
48
.github/workflows/pytest-matrix.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Run Pytest with different Python versions
|
||||
on: [workflow_call]
|
||||
|
||||
jobs:
|
||||
pytest-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
env:
|
||||
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
BEC_CORE_BRANCH: main # Set the branch you want for bec
|
||||
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
PROJECT_PATH: ${{ github.repository }}
|
||||
QTWEBENGINE_DISABLE_SANDBOX: 1
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Clone and install dependencies
|
||||
run: |
|
||||
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
git clone --branch $BEC_CORE_BRANCH https://github.com/bec-project/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://github.com/bec-project/ophyd_devices.git
|
||||
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
pip install -e ./ophyd_devices
|
||||
pip install -e ./bec/bec_lib[dev]
|
||||
pip install -e ./bec/bec_ipython_client
|
||||
pip install -e .[dev,pyside6]
|
||||
|
||||
- name: Run Pytest
|
||||
run: |
|
||||
pip install pytest pytest-random-order
|
||||
pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
64
.github/workflows/pytest.yml
vendored
64
.github/workflows/pytest.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: Run Pytest with Coverage
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'Pull request number'
|
||||
required: false
|
||||
type: number
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
required: true
|
||||
|
||||
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
BEC_CORE_BRANCH: main # Set the branch you want for bec
|
||||
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
PROJECT_PATH: ${{ github.repository }}
|
||||
QTWEBENGINE_DISABLE_SANDBOX: 1
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Clone and install dependencies
|
||||
run: |
|
||||
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
git clone --branch $BEC_CORE_BRANCH https://github.com/bec-project/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://github.com/bec-project/ophyd_devices.git
|
||||
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
pip install -e ./ophyd_devices
|
||||
pip install -e ./bec/bec_lib[dev]
|
||||
pip install -e ./bec/bec_ipython_client
|
||||
pip install -e .[dev,pyside6]
|
||||
|
||||
- name: Run Pytest with Coverage
|
||||
id: coverage
|
||||
run: pytest --random-order --cov --cov-config=pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail tests/unit_tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: bec-project/bec_widgets
|
||||
@@ -13,7 +13,7 @@ variables:
|
||||
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.
|
||||
description: Whether to run additional tests against min/max/random selection of dependencies. Set to 1 for running.
|
||||
value: 0
|
||||
|
||||
workflow:
|
||||
@@ -77,7 +77,7 @@ formatter:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
script:
|
||||
- pip install -e ./[dev]
|
||||
- pip install bec_lib[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:
|
||||
@@ -162,20 +162,6 @@ tests:
|
||||
- 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:
|
||||
@@ -203,7 +189,7 @@ test-matrix:
|
||||
end-2-end-conda:
|
||||
stage: End2End
|
||||
needs: []
|
||||
image: continuumio/miniconda3:25.1.1-2
|
||||
image: continuumio/miniconda3
|
||||
allow_failure: false
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
@@ -230,7 +216,7 @@ end-2-end-conda:
|
||||
- pip install -e ./ophyd_devices
|
||||
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end
|
||||
- pytest -v --files-path ./ --start-servers --flush-redis --random-order ./tests/end-2-end
|
||||
|
||||
artifacts:
|
||||
when: on_failure
|
||||
@@ -245,7 +231,7 @@ end-2-end-conda:
|
||||
- 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.*$/"
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^pre_release.*$/'
|
||||
|
||||
semver:
|
||||
stage: Deploy
|
||||
|
||||
760
CHANGELOG.md
760
CHANGELOG.md
@@ -1,766 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v2.3.0 (2025-05-09)
|
||||
|
||||
### Features
|
||||
|
||||
- **bec_connector**: Ability to change object name during runtime
|
||||
([`dc151cd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dc151cdfe39f1f0507eeee307a35c1677ae4d8c5))
|
||||
|
||||
|
||||
## v2.2.0 (2025-05-09)
|
||||
|
||||
### Features
|
||||
|
||||
- **launcher**: Add support for launching plugin widget
|
||||
([`1fb680a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fb680abb40668e72007c245f32c80112466c46e))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **launch_window**: Widget tile added
|
||||
([`b9e56c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b9e56c96cbae561beb893cedb7d18e9b6a7bfc76))
|
||||
|
||||
|
||||
## v2.1.3 (2025-05-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bec-dispatcher**: Fix reference to boundmethods to avoid duplicated subscriptions
|
||||
([`cf59d31`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf59d311132cd1a21f1893c19cc9f2a7e45101d0))
|
||||
|
||||
|
||||
## v2.1.2 (2025-05-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **waveform**: Ignore callbacks for on_async_readback from QtSender objects that are already
|
||||
destroyed; closes #497
|
||||
([`64a4824`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/64a48240546846fdf4541c2adf3a0a5a0829f948))
|
||||
|
||||
### Build System
|
||||
|
||||
- Remove flush-redis from ci job
|
||||
([`a6c479e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a6c479e42ea2a47c45e5a323bb3072bab503ecf1))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **bec-progressbar**: Add private method for bec_progressbar, udate client file
|
||||
([`37f0024`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37f002427ad5da01164ae3b0f4983695fe61c243))
|
||||
|
||||
- **bec-status-box**: Add get_server_state user_access method to BECStatusBox
|
||||
([`1619446`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1619446ec9839cfa1c666a3790a0c2abc449c4a8))
|
||||
|
||||
|
||||
## v2.1.1 (2025-05-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Import add operator in client
|
||||
([`55f7efc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/55f7efc4f586128dfb66fc6a8eb5d3a9f32bf61e))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Supply bec designer filename to function
|
||||
([`be72c9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be72c9f2708c93dab24d4383f5622e38cf1dc8a2))
|
||||
|
||||
|
||||
## v2.1.0 (2025-05-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Ensure rpc object do not collide with protected names
|
||||
([`94463af`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/94463afdba11fe2da5958a371ef49572889b8622))
|
||||
|
||||
### Chores
|
||||
|
||||
- **formatter**: Upgrade to black v25
|
||||
([`452124b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/452124b528c41db14d1e34ab98db95f6f7230ad6))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Install dev dependencies for formatter
|
||||
([`fff4af2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fff4af2489bdea0cf4f6f8db68db59fba411c25e))
|
||||
|
||||
### Features
|
||||
|
||||
- **SafeSlot**: Slot parameters can be overridden with kwarg; add option to raise
|
||||
([`9387275`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/93872758517177503b1f868376a6095670131844))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **colormap_widget**: Widget is rounded
|
||||
([`02563b1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/02563b10f3c90bddc069446dfe4137aa5a9727cb))
|
||||
|
||||
### Testing
|
||||
|
||||
- **Dock**: Add validation for new dock creation with invalid name
|
||||
([`c16b9dc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c16b9dce9ce629b794d731cd7f3282a59f8b8c59))
|
||||
|
||||
|
||||
## v2.0.3 (2025-05-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **generate_cli**: Apply isort config
|
||||
([`770dbd4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/770dbd4b63baba588871a4d4ffa77d44872d085b))
|
||||
|
||||
- **image_item**: Wrong user access name for rotation
|
||||
([`58a0bc7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58a0bc79742e7e7578988711a9840ed6041d9a69))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Add job to test that the generated client is up to date
|
||||
([`d22035f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d22035f8974ac51ae1b6efc0e2b3749ca0a674ff))
|
||||
|
||||
|
||||
## v2.0.2 (2025-05-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **plot_base**: No content margin for plot_widget window
|
||||
([`1b78840`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b78840fd87ea0f156c73beeb57c6c06f685f7b1))
|
||||
|
||||
|
||||
## v2.0.1 (2025-04-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **dock_area**: Restore state safeguard to not pass none to pyqtgraph restoreState
|
||||
([`9079ddd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9079ddd7278ede7a9a12d7b39797154e83659c20))
|
||||
|
||||
|
||||
## v2.0.0 (2025-04-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add designer plugin for ScanMetadata
|
||||
([`43e1aa9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43e1aa9505cfa6e87b4fce1d065efb48b4111190))
|
||||
|
||||
- Add support for 'add_slice', add downsampling for performance improvements. add tests
|
||||
([`7f7891d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7f7891dfa54588f5d902448b760f141b183a7fa1))
|
||||
|
||||
- Broadcast context manager to emit registry changes just once
|
||||
([`a5f06c8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a5f06c8f8380156a763445a69df29ee0e62e434c))
|
||||
|
||||
- Bugfix in cleanup of ScatterWaveform ScatterCurve; closes #520
|
||||
([`1d09107`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1d091071e1179821bb1dcd47fb97f3d0959b972f))
|
||||
|
||||
- Change default colormap to plasma
|
||||
([`074bbbc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/074bbbc16648849bdfcfc28b2c520b0e38dd07c2))
|
||||
|
||||
- Create widget enum programatically
|
||||
([`7726d83`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7726d83b6834b8145e48e709e2f839fb0ec1b971))
|
||||
|
||||
- Ensure provided dock and dock_area names are valid and defaults are snake_case
|
||||
([`0ac14a7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0ac14a74b851578fff668fb8c6722f990130831d))
|
||||
|
||||
- Expose common classes from bec_widgets package
|
||||
([`28ae0d2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/28ae0d2b577d7c926ee54690898fe8e327e1229f))
|
||||
|
||||
- Forward parent to children
|
||||
([`1fc6125`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fc61253699425a2bf64a0f8b560f8474549b841))
|
||||
|
||||
- Import from qtpy instead of PySide6
|
||||
([`fef07ac`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fef07ac8e12399e7e49bcd673a5fc7cbf713bc50))
|
||||
|
||||
- Proper cleanup of progressbar
|
||||
([`8ff2063`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ff2063bc8978c5b2a97f720d5da055e8ec08f0c))
|
||||
|
||||
- Rpc access enabled for certain widgets.
|
||||
([`ef4a52c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef4a52cc17748f35ed627170b1025e6e028d70b8))
|
||||
|
||||
- Server shutdown widgets
|
||||
([`75b2446`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/75b24467def65284ea6b6114b25098437e31ec95))
|
||||
|
||||
- Support auto_range_x/y for viewAll during measurement
|
||||
([`af28e2e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af28e2e433c9b0233436da850be97cd63df90a74))
|
||||
|
||||
- Unique name for widgets, fix new method for docks; closes #534
|
||||
([`77f9d42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77f9d425765061f137c997062a3bf769a939bc64))
|
||||
|
||||
- Warning in logpanel
|
||||
([`1d7b423`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1d7b423bb307b6aae3879987776310c14380895d))
|
||||
|
||||
- chain a signal to the child BecLogsQueue rather than passing the signal instance in
|
||||
|
||||
- Wrap fetching plugin widgets in case of errors
|
||||
([`ef14831`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef148317dea9c7ff985b2a3ff06ccdb37258153f))
|
||||
|
||||
- **auto_updates**: Fix condition to skip auto update
|
||||
([`18e4ba6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/18e4ba6cfe9f67512efbd3989156de5670aab3fe))
|
||||
|
||||
- **bec_connector**: Add assertion to ensure BECConnector is used with a QObject; closes #475
|
||||
([`1921444`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1921444e152e06c4decc790452f3c496cf8ee961))
|
||||
|
||||
- **bec_connector**: Add setObjectName method to update object name and broadcast if registered;
|
||||
closes #472
|
||||
([`064343a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/064343acf2631e4ae62b2a5e08bc08087246570c))
|
||||
|
||||
- **bec_connector**: Call cleanup on widgets if the parent was deleted
|
||||
([`fc1cdc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc1cdc814fc3c44a571c20986bc627935f90ff91))
|
||||
|
||||
- **bec_connector**: Improve cleanup handling on deleted parent to prevent errors
|
||||
([`3709cdc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3709cdc86671e5219afca7a8e11bdd01f03dd30e))
|
||||
|
||||
- **bec_connector**: Move RPC registration into single shot method to ensure the rpc name is in sync
|
||||
([`3b16c9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3b16c9f5a2f7f16b23f25560b1e8fb4e42359ef0))
|
||||
|
||||
- **bec_queue**: Set parent for toolbar buttons
|
||||
([`cdc613b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cdc613b6e7d7eac806d458515321590e9344244a))
|
||||
|
||||
- **becconnector**: Widgets can be flagged as root widget, skipping the BECMainWindow in CLI usage
|
||||
([`061f348`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/061f3481daae6844a83c44e9caca7ed56a1bb100))
|
||||
|
||||
- **becconnector,widgets**: Parent_id is always fetched from the real bec widget parent; all widgets
|
||||
adjusted; hardcoded parent_ids removed
|
||||
([`f35f4c4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f35f4c4b295139b99a2dad9e8241f900d2565aeb))
|
||||
|
||||
- **BECGuiClient**: Add launch_script parameter to dock area creation
|
||||
([`06a4954`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06a4954d3da44c6805232d34e47e242b28ba7fd1))
|
||||
|
||||
- **cleanup**: Prevent double cleanup by tracking object destruction state
|
||||
([`fde9120`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fde912005db61a60707e7181c3425a4557bdc011))
|
||||
|
||||
- **cli**: Add type ignore comment to generated files
|
||||
([`d171255`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d1712552ffd1118845dc7121218df86ce10e8750))
|
||||
|
||||
- **client**: Import reduce
|
||||
([`8cca510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8cca510fa1cbda00a07edbef9d36fdd74e63d201))
|
||||
|
||||
- **client**: Regenerated client
|
||||
([`c97db6a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c97db6aaae81d08019a13c344414c16c42691654))
|
||||
|
||||
- **client**: Rpc API adjusted for DockArea, ImageItem and Waveform
|
||||
([`6ca4aa0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ca4aa0f9b9d5ace9fb1e174219f4da5617ebbac))
|
||||
|
||||
- **client_utils**: Simplify RPC client instantiation in BECGuiClient
|
||||
([`96b31a4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96b31a450998aca2b7ac94138b07223418d2bacd))
|
||||
|
||||
- **colormap_widget**: Size policy fixed
|
||||
([`1cc2a98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1cc2a9848906a7013e86687976d42d4b9676b25f))
|
||||
|
||||
- **compact_popup**: Forward close event
|
||||
([`e0f146b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e0f146beeb34367a4d3454a7012af4728d594b9b))
|
||||
|
||||
- **crosshair**: Adapted for 2D image
|
||||
([`a85402d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a85402dde1af1d9c4a154892c46422ac3e1f22f9))
|
||||
|
||||
- **curve**: Fix unique names for custom curves
|
||||
([`8e846d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e846d449955ded3cb8090e44ea36d26efccb80e))
|
||||
|
||||
- **dark-mode-button**: Fix parent passed to QObjects in various classes
|
||||
([`a06f060`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a06f0600c1c9a80436f01533a82905a6f3633895))
|
||||
|
||||
- **designer**: Avoid touching deleted widgets during init as QtDesigner will segfault
|
||||
([`4381fcc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4381fcc4c212cd03ce91f1638dc361c3315f8c45))
|
||||
|
||||
- **designer-plugin-generator**: Enhance super constructor validation for new style classes
|
||||
([`6318b2d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6318b2d822be0ded561a1afd0d485158614e2406))
|
||||
|
||||
- **device_input_base**: Removed enums from Pydantic models to make them serialisable
|
||||
([`43b747e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43b747ec8a761530d78b26650b0ec2ee4581ffaf))
|
||||
|
||||
- **dock_area**: Close BECMainWindow if dock area is central widget
|
||||
([`e725de3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e725de3c4504d43fbcad25d69c5cb8cbe7a70867))
|
||||
|
||||
- **docs**: Update copyright year to be dynamic
|
||||
([`f2d5b57`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f2d5b57e86d0c9d690b8d9f988035427608f0b4c))
|
||||
|
||||
- **entry_validator**: Validator reports list of signal if user chooses the wrong one
|
||||
([`da05877`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/da05877dd04fa618cdb45268cb62df602a5e808f))
|
||||
|
||||
- **image**: Imageitem remove adjusted to disconnect and remove current displayed image
|
||||
([`98f159b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98f159b25f6bf7e1f2dd76726d7ab66a0baf88de))
|
||||
|
||||
- **launch_window**: Redesign
|
||||
([`7e65d4f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7e65d4f2d6d840d3895e023f5cd090a56ea6e5f3))
|
||||
|
||||
- **launch_window**: Return None when cancelling the ui file launcher
|
||||
([`b3dbe92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b3dbe922dea2cea9190d1583bd6b69f1a45d6b90))
|
||||
|
||||
- **launch_window**: Update LaunchTile icon to use new UI loader tile image
|
||||
([`3cd6e05`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3cd6e05b2478654210049ca8e1756ad592f1da81))
|
||||
|
||||
- **launcher**: Hide launcher when launcher is closed even though it is not the last widget
|
||||
([`6e7920c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6e7920c119824650006e7357ca2f4ff95d413e13))
|
||||
|
||||
- **lmfit_dialog_vertical**: Vertical sizePolicy fixed
|
||||
([`584b945`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/584b94500565a33e5daed86b7552ec54f1135cf6))
|
||||
|
||||
- **main_window**: Connected to theme change
|
||||
([`11feeff`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/11feeff37ce0b02fcbc8e506c67c14e1fc5e0cb6))
|
||||
|
||||
- **main_window**: Show app id only when connected to redis
|
||||
([`be72268`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be722683a7cc7b215c572f9c2e996839b010b64e))
|
||||
|
||||
- **moduar-toolbar**: Fix cleanup of modular toolbar and dock_area
|
||||
([`c70cd9d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c70cd9d6e8f7ea9d5f81b10ac437cdcc9ee900e9))
|
||||
|
||||
- **motor_map**: Limit map creating optimized
|
||||
([`9f2a083`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9f2a083abbcfb465ebea9acee8263dcc9a6da5d9))
|
||||
|
||||
- **plot_base**: Ability to set y label suffix
|
||||
([`890b501`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/890b50115fef845c2a77242fdb05863d2eec4a00))
|
||||
|
||||
- **plot_base**: Aspect ratio removed from the PlotBase
|
||||
([`19d8aeb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/19d8aeb16249a1093cfec124d0ebdf6af11d94a8))
|
||||
|
||||
- **plot_base**: Axis setting filter for relevant properties
|
||||
([`0204d9c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0204d9c86f9665dcefdcbe7f49ac23918d74dd66))
|
||||
|
||||
- **plot_base**: Do not enable inner axes when label is changed
|
||||
([`98eda03`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98eda03f4d6d449605d5559a1db44c900d93cb79))
|
||||
|
||||
- **plot_base**: Enable popup property fixed
|
||||
([`30db183`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30db18367e9c6d6375fda970a1bb255d966cba5a))
|
||||
|
||||
- **plot_base**: Fix cleanup of popups if popups are still open when PlotBase is closed
|
||||
([`39cf4dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39cf4ddd5a033ee7f589d508f765669186e776bc))
|
||||
|
||||
- **plot_base**: Improved handling of matplotlib exporter errors
|
||||
([`4f9514f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f9514fbd1ff0059248d3b7b5b4fcd85c3eb9c72))
|
||||
|
||||
- **plot_base**: Inner and outer axis setting in popup mode
|
||||
([`055b968`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/055b96818aa69d66119caee9a3e8c24575ce60b4))
|
||||
|
||||
- **plot_base**: Update mouse mode state on mode change
|
||||
([`fc24c8b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc24c8b3a5f9cd55fb3d49f753b53a65a2a0fa26))
|
||||
|
||||
- **plot_framework**: All widgets, popups and side menus cleanups adjusted
|
||||
([`337a332`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/337a332ed123f99729b8cf6869f7fe4b056c2b16))
|
||||
|
||||
- **plot_indicators**: Cleanup adjusted
|
||||
([`4865341`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/48653410101a2a38d5067fbfca7712d255d89625))
|
||||
|
||||
- **plot_indicators**: Plot indicators added to the PlotBase
|
||||
([`42e3b9c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/42e3b9c13786e67220874a1275a3d9ee9515541a))
|
||||
|
||||
- **positioner-indicator**: Fix property setters for position indicator
|
||||
([`1910993`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1910993b2b3d30ecb8e4977b4a362f46adae3c75))
|
||||
|
||||
- **progress-ring-bar**: Fix parent inheritance and cleanup of ring objects; closes #496
|
||||
([`b460ea9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b460ea9955318879ddfe4f9ae963249ba342bbb5))
|
||||
|
||||
- **ring-progress-bar**: Fix bug in disconnect slot of rings, enable 'scan' mode as default for init
|
||||
with first ring
|
||||
([`7c303d0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7c303d01294493a55fd26db8c1475e8c58b3e492))
|
||||
|
||||
- **ring_progress_bar**: Replaced hard-coded endpoints by MessageEndpoints
|
||||
([`e4e9feb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e4e9febc98268a4b6b9774b253419e88ea044811))
|
||||
|
||||
- **round_frame**: Orientation can be vertical
|
||||
([`c1bbb16`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c1bbb16dad481c628e7680180d7250ba8a560c46))
|
||||
|
||||
- **round_frame**: Roundframe removed from BECWidget inheritance
|
||||
([`b58a098`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b58a098ed4afbe62721fd2bf8497f363deecbfa6))
|
||||
|
||||
- **rpc**: Call close on container widget if needed
|
||||
([`a13de45`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a13de45131309771c1438407f3733a8c0897d495))
|
||||
|
||||
- **rpc-base**: Deprecate widget_name in favor of object_name; closes #499
|
||||
([`86647b9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/86647b9b7e2fa111105ae483808883a624fa4cd6))
|
||||
|
||||
- **rpc_base**: Ensure message wait event is set after processing RPC response
|
||||
([`4dc59aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4dc59aa5e9b15e5ec40401e80e7965acd88e2fce))
|
||||
|
||||
- **rpc_base**: Timeout run_rpc 3s
|
||||
([`8558b46`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8558b46114760a9434eaa827f81d5fd9d047112f))
|
||||
|
||||
- **rpc_register**: _lock and _skip_broad_cast moved to instance attributes
|
||||
([`8d17f7e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d17f7e32f81894294d7da472268e8d9eb3bb74b))
|
||||
|
||||
- **rpc_register**: Change add_rpc parameter type to BECConnector and add object_is_registered
|
||||
method
|
||||
([`82b8265`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/82b82659b7919b15d629375866302624b5b6e457))
|
||||
|
||||
- **rpc_register**: Lock changed to RLock
|
||||
([`6c90ca3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6c90ca31078d97124a3ad535ffe83da138558d67))
|
||||
|
||||
- **rpc_server**: Broadcasted data check
|
||||
([`c36852b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c36852b2ef762cdae3fde569bbd0d5f2f6f2725b))
|
||||
|
||||
- **rpc_server**: Enhance serialization logic for BECConnector objects and fix return types
|
||||
([`125afc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/125afc89073b4fc69a3f42650b3d4f6fa6ccaa47))
|
||||
|
||||
- **rpc_server**: Update _serialize_bec_connector to include wait parameter for registration check
|
||||
([`d6fccd1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d6fccd10f5d600ea67cf7b2a5ebb42295d15cdfe))
|
||||
|
||||
- **RPCReference**: Setattr added
|
||||
([`a2128ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a2128ad8d688995551c5e26974396fd0588b6804))
|
||||
|
||||
- **scan_control**: Restore scan parameters always regenerate the arg box, preventing infinite loop
|
||||
([`1f2db92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f2db927f50f4f30d43ebe52e39118c7d79994d4))
|
||||
|
||||
- **scan_matadata**: Parent passing
|
||||
([`4eaadd1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4eaadd1545885b111fce3f8cab527a77b8633ff3))
|
||||
|
||||
- **scatter_waveform,waveform**: Added QTimer to fetch the last data points after 500ms
|
||||
([`e6795dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6795dd87ccd93cfd53e22cd94d71bffe1ef54dd))
|
||||
|
||||
- **serialization**: Add serialization for qpointf
|
||||
([`3ddfeaa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3ddfeaa49fd4a7fdbff7cae47b90c25720f6dca0))
|
||||
|
||||
- **server**: Becdockarea type added
|
||||
([`4a74891`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4a74891184f112751258866b6bc9d800dbc5ed05))
|
||||
|
||||
- **server**: Remove window.hide() since widgets will be teared down on kill_server before siginit
|
||||
signals is sent
|
||||
([`58b0c7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58b0c7ddc1d0b85b35e7e18434c0b83aac01a735))
|
||||
|
||||
- **server**: Turn_off_the_lights cleanup fixed for parent_id widgets
|
||||
([`20a86ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/20a86ad325d36aa5aec73aeda7ff43ea9cc6c1f7))
|
||||
|
||||
- **setting_widget**: Added parent kwarg into all settings widgets in plotting framework
|
||||
([`94c2e2d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/94c2e2db6518402207b2a1077bb16403a8e61cee))
|
||||
|
||||
- **side_panel**: Side panel menu can be initialized without a title
|
||||
([`112eed6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/112eed694c0ef6eb80ec7a7cfdfbaacf732d5b9f))
|
||||
|
||||
- **toolbar**: Update action check handling logic for SwitchableToolBarAction
|
||||
([`ac08bdf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac08bdfab2162ac8fd103e60779a76d36e9a3765))
|
||||
|
||||
- **type hints**: Add future import to prevent sphinx from crashing
|
||||
([`aff5a51`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aff5a51f4c059ce21ec72cefc263f37df2491480))
|
||||
|
||||
- **waveform**: Dap curve flickering
|
||||
([`b03d2ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b03d2eaeed4263846c470bc45eba9208ced2370b))
|
||||
|
||||
- **waveform**: Error where scan history is empty
|
||||
([`288ea4d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/288ea4dbbde6d5f770c37f4daf377da9ec8fe729))
|
||||
|
||||
- **waveform**: Fix dap curve categorization logic
|
||||
([`b91f1fe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b91f1fe4879e43e71a1be49ce5a206efbae19315))
|
||||
|
||||
- **waveform**: Legend is correctly updated when changed from curve dialog
|
||||
([`c2d2c48`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c2d2c484cd1f133f45fe7147616c22c0b5fd5611))
|
||||
|
||||
- **waveform**: Signals for x device can be defined from gui
|
||||
([`39164fe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39164feb18f9e996d97814f58157892e8db816ae))
|
||||
|
||||
- **waveform, rpc_reference**: __getitem__ removed form waveform and rpc_reference
|
||||
([`3a82c95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3a82c95f60cb2b7f0b29a1ea5cdcbfa5bf602af8))
|
||||
|
||||
- **website-widget**: Add super().cleanup() in website widget
|
||||
([`8fbd54c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8fbd54c3aa864623e42b39a1ebf92ba098ba437d))
|
||||
|
||||
- **widgets**: Becconnector resolves hierarchy including objectName, parent, parent_id upon init;
|
||||
all widgets adjusted
|
||||
([`a1bec75`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a1bec7511549277da231928d989b16ecad0eed1b))
|
||||
|
||||
### Build System
|
||||
|
||||
- Pyside6 capped to 6.9
|
||||
([`9dabf2c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9dabf2c66c8023194964b9ad308e06197471f89f))
|
||||
|
||||
- **bec_lib**: Raised required version to 3.28.1
|
||||
([`a5f1f47`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a5f1f4781ed9787148053d71e0d12fefe42e142a))
|
||||
|
||||
- **dependencies**: Update min bec_lib version to 3.29
|
||||
([`eb0323b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb0323b989e96e89d2eb1ff7b648edb43f5fe198))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- **e2e**: E2e tests are saving logs
|
||||
([`d4106c5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d4106c548e2373463a48268fd991ded7f554e3a6))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add docs on widget plugins
|
||||
([`52a9f29`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/52a9f29bdcb20a9339a8970508bc0a93ba8bef5f))
|
||||
|
||||
- Add missing class doc strings for rpc-enabled widgets
|
||||
([`cfc8272`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cfc8272ac288541d1e20c0840bd2ce6fa930897c))
|
||||
|
||||
- Better document logpanel code
|
||||
([`d2c9075`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2c90757c21e040940a378325cad75c4d94470f9))
|
||||
|
||||
- Grammar improvement
|
||||
([`1fe052e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fe052e9da9e44bf9872db0d42218843a8e6d275))
|
||||
|
||||
- Remove BECFigure
|
||||
([`75cc45d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/75cc45d767970e985c566fd4aeccd4394f48dfa3))
|
||||
|
||||
- Remove BECFigure from docs, fix wrong api for docs of plotting widgets
|
||||
([`a1c859c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a1c859c7434357e6fb82f0912314c203fb73e890))
|
||||
|
||||
- Replaces instances of QtDesigner with BEC Designer for improved clarity
|
||||
([`60852e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/60852e228f80f0d2e74813f82bd30f1ba83ff154))
|
||||
|
||||
- Review quick_start
|
||||
([`4acf5be`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4acf5befb1dbcc69e8cc7da70ebf5663b9ec15f2))
|
||||
|
||||
- Update docs for v2
|
||||
([`25bd905`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/25bd905cef987a713e24aca178c04aef1ab59656))
|
||||
|
||||
- Update docs for various widgets
|
||||
([`b6695b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b6695b45d076dbf3896e94eedcf73d542022d764))
|
||||
|
||||
- Update quick_start
|
||||
([`afc818b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/afc818bf7d17f42703c3cebafd2b292b8444647a))
|
||||
|
||||
- Updated docs for v2 ([#531](https://gitlab.psi.ch/bec/bec_widgets/-/merge_requests/531),
|
||||
[`b4af2cc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4af2cc77aa0013f4547cd98345b0c77abb7101b))
|
||||
|
||||
- **auto_updates**: Update documentation for auto updates functionality and add launcher image
|
||||
([`6630ba1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6630ba1c421e566bf86ac38701a86eff624395d2))
|
||||
|
||||
- **lmfit**: Fix links
|
||||
([`5e4965f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5e4965fe1f88d18fcc7e6875777ff3eb01ab08ec))
|
||||
|
||||
- **plot_base**: Update docstrings for properties and setters
|
||||
([`b085ef6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b085ef6e730d529149bcb696b1ad4cd9c5220a83))
|
||||
|
||||
- **position-indicator**: Update docs for positioner indicator
|
||||
([`2f0d213`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2f0d213e32fd662bfffd4df73b9281fa30cef6e3))
|
||||
|
||||
### Features
|
||||
|
||||
- Add loader/helper for widget plugins
|
||||
([`ca2bb4f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca2bb4f9b42ebaac2fc544d3da36267d93e9903d))
|
||||
|
||||
- Add rpc broadcast
|
||||
([`2ba9b4c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2ba9b4cb236a2182261dfb88398d5ece733ba393))
|
||||
|
||||
- Add support for auto updates
|
||||
([`2511056`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2511056557daf0b5dd78d3e85ac4befb8bf8c316))
|
||||
|
||||
- Delete bec_app
|
||||
([`8e64b65`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e64b65c2d3b8a8f3c6e5376e694369b41733da4))
|
||||
|
||||
- Deprecated and delete alignment_1d gui
|
||||
([`27ea92d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/27ea92d120cc8ef01fff10341ce0954b4f7fed5d))
|
||||
|
||||
- Namespace update for gui, dock_area and docks.
|
||||
([`ac3c5a3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac3c5a38e449c2c3e4a1c61d5f9a59acfbf0cab5))
|
||||
|
||||
- **auto_update**: Add GUI highlight management for auto updates status
|
||||
([`5f272a6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5f272a66a4d4f65273d7b2a6709336cd3582d695))
|
||||
|
||||
- **auto_updates**: Enforce rpc widget class for subclasses of auto updates
|
||||
([`778230b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/778230b5edf5d24df8a10c78c90ea065510e8344))
|
||||
|
||||
- **image**: New Image widget based on new PlotBase
|
||||
([`cb39ff3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cb39ff3fbde99f4e4bed49dee8a5e5987d257b23))
|
||||
|
||||
- **launch_window**: Add custom UI file launching functionality and UI tile
|
||||
([`3089ca1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3089ca15ec4a8c110d11c57aff2da42f4af5bd08))
|
||||
|
||||
- **launch_window**: Add user access permissions
|
||||
([`8efa93d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8efa93d2d2c5e6c28008e1bbde89e5cc8a01d139))
|
||||
|
||||
- **launch_window**: Enhance auto update functionality with selector and dynamic loading
|
||||
([`2965323`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/29653239c5cf43313224cc5123d066fcba4b831b))
|
||||
|
||||
- **launcher**: Add option for launching with auto updates
|
||||
([`20a1c5d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/20a1c5ddb3cd0763ce69bba5a893f54c56678706))
|
||||
|
||||
- **main_window**: Add launcher menu and functionality to show launcher
|
||||
([`55baa84`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/55baa84eb6723b30b407092bc36f826b826cc934))
|
||||
|
||||
- **motor_map**: New MotorMap widget based on PlotBase
|
||||
([`fec26d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fec26d793e14965a719a4d038838418b9a7603bb))
|
||||
|
||||
- **multi_waveform**: Multi-waveform widget based on new PlotBase
|
||||
([`77f9616`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77f96160ab348c1a65ceb55986ea4ea75f8be04a))
|
||||
|
||||
- **plugin_utils**: Add functionality to retrieve auto update classes from plugins
|
||||
([`c434af9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c434af9b92d68d08da87112ef424738e5e42ae6e))
|
||||
|
||||
- **positioner_box**: Add units QLabel to device UI components and update visibility logic
|
||||
([`f653fc5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f653fc5f7ebf8ad5297facd739a8a49ea0a06c95))
|
||||
|
||||
- **scatter_waveform**: Scatter waveform widget based on new Plotbase
|
||||
([`95fcf01`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/95fcf016c32c52330acfd5900a3996c99c4ee01f))
|
||||
|
||||
- **server,launcher**: Rpc server separated with the launcher window introduced
|
||||
([`5f27a90`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5f27a9098903ffd8ec27c1b45565f1c113892cca))
|
||||
|
||||
- **slot**: Add 'verify_sender' argument to SafeSlot for sender verification
|
||||
([`8eef425`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8eef4253b0507f60f50c06ed48b59a1b19b29644))
|
||||
|
||||
- **ui_launch_window**: Add UILaunchWindow class
|
||||
([`45cd82e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/45cd82e6354c72e1e35cd6366aa7aad93f8b12ca))
|
||||
|
||||
- **waveform**: New Waveform widget based on NextGen PlotBase
|
||||
([`4bec181`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4bec181f3aff34d9de7d3f9ec012b641c125a661))
|
||||
|
||||
- **widget_io**: Added handler for Sliders
|
||||
([`1a0097e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1a0097e02728b6470217d3a574260f376776d81f))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Add fallback to 'index' plotting in case of missmatch in length
|
||||
([`515d7ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/515d7ad05584086a8e8ac626b476d629e27aacf3))
|
||||
|
||||
- Add pragma no cover to various TYPE_CHECKING
|
||||
([`f88dfc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f88dfc8f1bbc0819736a4f32bf21682366fd3437))
|
||||
|
||||
- Add support to plot against x_data
|
||||
([`0e276d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0e276d4c09cddb459688aecf28684a963d8f6613))
|
||||
|
||||
- Add template for debugging the cli generator
|
||||
([`f89e74b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f89e74b199d007cf47f355a1c5e1f582daeea90a))
|
||||
|
||||
- Autoupdate disabled
|
||||
([`4e29291`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4e29291b3a0891657a8d2011bcaf1d6e65de125a))
|
||||
|
||||
- Cleanup MR
|
||||
([`0b00cd2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0b00cd24fd43dbc87c81f7dbfae816343f7da4c4))
|
||||
|
||||
- Cleanup rpc reference tracking, fix appquit, fix namespace updates edge cases
|
||||
([`7ba93ce`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7ba93ce934cc644ad6340f141a6a0888bd1d3d98))
|
||||
|
||||
- Cleanup, fix tests and _top_level dict/windows
|
||||
([`5872253`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58722531232b2290f9fd974bae24877c9d5451f4))
|
||||
|
||||
- Fix cleanup bug for BECConnector items, renamed _registry_state to _server_registry
|
||||
([`be83c7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be83c7d5f4bc04d110734b491727dc60d8dd61ef))
|
||||
|
||||
- Fix cleanup for various widgets, including RoundedFrame
|
||||
([`d05179a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d05179a519d6419c9631ffdf4fa6aa262966c2ed))
|
||||
|
||||
- Improve plotting behaviour from history
|
||||
([`ed2d958`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ed2d958de62223cd796c869b6c8b9b75170e66f5))
|
||||
|
||||
- Rearrange base of metadata forms for generic use
|
||||
([`d04770f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d04770fe913474ec9d4e06b056c85e720d1470c4))
|
||||
|
||||
- Set downsampling to auto=True, method 'peak', activate clipToView for (Async)-Curves and fix
|
||||
ViewAll hook from pg.view_box menu
|
||||
([`25820a1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/25820a1cdec2cff99ab0d6085aece0e3e7dd9092))
|
||||
|
||||
- Tidy client generation and add options
|
||||
([`b492591`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4925918f7acf31e40971814639be8a6c55d46df))
|
||||
|
||||
- **assets**: New icon for ui loader
|
||||
([`e5b5322`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e5b532274ede281456a14b02a99855302603490a))
|
||||
|
||||
- **auto_update**: Auto_update changed to be BECMainWindow; removed auto update logic from
|
||||
BECDockArea
|
||||
([`56c2827`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56c282714037f733bcfd8a659f34baadcd1aa223))
|
||||
|
||||
- **auto_updates**: Move cleanup method from user section to internal section
|
||||
([`ac9224e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac9224e5f2d3edcb5b1cc1cbc1a8583f81d0b912))
|
||||
|
||||
- **bec_connector**: Replace pyqtSlot with SafeSlot for consistency
|
||||
([`9d6d0b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d6d0b406a812e08ca8417415b5def98b40bdf92))
|
||||
|
||||
- **bec_figure**: Becfigure removed
|
||||
([`f76d931`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f76d9319bd13bb52b1ae2524c1c5e44a167cc330))
|
||||
|
||||
- **client_utils**: Remove unused auto update attributes from BECGuiClient
|
||||
([`b7795b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b7795b4d0ae21641bead0f1f1541f920ae95702a))
|
||||
|
||||
- **image_widget**: Old BECImageWidget removed
|
||||
([`de10609`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/de10609b3c714b80a14bf6940e86763d0779402b))
|
||||
|
||||
- **launch_window**: Remove cleanup method
|
||||
([`9a940bb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9a940bb8d58f37d1fc24ce4fdb38282d02349efb))
|
||||
|
||||
- **launcher,main_window**: Launcher window moved to inherit from BECMainWindow
|
||||
([`99383b7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99383b77150ca7c74c19c899a0e6a7879b770376))
|
||||
|
||||
- **motor_map_widget**: Becmotormapwidget removed
|
||||
([`f878e87`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f878e87ad545e0fe68292030d9f06dee693e0da2))
|
||||
|
||||
- **multi_waveform_widget**: Becmultiwaveformwidget removed
|
||||
([`7c31bbd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7c31bbd9c2e0230e54f4dca0f1e5c4d2cd6e7674))
|
||||
|
||||
- **plots**: Plot_next_gen module renamed to plots
|
||||
([`9fb9a1c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9fb9a1cfd2a94efd5e2a9fcbaa05d65c7b7105ee))
|
||||
|
||||
- **plots**: Waveform and image rpc api review
|
||||
([`a3de1f0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3de1f0a31dfb9048493fb61983167960577fb97))
|
||||
|
||||
- **rpc_reference**: Refactor rpc reference tracking
|
||||
([`bd5e251`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bd5e251ee9396633f419732e43411821726250aa))
|
||||
|
||||
- **rpc_server**: Add type hint for _get_becwidget_ancestor method parameter; minor cleanup of
|
||||
imports
|
||||
([`cb91ebc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cb91ebc0c34ae29b6b293996199f4624d36a3cc0))
|
||||
|
||||
- **rpc_server**: Add type hints and docstrings for heartbeat and registry update methods
|
||||
([`08168f2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/08168f28d3c5375b9ace9df0d7aa31e33adb97e9))
|
||||
|
||||
- **rpc_server**: Cli_server renamed to rpc_server
|
||||
([`6082e7a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6082e7a6907c2fe15e4e5ebca857fbf8f222d192))
|
||||
|
||||
- **tests**: Create dummy scan item moved to client_mocks.py
|
||||
([`0dd9617`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0dd9617e6e5ea756edc344c324451480a62bdae2))
|
||||
|
||||
- **ui_loader**: Remove unnecessary parent_id handling
|
||||
([`d60cf6c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d60cf6c843ecc135a6065d1e913f9f6abb1a483d))
|
||||
|
||||
- **ui_loader**: Remove unused import
|
||||
([`a6ce312`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a6ce312f7c60c2babcc37127f7c69d54c1b32573))
|
||||
|
||||
- **utils**: Qt_utils moved to utils
|
||||
([`be552d3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be552d3ece97e7f472c4534b4af8438b95c518aa))
|
||||
|
||||
- **waveform_widget**: Removed and replaced by Waveform
|
||||
([`96cff49`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96cff49cd4453fa70d8802653d5afe62d71c6b2a))
|
||||
|
||||
### Testing
|
||||
|
||||
- Add function scoped rpc_widgets e2e test; closes #510
|
||||
([`36dc174`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/36dc174bfedf212532658b84f8ab64971863d292))
|
||||
|
||||
- Add IPython client GUI object test module with tab completion
|
||||
([`e3d0d55`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e3d0d5566c50f6a80ba861d4e3e0789f17785a46))
|
||||
|
||||
- Add tests for name creation of custom curves, and object name handling
|
||||
([`99d7623`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99d76236cac63042f0d7d1db580dde8aa7cfd214))
|
||||
|
||||
- Disable test_bec_dock_rpc_e2e module, issue to fix this created #450
|
||||
([`17f2dda`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17f2dda977025bc422e26289293d3fcbd224a6f6))
|
||||
|
||||
- Fix rpc widgets e2e test
|
||||
([`113938e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/113938e71a6dacba37164069e2c795cc9db168d4))
|
||||
|
||||
- Fix tests for launcher close / hide behavior
|
||||
([`23fee22`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/23fee22ef8f2c22f191dfc1da57b921484ede6cd))
|
||||
|
||||
- Fix tests for namespace updates
|
||||
([`f3d3c94`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f3d3c9425d3ed619b978427cea782137beedfb59))
|
||||
|
||||
- Qapp must shutdown cli server before checking for leaked QTimer
|
||||
([`d066051`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d06605122e5c2e225650b44ebfc047daa5aa6f55))
|
||||
|
||||
- **bec_connector**: Becconnector requires a QObject
|
||||
([`23bdd95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/23bdd95d8c6311d989cb3b807921e3fb2a3d62a0))
|
||||
|
||||
- **device_signal_input**: Fix init of device input widget
|
||||
([`31c3b64`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/31c3b64d7b157e5e26d44e5288afabef343c5e13))
|
||||
|
||||
- **e2e**: E2e tests adjusted for new plotting framework
|
||||
([`378398a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/378398a29b34e43f0cca0a49b08adfcb144e4777))
|
||||
|
||||
- **generate_cli**: Fix reference output
|
||||
([`a8adb06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8adb064f5011e1708ee2dc0090326f533407260))
|
||||
|
||||
- **launch_window**: Add test for launching UI file that raises ValueError for QMainWindow
|
||||
([`33a8a76`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33a8a767f31a57bcfda624d503ed39e0e4578dcb))
|
||||
|
||||
- **launch_window**: Add unit tests for LaunchWindow initialization and custom UI file launching
|
||||
([`d5e422c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d5e422c7fc0e169c35d7f206937e8c7902fbf123))
|
||||
|
||||
- **launch_window**: Tests for default and plugin auto updates
|
||||
([`e10f5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e10f5ec088c6937beb26ec468f510a209c7cc782))
|
||||
|
||||
- **plot_base**: Test for plot base re-enabled
|
||||
([`b51d637`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b51d637c5ff3418420801cd9b457fc073fa98adc))
|
||||
|
||||
- **plot_indicators**: Tests adapted to not be dependent on BECWaveformWidget
|
||||
([`360fe4c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/360fe4c9c3b5c3c1f26e97cb795aef8f4aba3b46))
|
||||
|
||||
- **setting_dialog**: Test that settings reject calls cleanup
|
||||
([`8914f1d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8914f1d50600cab588a6cbecb08d85bfd1a715a1))
|
||||
|
||||
- **unit_tests**: Unit tests adjusted to use a modern plotting framework instead of BECFigure
|
||||
([`6ade934`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ade93435632fa66fb012d92f9b8b548d96e718f))
|
||||
|
||||
|
||||
## v1.25.1 (2025-03-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2025, Paul Scherrer Institute
|
||||
Copyright (c) 2023, bec
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
||||
11
README.md
11
README.md
@@ -1,16 +1,5 @@
|
||||
# BEC Widgets
|
||||
|
||||
|
||||
[](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/bec-widgets/)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://www.python.org)
|
||||
[](https://doc.qt.io/qtforpython/)
|
||||
[](https://conventionalcommits.org)
|
||||
[](https://codecov.io/gh/bec-project/bec_widgets)
|
||||
|
||||
|
||||
**⚠️ Important Notice:**
|
||||
|
||||
🚨 **PyQt6 is no longer supported** due to incompatibilities with Qt Designer. Please use **PySide6** instead. 🚨
|
||||
|
||||
199
bec_widgets/applications/alignment/alignment_1d/alignment_1d.py
Normal file
199
bec_widgets/applications/alignment/alignment_1d/alignment_1d.py
Normal file
@@ -0,0 +1,199 @@
|
||||
""" This module contains the GUI for the 1D alignment application.
|
||||
It is a preliminary version of the GUI, which will be added to the main branch and steadily updated to be improved.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from bec_lib.device import Signal as BECSignal
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
||||
from bec_widgets.widgets.control.device_control.positioner_group.positioner_group import (
|
||||
PositionerGroup,
|
||||
)
|
||||
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
||||
from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import BECProgressBar
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# FIXME BECWaveFormWidget is gone, this app will not work until adapted to new Waveform
|
||||
class Alignment1D:
|
||||
"""Alignment GUI to perform 1D scans"""
|
||||
|
||||
def __init__(self, client=None, gui_id: Optional[str] = None) -> None:
|
||||
"""Initialization
|
||||
|
||||
Args:
|
||||
config: Configuration of the application.
|
||||
client: BEC client object.
|
||||
gui_id: GUI ID.
|
||||
"""
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
QApplication.instance().aboutToQuit.connect(self.close)
|
||||
self.dev = self.client.device_manager.devices
|
||||
|
||||
self._accent_colors = get_accent_colors()
|
||||
self.ui_file = "alignment_1d.ui"
|
||||
self.ui = None
|
||||
self.progress_bar = None
|
||||
self.waveform = None
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialise the UI from QT Designer file"""
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader(None).loader(os.path.join(current_path, self.ui_file))
|
||||
# Customize the plotting widget
|
||||
self.waveform = self.ui.findChild(BECWaveformWidget, "bec_waveform_widget")
|
||||
self._customise_bec_waveform_widget()
|
||||
# Setup comboboxes for motor and signal selection
|
||||
# FIXME after changing the filtering in the combobox
|
||||
self._setup_signal_combobox()
|
||||
# Setup motor indicator
|
||||
self._setup_motor_indicator()
|
||||
# Setup progress bar
|
||||
self._setup_progress_bar()
|
||||
# Add actions buttons
|
||||
self._customise_buttons()
|
||||
# Hook scaninfo updates
|
||||
self.bec_dispatcher.connect_slot(self.scan_status_callback, MessageEndpoints.scan_status())
|
||||
|
||||
def show(self):
|
||||
return self.ui.show()
|
||||
|
||||
##############################
|
||||
############ SLOTS ###########
|
||||
##############################
|
||||
|
||||
@Slot(dict, dict)
|
||||
def scan_status_callback(self, content: dict, _) -> None:
|
||||
"""This slot allows to enable/disable the UI critical components when a scan is running"""
|
||||
if content["status"] in ["open"]:
|
||||
self.enable_ui(False)
|
||||
elif content["status"] in ["aborted", "halted", "closed"]:
|
||||
self.enable_ui(True)
|
||||
|
||||
@Slot(tuple)
|
||||
def move_to_center(self, move_request: tuple) -> None:
|
||||
"""Move the selected motor to the center"""
|
||||
motor = self.ui.device_combobox.currentText()
|
||||
if move_request[0] in ["center", "center1", "center2"]:
|
||||
pos = move_request[1]
|
||||
self.dev.get(motor).move(float(pos), relative=False)
|
||||
|
||||
@Slot()
|
||||
def reset_progress_bar(self) -> None:
|
||||
"""Reset the progress bar"""
|
||||
self.progress_bar.set_value(0)
|
||||
self.progress_bar.set_minimum(0)
|
||||
|
||||
@Slot(dict, dict)
|
||||
def update_progress_bar(self, content: dict, _) -> None:
|
||||
"""Hook to update the progress bar
|
||||
|
||||
Args:
|
||||
content: Content of the scan progress message.
|
||||
metadata: Metadata of the message.
|
||||
"""
|
||||
if content["max_value"] == 0:
|
||||
self.progress_bar.set_value(0)
|
||||
return
|
||||
self.progress_bar.set_maximum(content["max_value"])
|
||||
self.progress_bar.set_value(content["value"])
|
||||
|
||||
@Slot()
|
||||
def clear_queue(self) -> None:
|
||||
"""Clear the scan queue"""
|
||||
self.queue.request_queue_reset()
|
||||
|
||||
##############################
|
||||
######## END OF SLOTS ########
|
||||
##############################
|
||||
|
||||
def enable_ui(self, enable: bool) -> None:
|
||||
"""Enable or disable the UI components"""
|
||||
# Enable/disable motor and signal selection
|
||||
self.ui.device_combobox_2.setEnabled(enable)
|
||||
# Enable/disable DAP selection
|
||||
self.ui.dap_combo_box.setEnabled(enable)
|
||||
# Enable/disable Scan Button
|
||||
# self.ui.scan_button.setEnabled(enable)
|
||||
# Disable move to buttons in LMFitDialog
|
||||
self.ui.findChild(LMFitDialog).set_actions_enabled(enable)
|
||||
|
||||
def _customise_buttons(self) -> None:
|
||||
"""Add action buttons for the Action Control.
|
||||
In addition, we are adding a callback to also clear the queue to the stop button
|
||||
to ensure that upon clicking the button, no scans from another client may be queued
|
||||
which would be confusing without the queue widget.
|
||||
"""
|
||||
fit_dialog = self.ui.findChild(LMFitDialog)
|
||||
fit_dialog.active_action_list = ["center", "center1", "center2"]
|
||||
fit_dialog.move_action.connect(self.move_to_center)
|
||||
stop_button = self.ui.findChild(StopButton)
|
||||
stop_button.button.setText("Stop and Clear Queue")
|
||||
stop_button.button.clicked.connect(self.clear_queue)
|
||||
|
||||
def _customise_bec_waveform_widget(self) -> None:
|
||||
"""Customise the BEC Waveform Widget, i.e. clear the toolbar"""
|
||||
self.waveform.toolbar.clear()
|
||||
|
||||
def _setup_motor_indicator(self) -> None:
|
||||
"""Setup the arrow item"""
|
||||
self.waveform.waveform.tick_item.add_to_plot()
|
||||
positioner_box = self.ui.findChild(PositionerGroup)
|
||||
positioner_box.position_update.connect(self.waveform.waveform.tick_item.set_position)
|
||||
self.waveform.waveform.tick_item.set_position(0)
|
||||
|
||||
def _setup_signal_combobox(self) -> None:
|
||||
"""Setup signal selection"""
|
||||
# FIXME after changing the filtering in the combobox
|
||||
signals = [name for name in self.dev if isinstance(self.dev.get(name), BECSignal)]
|
||||
self.ui.device_combobox_2.setCurrentText(signals[0])
|
||||
self.ui.device_combobox_2.set_device_filter("Signal")
|
||||
|
||||
def _setup_progress_bar(self) -> None:
|
||||
"""Setup progress bar"""
|
||||
# FIXME once the BECScanProgressBar is implemented
|
||||
self.progress_bar = self.ui.findChild(BECProgressBar, "bec_progress_bar")
|
||||
self.progress_bar.set_value(0)
|
||||
self.ui.bec_waveform_widget.new_scan.connect(self.reset_progress_bar)
|
||||
self.bec_dispatcher.connect_slot(self.update_progress_bar, MessageEndpoints.scan_progress())
|
||||
|
||||
def close(self):
|
||||
logger.info("Disconnecting", repr(self.bec_dispatcher))
|
||||
self.bec_dispatcher.disconnect_all()
|
||||
logger.info("Shutting down BEC Client", repr(self.client))
|
||||
self.client.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "alignment_1d.png"), size=QSize(48, 48)
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
window = Alignment1D()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
615
bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui
Normal file
615
bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui
Normal file
@@ -0,0 +1,615 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>mainWindow</class>
|
||||
<widget class="QMainWindow" name="mainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1611</width>
|
||||
<height>1019</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Alignment tool</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="widget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="DarkModeButton" name="dark_mode_button"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECStatusBox" name="bec_status_box">
|
||||
<property name="compact_view" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="label" stdset="0">
|
||||
<string>BEC Servers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECQueue" name="bec_queue">
|
||||
<property name="compact_view" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radioButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>SLS Light On</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radioButton_3">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BEAMLINE Checks</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="StopButton" name="stop_button">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECProgressBar" name="bec_progress_bar">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="ControlTab">
|
||||
<attribute name="title">
|
||||
<string>Alignment Control</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_4" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="ScanControl" name="scan_control">
|
||||
<property name="current_scan" stdset="0">
|
||||
<string>line_scan</string>
|
||||
</property>
|
||||
<property name="hide_arg_box" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="hide_scan_selection_combobox" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_add_remove_buttons" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PositionerGroup" name="positioner_group"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>4</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_2" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Monitor</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DeviceComboBox" name="device_combobox_2"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>LMFit Model</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DapComboBox" name="dap_combo_box"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Enable ROI</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ToggleSwitch" name="toggle_switch">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>3</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Activate linear region select for LMFit</string>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LayoutDirection::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_8">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECWaveformWidget" name="bec_waveform_widget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="clear_curves_on_plot_update" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="LMFitDialog" name="lm_fit_dialog">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>190</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="always_show_latest" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_curve_selection" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_summary" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Logbook</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="WebsiteWidget" name="website_widget">
|
||||
<property name="url" stdset="0">
|
||||
<string>https://scilog.psi.ch/login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DapComboBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dap_combo_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>StopButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>stop_button</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WebsiteWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>website_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECQueue</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_queue</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ScanControl</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>scan_control</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECProgressBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_progress_bar</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DarkModeButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dark_mode_button</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PositionerGroup</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>positioner_group</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECWaveformWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_waveform_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DeviceComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>device_combobox</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>LMFitDialog</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>lm_fit_dialog</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECStatusBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_status_box</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>toggle_switch</sender>
|
||||
<signal>enabled(bool)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>toogle_roi_select(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1042</x>
|
||||
<y>212</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>322</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>bec_waveform_widget</sender>
|
||||
<signal>dap_summary_update(QVariantMap,QVariantMap)</signal>
|
||||
<receiver>lm_fit_dialog</receiver>
|
||||
<slot>update_summary_tree(QVariantMap,QVariantMap)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1099</x>
|
||||
<y>258</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1157</x>
|
||||
<y>929</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>device_combobox_2</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>plot(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>577</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>427</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>device_combobox_2</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>dap_combo_box</receiver>
|
||||
<slot>select_y_axis(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>577</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>909</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>dap_combo_box</sender>
|
||||
<signal>new_dap_config(QString,QString,QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>add_dap(QString,QString,QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>909</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>447</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>positioner_group</receiver>
|
||||
<slot>set_positioners(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>230</x>
|
||||
<y>306</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>187</x>
|
||||
<y>926</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>set_x(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>187</x>
|
||||
<y>356</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>972</x>
|
||||
<y>509</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>dap_combo_box</receiver>
|
||||
<slot>select_x_axis(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>187</x>
|
||||
<y>356</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>794</x>
|
||||
<y>202</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
84
bec_widgets/applications/bec_app.py
Normal file
84
bec_widgets/applications/bec_app.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Launcher for BEC GUI Applications
|
||||
|
||||
Application must be located in bec_widgets/applications ;
|
||||
in order for the launcher to find the application, it has to be put in
|
||||
a subdirectory with the same name as the main Python module:
|
||||
|
||||
/bec_widgets/applications
|
||||
├── alignment
|
||||
│ └── alignment_1d
|
||||
│ └── alignment_1d.py
|
||||
├── other_app
|
||||
└── other_app.py
|
||||
|
||||
The tree above would contain 2 applications, alignment_1d and other_app.
|
||||
|
||||
The Python module for the application must have `if __name__ == "__main__":`
|
||||
in order for the launcher to execute it (it is run with `python -m`).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
MODULE_PATH = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def find_apps(base_dir: str) -> list[str]:
|
||||
matching_modules = []
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
parent_dir = os.path.basename(root)
|
||||
|
||||
for file in files:
|
||||
if file.endswith(".py") and file != "__init__.py":
|
||||
file_name_without_ext = os.path.splitext(file)[0]
|
||||
|
||||
if file_name_without_ext == parent_dir:
|
||||
rel_path = os.path.relpath(root, base_dir)
|
||||
module_path = rel_path.replace(os.sep, ".")
|
||||
|
||||
module_name = f"{module_path}.{file_name_without_ext}"
|
||||
matching_modules.append((file_name_without_ext, module_name))
|
||||
|
||||
return matching_modules
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="BEC application launcher")
|
||||
|
||||
parser.add_argument("-m", "--module", type=str, help="The module to run (string argument).")
|
||||
|
||||
# Add a positional argument for the module, which acts as a fallback if -m is not provided
|
||||
parser.add_argument(
|
||||
"positional_module",
|
||||
nargs="?", # This makes the positional argument optional
|
||||
help="Positional argument that is treated as module if -m is not specified.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
# If the -m/--module is not provided, fallback to the positional argument
|
||||
module = args.module if args.module else args.positional_module
|
||||
|
||||
if module:
|
||||
for app_name, app_module in find_apps(MODULE_PATH):
|
||||
if module in (app_name, app_module):
|
||||
print("Starting:", app_name)
|
||||
python_executable = sys.executable
|
||||
|
||||
# Replace the current process with the new Python module
|
||||
os.execvp(
|
||||
python_executable,
|
||||
[python_executable, "-m", f"bec_widgets.applications.{app_module}"],
|
||||
)
|
||||
print(f"Error: cannot find application {module}")
|
||||
|
||||
# display list of apps
|
||||
print("Available applications:")
|
||||
for app, _ in find_apps(MODULE_PATH):
|
||||
print(f" - {app}")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,11 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
|
||||
|
||||
def dock_area(object_name: str | None = None) -> BECDockArea:
|
||||
_dock_area = BECDockArea(object_name=object_name, root_widget=True)
|
||||
_dock_area = BECDockArea(object_name=object_name)
|
||||
return _dock_area
|
||||
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Qt, Signal # type: ignore
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtGui import QPainter, QPainterPath, QPixmap
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -21,10 +21,8 @@ from qtpy.QtWidgets import (
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
|
||||
from bec_widgets.utils.round_frame import RoundedFrame
|
||||
from bec_widgets.utils.toolbar import ModularToolBar
|
||||
@@ -37,8 +35,6 @@ from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
@@ -145,13 +141,12 @@ class LaunchWindow(BECMainWindow):
|
||||
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
|
||||
|
||||
self.app = QApplication.instance()
|
||||
self.tiles: dict[str, LaunchTile] = {}
|
||||
|
||||
# Toolbar
|
||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||
self.toolbar = ModularToolBar(parent=self)
|
||||
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
|
||||
self.spacer = QWidget(self)
|
||||
self.spacer = QWidget()
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar.addWidget(self.spacer)
|
||||
self.toolbar.addWidget(self.dark_mode_button)
|
||||
@@ -161,104 +156,53 @@ class LaunchWindow(BECMainWindow):
|
||||
self.central_widget.layout = QHBoxLayout(self.central_widget)
|
||||
self.setCentralWidget(self.central_widget)
|
||||
|
||||
self.register_tile(
|
||||
name="dock_area",
|
||||
self.tile_dock_area = LaunchTile(
|
||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||
top_label="Get started",
|
||||
main_label="BEC Dock Area",
|
||||
description="Highly flexible and customizable dock area application with modular widgets.",
|
||||
action_button=lambda: self.launch("dock_area"),
|
||||
show_selector=False,
|
||||
)
|
||||
self.tile_dock_area.setFixedSize(*self.TILE_SIZE)
|
||||
|
||||
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
|
||||
self._update_available_auto_updates()
|
||||
)
|
||||
self.register_tile(
|
||||
name="auto_update",
|
||||
self.tile_auto_update = LaunchTile(
|
||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "auto_update.png"),
|
||||
top_label="Get automated",
|
||||
main_label="BEC Auto Update Dock Area",
|
||||
description="Dock area with auto update functionality for BEC widgets plotting.",
|
||||
action_button=self._open_auto_update,
|
||||
show_selector=True,
|
||||
selector_items=list(self.available_auto_updates.keys()) + ["Default"],
|
||||
)
|
||||
self.tile_auto_update.setFixedSize(*self.TILE_SIZE)
|
||||
|
||||
self.register_tile(
|
||||
name="custom_ui_file",
|
||||
self.tile_ui_file = LaunchTile(
|
||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "ui_loader_tile.png"),
|
||||
top_label="Get customized",
|
||||
main_label="Launch Custom UI File",
|
||||
description="GUI application with custom UI file.",
|
||||
action_button=self._open_custom_ui_file,
|
||||
show_selector=False,
|
||||
)
|
||||
self.tile_ui_file.setFixedSize(*self.TILE_SIZE)
|
||||
|
||||
# plugin widgets
|
||||
self.available_widgets: dict[str, BECWidget] = get_all_plugin_widgets()
|
||||
if self.available_widgets:
|
||||
plugin_repo_name = next(iter(self.available_widgets.values())).__module__.split(".")[0]
|
||||
plugin_repo_name = plugin_repo_name.removesuffix("_bec").upper()
|
||||
self.register_tile(
|
||||
name="widget",
|
||||
icon_path=os.path.join(
|
||||
MODULE_PATH, "assets", "app_icons", "widget_launch_tile.png"
|
||||
),
|
||||
top_label="Get quickly started",
|
||||
main_label=f"Launch a {plugin_repo_name} Widget",
|
||||
description=f"GUI application with one widget from the {plugin_repo_name} repository.",
|
||||
action_button=self._open_widget,
|
||||
show_selector=True,
|
||||
selector_items=list(self.available_widgets.keys()),
|
||||
)
|
||||
# Add tiles to the main layout
|
||||
self.central_widget.layout.addWidget(self.tile_dock_area)
|
||||
self.central_widget.layout.addWidget(self.tile_auto_update)
|
||||
self.central_widget.layout.addWidget(self.tile_ui_file)
|
||||
|
||||
# hacky solution no time to waste
|
||||
self.tiles = [self.tile_dock_area, self.tile_auto_update, self.tile_ui_file]
|
||||
|
||||
# Connect signals
|
||||
self.tile_dock_area.action_button.clicked.connect(lambda: self.launch("dock_area"))
|
||||
self.tile_auto_update.action_button.clicked.connect(self._open_auto_update)
|
||||
self.tile_ui_file.action_button.clicked.connect(self._open_custom_ui_file)
|
||||
self._update_theme()
|
||||
|
||||
self.register = RPCRegister()
|
||||
self.register.callbacks.append(self._turn_off_the_lights)
|
||||
self.register.broadcast()
|
||||
|
||||
def register_tile(
|
||||
self,
|
||||
name: str,
|
||||
icon_path: str | None = None,
|
||||
top_label: str | None = None,
|
||||
main_label: str | None = None,
|
||||
description: str | None = None,
|
||||
action_button: Callable | None = None,
|
||||
show_selector: bool = False,
|
||||
selector_items: list[str] | None = None,
|
||||
):
|
||||
"""
|
||||
Register a tile in the launcher window.
|
||||
|
||||
Args:
|
||||
name(str): The name of the tile.
|
||||
icon_path(str): The path to the icon.
|
||||
top_label(str): The top label of the tile.
|
||||
main_label(str): The main label of the tile.
|
||||
description(str): The description of the tile.
|
||||
action_button(callable): The action to be performed when the button is clicked.
|
||||
show_selector(bool): Whether to show a selector or not.
|
||||
selector_items(list[str]): The items to be shown in the selector.
|
||||
"""
|
||||
|
||||
tile = LaunchTile(
|
||||
icon_path=icon_path,
|
||||
top_label=top_label,
|
||||
main_label=main_label,
|
||||
description=description,
|
||||
show_selector=show_selector,
|
||||
# Auto updates
|
||||
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
|
||||
self._update_available_auto_updates()
|
||||
)
|
||||
tile.setFixedSize(*self.TILE_SIZE)
|
||||
if action_button:
|
||||
tile.action_button.clicked.connect(action_button)
|
||||
if show_selector and selector_items:
|
||||
tile.selector.addItems(selector_items)
|
||||
self.central_widget.layout.addWidget(tile)
|
||||
|
||||
self.tiles[name] = tile
|
||||
if self.tile_auto_update.selector is not None:
|
||||
self.tile_auto_update.selector.addItems(
|
||||
list(self.available_auto_updates.keys()) + ["Default"]
|
||||
)
|
||||
|
||||
def launch(
|
||||
self,
|
||||
@@ -266,7 +210,7 @@ class LaunchWindow(BECMainWindow):
|
||||
name: str | None = None,
|
||||
geometry: tuple[int, int, int, int] | None = None,
|
||||
**kwargs,
|
||||
) -> QWidget | None:
|
||||
) -> QWidget:
|
||||
"""Launch the specified script. If the launch script creates a QWidget, it will be
|
||||
embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown
|
||||
as a separate window.
|
||||
@@ -287,8 +231,6 @@ class LaunchWindow(BECMainWindow):
|
||||
raise ValueError(
|
||||
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
|
||||
)
|
||||
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||
|
||||
else:
|
||||
name = "dock_area"
|
||||
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
||||
@@ -300,20 +242,12 @@ class LaunchWindow(BECMainWindow):
|
||||
|
||||
if launch_script == "custom_ui_file":
|
||||
ui_file = kwargs.pop("ui_file", None)
|
||||
if not ui_file:
|
||||
return None
|
||||
return self._launch_custom_ui_file(ui_file)
|
||||
|
||||
if launch_script == "auto_update":
|
||||
auto_update = kwargs.pop("auto_update", None)
|
||||
return self._launch_auto_update(auto_update)
|
||||
|
||||
if launch_script == "widget":
|
||||
widget = kwargs.pop("widget", None)
|
||||
if widget is None:
|
||||
raise ValueError("Widget name must be provided.")
|
||||
return self._launch_widget(widget)
|
||||
|
||||
launch = getattr(bw_launch, launch_script, None)
|
||||
if launch is None:
|
||||
raise ValueError(f"Launch script {launch_script} not found.")
|
||||
@@ -331,7 +265,6 @@ class LaunchWindow(BECMainWindow):
|
||||
else:
|
||||
window = BECMainWindow()
|
||||
window.setCentralWidget(result_widget)
|
||||
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
|
||||
window.show()
|
||||
return result_widget
|
||||
|
||||
@@ -341,8 +274,6 @@ class LaunchWindow(BECMainWindow):
|
||||
raise ValueError("UI file must be provided for custom UI file launch.")
|
||||
filename = os.path.basename(ui_file).split(".")[0]
|
||||
|
||||
WidgetContainerUtils.raise_for_invalid_name(filename)
|
||||
|
||||
tree = ET.parse(ui_file)
|
||||
root = tree.getroot()
|
||||
# Check if the top-level widget is a QMainWindow
|
||||
@@ -380,28 +311,11 @@ class LaunchWindow(BECMainWindow):
|
||||
window.show()
|
||||
return window
|
||||
|
||||
def _launch_widget(self, widget: type[BECWidget]) -> QWidget:
|
||||
name = pascal_to_snake(widget.__name__)
|
||||
|
||||
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||
|
||||
window = BECMainWindow()
|
||||
|
||||
widget_instance = widget(root_widget=True, object_name=name)
|
||||
assert isinstance(widget_instance, QWidget)
|
||||
QApplication.processEvents()
|
||||
|
||||
window.setCentralWidget(widget_instance)
|
||||
window.resize(window.minimumSizeHint())
|
||||
window.setWindowTitle(f"BEC - {widget_instance.objectName()}")
|
||||
window.show()
|
||||
return window
|
||||
|
||||
def apply_theme(self, theme: str):
|
||||
"""
|
||||
Change the theme of the application.
|
||||
"""
|
||||
for tile in self.tiles.values():
|
||||
for tile in self.tiles:
|
||||
tile.apply_theme(theme)
|
||||
|
||||
super().apply_theme(theme)
|
||||
@@ -410,25 +324,14 @@ class LaunchWindow(BECMainWindow):
|
||||
"""
|
||||
Open the auto update window.
|
||||
"""
|
||||
if self.tiles["auto_update"].selector is None:
|
||||
if self.tile_auto_update.selector is None:
|
||||
auto_update = None
|
||||
else:
|
||||
auto_update = self.tiles["auto_update"].selector.currentText()
|
||||
auto_update = self.tile_auto_update.selector.currentText()
|
||||
if auto_update == "Default":
|
||||
auto_update = None
|
||||
return self.launch("auto_update", auto_update=auto_update)
|
||||
|
||||
def _open_widget(self):
|
||||
"""
|
||||
Open a widget from the available widgets.
|
||||
"""
|
||||
if self.tiles["widget"].selector is None:
|
||||
return
|
||||
widget = self.tiles["widget"].selector.currentText()
|
||||
if widget not in self.available_widgets:
|
||||
raise ValueError(f"Widget {widget} not found in available widgets.")
|
||||
return self.launch("widget", widget=self.available_widgets[widget])
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def _open_custom_ui_file(self):
|
||||
"""
|
||||
@@ -468,45 +371,6 @@ class LaunchWindow(BECMainWindow):
|
||||
super().showEvent(event)
|
||||
self.setFixedSize(self.size())
|
||||
|
||||
def _launcher_is_last_widget(self, connections: dict) -> bool:
|
||||
"""
|
||||
Check if the launcher is the last widget in the application.
|
||||
"""
|
||||
|
||||
remaining_connections = [
|
||||
connection for connection in connections.values() if connection.parent_id != self.gui_id
|
||||
]
|
||||
return len(remaining_connections) <= 1
|
||||
|
||||
def _turn_off_the_lights(self, connections: dict):
|
||||
"""
|
||||
If there is only one connection remaining, it is the launcher, so we show it.
|
||||
Once the launcher is closed as the last window, we quit the application.
|
||||
"""
|
||||
if self._launcher_is_last_widget(connections):
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
self.raise_()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(True) # type: ignore
|
||||
return
|
||||
|
||||
self.hide()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(False) # type: ignore
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""
|
||||
Close the launcher window.
|
||||
"""
|
||||
connections = self.register.list_all_connections()
|
||||
if self._launcher_is_last_widget(connections):
|
||||
event.accept()
|
||||
return
|
||||
|
||||
event.ignore()
|
||||
self.hide()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
@@ -6,8 +6,6 @@ from __future__ import annotations
|
||||
import enum
|
||||
import inspect
|
||||
import traceback
|
||||
from functools import reduce
|
||||
from operator import add
|
||||
from typing import Literal, Optional
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
@@ -27,31 +25,22 @@ class _WidgetsEnumType(str, enum.Enum):
|
||||
|
||||
|
||||
_Widgets = {
|
||||
"AbortButton": "AbortButton",
|
||||
"BECDockArea": "BECDockArea",
|
||||
"BECProgressBar": "BECProgressBar",
|
||||
"BECQueue": "BECQueue",
|
||||
"BECStatusBox": "BECStatusBox",
|
||||
"DapComboBox": "DapComboBox",
|
||||
"DarkModeButton": "DarkModeButton",
|
||||
"DeviceBrowser": "DeviceBrowser",
|
||||
"DeviceComboBox": "DeviceComboBox",
|
||||
"DeviceLineEdit": "DeviceLineEdit",
|
||||
"Image": "Image",
|
||||
"LogPanel": "LogPanel",
|
||||
"Minesweeper": "Minesweeper",
|
||||
"MotorMap": "MotorMap",
|
||||
"MultiWaveform": "MultiWaveform",
|
||||
"PositionIndicator": "PositionIndicator",
|
||||
"PositionerBox": "PositionerBox",
|
||||
"PositionerBox2D": "PositionerBox2D",
|
||||
"PositionerControlLine": "PositionerControlLine",
|
||||
"ResetButton": "ResetButton",
|
||||
"ResumeButton": "ResumeButton",
|
||||
"RingProgressBar": "RingProgressBar",
|
||||
"ScanControl": "ScanControl",
|
||||
"ScatterWaveform": "ScatterWaveform",
|
||||
"StopButton": "StopButton",
|
||||
"TextBox": "TextBox",
|
||||
"VSCodeEditor": "VSCodeEditor",
|
||||
"Waveform": "Waveform",
|
||||
@@ -87,16 +76,6 @@ except ImportError as e:
|
||||
logger.error(f"Failed loading plugins: \n{reduce(add, traceback.format_exception(e))}")
|
||||
|
||||
|
||||
class AbortButton(RPCBase):
|
||||
"""A button that abort the scan."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class AutoUpdates(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -470,12 +449,6 @@ class BECProgressBar(RPCBase):
|
||||
>>> progressbar.label_template = "$value / $percentage %"
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_label(self) -> str:
|
||||
"""
|
||||
Return the label text. mostly used for testing rpc.
|
||||
"""
|
||||
|
||||
|
||||
class BECQueue(RPCBase):
|
||||
"""Widget to display the BEC queue."""
|
||||
@@ -490,12 +463,6 @@ class BECQueue(RPCBase):
|
||||
class BECStatusBox(RPCBase):
|
||||
"""An autonomous widget to display the status of BEC services."""
|
||||
|
||||
@rpc_call
|
||||
def get_server_state(self) -> "str":
|
||||
"""
|
||||
Get the state ("RUNNING", "BUSY", "IDLE", "ERROR") of the BEC server
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
@@ -698,14 +665,6 @@ class DapComboBox(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class DarkModeButton(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class DeviceBrowser(RPCBase):
|
||||
"""DeviceBrowser is a widget that displays all available devices in the current BEC session."""
|
||||
|
||||
@@ -964,6 +923,48 @@ class Image(RPCBase):
|
||||
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
|
||||
@rpc_call
|
||||
def color_map(self) -> "str":
|
||||
@@ -1158,16 +1159,16 @@ class Image(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def num_rotation_90(self) -> "int":
|
||||
def rotation(self) -> "int":
|
||||
"""
|
||||
The number of 90° rotations to apply counterclockwise.
|
||||
The number of 90° rotations to apply.
|
||||
"""
|
||||
|
||||
@num_rotation_90.setter
|
||||
@rotation.setter
|
||||
@rpc_call
|
||||
def num_rotation_90(self) -> "int":
|
||||
def rotation(self) -> "int":
|
||||
"""
|
||||
The number of 90° rotations to apply counterclockwise.
|
||||
The number of 90° rotations to apply.
|
||||
"""
|
||||
|
||||
@property
|
||||
@@ -1330,14 +1331,14 @@ class ImageItem(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def num_rotation_90(self) -> "Optional[int]":
|
||||
def rotation(self) -> "Optional[int]":
|
||||
"""
|
||||
Get or set the number of 90° rotations to apply.
|
||||
"""
|
||||
|
||||
@num_rotation_90.setter
|
||||
@rotation.setter
|
||||
@rpc_call
|
||||
def num_rotation_90(self) -> "Optional[int]":
|
||||
def rotation(self) -> "Optional[int]":
|
||||
"""
|
||||
Get or set the number of 90° rotations to apply.
|
||||
"""
|
||||
@@ -1387,9 +1388,6 @@ class LogPanel(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class MotorMap(RPCBase):
|
||||
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
||||
|
||||
@@ -2168,7 +2166,7 @@ class MultiWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def plot(self, monitor: "str", color_palette: "str | None" = "plasma"):
|
||||
def plot(self, monitor: "str", color_palette: "str | None" = "magma"):
|
||||
"""
|
||||
Create a plot for the given monitor.
|
||||
Args:
|
||||
@@ -2198,10 +2196,7 @@ class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
"""
|
||||
Set the position of the indicator
|
||||
|
||||
Args:
|
||||
position: The new position of the indicator
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -2221,13 +2216,6 @@ class PositionIndicator(RPCBase):
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
|
||||
@vertical.setter
|
||||
@rpc_call
|
||||
def vertical(self):
|
||||
"""
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def indicator_width(self):
|
||||
@@ -2235,13 +2223,6 @@ class PositionIndicator(RPCBase):
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
|
||||
@indicator_width.setter
|
||||
@rpc_call
|
||||
def indicator_width(self):
|
||||
"""
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rounded_corners(self):
|
||||
@@ -2249,61 +2230,6 @@ class PositionIndicator(RPCBase):
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
|
||||
@rounded_corners.setter
|
||||
@rpc_call
|
||||
def rounded_corners(self):
|
||||
"""
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox(RPCBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox2D(RPCBase):
|
||||
"""Simple Widget to control two positioners in box form"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner_hor(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner_ver(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerControlLine(RPCBase):
|
||||
"""A widget that controls a single device."""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerGroup(RPCBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
@@ -2317,26 +2243,6 @@ class PositionerGroup(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class ResetButton(RPCBase):
|
||||
"""A button that resets the scan queue."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class ResumeButton(RPCBase):
|
||||
"""A button that continue scan queue."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class Ring(RPCBase):
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
@@ -2465,9 +2371,9 @@ class RingProgressBar(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rings(self) -> "list[Ring]":
|
||||
def rings(self):
|
||||
"""
|
||||
Returns a list of all rings in the progress bar.
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -2941,7 +2847,7 @@ class ScatterWaveform(RPCBase):
|
||||
x_entry: "None | str" = None,
|
||||
y_entry: "None | str" = None,
|
||||
z_entry: "None | str" = None,
|
||||
color_map: "str | None" = "plasma",
|
||||
color_map: "str | None" = "magma",
|
||||
label: "str | None" = None,
|
||||
validate_bec: "bool" = True,
|
||||
) -> "ScatterCurve":
|
||||
@@ -2981,16 +2887,6 @@ class ScatterWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class StopButton(RPCBase):
|
||||
"""A button that stops the current scan."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class TextBox(RPCBase):
|
||||
"""A widget that displays text in plain and HTML format"""
|
||||
|
||||
@@ -3335,20 +3231,6 @@ class Waveform(RPCBase):
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_entry(self) -> "str | None":
|
||||
"""
|
||||
The x signal name.
|
||||
"""
|
||||
|
||||
@x_entry.setter
|
||||
@rpc_call
|
||||
def x_entry(self) -> "str | None":
|
||||
"""
|
||||
The x signal name.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color_palette(self) -> "str":
|
||||
|
||||
@@ -20,7 +20,6 @@ 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.utils.serialization import register_serializer_extension
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.messages import GUIRegistryStateMessage
|
||||
@@ -32,8 +31,7 @@ logger = bec_logger.logger
|
||||
IGNORE_WIDGETS = ["LaunchWindow"]
|
||||
|
||||
RegistryState: TypeAlias = dict[
|
||||
Literal["gui_id", "name", "widget_class", "config", "__rpc__", "container_proxy"],
|
||||
str | bool | dict,
|
||||
Literal["gui_id", "name", "widget_class", "config", "__rpc__"], str | bool | dict
|
||||
]
|
||||
|
||||
# pylint: disable=redefined-outer-scope
|
||||
@@ -216,7 +214,6 @@ class BECGuiClient(RPCBase):
|
||||
self._server_registry: dict[str, RegistryState] = {}
|
||||
self._ipython_registry: dict[str, RPCReference] = {}
|
||||
self.available_widgets = AvailableWidgetsNamespace()
|
||||
register_serializer_extension()
|
||||
|
||||
####################
|
||||
#### Client API ####
|
||||
@@ -278,8 +275,6 @@ class BECGuiClient(RPCBase):
|
||||
name: str | None = None,
|
||||
wait: bool = True,
|
||||
geometry: tuple[int, int, int, int] | None = None,
|
||||
launch_script: str = "dock_area",
|
||||
**kwargs,
|
||||
) -> client.BECDockArea:
|
||||
"""Create a new top-level dock area.
|
||||
|
||||
@@ -295,11 +290,11 @@ class BECGuiClient(RPCBase):
|
||||
if wait:
|
||||
with wait_for_server(self):
|
||||
widget = self.launcher._run_rpc(
|
||||
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
||||
"launch", "dock_area", name, geometry
|
||||
) # pylint: disable=protected-access
|
||||
return widget
|
||||
widget = self.launcher._run_rpc(
|
||||
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
||||
"new_dock_area", name, geometry
|
||||
) # pylint: disable=protected-access
|
||||
return widget
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ class ClientGenerator:
|
||||
"""import enum
|
||||
import inspect
|
||||
import traceback
|
||||
from functools import reduce
|
||||
from operator import add
|
||||
from typing import Literal, Optional
|
||||
"""
|
||||
if self._base
|
||||
@@ -223,18 +221,18 @@ class {class_name}(RPCBase):"""
|
||||
# Combine header and content, then format with black
|
||||
full_content = self.header + "\n" + self.content
|
||||
try:
|
||||
formatted_content = black.format_str(full_content, mode=black.Mode(line_length=100))
|
||||
formatted_content = black.format_str(full_content, mode=black.FileMode(line_length=100))
|
||||
except black.NothingChanged:
|
||||
formatted_content = full_content
|
||||
|
||||
config = isort.Config(
|
||||
isort.Config(
|
||||
profile="black",
|
||||
line_length=100,
|
||||
multi_line_output=3,
|
||||
include_trailing_comma=False,
|
||||
include_trailing_comma=True,
|
||||
known_first_party=["bec_widgets"],
|
||||
)
|
||||
formatted_content = isort.code(formatted_content, config=config)
|
||||
formatted_content = isort.code(formatted_content)
|
||||
|
||||
with open(file_name, "w", encoding="utf-8") as file:
|
||||
file.write(formatted_content)
|
||||
@@ -317,7 +315,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
sys.argv = ["bw-generate-cli", "--target", "bec_widgets"]
|
||||
main()
|
||||
|
||||
@@ -130,7 +130,6 @@ class RPCBase:
|
||||
config: dict | None = None,
|
||||
object_name: str | None = None,
|
||||
parent=None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
|
||||
self._config = config if config is not None else {}
|
||||
@@ -145,21 +144,21 @@ class RPCBase:
|
||||
def __repr__(self):
|
||||
type_ = type(self)
|
||||
qualname = type_.__qualname__
|
||||
return f"<{qualname} with name: {self.object_name}>"
|
||||
return f"<{qualname} with name: {self.widget_name}>"
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the widget.
|
||||
"""
|
||||
obj = self._root._server_registry.get(self._gui_id)
|
||||
if obj is None:
|
||||
raise ValueError(f"Widget {self._gui_id} not found.")
|
||||
if proxy := obj.get("container_proxy"):
|
||||
assert isinstance(proxy, str)
|
||||
self._run_rpc("remove", gui_id=proxy)
|
||||
return
|
||||
self._run_rpc("remove")
|
||||
|
||||
@property
|
||||
def widget_name(self):
|
||||
"""
|
||||
Get the widget name.
|
||||
"""
|
||||
return self.object_name
|
||||
|
||||
@property
|
||||
def _root(self) -> BECGuiClient:
|
||||
"""
|
||||
@@ -172,15 +171,7 @@ class RPCBase:
|
||||
parent = parent._parent
|
||||
return parent # type: ignore
|
||||
|
||||
def _run_rpc(
|
||||
self,
|
||||
method,
|
||||
*args,
|
||||
wait_for_rpc_response=True,
|
||||
timeout=5,
|
||||
gui_id: str | None = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=5, **kwargs) -> Any:
|
||||
"""
|
||||
Run the RPC call.
|
||||
|
||||
@@ -188,8 +179,6 @@ class RPCBase:
|
||||
method: The method to call.
|
||||
args: The arguments to pass to the method.
|
||||
wait_for_rpc_response: Whether to wait for the RPC response.
|
||||
timeout: The timeout for the RPC response.
|
||||
gui_id: The GUI ID to use for the RPC call. If None, the default GUI ID is used.
|
||||
kwargs: The keyword arguments to pass to the method.
|
||||
|
||||
Returns:
|
||||
@@ -198,7 +187,7 @@ class RPCBase:
|
||||
request_id = str(uuid.uuid4())
|
||||
rpc_msg = messages.GUIInstructionMessage(
|
||||
action=method,
|
||||
parameter={"args": args, "kwargs": kwargs, "gui_id": gui_id or self._gui_id},
|
||||
parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
|
||||
metadata={"request_id": request_id},
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@@ -28,6 +28,7 @@ class SimpleFileLikeFromLogOutputFunc:
|
||||
def __init__(self, log_func):
|
||||
self._log_func = log_func
|
||||
self._buffer = []
|
||||
self.encoding = "utf8"
|
||||
|
||||
def write(self, buffer):
|
||||
self._buffer.append(buffer)
|
||||
@@ -83,6 +84,29 @@ class GUIServer:
|
||||
service_config = ServiceConfig()
|
||||
return service_config
|
||||
|
||||
def _turn_off_the_lights(self, connections: dict):
|
||||
"""
|
||||
If there is only one connection remaining, it is the launcher, so we show it.
|
||||
Once the launcher is closed as the last window, we quit the application.
|
||||
"""
|
||||
self.launcher_window = cast(LaunchWindow, self.launcher_window)
|
||||
|
||||
remaining_connections = [
|
||||
connection
|
||||
for connection in connections.values()
|
||||
if connection.parent_id != self.launcher_window.gui_id
|
||||
]
|
||||
if len(remaining_connections) <= 1:
|
||||
self.launcher_window.show()
|
||||
self.launcher_window.activateWindow()
|
||||
self.launcher_window.raise_()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(True)
|
||||
else:
|
||||
self.launcher_window.hide()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
def _run(self):
|
||||
"""
|
||||
Run the GUI server.
|
||||
@@ -102,6 +126,10 @@ class GUIServer:
|
||||
self.app.aboutToQuit.connect(self.shutdown)
|
||||
self.app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
register = RPCRegister()
|
||||
register.callbacks.append(self._turn_off_the_lights)
|
||||
register.broadcast()
|
||||
|
||||
if self.gui_class:
|
||||
# If the server is started with a specific gui class, we launch it.
|
||||
# This will automatically hide the launcher.
|
||||
|
||||
@@ -43,22 +43,22 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"pg": pg,
|
||||
"wh": wh,
|
||||
"dock": self.dock,
|
||||
# "im": self.im,
|
||||
# "mi": self.mi,
|
||||
# "mm": self.mm,
|
||||
# "lm": self.lm,
|
||||
# "btn1": self.btn1,
|
||||
# "btn2": self.btn2,
|
||||
# "btn3": self.btn3,
|
||||
# "btn4": self.btn4,
|
||||
# "btn5": self.btn5,
|
||||
# "btn6": self.btn6,
|
||||
# "pb": self.pb,
|
||||
# "pi": self.pi,
|
||||
# "wf": self.wf,
|
||||
# "scatter": self.scatter,
|
||||
# "scatter_mi": self.scatter,
|
||||
# "mwf": self.mwf,
|
||||
"im": self.im,
|
||||
"mi": self.mi,
|
||||
"mm": self.mm,
|
||||
"lm": self.lm,
|
||||
"btn1": self.btn1,
|
||||
"btn2": self.btn2,
|
||||
"btn3": self.btn3,
|
||||
"btn4": self.btn4,
|
||||
"btn5": self.btn5,
|
||||
"btn6": self.btn6,
|
||||
"pb": self.pb,
|
||||
"pi": self.pi,
|
||||
"wf": self.wf,
|
||||
"scatter": self.scatter,
|
||||
"scatter_mi": self.scatter,
|
||||
"mwf": self.mwf,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -77,76 +77,76 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
first_tab_layout.addWidget(self.dock)
|
||||
tab_widget.addTab(first_tab, "Dock Area")
|
||||
|
||||
# third_tab = QWidget()
|
||||
# third_tab_layout = QVBoxLayout(third_tab)
|
||||
# self.lm = LayoutManagerWidget()
|
||||
# third_tab_layout.addWidget(self.lm)
|
||||
# tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||
#
|
||||
# fourth_tab = QWidget()
|
||||
# fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||
# self.pb = PlotBase()
|
||||
# self.pi = self.pb.plot_item
|
||||
# fourth_tab_layout.addWidget(self.pb)
|
||||
# tab_widget.addTab(fourth_tab, "PlotBase")
|
||||
#
|
||||
# tab_widget.setCurrentIndex(3)
|
||||
#
|
||||
third_tab = QWidget()
|
||||
third_tab_layout = QVBoxLayout(third_tab)
|
||||
self.lm = LayoutManagerWidget()
|
||||
third_tab_layout.addWidget(self.lm)
|
||||
tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||
|
||||
fourth_tab = QWidget()
|
||||
fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||
self.pb = PlotBase()
|
||||
self.pi = self.pb.plot_item
|
||||
fourth_tab_layout.addWidget(self.pb)
|
||||
tab_widget.addTab(fourth_tab, "PlotBase")
|
||||
|
||||
tab_widget.setCurrentIndex(3)
|
||||
|
||||
group_box = QGroupBox("Jupyter Console", splitter)
|
||||
group_box_layout = QVBoxLayout(group_box)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
group_box_layout.addWidget(self.console)
|
||||
#
|
||||
# # Some buttons for layout testing
|
||||
# self.btn1 = QPushButton("Button 1")
|
||||
# self.btn2 = QPushButton("Button 2")
|
||||
# self.btn3 = QPushButton("Button 3")
|
||||
# self.btn4 = QPushButton("Button 4")
|
||||
# self.btn5 = QPushButton("Button 5")
|
||||
# self.btn6 = QPushButton("Button 6")
|
||||
#
|
||||
# fifth_tab = QWidget()
|
||||
# fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
# self.wf = Waveform()
|
||||
# fifth_tab_layout.addWidget(self.wf)
|
||||
# tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
# tab_widget.setCurrentIndex(4)
|
||||
#
|
||||
# sixth_tab = QWidget()
|
||||
# sixth_tab_layout = QVBoxLayout(sixth_tab)
|
||||
# self.im = Image()
|
||||
# self.mi = self.im.main_image
|
||||
# sixth_tab_layout.addWidget(self.im)
|
||||
# tab_widget.addTab(sixth_tab, "Image Next Gen")
|
||||
# tab_widget.setCurrentIndex(5)
|
||||
#
|
||||
# seventh_tab = QWidget()
|
||||
# seventh_tab_layout = QVBoxLayout(seventh_tab)
|
||||
# self.scatter = ScatterWaveform()
|
||||
# self.scatter_mi = self.scatter.main_curve
|
||||
# self.scatter.plot("samx", "samy", "bpm4i")
|
||||
# seventh_tab_layout.addWidget(self.scatter)
|
||||
# tab_widget.addTab(seventh_tab, "Scatter Waveform")
|
||||
# tab_widget.setCurrentIndex(6)
|
||||
#
|
||||
# eighth_tab = QWidget()
|
||||
# eighth_tab_layout = QVBoxLayout(eighth_tab)
|
||||
# self.mm = MotorMap()
|
||||
# eighth_tab_layout.addWidget(self.mm)
|
||||
# tab_widget.addTab(eighth_tab, "Motor Map")
|
||||
# tab_widget.setCurrentIndex(7)
|
||||
#
|
||||
# ninth_tab = QWidget()
|
||||
# ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||
# self.mwf = MultiWaveform()
|
||||
# ninth_tab_layout.addWidget(self.mwf)
|
||||
# tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||
# tab_widget.setCurrentIndex(8)
|
||||
#
|
||||
# # add stuff to the new Waveform widget
|
||||
# self._init_waveform()
|
||||
#
|
||||
# self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
# Some buttons for layout testing
|
||||
self.btn1 = QPushButton("Button 1")
|
||||
self.btn2 = QPushButton("Button 2")
|
||||
self.btn3 = QPushButton("Button 3")
|
||||
self.btn4 = QPushButton("Button 4")
|
||||
self.btn5 = QPushButton("Button 5")
|
||||
self.btn6 = QPushButton("Button 6")
|
||||
|
||||
fifth_tab = QWidget()
|
||||
fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
self.wf = Waveform()
|
||||
fifth_tab_layout.addWidget(self.wf)
|
||||
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
tab_widget.setCurrentIndex(4)
|
||||
|
||||
sixth_tab = QWidget()
|
||||
sixth_tab_layout = QVBoxLayout(sixth_tab)
|
||||
self.im = Image()
|
||||
self.mi = self.im.main_image
|
||||
sixth_tab_layout.addWidget(self.im)
|
||||
tab_widget.addTab(sixth_tab, "Image Next Gen")
|
||||
tab_widget.setCurrentIndex(5)
|
||||
|
||||
seventh_tab = QWidget()
|
||||
seventh_tab_layout = QVBoxLayout(seventh_tab)
|
||||
self.scatter = ScatterWaveform()
|
||||
self.scatter_mi = self.scatter.main_curve
|
||||
self.scatter.plot("samx", "samy", "bpm4i")
|
||||
seventh_tab_layout.addWidget(self.scatter)
|
||||
tab_widget.addTab(seventh_tab, "Scatter Waveform")
|
||||
tab_widget.setCurrentIndex(6)
|
||||
|
||||
eighth_tab = QWidget()
|
||||
eighth_tab_layout = QVBoxLayout(eighth_tab)
|
||||
self.mm = MotorMap()
|
||||
eighth_tab_layout.addWidget(self.mm)
|
||||
tab_widget.addTab(eighth_tab, "Motor Map")
|
||||
tab_widget.setCurrentIndex(7)
|
||||
|
||||
ninth_tab = QWidget()
|
||||
ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||
self.mwf = MultiWaveform()
|
||||
ninth_tab_layout.addWidget(self.mwf)
|
||||
tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||
tab_widget.setCurrentIndex(8)
|
||||
|
||||
# add stuff to the new Waveform widget
|
||||
self._init_waveform()
|
||||
|
||||
self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
def _init_waveform(self):
|
||||
self.wf.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
|
||||
@@ -174,7 +174,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
icon = material_icon("terminal", color=(255, 255, 255, 255), filled=True)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
bec_dispatcher = BECDispatcher(gui_id="jupyter_console")
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
@@ -15,7 +14,8 @@ from qtpy.QtCore import QObject, QRunnable, QThreadPool, QTimer, Signal
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility, SafeSlot
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.utils.error_popups import SafeSlot as pyqtSlot
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
||||
|
||||
@@ -85,21 +85,9 @@ class BECConnector:
|
||||
gui_id: str | None = None,
|
||||
object_name: str | None = None,
|
||||
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
|
||||
root_widget: bool = False,
|
||||
parent_id: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
BECConnector mixin class to handle BEC client and device manager.
|
||||
|
||||
Args:
|
||||
client(BECClient, optional): The BEC client.
|
||||
config(ConnectionConfig, optional): The connection configuration with specific gui id.
|
||||
gui_id(str, optional): The GUI ID.
|
||||
object_name(str, optional): The object name.
|
||||
parent_dock(BECDock, optional): The parent dock.# TODO should go away -> issue created #473
|
||||
root_widget(bool, optional): If set to True, the parent_id will be always set to None, thus enforcing that the widget is accessible as a root widget of the BECGuiClient object.
|
||||
**kwargs:
|
||||
"""
|
||||
# Extract object_name from kwargs to not pass it to Qt class
|
||||
object_name = object_name or kwargs.pop("objectName", None)
|
||||
# Ensure the parent is always the first argument for QObject
|
||||
@@ -111,9 +99,6 @@ class BECConnector:
|
||||
self, QObject
|
||||
), "BECConnector must be used with a QObject or any qt related class."
|
||||
|
||||
# flag to check if the object was destroyed and its cleanup was called
|
||||
self._destroyed = False
|
||||
|
||||
# BEC related connections
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
@@ -123,7 +108,7 @@ class BECConnector:
|
||||
if not self.client in BECConnector.EXIT_HANDLERS:
|
||||
# register function to clean connections at exit;
|
||||
# the function depends on BECClient, and BECDispatcher
|
||||
@SafeSlot()
|
||||
@pyqtSlot()
|
||||
def terminate(client=self.client, dispatcher=self.bec_dispatcher):
|
||||
logger.info("Disconnecting", repr(dispatcher))
|
||||
dispatcher.disconnect_all()
|
||||
@@ -143,6 +128,7 @@ class BECConnector:
|
||||
)
|
||||
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
|
||||
self.parent_id = parent_id
|
||||
# If the gui_id is passed, it should be respected. However, this should be revisted since
|
||||
# the gui_id has to be unique, and may no longer be.
|
||||
if gui_id:
|
||||
@@ -161,8 +147,10 @@ class BECConnector:
|
||||
|
||||
# 2) Enforce unique objectName among siblings with the same BECConnector parent
|
||||
self.setParent(parent)
|
||||
if isinstance(self.parent(), QObject) and hasattr(self, "cleanup"):
|
||||
self.parent().destroyed.connect(self._run_cleanup_on_deleted_parent)
|
||||
if parent_id is None:
|
||||
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||
if connector_parent is not None:
|
||||
self.parent_id = connector_parent.gui_id
|
||||
|
||||
# Error popups
|
||||
self.error_utility = ErrorPopupUtility()
|
||||
@@ -171,49 +159,6 @@ class BECConnector:
|
||||
# Store references to running workers so they're not garbage collected prematurely.
|
||||
self._workers = []
|
||||
|
||||
# If set to True, the parent_id will be always set to None, thus enforcing that the widget is accessible as a root widget of the BECGuiClient object.
|
||||
self.root_widget = root_widget
|
||||
|
||||
QTimer.singleShot(0, self._update_object_name)
|
||||
|
||||
@property
|
||||
def parent_id(self) -> str | None:
|
||||
try:
|
||||
if self.root_widget:
|
||||
return None
|
||||
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||
return connector_parent.gui_id if connector_parent else None
|
||||
except:
|
||||
logger.error(f"Error getting parent_id for {self.__class__.__name__}")
|
||||
|
||||
@SafeSlot()
|
||||
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:
|
||||
"""
|
||||
Change the object name of the widget. Unregister old name and register the new one.
|
||||
|
||||
Args:
|
||||
name (str): The new object name.
|
||||
"""
|
||||
self.rpc_register.remove_rpc(self)
|
||||
self.setObjectName(name.replace("-", "_").replace(" ", "_"))
|
||||
QTimer.singleShot(0, self._update_object_name)
|
||||
|
||||
def _update_object_name(self) -> None:
|
||||
@@ -283,7 +228,7 @@ class BECConnector:
|
||||
if self.rpc_register.object_is_registered(self):
|
||||
self.rpc_register.broadcast()
|
||||
|
||||
def submit_task(self, fn, *args, on_complete: SafeSlot = None, **kwargs) -> Worker:
|
||||
def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker:
|
||||
"""
|
||||
Submit a task to run in a separate thread. The task will run the specified
|
||||
function with the provided arguments and emit the completed signal when done.
|
||||
@@ -411,7 +356,7 @@ class BECConnector:
|
||||
file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
|
||||
save_yaml(file_path, self._config_dict)
|
||||
|
||||
# @SafeSlot(str)
|
||||
# @pyqtSlot(str)
|
||||
def _set_gui_id(self, gui_id: str) -> None:
|
||||
"""
|
||||
Set the GUI ID for the widget.
|
||||
@@ -443,7 +388,7 @@ class BECConnector:
|
||||
self.client = client
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@SafeSlot(ConnectionConfig) # TODO can be also dict
|
||||
@pyqtSlot(ConnectionConfig) # TODO can be also dict
|
||||
def on_config_update(self, config: ConnectionConfig | dict) -> None:
|
||||
"""
|
||||
Update the configuration for the widget.
|
||||
|
||||
@@ -16,9 +16,9 @@ if PYSIDE6:
|
||||
from PySide6.scripts.pyside_tool import (
|
||||
_extend_path_var,
|
||||
init_virtual_env,
|
||||
qt_tool_wrapper,
|
||||
is_pyenv_python,
|
||||
is_virtual_env,
|
||||
qt_tool_wrapper,
|
||||
ui_tool_binary,
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ def list_editable_packages() -> set[str]:
|
||||
return editable_packages
|
||||
|
||||
|
||||
def patch_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||
def patch_designer(): # pragma: no cover
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
@@ -119,7 +119,7 @@ def patch_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||
editable_packages = list_editable_packages()
|
||||
for pckg in editable_packages:
|
||||
_extend_path_var("PYTHONPATH", pckg, True)
|
||||
qt_tool_wrapper(ui_tool_binary("designer"), cmd_args)
|
||||
qt_tool_wrapper(ui_tool_binary("designer"), sys.argv[1:])
|
||||
|
||||
|
||||
def find_plugin_paths(base_path: Path):
|
||||
@@ -147,7 +147,7 @@ def set_plugin_environment_variable(plugin_paths):
|
||||
|
||||
|
||||
# Patch the designer function
|
||||
def open_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||
def main(): # pragma: no cover
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Exiting...")
|
||||
return
|
||||
@@ -160,11 +160,7 @@ def open_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||
|
||||
set_plugin_environment_variable(plugin_paths)
|
||||
|
||||
patch_designer(cmd_args)
|
||||
|
||||
|
||||
def main():
|
||||
open_designer(sys.argv[1:])
|
||||
patch_designer()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
@@ -4,9 +4,8 @@ import collections
|
||||
import random
|
||||
import string
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, DefaultDict, Hashable, Union
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import louie
|
||||
import redis
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.logger import bec_logger
|
||||
@@ -15,52 +14,30 @@ from bec_lib.service_config import ServiceConfig
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
from bec_widgets.utils.serialization import register_serializer_extension
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.endpoints import EndpointInfo
|
||||
|
||||
from bec_widgets.utils.rpc_server import RPCServer
|
||||
|
||||
|
||||
class QtThreadSafeCallback(QObject):
|
||||
"""QtThreadSafeCallback is a wrapper around a callback function to make it thread-safe for Qt."""
|
||||
|
||||
cb_signal = pyqtSignal(dict, dict)
|
||||
|
||||
def __init__(self, cb: Callable, cb_info: dict | None = None):
|
||||
"""
|
||||
Initialize the QtThreadSafeCallback.
|
||||
|
||||
Args:
|
||||
cb (Callable): The callback function to be wrapped.
|
||||
cb_info (dict, optional): Additional information about the callback. Defaults to None.
|
||||
"""
|
||||
def __init__(self, cb):
|
||||
super().__init__()
|
||||
self.cb_info = cb_info
|
||||
|
||||
self.cb = cb
|
||||
self.cb_ref = louie.saferef.safe_ref(cb)
|
||||
self.cb_signal.connect(self.cb)
|
||||
self.topics = set()
|
||||
|
||||
def __hash__(self):
|
||||
# make 2 differents QtThreadSafeCallback to look
|
||||
# identical when used as dictionary keys, if the
|
||||
# callback is the same
|
||||
return f"{id(self.cb_ref)}{self.cb_info}".__hash__()
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, QtThreadSafeCallback):
|
||||
return False
|
||||
return self.cb_ref == other.cb_ref and self.cb_info == other.cb_info
|
||||
return id(self.cb)
|
||||
|
||||
def __call__(self, msg_content, metadata):
|
||||
if self.cb_ref() is None:
|
||||
# callback has been deleted
|
||||
return
|
||||
self.cb_signal.emit(msg_content, metadata)
|
||||
|
||||
|
||||
@@ -107,7 +84,7 @@ class BECDispatcher:
|
||||
cls,
|
||||
client=None,
|
||||
config: str | ServiceConfig | None = None,
|
||||
gui_id: str | None = None,
|
||||
gui_id: str = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -120,9 +97,7 @@ class BECDispatcher:
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._registered_slots: DefaultDict[Hashable, QtThreadSafeCallback] = (
|
||||
collections.defaultdict()
|
||||
)
|
||||
self._slots = collections.defaultdict(set)
|
||||
self.client = client
|
||||
|
||||
if self.client is None:
|
||||
@@ -145,8 +120,6 @@ class BECDispatcher:
|
||||
except redis.exceptions.ConnectionError:
|
||||
logger.warning("Could not connect to Redis, skipping start of BECClient.")
|
||||
|
||||
register_serializer_extension()
|
||||
|
||||
logger.success("Initialized BECDispatcher")
|
||||
|
||||
self.start_cli_server(gui_id=gui_id)
|
||||
@@ -164,7 +137,6 @@ class BECDispatcher:
|
||||
self,
|
||||
slot: Callable,
|
||||
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
|
||||
cb_info: dict | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Connect widget's qt slot, so that it is called on new pub/sub topic message.
|
||||
@@ -173,15 +145,11 @@ class BECDispatcher:
|
||||
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
||||
the corresponding pub/sub message
|
||||
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
|
||||
cb_info (dict | None): A dictionary containing information about the callback. Defaults to None.
|
||||
"""
|
||||
qt_slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info)
|
||||
if qt_slot not in self._registered_slots:
|
||||
self._registered_slots[qt_slot] = qt_slot
|
||||
qt_slot = self._registered_slots[qt_slot]
|
||||
self.client.connector.register(topics, cb=qt_slot, **kwargs)
|
||||
slot = QtThreadSafeCallback(slot)
|
||||
self.client.connector.register(topics, cb=slot, **kwargs)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
qt_slot.topics.update(set(topics_str))
|
||||
self._slots[slot].update(set(topics_str))
|
||||
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
||||
"""
|
||||
@@ -194,16 +162,16 @@ class BECDispatcher:
|
||||
# find the right slot to disconnect from ;
|
||||
# slot callbacks are wrapped in QtThreadSafeCallback objects,
|
||||
# but the slot we receive here is the original callable
|
||||
for connected_slot in self._registered_slots.values():
|
||||
for connected_slot in self._slots:
|
||||
if connected_slot.cb == slot:
|
||||
break
|
||||
else:
|
||||
return
|
||||
self.client.connector.unregister(topics, cb=connected_slot)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._registered_slots[connected_slot].topics.difference_update(set(topics_str))
|
||||
if not self._registered_slots[connected_slot].topics:
|
||||
del self._registered_slots[connected_slot]
|
||||
self._slots[connected_slot].difference_update(set(topics_str))
|
||||
if not self._slots[connected_slot]:
|
||||
del self._slots[connected_slot]
|
||||
|
||||
def disconnect_topics(self, topics: Union[str, list]):
|
||||
"""
|
||||
@@ -214,16 +182,11 @@ class BECDispatcher:
|
||||
"""
|
||||
self.client.connector.unregister(topics)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
|
||||
remove_slots = []
|
||||
for connected_slot in self._registered_slots.values():
|
||||
connected_slot.topics.difference_update(set(topics_str))
|
||||
|
||||
if not connected_slot.topics:
|
||||
remove_slots.append(connected_slot)
|
||||
|
||||
for connected_slot in remove_slots:
|
||||
self._registered_slots.pop(connected_slot, None)
|
||||
for slot in list(self._slots.keys()):
|
||||
slot_topics = self._slots[slot]
|
||||
slot_topics.difference_update(set(topics_str))
|
||||
if not slot_topics:
|
||||
del self._slots[slot]
|
||||
|
||||
def disconnect_all(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -21,7 +21,7 @@ def _submodule_specs(module: ModuleType) -> tuple[ModuleSpec | None, ...]:
|
||||
|
||||
|
||||
def _loaded_submodules_from_specs(
|
||||
submodule_specs: tuple[ModuleSpec | None, ...],
|
||||
submodule_specs: tuple[ModuleSpec | None, ...]
|
||||
) -> Generator[ModuleType, None, None]:
|
||||
"""Load all submodules from the given specs."""
|
||||
for submodule in (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
||||
""" This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
||||
Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is
|
||||
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
||||
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
||||
analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy
|
||||
will allow you to decide by yourself when to unblock and execute the callback again."""
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class BECWidget(BECConnector):
|
||||
gui_id: str | None = None,
|
||||
theme_update: bool = False,
|
||||
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
|
||||
parent_id: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -54,7 +55,12 @@ class BECWidget(BECConnector):
|
||||
"""
|
||||
|
||||
super().__init__(
|
||||
client=client, config=config, gui_id=gui_id, parent_dock=parent_dock, **kwargs
|
||||
client=client,
|
||||
config=config,
|
||||
gui_id=gui_id,
|
||||
parent_dock=parent_dock,
|
||||
parent_id=parent_id,
|
||||
**kwargs,
|
||||
)
|
||||
if not isinstance(self, QObject):
|
||||
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
||||
@@ -106,8 +112,6 @@ class BECWidget(BECConnector):
|
||||
def closeEvent(self, event):
|
||||
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
||||
try:
|
||||
if not self._destroyed:
|
||||
self.cleanup()
|
||||
self._destroyed = True
|
||||
self.cleanup()
|
||||
finally:
|
||||
super().closeEvent(event) # pylint: disable=no-member
|
||||
|
||||
@@ -11,7 +11,7 @@ from pydantic_core import PydanticCustomError
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
from bec_qthemes._main import AccentColors
|
||||
|
||||
|
||||
|
||||
@@ -266,5 +266,3 @@ class CompactPopupWidget(QWidget):
|
||||
# to ensure proper resources cleanup
|
||||
for child in self.container.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
|
||||
child.close()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Type
|
||||
import itertools
|
||||
from typing import Literal, Type
|
||||
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.cli.client_utils import BECGuiClient
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
|
||||
|
||||
class WidgetContainerUtils:
|
||||
@@ -72,36 +73,3 @@ class WidgetContainerUtils:
|
||||
return None
|
||||
else:
|
||||
raise ValueError(f"No widget of class {widget_class} found.")
|
||||
|
||||
@staticmethod
|
||||
def name_is_protected(name: str, container: Any = None) -> bool:
|
||||
"""
|
||||
Check if the name is not protected.
|
||||
|
||||
Args:
|
||||
name(str): The name to be checked.
|
||||
|
||||
Returns:
|
||||
bool: True if the name is not protected, False otherwise.
|
||||
"""
|
||||
if container is None:
|
||||
container = BECGuiClient
|
||||
gui_client_methods = set(filter(lambda x: not x.startswith("_"), dir(container)))
|
||||
return name in gui_client_methods
|
||||
|
||||
@staticmethod
|
||||
def raise_for_invalid_name(name: str, container: Any = None) -> None:
|
||||
"""
|
||||
Check if the name is valid. If not, raise a ValueError.
|
||||
|
||||
Args:
|
||||
name(str): The name to be checked.
|
||||
Raises:
|
||||
ValueError: If the name is not valid.
|
||||
"""
|
||||
if not WidgetContainerUtils.has_name_valid_chars(name):
|
||||
raise ValueError(
|
||||
f"Name '{name}' contains invalid characters. Only alphanumeric characters, underscores, and dashes are allowed."
|
||||
)
|
||||
if WidgetContainerUtils.name_is_protected(name, container):
|
||||
raise ValueError(f"Name '{name}' is protected. Please choose another name.")
|
||||
|
||||
@@ -96,55 +96,23 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
||||
|
||||
'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
|
||||
otherwise error display is left to the original exception hook
|
||||
'verify_sender' keyword argument can be passed with boolean value if the sender should be verified
|
||||
before executing the slot. If True, the slot will only execute if the sender is a QObject. This is
|
||||
useful to prevent function calls from already deleted objects.
|
||||
'raise_error' keyword argument can be passed with boolean value if the error should be raised
|
||||
after the error is displayed. This is useful to propagate the error to the caller but should be used
|
||||
with great care to avoid segfaults.
|
||||
|
||||
The keywords above are stored in a container which can be overridden by passing
|
||||
'_override_slot_params' keyword argument with a dictionary containing the keywords to override.
|
||||
This is useful to override the default behavior of the decorator for a specific function call.
|
||||
|
||||
"""
|
||||
_slot_params = {
|
||||
"popup_error": bool(slot_kwargs.pop("popup_error", False)),
|
||||
"verify_sender": bool(slot_kwargs.pop("verify_sender", False)),
|
||||
"raise_error": bool(slot_kwargs.pop("raise_error", False)),
|
||||
}
|
||||
popup_error = bool(slot_kwargs.pop("popup_error", False))
|
||||
|
||||
def error_managed(method):
|
||||
@Slot(*slot_args, **slot_kwargs)
|
||||
@functools.wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
_override_slot_params = kwargs.pop("_override_slot_params", {})
|
||||
_slot_params.update(_override_slot_params)
|
||||
try:
|
||||
if not _slot_params["verify_sender"] or len(args) == 0:
|
||||
return method(*args, **kwargs)
|
||||
|
||||
_instance = args[0]
|
||||
if not isinstance(_instance, QObject):
|
||||
return method(*args, **kwargs)
|
||||
sender = _instance.sender()
|
||||
if sender is None:
|
||||
logger.info(
|
||||
f"Sender is None for {method.__module__}.{method.__qualname__}, "
|
||||
"skipping method call."
|
||||
)
|
||||
return
|
||||
return method(*args, **kwargs)
|
||||
|
||||
except Exception:
|
||||
slot_name = f"{method.__module__}.{method.__qualname__}"
|
||||
error_msg = traceback.format_exc()
|
||||
if _slot_params["popup_error"]:
|
||||
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=True)
|
||||
if popup_error:
|
||||
ErrorPopupUtility().custom_exception_hook(
|
||||
*sys.exc_info(), popup_error=popup_error
|
||||
)
|
||||
logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
|
||||
if _slot_params["raise_error"]:
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class ExpandableGroupFrame(QFrame):
|
||||
self._title_layout.addWidget(self._expansion_button)
|
||||
self._title_layout.addWidget(self._title)
|
||||
|
||||
self._contents = QWidget(self)
|
||||
self._contents = QWidget()
|
||||
self._layout.addWidget(self._contents)
|
||||
|
||||
self._expansion_button.clicked.connect(self.switch_expanded_state)
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from types import NoneType
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from qtpy.QtCore import Signal # type: ignore
|
||||
from qtpy.QtWidgets import QGridLayout, QLabel, QLayout, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils.forms_from_types.items import FormItemSpec, widget_from_type
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class TypedForm(BECWidget, QWidget):
|
||||
PLUGIN = True
|
||||
ICON_NAME = "list_alt"
|
||||
|
||||
value_changed = Signal()
|
||||
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
items: list[tuple[str, type]] | None = None,
|
||||
form_item_specs: list[FormItemSpec] | None = None,
|
||||
client=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Widget with a list of form items based on a list of types.
|
||||
|
||||
Args:
|
||||
items (list[tuple[str, type]]): list of tuples of a name for the field and its type.
|
||||
Should be a type supported by the logic in items.py
|
||||
form_item_specs (list[FormItemSpec]): list of form item specs, equivalent to items.
|
||||
only one of items or form_item_specs should be
|
||||
supplied.
|
||||
|
||||
"""
|
||||
if (items is not None and form_item_specs is not None) or (
|
||||
items is None and form_item_specs is None
|
||||
):
|
||||
raise ValueError("Must specify one and only one of items and form_item_specs")
|
||||
super().__init__(parent=parent, client=client, **kwargs)
|
||||
self._items = (
|
||||
form_item_specs
|
||||
if form_item_specs is not None
|
||||
else [
|
||||
FormItemSpec(name=name, item_type=item_type)
|
||||
for name, item_type in items # type: ignore
|
||||
]
|
||||
)
|
||||
self._layout = QVBoxLayout()
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self._layout)
|
||||
|
||||
self._form_grid_container = QWidget(parent=self)
|
||||
self._form_grid = QWidget(parent=self._form_grid_container)
|
||||
self._layout.addWidget(self._form_grid_container)
|
||||
self._form_grid_container.setLayout(QVBoxLayout())
|
||||
self._form_grid.setLayout(self._new_grid_layout())
|
||||
|
||||
self.populate()
|
||||
|
||||
def populate(self):
|
||||
self._clear_grid()
|
||||
for r, item in enumerate(self._items):
|
||||
self._add_griditem(item, r)
|
||||
|
||||
def _add_griditem(self, item: FormItemSpec, row: int):
|
||||
grid = self._form_grid.layout()
|
||||
label = QLabel(item.name)
|
||||
label.setProperty("_model_field_name", item.name)
|
||||
label.setToolTip(item.info.description or item.name)
|
||||
grid.addWidget(label, row, 0)
|
||||
widget = widget_from_type(item.item_type)(parent=self, spec=item)
|
||||
widget.valueChanged.connect(self.value_changed)
|
||||
grid.addWidget(widget, row, 1)
|
||||
|
||||
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
|
||||
grid: QGridLayout = self._form_grid.layout() # type: ignore
|
||||
return {
|
||||
grid.itemAtPosition(i, 0)
|
||||
.widget()
|
||||
.property("_model_field_name"): grid.itemAtPosition(i, 1)
|
||||
.widget()
|
||||
.getValue() # type: ignore # we only add 'DynamicFormItem's here
|
||||
for i in range(grid.rowCount())
|
||||
}
|
||||
|
||||
def _clear_grid(self):
|
||||
if (old_layout := self._form_grid.layout()) is not None:
|
||||
while old_layout.count():
|
||||
item = old_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
widget.deleteLater()
|
||||
old_layout.deleteLater()
|
||||
self._form_grid.deleteLater()
|
||||
self._form_grid = QWidget()
|
||||
|
||||
self._form_grid.setLayout(self._new_grid_layout())
|
||||
self._form_grid_container.layout().addWidget(self._form_grid)
|
||||
|
||||
self._form_grid.adjustSize()
|
||||
self._form_grid_container.adjustSize()
|
||||
self.adjustSize()
|
||||
|
||||
def _new_grid_layout(self):
|
||||
new_grid = QGridLayout()
|
||||
new_grid.setContentsMargins(0, 0, 0, 0)
|
||||
new_grid.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
||||
return new_grid
|
||||
|
||||
|
||||
class PydanticModelForm(TypedForm):
|
||||
metadata_updated = Signal(dict)
|
||||
metadata_cleared = Signal(NoneType)
|
||||
|
||||
def __init__(self, parent=None, metadata_model: type[BaseModel] = None, client=None, **kwargs):
|
||||
"""
|
||||
A form generated from a pydantic model.
|
||||
|
||||
Args:
|
||||
metadata_model (type[BaseModel]): the model class for which to generate a form.
|
||||
"""
|
||||
self._md_schema = metadata_model
|
||||
super().__init__(parent=parent, form_item_specs=self._form_item_specs(), client=client)
|
||||
|
||||
self._validity = CompactPopupWidget()
|
||||
self._validity.compact_view = True # type: ignore
|
||||
self._validity.label = "Metadata validity" # type: ignore
|
||||
self._validity.compact_show_popup.setIcon(
|
||||
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
|
||||
)
|
||||
self._validity_message = QLabel("Not yet validated")
|
||||
self._validity.addWidget(self._validity_message)
|
||||
self._layout.addWidget(self._validity)
|
||||
self.value_changed.connect(self.validate_form)
|
||||
|
||||
def set_schema(self, schema: type[BaseModel]):
|
||||
self._md_schema = schema
|
||||
self.populate()
|
||||
|
||||
def _form_item_specs(self):
|
||||
return [
|
||||
FormItemSpec(name=name, info=info, item_type=info.annotation)
|
||||
for name, info in self._md_schema.model_fields.items()
|
||||
]
|
||||
|
||||
def update_items_from_schema(self):
|
||||
self._items = self._form_item_specs()
|
||||
|
||||
def populate(self):
|
||||
self.update_items_from_schema()
|
||||
super().populate()
|
||||
|
||||
def get_form_data(self):
|
||||
"""Get the entered metadata as a dict."""
|
||||
return self._dict_from_grid()
|
||||
|
||||
def validate_form(self, *_) -> bool:
|
||||
"""validate the currently entered metadata against the pydantic schema.
|
||||
If successful, returns on metadata_emitted and returns true.
|
||||
Otherwise, emits on metadata_cleared and returns false."""
|
||||
try:
|
||||
metadata_dict = self.get_form_data()
|
||||
self._md_schema.model_validate(metadata_dict)
|
||||
self._validity.set_global_state("success")
|
||||
self._validity_message.setText("No errors!")
|
||||
self.metadata_updated.emit(metadata_dict)
|
||||
return True
|
||||
except ValidationError as e:
|
||||
self._validity.set_global_state("emergency")
|
||||
self._validity_message.setText(str(e))
|
||||
self.metadata_cleared.emit(None)
|
||||
return False
|
||||
@@ -5,8 +5,6 @@ from typing import NamedTuple
|
||||
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||
|
||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
|
||||
|
||||
|
||||
@@ -24,7 +22,7 @@ class DesignerPluginInfo:
|
||||
def __init__(self, plugin_class):
|
||||
self.plugin_class = plugin_class
|
||||
self.plugin_name_pascal = plugin_class.__name__
|
||||
self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal)
|
||||
self.plugin_name_snake = self.pascal_to_snake(self.plugin_name_pascal)
|
||||
self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}"
|
||||
plugin_module = (
|
||||
".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin"
|
||||
@@ -40,6 +38,21 @@ class DesignerPluginInfo:
|
||||
|
||||
self.base_path = os.path.dirname(inspect.getfile(plugin_class))
|
||||
|
||||
@staticmethod
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""
|
||||
Convert PascalCase to snake_case.
|
||||
|
||||
Args:
|
||||
name (str): The name to be converted.
|
||||
|
||||
Returns:
|
||||
str: The converted name.
|
||||
"""
|
||||
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
||||
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
|
||||
return s2.lower()
|
||||
|
||||
|
||||
class DesignerPluginGenerator:
|
||||
def __init__(self, widget: type):
|
||||
@@ -109,16 +122,6 @@ class DesignerPluginGenerator:
|
||||
or bool(init_source.find("super().__init__(parent)") > 0)
|
||||
)
|
||||
|
||||
# for the new style classes, we only have one super call. We can therefore check if the
|
||||
# number of __init__ calls is 2 (the class itself and the super class)
|
||||
num_inits = re.findall(r"__init__", init_source)
|
||||
if len(num_inits) == 2 and not super_init_found:
|
||||
super_init_found = bool(
|
||||
init_source.find("super().__init__(parent=parent") > 0
|
||||
or init_source.find("super().__init__(parent,") > 0
|
||||
or init_source.find("super().__init__(parent)") > 0
|
||||
)
|
||||
|
||||
if not cls_init_found and not super_init_found:
|
||||
raise ValueError(
|
||||
f"Widget class {self.widget.__name__} must call the super constructor with parent."
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
||||
The class is mainly designed for usage with the BECWaveform and 1D plots."""
|
||||
|
||||
from __future__ import annotations
|
||||
""" Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
||||
The class is mainly designed for usage with the BECWaveform and 1D plots. """
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import re
|
||||
|
||||
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""
|
||||
Convert PascalCase to snake_case.
|
||||
|
||||
Args:
|
||||
name (str): The name to be converted.
|
||||
|
||||
Returns:
|
||||
str: The converted name.
|
||||
"""
|
||||
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
||||
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
|
||||
return s2.lower()
|
||||
@@ -18,9 +18,8 @@ from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib import messages
|
||||
from qtpy.QtCore import QObject
|
||||
else:
|
||||
@@ -84,7 +83,6 @@ class RPCServer:
|
||||
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
||||
self._heartbeat_timer.start(200)
|
||||
self._registry_update_callbacks = []
|
||||
self._broadcasted_data = {}
|
||||
|
||||
self.status = messages.BECStatus.RUNNING
|
||||
logger.success(f"Server started with gui_id: {self.gui_id}")
|
||||
@@ -191,9 +189,6 @@ class RPCServer:
|
||||
if not getattr(val, "RPC", True):
|
||||
continue
|
||||
data[key] = self._serialize_bec_connector(val)
|
||||
if self._broadcasted_data == data:
|
||||
return
|
||||
self._broadcasted_data = data
|
||||
|
||||
logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
|
||||
self.client.connector.xadd(
|
||||
@@ -217,15 +212,6 @@ class RPCServer:
|
||||
config_dict = connector.config.model_dump()
|
||||
config_dict["parent_id"] = getattr(connector, "parent_id", None)
|
||||
|
||||
try:
|
||||
parent = connector.parent()
|
||||
if isinstance(parent, BECMainWindow):
|
||||
container_proxy = parent.gui_id
|
||||
else:
|
||||
container_proxy = None
|
||||
except Exception:
|
||||
container_proxy = None
|
||||
|
||||
if wait:
|
||||
while not self.rpc_register.object_is_registered(connector):
|
||||
QApplication.processEvents()
|
||||
@@ -239,7 +225,6 @@ class RPCServer:
|
||||
"object_name": connector.object_name or connector.__class__.__name__,
|
||||
"widget_class": widget_class,
|
||||
"config": config_dict,
|
||||
"container_proxy": container_proxy,
|
||||
"__rpc__": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
from bec_lib.serialization import msgpack
|
||||
from qtpy.QtCore import QPointF
|
||||
|
||||
|
||||
def register_serializer_extension():
|
||||
"""
|
||||
Register the serializer extension for the BECConnector.
|
||||
"""
|
||||
if not module_is_registered("bec_widgets.utils.serialization"):
|
||||
msgpack.register_object_hook(encode_qpointf, decode_qpointf)
|
||||
|
||||
|
||||
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
|
||||
@@ -31,7 +31,6 @@ class SidePanel(QWidget):
|
||||
panel_max_width: int = 200,
|
||||
animation_duration: int = 200,
|
||||
animations_enabled: bool = True,
|
||||
show_toolbar: bool = True,
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
@@ -41,7 +40,6 @@ class SidePanel(QWidget):
|
||||
self._panel_max_width = panel_max_width
|
||||
self._animation_duration = animation_duration
|
||||
self._animations_enabled = animations_enabled
|
||||
self._show_toolbar = show_toolbar
|
||||
|
||||
self._panel_width = 0
|
||||
self._panel_height = 0
|
||||
@@ -61,7 +59,7 @@ class SidePanel(QWidget):
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="vertical")
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="vertical")
|
||||
|
||||
self.container = QWidget()
|
||||
self.container.layout = QVBoxLayout(self.container)
|
||||
@@ -73,14 +71,13 @@ class SidePanel(QWidget):
|
||||
self.stack_widget.setMinimumWidth(5)
|
||||
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
||||
|
||||
if self._orientation in ("left", "right"):
|
||||
if self._show_toolbar:
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
if self._orientation == "left":
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
self.main_layout.addWidget(self.container)
|
||||
else:
|
||||
self.main_layout.addWidget(self.container)
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
|
||||
if self._orientation == "left":
|
||||
self.main_layout.addWidget(self.container)
|
||||
else:
|
||||
self.main_layout.insertWidget(0, self.container)
|
||||
self.container.layout.addWidget(self.stack_widget)
|
||||
|
||||
self.menu_anim = QPropertyAnimation(self, b"panel_width")
|
||||
@@ -92,7 +89,7 @@ class SidePanel(QWidget):
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||
|
||||
self.container = QWidget()
|
||||
self.container.layout = QVBoxLayout(self.container)
|
||||
@@ -105,13 +102,11 @@ class SidePanel(QWidget):
|
||||
self.stack_widget.setMaximumHeight(self._panel_max_width)
|
||||
|
||||
if self._orientation == "top":
|
||||
if self._show_toolbar:
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
self.main_layout.addWidget(self.container)
|
||||
else:
|
||||
self.main_layout.addWidget(self.container)
|
||||
if self._show_toolbar:
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
|
||||
self.container.layout.addWidget(self.stack_widget)
|
||||
|
||||
@@ -238,24 +233,21 @@ class SidePanel(QWidget):
|
||||
|
||||
def add_menu(
|
||||
self,
|
||||
action_id: str,
|
||||
icon_name: str,
|
||||
tooltip: str,
|
||||
widget: QWidget,
|
||||
action_id: str | None = None,
|
||||
icon_name: str | None = None,
|
||||
tooltip: str | None = None,
|
||||
title: str | None = None,
|
||||
) -> int:
|
||||
):
|
||||
"""
|
||||
Add a menu to the side panel.
|
||||
|
||||
Args:
|
||||
action_id(str): The ID of the action.
|
||||
icon_name(str): The name of the icon.
|
||||
tooltip(str): The tooltip for the action.
|
||||
widget(QWidget): The widget to add to the panel.
|
||||
action_id(str | None): The ID of the action. Optional if no toolbar action is needed.
|
||||
icon_name(str | None): The name of the icon. Optional if no toolbar action is needed.
|
||||
tooltip(str | None): The tooltip for the action. Optional if no toolbar action is needed.
|
||||
title(str | None): The title of the panel.
|
||||
|
||||
Returns:
|
||||
int: The index of the added panel, which can be used with show_panel() and switch_to().
|
||||
title(str): The title of the panel.
|
||||
"""
|
||||
# container_widget: top-level container for the stacked page
|
||||
container_widget = QWidget()
|
||||
@@ -286,35 +278,32 @@ class SidePanel(QWidget):
|
||||
index = self.stack_widget.count()
|
||||
self.stack_widget.addWidget(container_widget)
|
||||
|
||||
# Add an action to the toolbar if action_id, icon_name, and tooltip are provided
|
||||
if action_id is not None and icon_name is not None and tooltip is not None:
|
||||
action = MaterialIconAction(icon_name=icon_name, tooltip=tooltip, checkable=True)
|
||||
self.toolbar.add_action(action_id, action, target_widget=self)
|
||||
# Add an action to the toolbar
|
||||
action = MaterialIconAction(icon_name=icon_name, tooltip=tooltip, checkable=True)
|
||||
self.toolbar.add_action(action_id, action, target_widget=self)
|
||||
|
||||
def on_action_toggled(checked: bool):
|
||||
if self.switching_actions:
|
||||
return
|
||||
def on_action_toggled(checked: bool):
|
||||
if self.switching_actions:
|
||||
return
|
||||
|
||||
if checked:
|
||||
if self.current_action and self.current_action != action.action:
|
||||
self.switching_actions = True
|
||||
self.current_action.setChecked(False)
|
||||
self.switching_actions = False
|
||||
if checked:
|
||||
if self.current_action and self.current_action != action.action:
|
||||
self.switching_actions = True
|
||||
self.current_action.setChecked(False)
|
||||
self.switching_actions = False
|
||||
|
||||
self.current_action = action.action
|
||||
self.current_action = action.action
|
||||
|
||||
if not self.panel_visible:
|
||||
self.show_panel(index)
|
||||
else:
|
||||
self.switch_to(index)
|
||||
if not self.panel_visible:
|
||||
self.show_panel(index)
|
||||
else:
|
||||
if self.current_action == action.action:
|
||||
self.current_action = None
|
||||
self.hide_panel()
|
||||
self.switch_to(index)
|
||||
else:
|
||||
if self.current_action == action.action:
|
||||
self.current_action = None
|
||||
self.hide_panel()
|
||||
|
||||
action.action.toggled.connect(on_action_toggled)
|
||||
|
||||
return index
|
||||
action.action.toggled.connect(on_action_toggled)
|
||||
|
||||
|
||||
############################################
|
||||
@@ -343,56 +332,41 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
||||
self.add_side_menus()
|
||||
|
||||
def add_side_menus(self):
|
||||
# Example 1: With action, icon, and tooltip
|
||||
widget1 = QWidget()
|
||||
layout1 = QVBoxLayout(widget1)
|
||||
for i in range(15):
|
||||
layout1.addWidget(QLabel(f"Widget 1 label row {i}"))
|
||||
self.side_panel.add_menu(
|
||||
widget=widget1,
|
||||
action_id="widget1",
|
||||
icon_name="counter_1",
|
||||
tooltip="Show Widget 1",
|
||||
widget=widget1,
|
||||
title="Widget 1 Panel",
|
||||
)
|
||||
|
||||
# Example 2: With action, icon, and tooltip
|
||||
widget2 = QWidget()
|
||||
layout2 = QVBoxLayout(widget2)
|
||||
layout2.addWidget(QLabel("Short widget 2 content"))
|
||||
self.side_panel.add_menu(
|
||||
widget=widget2,
|
||||
action_id="widget2",
|
||||
icon_name="counter_2",
|
||||
tooltip="Show Widget 2",
|
||||
widget=widget2,
|
||||
title="Widget 2 Panel",
|
||||
)
|
||||
|
||||
# Example 3: With action, icon, and tooltip
|
||||
widget3 = QWidget()
|
||||
layout3 = QVBoxLayout(widget3)
|
||||
for i in range(10):
|
||||
layout3.addWidget(QLabel(f"Line {i} for Widget 3"))
|
||||
self.side_panel.add_menu(
|
||||
widget=widget3,
|
||||
action_id="widget3",
|
||||
icon_name="counter_3",
|
||||
tooltip="Show Widget 3",
|
||||
widget=widget3,
|
||||
title="Widget 3 Panel",
|
||||
)
|
||||
|
||||
# Example 4: Without action, icon, and tooltip (can only be shown programmatically)
|
||||
widget4 = QWidget()
|
||||
layout4 = QVBoxLayout(widget4)
|
||||
layout4.addWidget(QLabel("This panel has no toolbar button"))
|
||||
layout4.addWidget(QLabel("It can only be shown programmatically"))
|
||||
self.hidden_panel_index = self.side_panel.add_menu(widget=widget4, title="Hidden Panel")
|
||||
|
||||
# Example of how to show the hidden panel programmatically after 3 seconds
|
||||
from qtpy.QtCore import QTimer
|
||||
|
||||
QTimer.singleShot(3000, lambda: self.side_panel.show_panel(self.hidden_panel_index))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -118,7 +118,7 @@ class IconAction(ToolBarAction):
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
icon = QIcon()
|
||||
icon.addFile(self.icon_path, size=QSize(20, 20))
|
||||
self.action = QAction(icon=icon, text=self.tooltip, parent=target)
|
||||
self.action = QAction(icon, self.tooltip, target)
|
||||
self.action.setCheckable(self.checkable)
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
@@ -128,7 +128,7 @@ class QtIconAction(ToolBarAction):
|
||||
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||
self.standard_icon = standard_icon
|
||||
self.icon = QApplication.style().standardIcon(standard_icon)
|
||||
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
||||
self.action = QAction(self.icon, self.tooltip, parent)
|
||||
self.action.setCheckable(self.checkable)
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
@@ -173,7 +173,7 @@ class MaterialIconAction(ToolBarAction):
|
||||
filled=self.filled,
|
||||
color=self.color,
|
||||
)
|
||||
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
||||
self.action = QAction(self.icon, self.tooltip, parent=parent)
|
||||
self.action.setCheckable(self.checkable)
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
@@ -212,12 +212,12 @@ class DeviceSelectionAction(ToolBarAction):
|
||||
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
widget = QWidget(parent=target)
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
if self.label is not None:
|
||||
label = QLabel(text=f"{self.label}", parent=target)
|
||||
label = QLabel(f"{self.label}")
|
||||
layout.addWidget(label)
|
||||
if self.device_combobox is not None:
|
||||
layout.addWidget(self.device_combobox)
|
||||
@@ -280,9 +280,7 @@ class SwitchableToolBarAction(ToolBarAction):
|
||||
self.main_button.clicked.connect(self._trigger_current_action)
|
||||
menu = QMenu(self.main_button)
|
||||
for key, action_obj in self.actions.items():
|
||||
menu_action = QAction(
|
||||
icon=action_obj.get_icon(), text=action_obj.tooltip, parent=self.main_button
|
||||
)
|
||||
menu_action = QAction(action_obj.get_icon(), action_obj.tooltip, self.main_button)
|
||||
menu_action.setIconVisibleInMenu(True)
|
||||
menu_action.setCheckable(self.checkable)
|
||||
menu_action.setChecked(key == self.current_key)
|
||||
@@ -371,13 +369,13 @@ class WidgetAction(ToolBarAction):
|
||||
toolbar (QToolBar): The toolbar to add the widget to.
|
||||
target (QWidget): The target widget for the action.
|
||||
"""
|
||||
self.container = QWidget(parent=target)
|
||||
self.container = QWidget()
|
||||
layout = QHBoxLayout(self.container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
if self.label is not None:
|
||||
label_widget = QLabel(text=f"{self.label}", parent=target)
|
||||
label_widget = QLabel(f"{self.label}")
|
||||
label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
layout.addWidget(label_widget)
|
||||
@@ -439,7 +437,7 @@ class ExpandableMenuAction(ToolBarAction):
|
||||
)
|
||||
menu = QMenu(button)
|
||||
for action_id, action in self.actions.items():
|
||||
sub_action = QAction(text=action.tooltip, parent=target)
|
||||
sub_action = QAction(action.tooltip, target)
|
||||
sub_action.setIconVisibleInMenu(True)
|
||||
if action.icon_path:
|
||||
icon = QIcon()
|
||||
@@ -523,7 +521,7 @@ class ModularToolBar(QToolBar):
|
||||
orientation: Literal["horizontal", "vertical"] = "horizontal",
|
||||
background_color: str = "rgba(0, 0, 0, 0)",
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.widgets = defaultdict(dict)
|
||||
self.background_color = background_color
|
||||
|
||||
@@ -12,11 +12,10 @@ from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
||||
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
@@ -131,6 +130,7 @@ class BECDock(BECWidget, Dock):
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
parent_dock_area: BECDockArea | None = None,
|
||||
parent_id: str | None = None,
|
||||
config: DockConfig | None = None,
|
||||
name: str | None = None,
|
||||
object_name: str | None = None,
|
||||
@@ -279,7 +279,6 @@ class BECDock(BECWidget, Dock):
|
||||
widgets.extend(dock.elements.keys())
|
||||
return widgets
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def new(
|
||||
self,
|
||||
widget: BECWidget | str,
|
||||
@@ -302,9 +301,6 @@ class BECDock(BECWidget, Dock):
|
||||
colspan(int): The number of columns the widget should span.
|
||||
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
||||
"""
|
||||
if name is not None:
|
||||
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
|
||||
|
||||
if row is None:
|
||||
row = self.layout.rowCount()
|
||||
|
||||
@@ -320,7 +316,11 @@ class BECDock(BECWidget, Dock):
|
||||
widget = cast(
|
||||
BECWidget,
|
||||
widget_handler.create_widget(
|
||||
widget_type=widget, object_name=name, parent_dock=self, parent=self
|
||||
widget_type=widget,
|
||||
object_name=name,
|
||||
parent_dock=self,
|
||||
parent_id=self.gui_id,
|
||||
parent=self,
|
||||
),
|
||||
)
|
||||
else:
|
||||
@@ -416,7 +416,6 @@ class BECDock(BECWidget, Dock):
|
||||
self.delete_all()
|
||||
self.widgets.clear()
|
||||
super().cleanup()
|
||||
self.deleteLater()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,6 @@ from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||
from bec_widgets.utils.toolbar import (
|
||||
ExpandableMenuAction,
|
||||
MaterialIconAction,
|
||||
@@ -102,8 +101,8 @@ class BECDockArea(BECWidget, QWidget):
|
||||
|
||||
self._instructions_visible = True
|
||||
|
||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||
self.dock_area = DockArea(parent=self)
|
||||
self.dark_mode_button = DarkModeButton(parent=self, parent_id=self.gui_id, toolbar=True)
|
||||
self.dock_area = DockArea()
|
||||
self.toolbar = ModularToolBar(
|
||||
parent=self,
|
||||
actions={
|
||||
@@ -182,7 +181,7 @@ class BECDockArea(BECWidget, QWidget):
|
||||
|
||||
self.layout.addWidget(self.toolbar)
|
||||
self.layout.addWidget(self.dock_area)
|
||||
self.spacer = QWidget(parent=self)
|
||||
self.spacer = QWidget()
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar.addWidget(self.spacer)
|
||||
self.toolbar.addWidget(self.dark_mode_button)
|
||||
@@ -243,8 +242,7 @@ class BECDockArea(BECWidget, QWidget):
|
||||
def _create_widget_from_toolbar(self, widget_name: str) -> None:
|
||||
# Run with RPC broadcast to namespace of all widgets
|
||||
with RPCRegister.delayed_broadcast():
|
||||
name = pascal_to_snake(widget_name)
|
||||
dock_name = WidgetContainerUtils.generate_unique_name(name, self.panels.keys())
|
||||
dock_name = WidgetContainerUtils.generate_unique_name(widget_name, self.panels.keys())
|
||||
self.new(name=dock_name, widget=widget_name)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions
|
||||
@@ -308,8 +306,6 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"""
|
||||
if state is None:
|
||||
state = self.config.docks_state
|
||||
if state is None:
|
||||
return
|
||||
self.dock_area.restoreState(state, missing=missing, extra=extra)
|
||||
|
||||
@SafeSlot()
|
||||
@@ -366,8 +362,6 @@ class BECDockArea(BECWidget, QWidget):
|
||||
f"Name {name} must be unique for docks, but already exists in DockArea "
|
||||
f"with name: {self.object_name} and id {self.gui_id}."
|
||||
)
|
||||
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
|
||||
|
||||
else: # Name is not provided
|
||||
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
|
||||
|
||||
@@ -376,6 +370,7 @@ class BECDockArea(BECWidget, QWidget):
|
||||
name=name, # this is dock name pyqtgraph property, this is displayed on label
|
||||
object_name=name, # this is a real qt object name passed to BECConnector
|
||||
parent_dock_area=self,
|
||||
parent_id=self.gui_id,
|
||||
closable=closable,
|
||||
)
|
||||
dock.config.position = position
|
||||
@@ -445,8 +440,12 @@ class BECDockArea(BECWidget, QWidget):
|
||||
Cleanup the dock area.
|
||||
"""
|
||||
self.delete_all()
|
||||
self.toolbar.close()
|
||||
self.toolbar.deleteLater()
|
||||
self.dark_mode_button.close()
|
||||
self.dark_mode_button.deleteLater()
|
||||
self.dock_area.close()
|
||||
self.dock_area.deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
def show(self):
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
from typing import Dict, Literal, Optional, Set, Tuple, Union
|
||||
@@ -53,7 +51,7 @@ class LayoutManagerWidget(QWidget):
|
||||
self,
|
||||
widget: QWidget | str,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
col: Optional[int] = None,
|
||||
rowspan: int = 1,
|
||||
colspan: int = 1,
|
||||
shift_existing: bool = True,
|
||||
@@ -138,39 +136,6 @@ class LayoutManagerWidget(QWidget):
|
||||
ref_row, ref_col, ref_rowspan, ref_colspan = self.widget_positions[reference_widget]
|
||||
|
||||
# Determine new widget position based on the specified relative position
|
||||
|
||||
# If adding to the left or right with shifting, shift the entire column
|
||||
if (
|
||||
position in ("left", "right")
|
||||
and shift_existing
|
||||
and shift_direction in ("left", "right")
|
||||
):
|
||||
column = ref_col
|
||||
# Collect all rows in this column and sort for safe shifting
|
||||
rows = sorted(
|
||||
{row for (row, col) in self.position_widgets.keys() if col == column},
|
||||
reverse=(shift_direction == "right"),
|
||||
)
|
||||
# Shift each widget in the column
|
||||
for r in rows:
|
||||
self.shift_widgets(direction=shift_direction, start_row=r, start_col=column)
|
||||
# Update reference widget's position after the column shift
|
||||
ref_row, ref_col, ref_rowspan, ref_colspan = self.widget_positions[reference_widget]
|
||||
new_row = ref_row
|
||||
# Compute insertion column based on relative position
|
||||
if position == "left":
|
||||
new_col = ref_col - ref_colspan
|
||||
else:
|
||||
new_col = ref_col + ref_colspan
|
||||
# Add the new widget without triggering another shift
|
||||
return self.add_widget(
|
||||
widget=widget,
|
||||
row=new_row,
|
||||
col=new_col,
|
||||
rowspan=rowspan,
|
||||
colspan=colspan,
|
||||
shift_existing=False,
|
||||
)
|
||||
if position == "left":
|
||||
new_row = ref_row
|
||||
new_col = ref_col - 1
|
||||
|
||||
@@ -11,7 +11,7 @@ class AbortButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "cancel"
|
||||
RPC = True
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -11,7 +11,7 @@ class ResetButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "restart_alt"
|
||||
RPC = True
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
@@ -11,7 +11,7 @@ class ResumeButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "resume"
|
||||
RPC = True
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
@@ -11,7 +11,7 @@ class StopButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "dangerous"
|
||||
RPC = True
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
@@ -54,20 +54,9 @@ class StopButton(BECWidget, QWidget):
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setLayout(QVBoxLayout())
|
||||
# Create and add the StopButton to the layout
|
||||
self.stop_button = StopButton()
|
||||
self.layout().addWidget(self.stop_button)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
app = QApplication([])
|
||||
my_gui = MyGui()
|
||||
my_gui.show()
|
||||
app = QApplication(sys.argv)
|
||||
w = StopButton()
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -12,16 +12,7 @@ class PositionIndicator(BECWidget, QWidget):
|
||||
Display a position within a defined range, e.g. motor limits.
|
||||
"""
|
||||
|
||||
USER_ACCESS = [
|
||||
"set_value",
|
||||
"set_range",
|
||||
"vertical",
|
||||
"vertical.setter",
|
||||
"indicator_width",
|
||||
"indicator_width.setter",
|
||||
"rounded_corners",
|
||||
"rounded_corners.setter",
|
||||
]
|
||||
USER_ACCESS = ["set_value", "set_range", "vertical", "indicator_width", "rounded_corners"]
|
||||
PLUGIN = True
|
||||
ICON_NAME = "horizontal_distribute"
|
||||
|
||||
@@ -218,12 +209,6 @@ class PositionIndicator(BECWidget, QWidget):
|
||||
@Slot(int)
|
||||
@Slot(float)
|
||||
def set_value(self, position: float):
|
||||
"""
|
||||
Set the position of the indicator
|
||||
|
||||
Args:
|
||||
position: The new position of the indicator
|
||||
"""
|
||||
self.position = position
|
||||
self.update()
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ class PositionerBox(PositionerBoxBase):
|
||||
dimensions = (234, 224)
|
||||
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
|
||||
USER_ACCESS = ["set_positioner"]
|
||||
device_changed = Signal(str, str)
|
||||
|
||||
@@ -33,7 +33,6 @@ class PositionerBox2D(PositionerBoxBase):
|
||||
ui_file = "positioner_box_2d.ui"
|
||||
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
|
||||
|
||||
device_changed_hor = Signal(str, str)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Module for a PositionerGroup widget to control a positioner device."""
|
||||
""" Module for a PositionerGroup widget to control a positioner device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class DeviceInputBase(BECWidget):
|
||||
ReadoutPriority.ON_REQUEST: "readout_on_request",
|
||||
}
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id: str | None = None, **kwargs):
|
||||
def __init__(self, client=None, config=None, gui_id: str | None = None, **kwargs):
|
||||
|
||||
if config is None:
|
||||
config = DeviceInputConfig(widget_class=self.__class__.__name__)
|
||||
@@ -90,9 +90,7 @@ class DeviceInputBase(BECWidget):
|
||||
if isinstance(config, dict):
|
||||
config = DeviceInputConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(
|
||||
parent=parent, client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs
|
||||
)
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
self._device_filter = []
|
||||
self._readout_filter = []
|
||||
@@ -397,7 +395,7 @@ class DeviceInputBase(BECWidget):
|
||||
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
||||
"""
|
||||
self.validate_device(device)
|
||||
dev = getattr(self.dev, device, None)
|
||||
dev = getattr(self.dev, device.lower(), None)
|
||||
if dev is None:
|
||||
raise ValueError(
|
||||
f"Device {device} is not found in the device manager {self.dev} as enabled device."
|
||||
|
||||
@@ -250,7 +250,7 @@ class DeviceSignalInputBase(BECWidget):
|
||||
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
||||
"""
|
||||
self.validate_device(device)
|
||||
dev = getattr(self.dev, device, None)
|
||||
dev = getattr(self.dev, device.lower(), None)
|
||||
if dev is None:
|
||||
logger.warning(f"Device {device} not found in devicemanager.")
|
||||
return None
|
||||
|
||||
@@ -140,7 +140,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
"""
|
||||
if self.validate_device(input_text) is True:
|
||||
self._is_valid_input = True
|
||||
self.device_selected.emit(input_text)
|
||||
self.device_selected.emit(input_text.lower())
|
||||
else:
|
||||
self._is_valid_input = False
|
||||
self.update()
|
||||
|
||||
@@ -147,7 +147,7 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
|
||||
"""
|
||||
if self.validate_device(input_text) is True:
|
||||
self._is_valid_input = True
|
||||
self.device_selected.emit(input_text)
|
||||
self.device_selected.emit(input_text.lower())
|
||||
else:
|
||||
self._is_valid_input = False
|
||||
self.update()
|
||||
|
||||
@@ -64,6 +64,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
default_scan: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
if config is None:
|
||||
config = ScanControlConfig(
|
||||
widget_class=self.__class__.__name__, allowed_scans=allowed_scans
|
||||
@@ -165,7 +166,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.layout.addStretch()
|
||||
|
||||
def _add_metadata_form(self):
|
||||
self._metadata_form = ScanMetadata(parent=self)
|
||||
self._metadata_form = ScanMetadata()
|
||||
self.layout.addWidget(self._metadata_form)
|
||||
self._metadata_form.update_with_new_scan(self.comboBox_scan_selection.currentText())
|
||||
self.scan_selected.connect(self._metadata_form.update_with_new_scan)
|
||||
|
||||
@@ -234,7 +234,7 @@ class ScanGroupBox(QGroupBox):
|
||||
continue
|
||||
if default == "_empty":
|
||||
default = None
|
||||
widget = widget_class(parent=self.parent(), arg_name=arg_name, default=default)
|
||||
widget = widget_class(arg_name=arg_name, default=default)
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
widget.set_device_filter(BECDeviceFilter.DEVICE)
|
||||
self.selected_devices[widget] = ""
|
||||
@@ -274,24 +274,12 @@ class ScanGroupBox(QGroupBox):
|
||||
for widget in self.widgets[-len(self.inputs) :]:
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
self.selected_devices[widget] = ""
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
self.widgets = self.widgets[: -len(self.inputs)]
|
||||
|
||||
selected_devices_str = " ".join(self.selected_devices.values())
|
||||
self.device_selected.emit(selected_devices_str.strip())
|
||||
|
||||
def remove_all_widget_bundles(self):
|
||||
"""Remove every widget bundle from the scan control layout."""
|
||||
for widget in list(self.widgets):
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
self.selected_devices.pop(widget, None)
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
self.layout.removeWidget(widget)
|
||||
self.widgets.clear()
|
||||
self.device_selected.emit("")
|
||||
|
||||
@Property(bool)
|
||||
def hide_add_remove_buttons(self):
|
||||
return self._hide_add_remove_buttons
|
||||
@@ -360,21 +348,10 @@ class ScanGroupBox(QGroupBox):
|
||||
self._set_kwarg_parameters(parameters)
|
||||
|
||||
def _set_arg_parameters(self, parameters: list):
|
||||
self.remove_all_widget_bundles()
|
||||
if not parameters:
|
||||
return
|
||||
|
||||
inputs_per_bundle = len(self.inputs)
|
||||
if inputs_per_bundle == 0:
|
||||
return
|
||||
|
||||
bundles_needed = -(-len(parameters) // inputs_per_bundle)
|
||||
|
||||
for row in range(1, bundles_needed + 1):
|
||||
self.add_input_widgets(self.inputs, row)
|
||||
|
||||
for i, value in enumerate(parameters):
|
||||
WidgetIO.set_value(self.widgets[i], value)
|
||||
while len(parameters) != len(self.widgets):
|
||||
self.add_widget_bundle()
|
||||
for i, parameter in enumerate(parameters):
|
||||
WidgetIO.set_value(self.widgets[i], parameter)
|
||||
|
||||
def _set_kwarg_parameters(self, parameters: dict):
|
||||
for widget in self.widgets:
|
||||
|
||||
@@ -16,7 +16,7 @@ class DapComboBox(BECWidget, QWidget):
|
||||
Args:
|
||||
parent: Parent widget.
|
||||
client: BEC client object.
|
||||
gui_id: GUI ID.
|
||||
gui_id: GUI ID.--
|
||||
default: Default device name.
|
||||
"""
|
||||
|
||||
@@ -154,7 +154,9 @@ class DapComboBox(BECWidget, QWidget):
|
||||
def populate_fit_model_combobox(self):
|
||||
"""Populate the fit_model_combobox with the devices."""
|
||||
# pylint: disable=protected-access
|
||||
self.available_models = [model for model in self.client.dap._available_dap_plugins.keys()]
|
||||
self.available_models = [
|
||||
model for model in self.client.dap._available_dap_plugins.keys()
|
||||
]
|
||||
self.fit_model_combobox.clear()
|
||||
self.fit_model_combobox.addItems(self.available_models)
|
||||
|
||||
|
||||
193
bec_widgets/widgets/dap/dap_task_manager/drag_and_drop.py
Normal file
193
bec_widgets/widgets/dap/dap_task_manager/drag_and_drop.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import inspect
|
||||
|
||||
from bec_lib.signature_serializer import deserialize_dtype
|
||||
from bec_server.data_processing.dap_framework_refactoring.dap_blocks import (
|
||||
BlockWithLotsOfArgs,
|
||||
DAPBlock,
|
||||
GradientBlock,
|
||||
SmoothBlock,
|
||||
)
|
||||
from pydantic.fields import FieldInfo
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDoubleSpinBox,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QRadioButton,
|
||||
QScrollArea,
|
||||
)
|
||||
from qtpy.QtCore import QMimeData, Qt, Signal
|
||||
from qtpy.QtGui import QDrag, QPixmap
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QHBoxLayout,
|
||||
QMainWindow,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
||||
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import widget_from_type
|
||||
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
||||
from tests.unit_tests.test_scan_metadata import metadata_widget
|
||||
|
||||
|
||||
class DragItem(ExpandableGroupFrame):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setContentsMargins(25, 5, 25, 5)
|
||||
self._layout = QVBoxLayout()
|
||||
self.set_layout(self._layout)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
if e.buttons() == Qt.MouseButton.LeftButton:
|
||||
drag = QDrag(self)
|
||||
mime = QMimeData()
|
||||
drag.setMimeData(mime)
|
||||
|
||||
pixmap = QPixmap(self.size())
|
||||
self.render(pixmap)
|
||||
drag.setPixmap(pixmap)
|
||||
|
||||
drag.exec(Qt.DropAction.MoveAction)
|
||||
|
||||
|
||||
class DragWidget(QWidget):
|
||||
"""
|
||||
Generic list sorting handler.
|
||||
"""
|
||||
|
||||
orderChanged = Signal(list)
|
||||
|
||||
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
# Store the orientation for drag checks later.
|
||||
self.orientation = orientation
|
||||
|
||||
if self.orientation == Qt.Orientation.Vertical:
|
||||
self.blayout = QVBoxLayout()
|
||||
else:
|
||||
self.blayout = QHBoxLayout()
|
||||
|
||||
self.setLayout(self.blayout)
|
||||
|
||||
def dragEnterEvent(self, e):
|
||||
e.accept()
|
||||
|
||||
def dropEvent(self, e):
|
||||
pos = e.position()
|
||||
widget = e.source()
|
||||
self.blayout.removeWidget(widget)
|
||||
|
||||
for n in range(self.blayout.count()):
|
||||
# Get the widget at each index in turn.
|
||||
w = self.blayout.itemAt(n).widget()
|
||||
if self.orientation == Qt.Orientation.Vertical:
|
||||
# Drag drop vertically.
|
||||
drop_here = pos.y() < w.y() + w.size().height() // 2
|
||||
else:
|
||||
# Drag drop horizontally.
|
||||
drop_here = pos.x() < w.x() + w.size().width() // 2
|
||||
|
||||
if drop_here:
|
||||
break
|
||||
|
||||
else:
|
||||
# We aren't on the left hand/upper side of any widget,
|
||||
# so we're at the end. Increment 1 to insert after.
|
||||
n += 1
|
||||
|
||||
self.blayout.insertWidget(n, widget)
|
||||
self.orderChanged.emit(self.get_item_data())
|
||||
|
||||
e.accept()
|
||||
|
||||
def add_item(self, item):
|
||||
self.blayout.addWidget(item)
|
||||
|
||||
def get_item_data(self):
|
||||
data = []
|
||||
for n in range(self.blayout.count()):
|
||||
# Get the widget at each index in turn.
|
||||
w: "DAPBlockWidget" = self.blayout.itemAt(n).widget()
|
||||
data.append(w._title.text())
|
||||
return data
|
||||
|
||||
|
||||
class DAPBlockWidget(BECWidget, DragItem):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
content: type[DAPBlock] = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
parent=parent,
|
||||
client=client,
|
||||
gui_id=gui_id,
|
||||
title=content.__name__,
|
||||
**kwargs,
|
||||
)
|
||||
self._content = content
|
||||
self.add_form(self._content)
|
||||
|
||||
def add_form(self, block_type: type[DAPBlock]):
|
||||
run_signature = inspect.signature(block_type.run)
|
||||
self._title.setText(block_type.__name__)
|
||||
layout = self._contents.layout()
|
||||
if layout is None:
|
||||
return
|
||||
self._add_widgets_for_signature(layout, run_signature)
|
||||
|
||||
def _add_widgets_for_signature(self, layout: QLayout, signature: inspect.Signature):
|
||||
for arg_name, arg_spec in signature.parameters.items():
|
||||
annotation: str | type = arg_spec.annotation
|
||||
if isinstance(annotation, str):
|
||||
annotation = deserialize_dtype(annotation) or annotation
|
||||
w = QWidget()
|
||||
w.setLayout(QHBoxLayout())
|
||||
w.layout().addWidget(QLabel(arg_name))
|
||||
w.layout().addWidget(
|
||||
widget_from_type(annotation)(
|
||||
FieldInfo(
|
||||
annotation=annotation
|
||||
) # FIXME this class should not be initialised directly...
|
||||
)
|
||||
)
|
||||
w.layout().addWidget(QLabel(str(annotation)))
|
||||
|
||||
layout.addWidget(w)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
|
||||
for block_type in [SmoothBlock, GradientBlock, BlockWithLotsOfArgs] * 2:
|
||||
item = DAPBlockWidget(content=block_type)
|
||||
self.drag.add_item(item)
|
||||
|
||||
# Print out the changed order.
|
||||
self.drag.orderChanged.connect(print)
|
||||
|
||||
container = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self.drag)
|
||||
layout.addStretch(1)
|
||||
container.setLayout(layout)
|
||||
|
||||
self.setCentralWidget(container)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
w = MainWindow()
|
||||
w.show()
|
||||
|
||||
app.exec()
|
||||
6
bec_widgets/widgets/dap/dap_task_manager/task_editor.py
Normal file
6
bec_widgets/widgets/dap/dap_task_manager/task_editor.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Panel to compose DAP task runs
|
||||
- new ones are added as tabs
|
||||
- can be enabled and disabled, continuous and oneoff
|
||||
- fill in extra kwargs using thing from MD widget
|
||||
- output to topic for the name, which looks like a data topic
|
||||
"""
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
BECConsole is a Qt widget that runs a Bash shell.
|
||||
BECConsole is a Qt widget that runs a Bash shell.
|
||||
|
||||
BECConsole VT100 emulation is powered by Pyte,
|
||||
(https://github.com/selectel/pyte).
|
||||
@@ -56,12 +56,12 @@ control_keys_mapping = {
|
||||
QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell)
|
||||
QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace)
|
||||
QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab)
|
||||
QtCore.Qt.Key_J: b"\x0a", # Ctrl-J (Line Feed)
|
||||
QtCore.Qt.Key_K: b"\x0b", # Ctrl-K (Vertical Tab)
|
||||
QtCore.Qt.Key_L: b"\x0c", # Ctrl-L (Form Feed)
|
||||
QtCore.Qt.Key_M: b"\x0d", # Ctrl-M (Carriage Return)
|
||||
QtCore.Qt.Key_N: b"\x0e", # Ctrl-N
|
||||
QtCore.Qt.Key_O: b"\x0f", # Ctrl-O
|
||||
QtCore.Qt.Key_J: b"\x0A", # Ctrl-J (Line Feed)
|
||||
QtCore.Qt.Key_K: b"\x0B", # Ctrl-K (Vertical Tab)
|
||||
QtCore.Qt.Key_L: b"\x0C", # Ctrl-L (Form Feed)
|
||||
QtCore.Qt.Key_M: b"\x0D", # Ctrl-M (Carriage Return)
|
||||
QtCore.Qt.Key_N: b"\x0E", # Ctrl-N
|
||||
QtCore.Qt.Key_O: b"\x0F", # Ctrl-O
|
||||
QtCore.Qt.Key_P: b"\x10", # Ctrl-P
|
||||
QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q
|
||||
QtCore.Qt.Key_R: b"\x12", # Ctrl-R
|
||||
@@ -72,10 +72,10 @@ control_keys_mapping = {
|
||||
QtCore.Qt.Key_W: b"\x17", # Ctrl-W
|
||||
QtCore.Qt.Key_X: b"\x18", # Ctrl-X
|
||||
QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y
|
||||
QtCore.Qt.Key_Z: b"\x1a", # Ctrl-Z
|
||||
QtCore.Qt.Key_Escape: b"\x1b", # Ctrl-Escape
|
||||
QtCore.Qt.Key_Backslash: b"\x1c", # Ctrl-\
|
||||
QtCore.Qt.Key_Underscore: b"\x1f", # Ctrl-_
|
||||
QtCore.Qt.Key_Z: b"\x1A", # Ctrl-Z
|
||||
QtCore.Qt.Key_Escape: b"\x1B", # Ctrl-Escape
|
||||
QtCore.Qt.Key_Backslash: b"\x1C", # Ctrl-\
|
||||
QtCore.Qt.Key_Underscore: b"\x1F", # Ctrl-_
|
||||
}
|
||||
|
||||
normal_keys_mapping = {
|
||||
@@ -89,7 +89,7 @@ normal_keys_mapping = {
|
||||
QtCore.Qt.Key_Left: b"\x02",
|
||||
QtCore.Qt.Key_Up: b"\x10",
|
||||
QtCore.Qt.Key_Right: b"\x06",
|
||||
QtCore.Qt.Key_Down: b"\x0e",
|
||||
QtCore.Qt.Key_Down: b"\x0E",
|
||||
QtCore.Qt.Key_PageUp: b"\x49",
|
||||
QtCore.Qt.Key_PageDown: b"\x51",
|
||||
QtCore.Qt.Key_F1: b"\x1b\x31",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
||||
AdditionalMetadataTable,
|
||||
AdditionalMetadataTableModel,
|
||||
)
|
||||
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
||||
|
||||
__all__ = ["ScanMetadata", "AdditionalMetadataTable", "AdditionalMetadataTableModel"]
|
||||
|
||||
@@ -2,13 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from decimal import Decimal
|
||||
from types import UnionType
|
||||
from typing import Callable, Protocol
|
||||
from typing import TYPE_CHECKING, Callable, get_args
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import Signal # type: ignore
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -35,22 +33,12 @@ from bec_widgets.widgets.editors.scan_metadata._util import (
|
||||
field_precision,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FormItemSpec(BaseModel):
|
||||
"""
|
||||
The specification for an item in a dynamically generated form. Uses a pydantic FieldInfo
|
||||
to store most annotation info, since one of the main purposes is to store data for
|
||||
forms genrated from pydantic models, but can also be composed from other sources or by hand.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
item_type: type | UnionType
|
||||
name: str
|
||||
info: FieldInfo = FieldInfo()
|
||||
|
||||
|
||||
class ClearableBoolEntry(QWidget):
|
||||
stateChanged = Signal()
|
||||
|
||||
@@ -94,20 +82,20 @@ class ClearableBoolEntry(QWidget):
|
||||
self._false.setToolTip(tooltip)
|
||||
|
||||
|
||||
class DynamicFormItem(QWidget):
|
||||
class MetadataWidget(QWidget):
|
||||
valueChanged = Signal()
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._spec = spec
|
||||
self._info = info
|
||||
self._layout = QHBoxLayout()
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
|
||||
self._default = field_default(self._spec.info)
|
||||
self._desc = self._spec.info.description
|
||||
self._default = field_default(self._info)
|
||||
self._desc = self._info.description
|
||||
self.setLayout(self._layout)
|
||||
self._add_main_widget()
|
||||
if clearable_required(spec.info):
|
||||
if clearable_required(info):
|
||||
self._add_clear_button()
|
||||
|
||||
@abstractmethod
|
||||
@@ -138,15 +126,15 @@ class DynamicFormItem(QWidget):
|
||||
self.valueChanged.emit()
|
||||
|
||||
|
||||
class StrMetadataField(DynamicFormItem):
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
class StrMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
self._main_widget.textChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
self._main_widget = QLineEdit()
|
||||
self._layout.addWidget(self._main_widget)
|
||||
min_length, max_length = (field_minlen(self._spec.info), field_maxlen(self._spec.info))
|
||||
min_length, max_length = field_minlen(self._info), field_maxlen(self._info)
|
||||
if max_length:
|
||||
self._main_widget.setMaxLength(max_length)
|
||||
self._main_widget.setToolTip(
|
||||
@@ -167,15 +155,15 @@ class StrMetadataField(DynamicFormItem):
|
||||
self._main_widget.setText(value)
|
||||
|
||||
|
||||
class IntMetadataField(DynamicFormItem):
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
class IntMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
self._main_widget.textChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
self._main_widget = QSpinBox()
|
||||
self._layout.addWidget(self._main_widget)
|
||||
min_, max_ = field_limits(self._spec.info, int)
|
||||
min_, max_ = field_limits(self._info, int)
|
||||
self._main_widget.setMinimum(min_)
|
||||
self._main_widget.setMaximum(max_)
|
||||
self._main_widget.setToolTip(f"(range {min_} to {max_}){self._describe()}")
|
||||
@@ -196,18 +184,18 @@ class IntMetadataField(DynamicFormItem):
|
||||
self._main_widget.setValue(value)
|
||||
|
||||
|
||||
class FloatDecimalMetadataField(DynamicFormItem):
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
class FloatDecimalMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
self._main_widget.textChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
self._main_widget = QDoubleSpinBox()
|
||||
self._layout.addWidget(self._main_widget)
|
||||
min_, max_ = field_limits(self._spec.info, int)
|
||||
min_, max_ = field_limits(self._info, int)
|
||||
self._main_widget.setMinimum(min_)
|
||||
self._main_widget.setMaximum(max_)
|
||||
precision = field_precision(self._spec.info)
|
||||
precision = field_precision(self._info)
|
||||
if precision:
|
||||
self._main_widget.setDecimals(precision)
|
||||
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
|
||||
@@ -230,13 +218,13 @@ class FloatDecimalMetadataField(DynamicFormItem):
|
||||
self._main_widget.setValue(value)
|
||||
|
||||
|
||||
class BoolMetadataField(DynamicFormItem):
|
||||
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
class BoolMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
self._main_widget.stateChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
if clearable_required(self._spec.info):
|
||||
if clearable_required(self._info):
|
||||
self._main_widget = ClearableBoolEntry()
|
||||
else:
|
||||
self._main_widget = QCheckBox()
|
||||
@@ -251,7 +239,7 @@ class BoolMetadataField(DynamicFormItem):
|
||||
self._main_widget.setChecked(value)
|
||||
|
||||
|
||||
def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
|
||||
def widget_from_type(annotation: type | None) -> Callable[[FieldInfo], MetadataWidget]:
|
||||
if annotation in [str, str | None]:
|
||||
return StrMetadataField
|
||||
if annotation in [int, int | None]:
|
||||
@@ -261,7 +249,9 @@ def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormIte
|
||||
if annotation in [bool, bool | None]:
|
||||
return BoolMetadataField
|
||||
else:
|
||||
logger.warning(f"Type {annotation} is not (yet) supported in metadata form creation.")
|
||||
logger.warning(
|
||||
f"Type {annotation} is not (yet) supported in metadata form creation."
|
||||
)
|
||||
return StrMetadataField
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from annotated_types import Ge, Gt, Le, Lt
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic_core import PydanticUndefined
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -16,20 +16,12 @@ from qtpy.QtWidgets import (
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
|
||||
class DictBackedTableModel(QAbstractTableModel):
|
||||
class AdditionalMetadataTableModel(QAbstractTableModel):
|
||||
def __init__(self, data):
|
||||
"""A model to go with DictBackedTable, which represents key-value pairs
|
||||
to be displayed in a TreeWidget.
|
||||
|
||||
Args:
|
||||
data (list[list[str]]): list of key-value pairs to initialise with"""
|
||||
super().__init__()
|
||||
self._data: list[list[str]] = data
|
||||
self._disallowed_keys: list[str] = []
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
# see QAbstractTableModel documentation for these methods
|
||||
|
||||
def headerData(
|
||||
self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole()
|
||||
) -> Any:
|
||||
@@ -57,10 +49,6 @@ class DictBackedTableModel(QAbstractTableModel):
|
||||
return False
|
||||
|
||||
def update_disallowed_keys(self, keys: list[str]):
|
||||
"""Set the list of keys which may not be used.
|
||||
|
||||
Args:
|
||||
keys (list[str]): list of keys which are forbidden."""
|
||||
self._disallowed_keys = keys
|
||||
for i, item in enumerate(self._data):
|
||||
if item[0] in self._disallowed_keys:
|
||||
@@ -107,21 +95,16 @@ class DictBackedTableModel(QAbstractTableModel):
|
||||
return dict(self._data)
|
||||
|
||||
|
||||
class DictBackedTable(QWidget):
|
||||
class AdditionalMetadataTable(QWidget):
|
||||
|
||||
delete_rows = Signal(list)
|
||||
|
||||
def __init__(self, initial_data: list[list[str]]):
|
||||
"""Widget which uses a DictBackedTableModel to display an editable table
|
||||
which can be extracted as a dict.
|
||||
|
||||
Args:
|
||||
initial_data (list[list[str]]): list of key-value pairs to initialise with
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self._layout = QHBoxLayout()
|
||||
self.setLayout(self._layout)
|
||||
self._table_model = DictBackedTableModel(initial_data)
|
||||
self._table_model = AdditionalMetadataTableModel(initial_data)
|
||||
self._table_view = QTreeView()
|
||||
self._table_view.setModel(self._table_model)
|
||||
self._table_view.setSizePolicy(
|
||||
@@ -143,21 +126,15 @@ class DictBackedTable(QWidget):
|
||||
self.delete_rows.connect(self._table_model.delete_rows)
|
||||
|
||||
def delete_selected_rows(self):
|
||||
"""Delete rows which are part of the selection model"""
|
||||
cells: list[QModelIndex] = self._table_view.selectionModel().selectedIndexes()
|
||||
row_indices = list({r.row() for r in cells})
|
||||
if row_indices:
|
||||
self.delete_rows.emit(row_indices)
|
||||
|
||||
def dump_dict(self):
|
||||
"""Get the current content of the table as a dict"""
|
||||
return self._table_model.dump_dict()
|
||||
|
||||
def update_disallowed_keys(self, keys: list[str]):
|
||||
"""Set the list of keys which may not be used.
|
||||
|
||||
Args:
|
||||
keys (list[str]): list of keys which are forbidden."""
|
||||
self._table_model.update_disallowed_keys(keys)
|
||||
|
||||
|
||||
@@ -167,6 +144,6 @@ if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
|
||||
window = DictBackedTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
||||
window = AdditionalMetadataTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
||||
window.show()
|
||||
app.exec()
|
||||
@@ -1,21 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from types import NoneType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.metadata_schema import get_metadata_schema_for_scan
|
||||
from pydantic import Field
|
||||
from qtpy.QtWidgets import QApplication, QComboBox, QHBoxLayout, QVBoxLayout, QWidget
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import Field, ValidationError
|
||||
from qtpy.QtCore import Signal # type: ignore
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
||||
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
|
||||
from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
|
||||
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import widget_from_type
|
||||
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
||||
AdditionalMetadataTable,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ScanMetadata(PydanticModelForm):
|
||||
class PydanticModelForm(BECWidget, QWidget):
|
||||
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
|
||||
metadata schema registry supplied in the plugin repo to find pydantic models
|
||||
associated with the scan type. Sets limits for numerical values if specified."""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "list_alt"
|
||||
|
||||
metadata_updated = Signal(dict)
|
||||
metadata_cleared = Signal(NoneType)
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
@@ -24,36 +55,126 @@ class ScanMetadata(PydanticModelForm):
|
||||
initial_extras: list[list[str]] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
|
||||
metadata schema registry supplied in the plugin repo to find pydantic models
|
||||
associated with the scan type. Sets limits for numerical values if specified.
|
||||
super().__init__(parent=parent, client=client, **kwargs)
|
||||
|
||||
Args:
|
||||
scan_name (str): The scan for which to generate a metadata form
|
||||
Initial_extras (list[list[str]]): Initial data with which to populate the additional
|
||||
metadata table - inner lists should be key-value pairs
|
||||
"""
|
||||
self.set_schema(scan_name)
|
||||
|
||||
# self.populate() gets called in super().__init__
|
||||
# so make sure self._additional_metadata exists
|
||||
self._additional_md_box = ExpandableGroupFrame("Additional metadata", expanded=False)
|
||||
self._layout = QVBoxLayout()
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self._layout)
|
||||
|
||||
self._required_md_box = ExpandableGroupFrame("Scan schema metadata")
|
||||
self._layout.addWidget(self._required_md_box)
|
||||
self._required_md_box_layout = QHBoxLayout()
|
||||
self._required_md_box.set_layout(self._required_md_box_layout)
|
||||
|
||||
self._md_grid = QWidget()
|
||||
self._required_md_box_layout.addWidget(self._md_grid)
|
||||
self._grid_container = QVBoxLayout()
|
||||
self._md_grid.setLayout(self._grid_container)
|
||||
self._new_grid_layout()
|
||||
self._grid_container.addLayout(self._md_grid_layout)
|
||||
|
||||
self._additional_md_box = ExpandableGroupFrame(
|
||||
"Additional metadata", expanded=False
|
||||
)
|
||||
self._layout.addWidget(self._additional_md_box)
|
||||
self._additional_md_box_layout = QHBoxLayout()
|
||||
self._additional_md_box.set_layout(self._additional_md_box_layout)
|
||||
|
||||
self._additional_metadata = DictBackedTable(initial_extras or [])
|
||||
self._scan_name = scan_name or ""
|
||||
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
|
||||
|
||||
super().__init__(parent=parent, metadata_model=self._md_schema, client=client, **kwargs)
|
||||
|
||||
self._layout.addWidget(self._additional_md_box)
|
||||
self._additional_metadata = AdditionalMetadataTable(initial_extras or [])
|
||||
self._additional_md_box_layout.addWidget(self._additional_metadata)
|
||||
|
||||
self._validity = CompactPopupWidget()
|
||||
self._validity.compact_view = True # type: ignore
|
||||
self._validity.label = "Metadata validity" # type: ignore
|
||||
self._validity.compact_show_popup.setIcon(
|
||||
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
|
||||
)
|
||||
self._validity_message = QLabel("Not yet validated")
|
||||
self._validity.addWidget(self._validity_message)
|
||||
self._layout.addWidget(self._validity)
|
||||
|
||||
self.populate()
|
||||
|
||||
@SafeSlot(str)
|
||||
def update_with_new_scan(self, scan_name: str):
|
||||
self.set_schema_from_scan(scan_name)
|
||||
self.set_schema(scan_name)
|
||||
self.populate()
|
||||
self.validate_form()
|
||||
|
||||
def validate_form(self, *_) -> bool:
|
||||
"""validate the currently entered metadata against the pydantic schema.
|
||||
If successful, returns on metadata_emitted and returns true.
|
||||
Otherwise, emits on metadata_cleared and returns false."""
|
||||
try:
|
||||
metadata_dict = self.get_full_model_dict()
|
||||
self._md_schema.model_validate(metadata_dict)
|
||||
self._validity.set_global_state("success")
|
||||
self._validity_message.setText("No errors!")
|
||||
self.metadata_updated.emit(metadata_dict)
|
||||
except ValidationError as e:
|
||||
self._validity.set_global_state("emergency")
|
||||
self._validity_message.setText(str(e))
|
||||
self.metadata_cleared.emit(None)
|
||||
|
||||
def get_full_model_dict(self):
|
||||
"""Get the entered metadata as a dict"""
|
||||
return self._additional_metadata.dump_dict() | self._dict_from_grid()
|
||||
|
||||
def set_schema(self, scan_name: str | None = None):
|
||||
self._scan_name = scan_name or ""
|
||||
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
|
||||
|
||||
def populate(self):
|
||||
self._clear_grid()
|
||||
self._populate()
|
||||
|
||||
def _populate(self):
|
||||
self._additional_metadata.update_disallowed_keys(
|
||||
list(self._md_schema.model_fields.keys())
|
||||
)
|
||||
for i, (field_name, info) in enumerate(self._md_schema.model_fields.items()):
|
||||
self._add_griditem(field_name, info, i)
|
||||
|
||||
def _add_griditem(self, field_name: str, info: FieldInfo, row: int):
|
||||
grid = self._md_grid_layout
|
||||
label = QLabel(info.title or field_name)
|
||||
label.setProperty("_model_field_name", field_name)
|
||||
label.setToolTip(info.description or field_name)
|
||||
grid.addWidget(label, row, 0)
|
||||
widget = widget_from_type(info.annotation)(info)
|
||||
widget.valueChanged.connect(self.validate_form)
|
||||
grid.addWidget(widget, row, 1)
|
||||
|
||||
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
|
||||
grid = self._md_grid_layout
|
||||
return {
|
||||
grid.itemAtPosition(i, 0)
|
||||
.widget()
|
||||
.property("_model_field_name"): grid.itemAtPosition(i, 1)
|
||||
.widget()
|
||||
.getValue() # type: ignore # we only add 'MetadataWidget's here
|
||||
for i in range(grid.rowCount())
|
||||
}
|
||||
|
||||
def _clear_grid(self):
|
||||
while self._md_grid_layout.count():
|
||||
item = self._md_grid_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
widget.deleteLater()
|
||||
self._md_grid_layout.deleteLater()
|
||||
self._new_grid_layout()
|
||||
self._grid_container.addLayout(self._md_grid_layout)
|
||||
self._md_grid.adjustSize()
|
||||
self.adjustSize()
|
||||
|
||||
def _new_grid_layout(self):
|
||||
self._md_grid_layout = QGridLayout()
|
||||
self._md_grid_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._md_grid_layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def hide_optional_metadata(self): # type: ignore
|
||||
"""Property to hide the optional metadata table."""
|
||||
@@ -68,25 +189,11 @@ class ScanMetadata(PydanticModelForm):
|
||||
"""
|
||||
self._additional_md_box.setVisible(not hide)
|
||||
|
||||
def get_form_data(self):
|
||||
"""Get the entered metadata as a dict"""
|
||||
return self._additional_metadata.dump_dict() | self._dict_from_grid()
|
||||
|
||||
def populate(self):
|
||||
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
|
||||
super().populate()
|
||||
|
||||
def set_schema_from_scan(self, scan_name: str | None):
|
||||
self._scan_name = scan_name or ""
|
||||
self.set_schema(get_metadata_schema_for_scan(self._scan_name))
|
||||
self.populate()
|
||||
class ScanMetadata(PydanticModelForm): ...
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=redefined-outer-name
|
||||
# pylint: disable=protected-access
|
||||
# pylint: disable=disallowed-name
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from bec_lib.metadata_schema import BasicScanMetadata
|
||||
@@ -94,15 +201,21 @@ if __name__ == "__main__": # pragma: no cover
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
class ExampleSchema1(BasicScanMetadata):
|
||||
abc: int = Field(gt=0, lt=2000, description="Heating temperature abc", title="A B C")
|
||||
foo: str = Field(max_length=12, description="Sample database code", default="DEF123")
|
||||
abc: int = Field(
|
||||
gt=0, lt=2000, description="Heating temperature abc", title="A B C"
|
||||
)
|
||||
foo: str = Field(
|
||||
max_length=12, description="Sample database code", default="DEF123"
|
||||
)
|
||||
xyz: Decimal = Field(decimal_places=4)
|
||||
baz: bool
|
||||
|
||||
class ExampleSchema2(BasicScanMetadata):
|
||||
checkbox_up_top: bool
|
||||
checkbox_again: bool = Field(
|
||||
title="Checkbox Again", description="this one defaults to True", default=True
|
||||
title="Checkbox Again",
|
||||
description="this one defaults to True",
|
||||
default=True,
|
||||
)
|
||||
different_items: int | None = Field(
|
||||
None, description="This is just one different item...", gt=-100, lt=0
|
||||
@@ -115,7 +228,11 @@ if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
with patch(
|
||||
"bec_lib.metadata_schema._get_metadata_schema_registry",
|
||||
lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3},
|
||||
lambda: {
|
||||
"scan1": ExampleSchema1,
|
||||
"scan2": ExampleSchema2,
|
||||
"scan3": ExampleSchema3,
|
||||
},
|
||||
):
|
||||
app = QApplication([])
|
||||
w = QWidget()
|
||||
|
||||
@@ -117,7 +117,6 @@ class WebsiteWidget(BECWidget, QWidget):
|
||||
Cleanup the widget
|
||||
"""
|
||||
self.website.page().deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -144,7 +144,7 @@ class Minesweeper(BECWidget, QWidget):
|
||||
PLUGIN = True
|
||||
ICON_NAME = "videogame_asset"
|
||||
USER_ACCESS = []
|
||||
RPC = True
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
@@ -27,7 +27,7 @@ logger = bec_logger.logger
|
||||
# noinspection PyDataclass
|
||||
class ImageConfig(ConnectionConfig):
|
||||
color_map: str = Field(
|
||||
"plasma", description="The colormap of the figure widget.", validate_default=True
|
||||
"magma", description="The colormap of the figure widget.", validate_default=True
|
||||
)
|
||||
color_bar: Literal["full", "simple"] | None = Field(
|
||||
None, description="The type of the color bar."
|
||||
@@ -79,6 +79,12 @@ class Image(PlotBase):
|
||||
"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",
|
||||
# ImageView Specific Settings
|
||||
"color_map",
|
||||
"color_map.setter",
|
||||
@@ -105,8 +111,8 @@ class Image(PlotBase):
|
||||
"fft.setter",
|
||||
"log",
|
||||
"log.setter",
|
||||
"num_rotation_90",
|
||||
"num_rotation_90.setter",
|
||||
"rotation",
|
||||
"rotation.setter",
|
||||
"transpose",
|
||||
"transpose.setter",
|
||||
"image",
|
||||
@@ -131,13 +137,13 @@ class Image(PlotBase):
|
||||
super().__init__(
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
self._main_image = ImageItem(parent_image=self)
|
||||
self._main_image = ImageItem(parent_image=self, parent_id=self.gui_id)
|
||||
|
||||
self.plot_item.addItem(self._main_image)
|
||||
self.scan_id = None
|
||||
|
||||
# Default Color map to plasma
|
||||
self.color_map = "plasma"
|
||||
# Default Color map to magma
|
||||
self.color_map = "magma"
|
||||
|
||||
################################################################################
|
||||
# Widget Specific GUI interactions
|
||||
@@ -650,21 +656,21 @@ class Image(PlotBase):
|
||||
self._main_image.log = enable
|
||||
|
||||
@SafeProperty(int)
|
||||
def num_rotation_90(self) -> int:
|
||||
def rotation(self) -> int:
|
||||
"""
|
||||
The number of 90° rotations to apply counterclockwise.
|
||||
The number of 90° rotations to apply.
|
||||
"""
|
||||
return self._main_image.num_rotation_90
|
||||
return self._main_image.rotation
|
||||
|
||||
@num_rotation_90.setter
|
||||
def num_rotation_90(self, value: int):
|
||||
@rotation.setter
|
||||
def rotation(self, value: int):
|
||||
"""
|
||||
Set the number of 90° rotations to apply counterclockwise.
|
||||
Set the number of 90° rotations to apply.
|
||||
|
||||
Args:
|
||||
value(int): The number of 90° rotations to apply.
|
||||
"""
|
||||
self._main_image.num_rotation_90 = value
|
||||
self._main_image.rotation = value
|
||||
|
||||
@SafeProperty(bool)
|
||||
def transpose(self) -> bool:
|
||||
@@ -756,19 +762,6 @@ class Image(PlotBase):
|
||||
self.selection_bundle.dim_combo_box,
|
||||
):
|
||||
combo.blockSignals(False)
|
||||
else:
|
||||
for combo in (
|
||||
self.selection_bundle.device_combo_box,
|
||||
self.selection_bundle.dim_combo_box,
|
||||
):
|
||||
combo.blockSignals(True)
|
||||
self.selection_bundle.device_combo_box.setCurrentText("")
|
||||
self.selection_bundle.dim_combo_box.setCurrentText("auto")
|
||||
for combo in (
|
||||
self.selection_bundle.device_combo_box,
|
||||
self.selection_bundle.dim_combo_box,
|
||||
):
|
||||
combo.blockSignals(False)
|
||||
|
||||
################################################################################
|
||||
# Image Update Methods
|
||||
@@ -819,7 +812,6 @@ class Image(PlotBase):
|
||||
self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
|
||||
)
|
||||
self._main_image.config.monitor = None
|
||||
self._sync_device_selection()
|
||||
|
||||
########################################
|
||||
# 1D updates
|
||||
|
||||
@@ -24,7 +24,7 @@ class ImageItemConfig(ConnectionConfig): # TODO review config
|
||||
monitor: str | None = Field(None, description="The name of the monitor.")
|
||||
monitor_type: Literal["1d", "2d", "auto"] = Field("auto", description="The type of monitor.")
|
||||
source: str | None = Field(None, description="The source of the curve.")
|
||||
color_map: str | None = Field("plasma", description="The color map of the image.")
|
||||
color_map: str | None = Field("magma", description="The color map of the image.")
|
||||
downsample: bool | None = Field(True, description="Whether to downsample the image.")
|
||||
opacity: float | None = Field(1.0, description="The opacity of the image.")
|
||||
v_range: tuple[float | int, float | int] | None = Field(
|
||||
@@ -61,8 +61,8 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
"fft.setter",
|
||||
"log",
|
||||
"log.setter",
|
||||
"num_rotation_90",
|
||||
"num_rotation_90.setter",
|
||||
"rotation",
|
||||
"rotation.setter",
|
||||
"transpose",
|
||||
"transpose.setter",
|
||||
"get_data",
|
||||
@@ -86,6 +86,7 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
self.set_parent(parent_image)
|
||||
else:
|
||||
self.parent_image = None
|
||||
self.parent_id = None
|
||||
super().__init__(config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.raw_data = None
|
||||
@@ -97,6 +98,7 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
|
||||
def set_parent(self, parent: BECConnector):
|
||||
self.parent_image = parent
|
||||
self.parent_id = parent.gui_id
|
||||
|
||||
def parent(self):
|
||||
return self.parent_image
|
||||
@@ -239,13 +241,13 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
self._process_image()
|
||||
|
||||
@property
|
||||
def num_rotation_90(self) -> Optional[int]:
|
||||
def rotation(self) -> Optional[int]:
|
||||
"""Get or set the number of 90° rotations to apply."""
|
||||
return self.config.processing.num_rotation_90
|
||||
return self.config.processing.rotation
|
||||
|
||||
@num_rotation_90.setter
|
||||
def num_rotation_90(self, value: Optional[int]):
|
||||
self.config.processing.num_rotation_90 = value
|
||||
@rotation.setter
|
||||
def rotation(self, value: Optional[int]):
|
||||
self.config.processing.rotation = value
|
||||
self._process_image()
|
||||
|
||||
@property
|
||||
@@ -273,7 +275,3 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
self.raw_data = None
|
||||
self.buffer = []
|
||||
self.max_len = 0
|
||||
|
||||
def remove(self):
|
||||
self.parent().disconnect_monitor(self.config.monitor)
|
||||
self.clear()
|
||||
|
||||
@@ -37,7 +37,7 @@ class ProcessingConfig(BaseModel):
|
||||
transpose: bool = Field(
|
||||
False, description="Whether to transpose the monitor data before displaying."
|
||||
)
|
||||
num_rotation_90: int = Field(
|
||||
rotation: int = Field(
|
||||
0, description="The rotation angle of the monitor data before displaying."
|
||||
)
|
||||
stats: ImageStats = Field(
|
||||
@@ -140,8 +140,8 @@ class ImageProcessor(QObject):
|
||||
"""Core processing logic without threading overhead."""
|
||||
if self.config.fft:
|
||||
data = self.FFT(data)
|
||||
if self.config.num_rotation_90 is not None:
|
||||
data = self.rotation(data, self.config.num_rotation_90)
|
||||
if self.config.rotation is not None:
|
||||
data = self.rotation(data, self.config.rotation)
|
||||
if self.config.transpose:
|
||||
data = self.transpose(data)
|
||||
if self.config.log:
|
||||
|
||||
@@ -40,7 +40,7 @@ class MonitorSelectionToolbarBundle(ToolbarBundle):
|
||||
self.add_action("monitor", WidgetAction(widget=self.device_combo_box, adjust_size=True))
|
||||
|
||||
# 2) Dimension combo box
|
||||
self.dim_combo_box = QComboBox(parent=self.target_widget)
|
||||
self.dim_combo_box = QComboBox()
|
||||
self.dim_combo_box.addItems(["auto", "1d", "2d"])
|
||||
self.dim_combo_box.setCurrentText("auto")
|
||||
self.dim_combo_box.setToolTip("Monitor Dimension")
|
||||
|
||||
@@ -55,24 +55,24 @@ class ImageProcessingToolbarBundle(ToolbarBundle):
|
||||
|
||||
@SafeSlot()
|
||||
def rotate_right(self):
|
||||
if self.target_widget.num_rotation_90 is None:
|
||||
if self.target_widget.rotation is None:
|
||||
return
|
||||
rotation = (self.target_widget.num_rotation_90 - 1) % 4
|
||||
self.target_widget.num_rotation_90 = rotation
|
||||
rotation = (self.target_widget.rotation - 1) % 4
|
||||
self.target_widget.rotation = rotation
|
||||
|
||||
@SafeSlot()
|
||||
def rotate_left(self):
|
||||
if self.target_widget.num_rotation_90 is None:
|
||||
if self.target_widget.rotation is None:
|
||||
return
|
||||
rotation = (self.target_widget.num_rotation_90 + 1) % 4
|
||||
self.target_widget.num_rotation_90 = rotation
|
||||
rotation = (self.target_widget.rotation + 1) % 4
|
||||
self.target_widget.rotation = rotation
|
||||
|
||||
@SafeSlot()
|
||||
def reset_settings(self):
|
||||
self.target_widget.fft = False
|
||||
self.target_widget.log = False
|
||||
self.target_widget.transpose = False
|
||||
self.target_widget.num_rotation_90 = 0
|
||||
self.target_widget.rotation = 0
|
||||
|
||||
self.fft.action.setChecked(False)
|
||||
self.log.action.setChecked(False)
|
||||
|
||||
@@ -25,7 +25,7 @@ logger = bec_logger.logger
|
||||
|
||||
class MultiWaveformConfig(ConnectionConfig):
|
||||
color_palette: str | None = Field(
|
||||
"plasma", description="The color palette of the figure widget.", validate_default=True
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
curve_limit: int | None = Field(
|
||||
200, description="The maximum number of curves to display on the plot."
|
||||
@@ -308,7 +308,7 @@ class MultiWaveform(PlotBase):
|
||||
################################################################################
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def plot(self, monitor: str, color_palette: str | None = "plasma"):
|
||||
def plot(self, monitor: str, color_palette: str | None = "magma"):
|
||||
"""
|
||||
Create a plot for the given monitor.
|
||||
Args:
|
||||
|
||||
@@ -29,7 +29,9 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||
|
||||
# Monitor Selection
|
||||
self.monitor = DeviceComboBox(
|
||||
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=ReadoutPriority.ASYNC
|
||||
device_filter=BECDeviceFilter.DEVICE,
|
||||
readout_priority_filter=ReadoutPriority.ASYNC,
|
||||
parent_id=self.target_widget.gui_id,
|
||||
)
|
||||
self.monitor.addItem("", None)
|
||||
self.monitor.setCurrentText("")
|
||||
@@ -38,7 +40,7 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||
self.add_action("monitor", WidgetAction(widget=self.monitor, adjust_size=False))
|
||||
|
||||
# Colormap Selection
|
||||
self.colormap_widget = BECColorMapWidget(cmap="plasma")
|
||||
self.colormap_widget = BECColorMapWidget(cmap="magma", parent_id=self.target_widget.gui_id)
|
||||
self.add_action("color_map", WidgetAction(widget=self.colormap_widget, adjust_size=False))
|
||||
|
||||
# Connect slots, a device will be connected upon change of any combobox
|
||||
|
||||
@@ -98,11 +98,10 @@ class PlotBase(BECWidget, QWidget):
|
||||
self._ui_mode = UIMode.POPUP if popups else UIMode.SIDE
|
||||
self.axis_settings_dialog = None
|
||||
self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
|
||||
self.plot_widget.ci.setContentsMargins(0, 0, 0, 0)
|
||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||
self.plot_widget.addItem(self.plot_item)
|
||||
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||
self._init_toolbar()
|
||||
|
||||
# PlotItem Addons
|
||||
@@ -796,7 +795,6 @@ class PlotBase(BECWidget, QWidget):
|
||||
"""
|
||||
self.plot_item.showAxis("top", value)
|
||||
self.plot_item.showAxis("right", value)
|
||||
|
||||
self.property_changed.emit("outer_axes", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
|
||||
@@ -816,7 +814,6 @@ class PlotBase(BECWidget, QWidget):
|
||||
"""
|
||||
self.plot_item.showAxis("bottom", value)
|
||||
self.plot_item.showAxis("left", value)
|
||||
|
||||
self._apply_x_label()
|
||||
self._apply_y_label()
|
||||
self.property_changed.emit("inner_axes", value)
|
||||
|
||||
@@ -38,7 +38,7 @@ class ScatterCurveConfig(ConnectionConfig):
|
||||
"solid", description="The style of the pen of the curve."
|
||||
)
|
||||
color_map: str | None = Field(
|
||||
"plasma", description="The color palette of the figure widget.", validate_default=True
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
x_device: ScatterDeviceSignal | None = Field(
|
||||
None, description="The x device signal of the scatter waveform."
|
||||
@@ -78,8 +78,8 @@ class ScatterCurve(BECConnector, pg.PlotDataItem):
|
||||
self.config = config
|
||||
name = config.label
|
||||
self.parent_item = parent_item
|
||||
object_name = name.replace("-", "_").replace(" ", "_") if name else None
|
||||
super().__init__(name=name, object_name=object_name, config=config, gui_id=gui_id, **kwargs)
|
||||
self.parent_id = self.parent_item.gui_id
|
||||
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.data_z = None # color scaling needs to be cashed for changing colormap
|
||||
self.apply_config()
|
||||
|
||||
@@ -30,7 +30,7 @@ logger = bec_logger.logger
|
||||
# noinspection PyDataclass
|
||||
class ScatterWaveformConfig(ConnectionConfig):
|
||||
color_map: str | None = Field(
|
||||
"plasma",
|
||||
"magma",
|
||||
description="The color map of the z scaling of scatter waveform.",
|
||||
validate_default=True,
|
||||
)
|
||||
@@ -266,7 +266,7 @@ class ScatterWaveform(PlotBase):
|
||||
x_entry: None | str = None,
|
||||
y_entry: None | str = None,
|
||||
z_entry: None | str = None,
|
||||
color_map: str | None = "plasma",
|
||||
color_map: str | None = "magma",
|
||||
label: str | None = None,
|
||||
validate_bec: bool = True,
|
||||
) -> ScatterCurve:
|
||||
@@ -333,13 +333,12 @@ class ScatterWaveform(PlotBase):
|
||||
|
||||
# To have only one main curve
|
||||
if self._main_curve is not None:
|
||||
self.rpc_register.remove_rpc(self._main_curve)
|
||||
self.rpc_register.broadcast()
|
||||
self.plot_item.removeItem(self._main_curve)
|
||||
self._main_curve.deleteLater()
|
||||
self._main_curve = None
|
||||
|
||||
self._main_curve = ScatterCurve(parent_item=self, config=config, name=config.label)
|
||||
self._main_curve = ScatterCurve(
|
||||
parent_item=self, config=config, gui_id=self.gui_id, name=config.label
|
||||
)
|
||||
self.plot_item.addItem(self._main_curve)
|
||||
|
||||
self.sync_signal_update.emit()
|
||||
|
||||
@@ -92,7 +92,8 @@ class Curve(BECConnector, pg.PlotDataItem):
|
||||
else:
|
||||
self.config = config
|
||||
self.parent_item = parent_item
|
||||
object_name = name.replace("-", "_").replace(" ", "_") if name else None
|
||||
self.parent_id = self.parent_item.gui_id
|
||||
object_name = name.replace("-", "_") if name else None
|
||||
super().__init__(name=name, object_name=object_name, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.apply_config()
|
||||
|
||||
@@ -31,9 +31,6 @@ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit
|
||||
)
|
||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||
from bec_widgets.widgets.plots.waveform.curve import CurveConfig, DeviceSignal
|
||||
from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
|
||||
ColorButtonNative,
|
||||
)
|
||||
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
@@ -43,6 +40,49 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ColorButton(QPushButton):
|
||||
"""A QPushButton subclass that displays a color.
|
||||
|
||||
The background is set to the given color and the button text is the hex code.
|
||||
The text color is chosen automatically (black if the background is light, white if dark)
|
||||
to guarantee good readability.
|
||||
"""
|
||||
|
||||
def __init__(self, color="#000000", parent=None):
|
||||
"""Initialize the color button.
|
||||
|
||||
Args:
|
||||
color (str): The initial color in hex format (e.g., '#000000').
|
||||
parent: Optional QWidget parent.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.set_color(color)
|
||||
|
||||
def set_color(self, color):
|
||||
"""Set the button's color and update its appearance.
|
||||
|
||||
Args:
|
||||
color (str or QColor): The new color to assign.
|
||||
"""
|
||||
if isinstance(color, QColor):
|
||||
self._color = color.name()
|
||||
else:
|
||||
self._color = color
|
||||
self._update_appearance()
|
||||
|
||||
def color(self):
|
||||
"""Return the current color in hex."""
|
||||
return self._color
|
||||
|
||||
def _update_appearance(self):
|
||||
"""Update the button style based on the background color's brightness."""
|
||||
c = QColor(self._color)
|
||||
brightness = c.lightnessF()
|
||||
text_color = "#000000" if brightness > 0.5 else "#FFFFFF"
|
||||
self.setStyleSheet(f"background-color: {self._color}; color: {text_color};")
|
||||
self.setText(self._color)
|
||||
|
||||
|
||||
class CurveRow(QTreeWidgetItem):
|
||||
DELETE_BUTTON_COLOR = "#CC181E"
|
||||
"""A unified row that can represent either a device or a DAP curve.
|
||||
@@ -153,7 +193,7 @@ class CurveRow(QTreeWidgetItem):
|
||||
def _init_style_controls(self):
|
||||
"""Create columns 3..6: color button, style combo, width spin, symbol spin."""
|
||||
# Color in col 3
|
||||
self.color_button = ColorButtonNative(color=self.config.color)
|
||||
self.color_button = ColorButton(self.config.color)
|
||||
self.color_button.clicked.connect(lambda: self._select_color(self.color_button))
|
||||
self.tree.setItemWidget(self, 3, self.color_button)
|
||||
|
||||
@@ -244,11 +284,6 @@ class CurveRow(QTreeWidgetItem):
|
||||
self.dap_combo.deleteLater()
|
||||
self.dap_combo = None
|
||||
|
||||
if getattr(self, "color_button", None) is not None:
|
||||
self.color_button.close()
|
||||
self.color_button.deleteLater()
|
||||
self.color_button = None
|
||||
|
||||
# Remove the item from the tree widget
|
||||
index = self.tree.indexOfTopLevelItem(self)
|
||||
if index != -1:
|
||||
@@ -302,8 +337,8 @@ class CurveRow(QTreeWidgetItem):
|
||||
self.config.label = f"{parent_conf.label}-{new_dap}"
|
||||
|
||||
# Common style fields
|
||||
self.config.color = self.color_button.color
|
||||
self.config.symbol_color = self.color_button.color
|
||||
self.config.color = self.color_button.color()
|
||||
self.config.symbol_color = self.color_button.color()
|
||||
self.config.pen_style = self.style_combo.currentText()
|
||||
self.config.pen_width = self.width_spin.value()
|
||||
self.config.symbol_size = self.symbol_spin.value()
|
||||
@@ -338,7 +373,7 @@ class CurveTree(BECWidget, QWidget):
|
||||
if self.waveform and hasattr(self.waveform, "color_palette"):
|
||||
self.color_palette = self.waveform.color_palette
|
||||
else:
|
||||
self.color_palette = "plasma"
|
||||
self.color_palette = "magma"
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@@ -351,7 +386,7 @@ class CurveTree(BECWidget, QWidget):
|
||||
|
||||
def _init_toolbar(self):
|
||||
"""Initialize the toolbar with actions: add, send, refresh, expand, collapse, renormalize."""
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||
add = MaterialIconAction(
|
||||
icon_name="add", tooltip="Add new curve", checkable=False, parent=self
|
||||
)
|
||||
@@ -378,7 +413,7 @@ class CurveTree(BECWidget, QWidget):
|
||||
self.toolbar.add_action("renormalize_colors", renorm_action, self)
|
||||
renorm_action.action.triggered.connect(lambda checked: self.renormalize_colors())
|
||||
|
||||
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "plasma")
|
||||
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "magma")
|
||||
self.toolbar.addWidget(self.colormap_widget)
|
||||
self.colormap_widget.colormap_changed_signal.connect(self.handle_colormap_changed)
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ from qtpy.QtWidgets import QApplication, QDialog, QHBoxLayout, QMainWindow, QVBo
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
||||
from bec_widgets.utils.colors import Colors, set_theme
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.utils.toolbar import MaterialIconAction
|
||||
@@ -31,7 +30,7 @@ logger = bec_logger.logger
|
||||
# noinspection PyDataclass
|
||||
class WaveformConfig(ConnectionConfig):
|
||||
color_palette: str | None = Field(
|
||||
"plasma", description="The color palette of the figure widget.", validate_default=True
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
@@ -90,8 +89,6 @@ class Waveform(PlotBase):
|
||||
"curves",
|
||||
"x_mode",
|
||||
"x_mode.setter",
|
||||
"x_entry",
|
||||
"x_entry.setter",
|
||||
"color_palette",
|
||||
"color_palette.setter",
|
||||
"plot",
|
||||
@@ -647,9 +644,7 @@ class Waveform(PlotBase):
|
||||
# Decide label if not provided
|
||||
if label is None:
|
||||
if source == "custom":
|
||||
label = WidgetContainerUtils.generate_unique_name(
|
||||
"Curve", [c.object_name for c in self.curves]
|
||||
)
|
||||
label = f"Curve {len(self.curves) + 1}"
|
||||
else:
|
||||
label = f"{y_name}-{y_entry}"
|
||||
|
||||
@@ -774,9 +769,7 @@ class Waveform(PlotBase):
|
||||
label = config.label
|
||||
if not label:
|
||||
# Fallback label
|
||||
label = WidgetContainerUtils.generate_unique_name(
|
||||
"Curve", [c.object_name for c in self.curves]
|
||||
)
|
||||
label = f"Curve {len(self.curves) + 1}"
|
||||
config.label = label
|
||||
|
||||
# Check for duplicates
|
||||
@@ -1182,11 +1175,10 @@ class Waveform(PlotBase):
|
||||
self.on_async_readback,
|
||||
MessageEndpoints.device_async_readback(self.scan_id, name),
|
||||
from_start=True,
|
||||
cb_info={"scan_id": self.scan_id},
|
||||
)
|
||||
logger.info(f"Setup async curve {name}")
|
||||
|
||||
@SafeSlot(dict, dict, verify_sender=True)
|
||||
@SafeSlot(dict, dict)
|
||||
def on_async_readback(self, msg, metadata):
|
||||
"""
|
||||
Get async data readback. This code needs to be fast, therefor we try
|
||||
@@ -1205,14 +1197,6 @@ class Waveform(PlotBase):
|
||||
msg(dict): Message with the async data.
|
||||
metadata(dict): Metadata of the message.
|
||||
"""
|
||||
sender = self.sender()
|
||||
if not hasattr(sender, "cb_info"):
|
||||
logger.info(f"Sender {sender} has no cb_info.")
|
||||
return
|
||||
scan_id = sender.cb_info.get("scan_id", None)
|
||||
if scan_id != self.scan_id:
|
||||
logger.info("Scan ID mismatch, ignoring async readback.")
|
||||
|
||||
instruction = metadata.get("async_update", {}).get("type")
|
||||
if instruction not in ["add", "add_slice", "replace"]:
|
||||
logger.warning(f"Invalid async update instruction: {instruction}")
|
||||
@@ -1221,7 +1205,6 @@ class Waveform(PlotBase):
|
||||
plot_mode = self.x_axis_mode["name"]
|
||||
for curve in self._async_curves:
|
||||
x_data = None # Reset x_data
|
||||
y_data = None # Reset y_data
|
||||
# Get the curve data
|
||||
async_data = msg["signals"].get(curve.config.signal.entry, None)
|
||||
if async_data is None:
|
||||
@@ -1765,10 +1748,12 @@ class Waveform(PlotBase):
|
||||
self.proxy_dap_request.cleanup()
|
||||
self.clear_all()
|
||||
if self.curve_settings_dialog is not None:
|
||||
self.curve_settings_dialog.reject()
|
||||
self.curve_settings_dialog.close()
|
||||
self.curve_settings_dialog.deleteLater()
|
||||
self.curve_settings_dialog = None
|
||||
if self.dap_summary_dialog is not None:
|
||||
self.dap_summary_dialog.reject()
|
||||
self.dap_summary_dialog.close()
|
||||
self.dap_summary_dialog.deleteLater()
|
||||
self.dap_summary_dialog = None
|
||||
super().cleanup()
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ class BECProgressBar(BECWidget, QWidget):
|
||||
"set_minimum",
|
||||
"label_template",
|
||||
"label_template.setter",
|
||||
"_get_label",
|
||||
]
|
||||
ICON_NAME = "page_control"
|
||||
|
||||
@@ -236,10 +235,6 @@ class BECProgressBar(BECWidget, QWidget):
|
||||
(value - self._user_minimum) / (self._user_maximum - self._user_minimum) * self._maximum
|
||||
)
|
||||
|
||||
def _get_label(self) -> str:
|
||||
"""Return the label text. mostly used for testing rpc."""
|
||||
return self.center_label.text()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -96,6 +96,7 @@ class Ring(BECConnector, QObject):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
parent_progress_widget=None,
|
||||
config: RingConfig | dict | None = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
@@ -110,8 +111,7 @@ class Ring(BECConnector, QObject):
|
||||
self.config = config
|
||||
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.parent_progress_widget = parent
|
||||
|
||||
self.parent_progress_widget = parent_progress_widget
|
||||
self.color = None
|
||||
self.background_color = None
|
||||
self.start_position = None
|
||||
@@ -244,10 +244,10 @@ class Ring(BECConnector, QObject):
|
||||
"""
|
||||
if self.config.connections.endpoint == endpoint and self.config.connections.slot == slot:
|
||||
return
|
||||
if self.config.connections.slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
getattr(self, self.config.connections.slot), self.config.connections.endpoint
|
||||
)
|
||||
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.config.connections.slot, self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections = ProgressbarConnections(slot=slot, endpoint=endpoint)
|
||||
self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ logger = bec_logger.logger
|
||||
|
||||
class RingProgressBarConfig(ConnectionConfig):
|
||||
color_map: Optional[str] = Field(
|
||||
"plasma", description="Color scheme for the progress bars.", validate_default=True
|
||||
"magma", description="Color scheme for the progress bars.", validate_default=True
|
||||
)
|
||||
min_number_of_bars: int | None = Field(
|
||||
1, description="Minimum number of progress bars to display."
|
||||
@@ -123,7 +123,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
|
||||
# For updating bar behaviour
|
||||
self._auto_updates = True
|
||||
self._rings = []
|
||||
self._rings = None
|
||||
|
||||
if num_bars is not None:
|
||||
self.config.num_bars = max(
|
||||
@@ -134,12 +134,11 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
self.enable_auto_updates(self.config.auto_updates)
|
||||
|
||||
@property
|
||||
def rings(self) -> list[Ring]:
|
||||
"""Returns a list of all rings in the progress bar."""
|
||||
def rings(self):
|
||||
return self._rings
|
||||
|
||||
@rings.setter
|
||||
def rings(self, value: list[Ring]):
|
||||
def rings(self, value):
|
||||
self._rings = value
|
||||
|
||||
def update_config(self, config: RingProgressBarConfig | dict):
|
||||
@@ -170,15 +169,15 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
)
|
||||
for i in range(self.config.num_bars)
|
||||
]
|
||||
self._rings = [Ring(parent=self, config=config) for config in self.config.rings]
|
||||
self._rings = [
|
||||
Ring(parent_progress_widget=self, config=config) for config in self.config.rings
|
||||
]
|
||||
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
|
||||
min_size = self._calculate_minimum_size()
|
||||
self.setMinimumSize(min_size)
|
||||
# Set outer ring to listen to scan progress
|
||||
self.rings[0].set_update(mode="scan")
|
||||
self.update()
|
||||
|
||||
def add_ring(self, **kwargs) -> Ring:
|
||||
@@ -200,7 +199,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
directions=-1,
|
||||
**kwargs,
|
||||
)
|
||||
ring = Ring(parent=self, config=ring_config)
|
||||
ring = Ring(parent_progress_widget=self, config=ring_config)
|
||||
self.config.num_bars += 1
|
||||
self._rings.append(ring)
|
||||
self.config.rings.append(ring.config)
|
||||
@@ -219,10 +218,6 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
index(int): Index of the progress bar to remove.
|
||||
"""
|
||||
ring = self._find_ring_by_index(index)
|
||||
self._cleanup_ring(ring)
|
||||
self.update()
|
||||
|
||||
def _cleanup_ring(self, ring: Ring) -> None:
|
||||
ring.reset_connection()
|
||||
self._rings.remove(ring)
|
||||
self.config.rings.remove(ring.config)
|
||||
@@ -230,10 +225,8 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
self._reindex_rings()
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
# Remove ring from rpc, afterwards call close event.
|
||||
ring.rpc_register.remove_rpc(ring)
|
||||
ring.deleteLater()
|
||||
# del ring
|
||||
del ring
|
||||
self.update()
|
||||
|
||||
def _reindex_rings(self):
|
||||
"""
|
||||
@@ -299,7 +292,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
widget_class="Ring", index=i, start_positions=90 * 16, directions=-1
|
||||
)
|
||||
self.config.rings.append(new_ring_config)
|
||||
new_ring = Ring(parent=self, config=new_ring_config)
|
||||
new_ring = Ring(parent_progress_widget=self, config=new_ring_config)
|
||||
self._rings.append(new_ring)
|
||||
|
||||
elif num_bars < current_num_bars:
|
||||
@@ -649,6 +642,6 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
for ring in self._rings:
|
||||
self._cleanup_ring(ring)
|
||||
ring.reset_connection()
|
||||
self._rings.clear()
|
||||
super().cleanup()
|
||||
|
||||
@@ -57,7 +57,6 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
# self.layout.addWidget(self.table)
|
||||
self.table.setColumnCount(4)
|
||||
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status", "Cancel"])
|
||||
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||
|
||||
@@ -74,16 +73,15 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
"""
|
||||
Set the toolbar.
|
||||
"""
|
||||
widget_label = QLabel(text="Live Queue", parent=self)
|
||||
widget_label = QLabel("Live Queue")
|
||||
widget_label.setStyleSheet("font-weight: bold;")
|
||||
self.toolbar = ModularToolBar(
|
||||
parent=self,
|
||||
actions={
|
||||
"widget_label": WidgetAction(widget=widget_label),
|
||||
"separator_1": SeparatorAction(),
|
||||
"resume": WidgetAction(widget=ResumeButton(parent=self, toolbar=False)),
|
||||
"stop": WidgetAction(widget=StopButton(parent=self, toolbar=False)),
|
||||
"reset": WidgetAction(widget=ResetButton(parent=self, toolbar=False)),
|
||||
"resume": WidgetAction(widget=ResumeButton(toolbar=False)),
|
||||
"stop": WidgetAction(widget=StopButton(toolbar=False)),
|
||||
"reset": WidgetAction(widget=ResetButton(toolbar=False)),
|
||||
},
|
||||
target_widget=self,
|
||||
)
|
||||
@@ -225,7 +223,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
Returns:
|
||||
AbortButton: The abort button.
|
||||
"""
|
||||
abort_button = AbortButton(parent=self, scan_id=scan_id)
|
||||
abort_button = AbortButton(scan_id=scan_id)
|
||||
|
||||
abort_button.button.setText("")
|
||||
abort_button.button.setIcon(
|
||||
@@ -233,6 +231,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
)
|
||||
abort_button.button.setStyleSheet("background-color: rgba(0,0,0,0) ")
|
||||
abort_button.button.setFlat(True)
|
||||
|
||||
return abort_button
|
||||
|
||||
def delete_selected_row(self):
|
||||
@@ -240,7 +239,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
button = self.sender()
|
||||
row = self.table.indexAt(button.pos()).row()
|
||||
self.table.removeRow(row)
|
||||
button.close()
|
||||
|
||||
button.deleteLater()
|
||||
|
||||
def reset_content(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.widgets.services.bec_status_box.status_item import StatusItem
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.client import BECClient
|
||||
|
||||
# TODO : Put normal imports back when Pydantic gets faster
|
||||
@@ -76,7 +76,6 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
|
||||
|
||||
PLUGIN = True
|
||||
CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"]
|
||||
USER_ACCESS = ["get_server_state", "remove"]
|
||||
|
||||
service_update = Signal(BECServiceInfoContainer)
|
||||
bec_core_state = Signal(str)
|
||||
@@ -135,10 +134,6 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
|
||||
"QTreeWidget::item:selected {}"
|
||||
)
|
||||
|
||||
def get_server_state(self) -> str:
|
||||
"""Get the state ("RUNNING", "BUSY", "IDLE", "ERROR") of the BEC server"""
|
||||
return self.status_container[self.box_name]["info"].status
|
||||
|
||||
def _create_status_widget(
|
||||
self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
|
||||
) -> StatusItem:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Module for a StatusItem widget to display status and metrics for a BEC service.
|
||||
""" Module for a StatusItem widget to display status and metrics for a BEC service.
|
||||
The widget is bound to be used with the BECStatusBox widget."""
|
||||
|
||||
import enum
|
||||
|
||||
@@ -7,7 +7,7 @@ from qtpy.QtCore import QMimeData, Qt
|
||||
from qtpy.QtGui import QDrag
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
from qtpy.QtGui import QMouseEvent
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Utilities for filtering and formatting in the LogPanel"""
|
||||
""" Utilities for filtering and formatting in the LogPanel"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
from collections import deque
|
||||
from functools import partial, reduce
|
||||
from re import Pattern
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.connector import ConnectorBase
|
||||
@@ -16,7 +16,7 @@ from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import LogLevel, bec_logger
|
||||
from bec_lib.messages import LogMessage, StatusMessage
|
||||
from PySide6.QtCore import QObject
|
||||
from qtpy.QtCore import QDateTime, Qt, Signal
|
||||
from qtpy.QtCore import QDateTime, Qt, Signal, SignalInstance # type: ignore
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -52,9 +52,6 @@ from bec_widgets.widgets.utility.logpanel._util import (
|
||||
simple_color_format,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtCore import SignalInstance
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
|
||||
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
||||
|
||||
|
||||
class ColorButtonNative(BECWidget, QPushButton):
|
||||
"""A QPushButton subclass that displays a color.
|
||||
|
||||
The background is set to the given color and the button text is the hex code.
|
||||
The text color is chosen automatically (black if the background is light, white if dark)
|
||||
to guarantee good readability.
|
||||
"""
|
||||
|
||||
RPC = False
|
||||
PLUGIN = True
|
||||
ICON_NAME = "colors"
|
||||
|
||||
def __init__(self, parent=None, color="#000000", **kwargs):
|
||||
"""Initialize the color button.
|
||||
|
||||
Args:
|
||||
parent: Optional QWidget parent.
|
||||
color (str): The initial color in hex format (e.g., '#000000').
|
||||
"""
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self.set_color(color)
|
||||
|
||||
@SafeSlot()
|
||||
def set_color(self, color):
|
||||
"""Set the button's color and update its appearance.
|
||||
|
||||
Args:
|
||||
color (str or QColor): The new color to assign.
|
||||
"""
|
||||
if isinstance(color, QColor):
|
||||
self._color = color.name()
|
||||
else:
|
||||
self._color = color
|
||||
self._update_appearance()
|
||||
|
||||
@SafeProperty("QColor")
|
||||
def color(self):
|
||||
"""Return the current color in hex."""
|
||||
return self._color
|
||||
|
||||
@color.setter
|
||||
def color(self, value):
|
||||
"""Set the button's color and update its appearance."""
|
||||
self.set_color(value)
|
||||
|
||||
def _update_appearance(self):
|
||||
"""Update the button style based on the background color's brightness."""
|
||||
c = QColor(self._color)
|
||||
brightness = c.lightnessF()
|
||||
text_color = "#000000" if brightness > 0.5 else "#FFFFFF"
|
||||
self.setStyleSheet(f"background-color: {self._color}; color: {text_color};")
|
||||
self.setText(self._color)
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['color_button_native.py']}
|
||||
@@ -1,56 +0,0 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
|
||||
ColorButtonNative,
|
||||
)
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='ColorButtonNative' name='color_button_native'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class ColorButtonNativePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = ColorButtonNative(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Buttons"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(ColorButtonNative.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "color_button_native"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "ColorButtonNative"
|
||||
|
||||
def toolTip(self):
|
||||
return "A QPushButton subclass that displays a color."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user