mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-08 09:47:52 +02:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
092bed38fa | ||
| 50c84a766a | |||
| d22a3317ba | |||
| 6df1d0c31f | |||
| 946752a4b0 | |||
| c1f62ad6cb | |||
| a5adf3a97d | |||
|
|
76e3e0b60f | ||
| f18eeb9c5d | |||
| 32ce8e2818 | |||
| 23413cffab | |||
|
|
4bbb8fa519 | ||
|
|
a972369a72 | ||
| cd81e7f9ba | |||
|
|
e2b8118f67 | ||
| 5f925ba4e3 | |||
| fc68d2cf2d | |||
| 627b49b33a | |||
| a51ef04cdf | |||
| 40f4bce285 | |||
| 2b9fe6c959 | |||
| c2e16429c9 | |||
|
|
85ce2aa136 | ||
| fd5af01842 | |||
| 8a214c8978 | |||
|
|
f3214445f2 | ||
| 6bf84aea25 | |||
|
|
aace071f11 | ||
| bf86a030a0 | |||
|
|
358c979bf2 | ||
| c1bdc506e8 | |||
|
|
4febfb79df | ||
| 0854175acb | |||
| e090ac49b7 | |||
| e4521d9528 | |||
| 1d0490fff4 | |||
| 10cbb9a05c | |||
| 7073e75adf | |||
| e42ffd7c01 | |||
| 2bd6d00899 | |||
| c2a918ef4b | |||
| 6bbf5126cf | |||
| 728d4efd96 | |||
|
|
7926969996 | ||
| 61e5bde15f | |||
|
|
c8aa770de3 | ||
| 4d5df9608a | |||
| b718b438ba | |||
|
|
2f978c93c4 | ||
| b4e0664011 | |||
|
|
45fbf4015d | ||
|
|
0d81bdd4dd | ||
|
|
bb4c30ad80 | ||
| 3fd09fceef | |||
| 8eb8225a7f | |||
| 491d04467c | |||
|
|
3bcff75107 | ||
| 608590c542 | |||
|
|
012f7cf970 | ||
| cd17a4aad9 | |||
| f0dc992586 | |||
| fd1f9941e0 | |||
| 3384ca02bd | |||
| 959cedbbd5 | |||
| ca4f97503b | |||
| 22beadcad0 | |||
| b9af36a4f1 | |||
|
|
bdff736aa2 | ||
| 7cda2ed846 | |||
| cd9d22d0b4 | |||
|
|
37b80e16a0 | ||
| 7f0098f153 | |||
| 8489ef4a69 | |||
| 13976557fb | |||
|
|
06ad87ce0a | ||
| 00e3713181 | |||
|
|
62020f9965 | ||
| 2373c7e996 | |||
|
|
1f3566c105 | ||
| b8ae7b2e96 | |||
| 23674ccf59 | |||
| 1d8069e391 | |||
| 44cc06137c | |||
| 46a91784d2 | |||
| debd347b64 | |||
|
|
a13c3c44c8 | ||
| 25b2737aac | |||
| cf97cc1805 | |||
| 694a6c4960 | |||
| 9caae4cf40 | |||
| 2b06e34ecf | |||
| a9c8995ac0 | |||
|
|
1262c66fd6 | ||
| bde523806f | |||
|
|
16bca25d9c | ||
| 130cc24b35 | |||
| 8b2d6052e8 | |||
| 530797a556 | |||
| c660e5141f | |||
| 900153bc0b | |||
| 8dc72656ef | |||
| 170be0c7d3 | |||
| 1925e6ac7f | |||
|
|
b6cef2d27b | ||
| a9fce175b7 | |||
| 783d042e8c | |||
|
|
319a4206f2 | ||
| 76439866c1 | |||
|
|
ca600b057e | ||
| 6c494258f8 | |||
| 63a8da680d | |||
|
|
0f2bde1a0a | ||
| 0c76b0c495 | |||
| e594de3ca3 | |||
| adaad4f4d5 | |||
| 39c316d6ea | |||
| 3ba0fc4b44 | |||
| a6fc7993a3 | |||
| 324a5bd3d9 | |||
| 8929778f07 | |||
|
|
72b5c46912 | ||
| 244bca4e1e | |||
|
|
c50ace5818 | ||
| 25f28c47e3 | |||
| db720e8fa4 | |||
|
|
f10140e0f3 | ||
| 09c5a443aa | |||
| 3f5ab142a3 | |||
|
|
422d06d141 | ||
| 371bc485d0 | |||
|
|
70970ecf00 | ||
| 3d59c25aa9 | |||
|
|
70a06c5fd1 | ||
| 7ba8863d6a | |||
|
|
00ea8bb6c6 | ||
| e841468892 | |||
| 48a0e5831f | |||
| 1e9dd4cd25 | |||
| d10328cb5c | |||
|
|
6b248e93f5 | ||
| bc3085ab8c | |||
| 9cba696afd | |||
|
|
881b7a7e9d | ||
| 29a26b19f9 | |||
|
|
cba4d47f76 | ||
| 9f3dcc3ab3 | |||
| 57f75bd4d5 | |||
| 4456297beb | |||
|
|
ae26b43fb1 | ||
| 7484f5160c | |||
| 6421050116 | |||
|
|
5a137d1219 | ||
| d5a40dabc7 | |||
| f3da6e959e | |||
| 3a103410e7 | |||
| 3378051250 |
64
.github/workflows/child_repos.yml
vendored
Normal file
64
.github/workflows/child_repos.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Run Pytest with Coverage
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
BEC_CORE_BRANCH:
|
||||
description: 'Branch for BEC Core'
|
||||
required: false
|
||||
default: 'main'
|
||||
type: string
|
||||
OPHYD_DEVICES_BRANCH:
|
||||
description: 'Branch for Ophyd Devices'
|
||||
required: false
|
||||
default: 'main'
|
||||
type: string
|
||||
BEC_WIDGETS_BRANCH:
|
||||
description: 'Branch for BEC Widgets'
|
||||
required: false
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
|
||||
jobs:
|
||||
bec:
|
||||
name: BEC Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -el {0}
|
||||
|
||||
steps:
|
||||
- name: Checkout BEC
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bec-project/bec
|
||||
ref: ${{ inputs.BEC_CORE_BRANCH }}
|
||||
|
||||
- name: Install BEC and dependencies
|
||||
uses: ./.github/actions/bec_install
|
||||
with:
|
||||
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }}
|
||||
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }}
|
||||
PYTHON_VERSION: '3.11'
|
||||
- name: Run Pytest
|
||||
run: |
|
||||
cd ./bec
|
||||
pip install pytest pytest-random-order
|
||||
pytest -v --maxfail=2 --junitxml=report.xml --random-order ./bec_server/tests ./bec_ipython_client/tests/client_tests ./bec_lib/tests
|
||||
bec-e2e-test:
|
||||
name: BEC End2End Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout BEC
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bec-project/bec
|
||||
ref: ${{ inputs.BEC_CORE_BRANCH }}
|
||||
|
||||
- name: Run E2E Tests
|
||||
uses: ./.github/actions/bec_e2e_install
|
||||
with:
|
||||
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }}
|
||||
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }}
|
||||
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH }}
|
||||
PYTHON_VERSION: '3.11'
|
||||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -57,4 +57,24 @@ jobs:
|
||||
end2end-test:
|
||||
needs: [check_pr_status, formatter]
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: ./.github/workflows/end2end-conda.yml
|
||||
uses: ./.github/workflows/end2end-conda.yml
|
||||
|
||||
child-repos:
|
||||
needs: [check_pr_status, formatter]
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: ./.github/workflows/child_repos.yml
|
||||
with:
|
||||
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
|
||||
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main'}}
|
||||
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
|
||||
|
||||
plugin_repos:
|
||||
needs: [check_pr_status, formatter]
|
||||
if: needs.check_pr_status.outputs.branch-pr == ''
|
||||
uses: bec-project/bec/.github/workflows/plugin_repos.yml@main
|
||||
with:
|
||||
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
|
||||
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
|
||||
|
||||
secrets:
|
||||
GH_READ_TOKEN: ${{ secrets.GH_READ_TOKEN }}
|
||||
2
.github/workflows/formatter.yml
vendored
2
.github/workflows/formatter.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
isort --check --diff ./
|
||||
|
||||
- name: Check for disallowed imports from PySide
|
||||
run: '! grep -re "from PySide6\." bec_widgets/ | grep -v -e "PySide6.QtDesigner" -e "PySide6.scripts"'
|
||||
run: '! grep -re "from PySide6\." bec_widgets/ tests/ | grep -v -e "PySide6.QtDesigner" -e "PySide6.scripts"'
|
||||
|
||||
Pylint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/pytest-matrix.yml
vendored
2
.github/workflows/pytest-matrix.yml
vendored
@@ -56,4 +56,4 @@ jobs:
|
||||
- name: Run Pytest
|
||||
run: |
|
||||
pip install pytest pytest-random-order
|
||||
pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
|
||||
606
CHANGELOG.md
606
CHANGELOG.md
@@ -1,6 +1,612 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v2.33.3 (2025-07-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scan-history-view**: Account for async loading of scan history
|
||||
([`6df1d0c`](https://github.com/bec-project/bec_widgets/commit/6df1d0c31fb58c25b01e95e2247277ff2dd5d00e))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Improve scan history performance on loading full scan lists
|
||||
([`a5adf3a`](https://github.com/bec-project/bec_widgets/commit/a5adf3a97d9ff05cef833445c1e6cd8f35a9a2fa))
|
||||
|
||||
- Make ids a set, cleanup
|
||||
([`c1f62ad`](https://github.com/bec-project/bec_widgets/commit/c1f62ad6cb00d9b392a8e0b6247f5260dfb37256))
|
||||
|
||||
- Use client callback for scan history reload
|
||||
([`d22a331`](https://github.com/bec-project/bec_widgets/commit/d22a3317baeccfcc4e074dcef4e3912301d210c5))
|
||||
|
||||
- **scan-history**: Add spinner for loading time of history
|
||||
([`50c84a7`](https://github.com/bec-project/bec_widgets/commit/50c84a766a2b021768fb2c0e8ee00b8e5f058ba7))
|
||||
|
||||
- **scan-history**: Fix insert logic; cleanup
|
||||
([`946752a`](https://github.com/bec-project/bec_widgets/commit/946752a4b05804c2f59cb5c21e4c1d11709a7d44))
|
||||
|
||||
|
||||
## v2.33.2 (2025-07-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Delete choice dialog on close
|
||||
([`23413cf`](https://github.com/bec-project/bec_widgets/commit/23413cffabe721e35bb5bb726ec34d74dc4ffe05))
|
||||
|
||||
- Display short lists in SignalDisplay
|
||||
([`4bbb8fa`](https://github.com/bec-project/bec_widgets/commit/4bbb8fa519e8a90eebfcfa34e157493c9baa7880))
|
||||
|
||||
- Don't warn on empty DeviceEdit init
|
||||
([`f18eeb9`](https://github.com/bec-project/bec_widgets/commit/f18eeb9c5dccbd9348b6ee6d1477a8b7925d40fc))
|
||||
|
||||
- Remove config, directly set device+signal
|
||||
([`32ce8e2`](https://github.com/bec-project/bec_widgets/commit/32ce8e2818ceacda87e48399e3ed4df0cabb2335))
|
||||
|
||||
|
||||
## v2.33.1 (2025-07-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **cli**: Ensure guis are not started twice
|
||||
([`cd81e7f`](https://github.com/bec-project/bec_widgets/commit/cd81e7f9ba40be23f6b930d250f743276720b277))
|
||||
|
||||
|
||||
## v2.33.0 (2025-07-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **monaco**: Forward text changed signal
|
||||
([`a51ef04`](https://github.com/bec-project/bec_widgets/commit/a51ef04cdf0ac8abdb7008d78b13c75b86ce9e06))
|
||||
|
||||
### Build System
|
||||
|
||||
- Update bec and qtmonaco min dependencies
|
||||
([`5f925ba`](https://github.com/bec-project/bec_widgets/commit/5f925ba4e3840219e4473d6346ece6746076f718))
|
||||
|
||||
### Features
|
||||
|
||||
- **monaco**: Add insert, delete and lsp header
|
||||
([`fc68d2c`](https://github.com/bec-project/bec_widgets/commit/fc68d2cf2d6b161d8e3b9fc9daf6185d9197deba))
|
||||
|
||||
- **monaco**: Add vim mode
|
||||
([`627b49b`](https://github.com/bec-project/bec_widgets/commit/627b49b33a30e45b2bfecb57f090eecfa31af09d))
|
||||
|
||||
- **web console**: Add set_readonly method
|
||||
([`c2e1642`](https://github.com/bec-project/bec_widgets/commit/c2e16429c91de7cc0e672ba36224e9031c1c4234))
|
||||
|
||||
- **web console**: Add signal to indicate when the js backend is initialized
|
||||
([`2b9fe6c`](https://github.com/bec-project/bec_widgets/commit/2b9fe6c9590c8d18b7542307273176e118828681))
|
||||
|
||||
### Testing
|
||||
|
||||
- **web console**: Add tests for the web console
|
||||
([`40f4bce`](https://github.com/bec-project/bec_widgets/commit/40f4bce2854bcf333ce261229bd1703b80ced538))
|
||||
|
||||
|
||||
## v2.32.0 (2025-07-29)
|
||||
|
||||
### Features
|
||||
|
||||
- **dock area**: Add screenshot toolbar action
|
||||
([`fd5af01`](https://github.com/bec-project/bec_widgets/commit/fd5af0184279400ca6d8e5d2042f31be88d180f3))
|
||||
|
||||
- **rpc_timeout**: Add decorator to override the rpc timeout
|
||||
([`8a214c8`](https://github.com/bec-project/bec_widgets/commit/8a214c897899d0d94d5f262591a001c127d1b155))
|
||||
|
||||
|
||||
## v2.31.3 (2025-07-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **waveform**: Fallback mechanism for auto mode to use index if scan_report_devices are not
|
||||
available
|
||||
([`6bf84ae`](https://github.com/bec-project/bec_widgets/commit/6bf84aea2508ff01fe201c045ec055684da88593))
|
||||
|
||||
|
||||
## v2.31.2 (2025-07-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bec widgets**: Always call cleanup of child widgets on cleanup
|
||||
([`bf86a03`](https://github.com/bec-project/bec_widgets/commit/bf86a030a08b325a08e031ff71d0716a2f2f122b))
|
||||
|
||||
|
||||
## v2.31.1 (2025-07-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **image_base**: Fix cleanup of uninitialized image layer
|
||||
([`c1bdc50`](https://github.com/bec-project/bec_widgets/commit/c1bdc506e8099f178acdccbe0e1109deeeaaca38))
|
||||
|
||||
|
||||
## v2.31.0 (2025-07-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bec_main_window**: Main window have unified status bar on macOS
|
||||
([`1d0490f`](https://github.com/bec-project/bec_widgets/commit/1d0490fff428d51f2cdb7d35a954a7cd62cbb65c))
|
||||
|
||||
- **color_button_native**: Removed BECWidget inheritance
|
||||
([`e42ffd7`](https://github.com/bec-project/bec_widgets/commit/e42ffd7c015a026d8e0967ac6b5866cbbea7bfed))
|
||||
|
||||
- **decimal_spinbox**: Removed BECWidget inheritance
|
||||
([`2bd6d00`](https://github.com/bec-project/bec_widgets/commit/2bd6d0089955172134afb4d39939890026ed43f0))
|
||||
|
||||
- **launch_window**: Logic for custom main window apps adjusted
|
||||
([`e090ac4`](https://github.com/bec-project/bec_widgets/commit/e090ac49b72fa15ebf1c09164ff3c6de577cb939))
|
||||
|
||||
- **plugin_utils**: Plugins can be created from QWidgets, no need for BECWidget base class for
|
||||
plugin creation
|
||||
([`c2a918e`](https://github.com/bec-project/bec_widgets/commit/c2a918ef4b77ccd7fa43d1bc0b907d55a17a6c95))
|
||||
|
||||
- **scan_progressbar**: Added kwargs to init
|
||||
([`7073e75`](https://github.com/bec-project/bec_widgets/commit/7073e75adf0eeb81f4f8e27eb99fc1b7a395c751))
|
||||
|
||||
- **utils**: Plugin template createWidget do not initialise widgets by default
|
||||
([`728d4ef`](https://github.com/bec-project/bec_widgets/commit/728d4efd9646ffcecd7d1a2f70988a7d7c799124))
|
||||
|
||||
- **widgets**: Added missing __init__ files
|
||||
([`6bbf512`](https://github.com/bec-project/bec_widgets/commit/6bbf5126cf586063ed08d6cd489d6a9af28eac35))
|
||||
|
||||
### Features
|
||||
|
||||
- **bec_main_window**: Plugin and rpc created
|
||||
([`e4521d9`](https://github.com/bec-project/bec_widgets/commit/e4521d95286bbc598c3c05f357d247d950477b71))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **widgets**: All plugins regenerated
|
||||
([`10cbb9a`](https://github.com/bec-project/bec_widgets/commit/10cbb9a05cb96a791448caff4ffc4115b76146d7))
|
||||
|
||||
### Testing
|
||||
|
||||
- **launch_window**: Mainwindow raise test removed, features is supported now
|
||||
([`0854175`](https://github.com/bec-project/bec_widgets/commit/0854175acbda1d4de71358aec028539552a26448))
|
||||
|
||||
|
||||
## v2.30.6 (2025-07-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **waveform**: Autorange is applied with 150ms delay after curve is added
|
||||
([`61e5bde`](https://github.com/bec-project/bec_widgets/commit/61e5bde15f0e1ebe185ddbe81cd71ad581ae6009))
|
||||
|
||||
|
||||
## v2.30.5 (2025-07-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **positioner-box**: Test to fix handling of none integer values for precision
|
||||
([`b718b43`](https://github.com/bec-project/bec_widgets/commit/b718b438bacff6eb6cd6015f1a67dcf75c05dce4))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **positioner-box**: Cleanup, accept float precision
|
||||
([`4d5df96`](https://github.com/bec-project/bec_widgets/commit/4d5df9608a9438b9f6d7508c323eb3772e53f37d))
|
||||
|
||||
|
||||
## v2.30.4 (2025-07-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **cli**: Remove stderr from cli output when not using rpc
|
||||
([`b4e0664`](https://github.com/bec-project/bec_widgets/commit/b4e0664011682cae9966aa2632210a6b60e11714))
|
||||
|
||||
|
||||
## v2.30.3 (2025-07-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Cleanup subscriptions in device browser
|
||||
([`0d81bdd`](https://github.com/bec-project/bec_widgets/commit/0d81bdd4ddb4ec474a414b107cbc7fc865253934))
|
||||
|
||||
|
||||
## v2.30.2 (2025-07-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Factor out device name function and add test
|
||||
([`8eb8225`](https://github.com/bec-project/bec_widgets/commit/8eb8225a7f56014d6093aa142b3a5d071837982e))
|
||||
|
||||
- **rpc_base**: Rpc_call wrapper passes full_name for Devices indeed of name
|
||||
([`491d044`](https://github.com/bec-project/bec_widgets/commit/491d04467c8ce4e116d61e614895d1dcc6b4b201))
|
||||
|
||||
### Testing
|
||||
|
||||
- **test_plotting_framework_e2e**: Added test for waveform with passing device from dev container
|
||||
([`3fd09fc`](https://github.com/bec-project/bec_widgets/commit/3fd09fceef2ffa7e7c3eee20176304bafb00d0db))
|
||||
|
||||
|
||||
## v2.30.1 (2025-07-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Ignore KeyError in SignalLabel
|
||||
([`608590c`](https://github.com/bec-project/bec_widgets/commit/608590c5421368d5bba0e4b0f5187d90cac323be))
|
||||
|
||||
|
||||
## v2.30.0 (2025-07-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **device_browser**: Display signal for signals
|
||||
([`3384ca0`](https://github.com/bec-project/bec_widgets/commit/3384ca02bdb5a2798ad3339ecf3e2ba7c121e28f))
|
||||
|
||||
- **device_signal_display**: Don't read omitted
|
||||
([`b9af36a`](https://github.com/bec-project/bec_widgets/commit/b9af36a4f1c91e910d4fc738b17b90e92287a7e3))
|
||||
|
||||
- **signal_label**: Rewrite reading selection logic
|
||||
([`cd17a4a`](https://github.com/bec-project/bec_widgets/commit/cd17a4aad905296eb0460ecc27e5920f5c2e8fe5))
|
||||
|
||||
- **signal_label**: Show all signals by default
|
||||
([`22beadc`](https://github.com/bec-project/bec_widgets/commit/22beadcad061b328c986414f30fef57b64bad693))
|
||||
|
||||
- **signal_label**: Update signal from dialog correctly
|
||||
([`959cedb`](https://github.com/bec-project/bec_widgets/commit/959cedbbd5a123eef5f3370287bf6476c48caab9))
|
||||
|
||||
- **signal_label**: Use read() instead of get() for init
|
||||
([`f0dc992`](https://github.com/bec-project/bec_widgets/commit/f0dc99258607a5cc8af51686d01f7fd54ae2779f))
|
||||
|
||||
### Chores
|
||||
|
||||
- Update client.py
|
||||
([`fd1f994`](https://github.com/bec-project/bec_widgets/commit/fd1f9941e046b7ae1e247dde39c20bcbc37ac189))
|
||||
|
||||
### Features
|
||||
|
||||
- **signal_label**: Property to display array data or not
|
||||
([`ca4f975`](https://github.com/bec-project/bec_widgets/commit/ca4f97503bf06363e8e8a5d494a9857223da4104))
|
||||
|
||||
|
||||
## v2.29.0 (2025-07-22)
|
||||
|
||||
### Features
|
||||
|
||||
- **notification_banner**: Notification centre for alarms implemented into BECMainWindow
|
||||
([`cd9d22d`](https://github.com/bec-project/bec_widgets/commit/cd9d22d0b40d633af76cb1188b57feb7b6a5dbf2))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **notification_banner**: Becnotificationbroker done as singleton to sync all windows in the
|
||||
session
|
||||
([`7cda2ed`](https://github.com/bec-project/bec_widgets/commit/7cda2ed846d3c27799f4f15f6c5c667631b1ca55))
|
||||
|
||||
|
||||
## v2.28.0 (2025-07-21)
|
||||
|
||||
### Features
|
||||
|
||||
- Disable editing while scan active
|
||||
([`1397655`](https://github.com/bec-project/bec_widgets/commit/13976557fbdb71a1161029521d81a655d25dd134))
|
||||
|
||||
- Remove and readd device for config changes
|
||||
([`8489ef4`](https://github.com/bec-project/bec_widgets/commit/8489ef4a69d69b39648b1a9270012f14f95c6121))
|
||||
|
||||
- Save and load config from devicebrowser
|
||||
([`7f0098f`](https://github.com/bec-project/bec_widgets/commit/7f0098f1533d419cc75801c4d6cbea485c7bbf94))
|
||||
|
||||
|
||||
## v2.27.1 (2025-07-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **image_roi_tree**: Rois signals are disconnected when roi tree widget is closed
|
||||
([`00e3713`](https://github.com/bec-project/bec_widgets/commit/00e3713181916a432e4e9dec8a0d80205914cf77))
|
||||
|
||||
|
||||
## v2.27.0 (2025-07-17)
|
||||
|
||||
### Features
|
||||
|
||||
- Add monaco editor
|
||||
([`2373c7e`](https://github.com/bec-project/bec_widgets/commit/2373c7e996566a5b84c5a50e1c3e69de885713db))
|
||||
|
||||
|
||||
## v2.26.0 (2025-07-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **config label**: Reset offset when toggling the label action
|
||||
([`b8ae7b2`](https://github.com/bec-project/bec_widgets/commit/b8ae7b2e96071b6dc59dae7ffa72bbedc6aaea23))
|
||||
|
||||
- **performance_bundle**: Fix performance bundle cleanup
|
||||
([`23674cc`](https://github.com/bec-project/bec_widgets/commit/23674ccf592a2caa0b57ae64ad1499c270b7d469))
|
||||
|
||||
### Features
|
||||
|
||||
- **device combobox**: Add option to insert an empty element
|
||||
([`debd347`](https://github.com/bec-project/bec_widgets/commit/debd347b64a3d2ca07ddcd5ef3a3394d1ffb67e3))
|
||||
|
||||
- **heatmap**: Add interpolation and oversampling UI components
|
||||
([`1d8069e`](https://github.com/bec-project/bec_widgets/commit/1d8069e391412e3096a3c1e7181398dd4e609650))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **image_base**: Cleanup
|
||||
([`46a9178`](https://github.com/bec-project/bec_widgets/commit/46a91784d237137128965ad585e38085e931e5d4))
|
||||
|
||||
### Testing
|
||||
|
||||
- **history**: Add history message helper methods to conftest
|
||||
([`44cc061`](https://github.com/bec-project/bec_widgets/commit/44cc06137ccfbc087bdd3005156ff28effe05f23))
|
||||
|
||||
|
||||
## v2.25.0 (2025-07-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bec-progressbar**: Add flag for theme update
|
||||
([`694a6c4`](https://github.com/bec-project/bec_widgets/commit/694a6c49608b68e25dc0c76b58855b96f3f0ef0b))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- **bec**: Add child_repos test for bec (unit and e2e tests)
|
||||
([`a9c8995`](https://github.com/bec-project/bec_widgets/commit/a9c8995ac0b39f6bc327887f43f7d4d6e6e89db2))
|
||||
|
||||
- **plugin**: Add plugin repository test to BW ci
|
||||
([`2b06e34`](https://github.com/bec-project/bec_widgets/commit/2b06e34ecff8c0a92a2b235f375e837729736b2a))
|
||||
|
||||
### Features
|
||||
|
||||
- **scan-history-browser**: Add history browser and history metadata viewer
|
||||
([`9caae4c`](https://github.com/bec-project/bec_widgets/commit/9caae4cf40d3876175b827abb735ae227ae0bcea))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Add additional components for history metadata, device view and popup ui
|
||||
([`cf97cc1`](https://github.com/bec-project/bec_widgets/commit/cf97cc1805e16073c7849d1f9375e2ebd2176b70))
|
||||
|
||||
- Cleanup, add compact popup view for scan_history_browser and update tests
|
||||
([`25b2737`](https://github.com/bec-project/bec_widgets/commit/25b2737aacfaa45f255afb6ebf467d5781165a8e))
|
||||
|
||||
|
||||
## v2.24.1 (2025-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Update signal label for device_edit changes
|
||||
([`bde5238`](https://github.com/bec-project/bec_widgets/commit/bde523806fdb6ab224b485f65b615f89dfe20b7b))
|
||||
|
||||
|
||||
## v2.24.0 (2025-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Hide validity LED, show message as tooltip
|
||||
([`530797a`](https://github.com/bec-project/bec_widgets/commit/530797a5568957dde9f47f417310f5c4d2493906))
|
||||
|
||||
- Validate some config data
|
||||
([`c660e51`](https://github.com/bec-project/bec_widgets/commit/c660e5141f191a782c224ee1b83536793639eecb))
|
||||
|
||||
- **device_browser**: Un-nest exception
|
||||
([`8b2d605`](https://github.com/bec-project/bec_widgets/commit/8b2d6052e808f8b4063e5f45c40e4460524f044e))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Docstring for config dialog
|
||||
([`1925e6a`](https://github.com/bec-project/bec_widgets/commit/1925e6ac7f98875eb5980637ae3293e22b459e28))
|
||||
|
||||
### Features
|
||||
|
||||
- (#495) add devices through browser
|
||||
([`170be0c`](https://github.com/bec-project/bec_widgets/commit/170be0c7d3bb1f6e5f2305958909ef68cd987fbd))
|
||||
|
||||
- **#495**: Add validation against existing device names
|
||||
([`900153b`](https://github.com/bec-project/bec_widgets/commit/900153bc0b8cec7bad82e23b3772c66e84900a17))
|
||||
|
||||
- **device_browser**: Connect update to item refresh
|
||||
([`130cc24`](https://github.com/bec-project/bec_widgets/commit/130cc24b351684358558ab81c0111f10f9abb11f))
|
||||
|
||||
- **device_browser**: Device deletion from config
|
||||
([`8dc7265`](https://github.com/bec-project/bec_widgets/commit/8dc72656ef46ae7be886f9da59beb768f5381b4f))
|
||||
|
||||
|
||||
## v2.23.0 (2025-07-11)
|
||||
|
||||
### Features
|
||||
|
||||
- **widget_finder**: Widget to fetch any other widget by class from currently running app
|
||||
([`a9fce17`](https://github.com/bec-project/bec_widgets/commit/a9fce175b720ad85a5cefcab99d79fbcb971ff4a))
|
||||
|
||||
- **widget_io**: Utility function to find widget in the app by class
|
||||
([`783d042`](https://github.com/bec-project/bec_widgets/commit/783d042e8c469774fc8407921462a99c96f6d408))
|
||||
|
||||
|
||||
## v2.22.2 (2025-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **plot_base**: Autorange takes into account only visible curves
|
||||
([`7643986`](https://github.com/bec-project/bec_widgets/commit/76439866c1fd09cb7d9d48dfccdc7b1943bfbc0f))
|
||||
|
||||
|
||||
## v2.22.1 (2025-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **crosshair**: Crosshair mouse_moved can be set manually
|
||||
([`63a8da6`](https://github.com/bec-project/bec_widgets/commit/63a8da680d263a50102aacf463ec6f6252562f9d))
|
||||
|
||||
- **heatmap**: Fix pixel size calculation for arbitrary shapes
|
||||
([`6c49425`](https://github.com/bec-project/bec_widgets/commit/6c494258f82059a2472f43bb8287390ce1aba704))
|
||||
|
||||
|
||||
## v2.22.0 (2025-07-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **crosshair**: Add slot to reset mouse markers
|
||||
([`adaad4f`](https://github.com/bec-project/bec_widgets/commit/adaad4f4d5ebf775a337e23a944ba9eb289d01a0))
|
||||
|
||||
- **crosshair**: Fix crosshair support for transformations
|
||||
([`3ba0fc4`](https://github.com/bec-project/bec_widgets/commit/3ba0fc4b442e5926f27a13f09d628c30987f2cf8))
|
||||
|
||||
- **image**: Reset crosshair on new scan
|
||||
([`e594de3`](https://github.com/bec-project/bec_widgets/commit/e594de3ca39970f91f5842693eeb1fac393eaa34))
|
||||
|
||||
- **image item**: Fix processor for nans in images
|
||||
([`39c316d`](https://github.com/bec-project/bec_widgets/commit/39c316d6eadfdfbd483661b67720a7e224a46712))
|
||||
|
||||
- **image_base**: Move cbar init to image base
|
||||
([`8929778`](https://github.com/bec-project/bec_widgets/commit/8929778f073c40a9eabba7eda2415fc9af1072bb))
|
||||
|
||||
- **image_processor**: Support for nans in nd arrays
|
||||
([`a6fc799`](https://github.com/bec-project/bec_widgets/commit/a6fc7993a3d22cfd086310c8e6dad3f9f3d1e9fe))
|
||||
|
||||
### Features
|
||||
|
||||
- Add heatmap widget
|
||||
([`0c76b0c`](https://github.com/bec-project/bec_widgets/commit/0c76b0c49598d1456aab266b483de327788028fd))
|
||||
|
||||
- **image_item**: Add support for qtransform
|
||||
([`324a5bd`](https://github.com/bec-project/bec_widgets/commit/324a5bd3d9ed278495c6ba62453b02061900ae32))
|
||||
|
||||
|
||||
## v2.21.4 (2025-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **image_roi_tree**: Changing color dialog from ColorButtonNative is open once
|
||||
([`244bca4`](https://github.com/bec-project/bec_widgets/commit/244bca4e1ec7c00109534b9f503ff2eb125c1ffe))
|
||||
|
||||
|
||||
## v2.21.3 (2025-07-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector**: Remove safeslot for now
|
||||
([`25f28c4`](https://github.com/bec-project/bec_widgets/commit/25f28c47e32af1be7778803dc27d8c2a367172ed))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **toolbar**: Split toolbar into components, bundles and connections
|
||||
([`db720e8`](https://github.com/bec-project/bec_widgets/commit/db720e8fa46bb2fb10c73afa1b4f039cd256d68b))
|
||||
|
||||
|
||||
## v2.21.2 (2025-06-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **waveform**: Fix waveform categorisation for aborted scans
|
||||
([`09c5a44`](https://github.com/bec-project/bec_widgets/commit/09c5a443aac675f02fa1e38179deb9863af152e2))
|
||||
|
||||
### Testing
|
||||
|
||||
- Assert config for equality, not identity
|
||||
([`3f5ab14`](https://github.com/bec-project/bec_widgets/commit/3f5ab142a3cb5446261c4faebdc7b13f10ef4a80))
|
||||
|
||||
|
||||
## v2.21.1 (2025-06-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **sbb monitor**: Add missing pyproject file
|
||||
([`371bc48`](https://github.com/bec-project/bec_widgets/commit/371bc485d060404433082c9e3e00780961ce6ae3))
|
||||
|
||||
|
||||
## v2.21.0 (2025-06-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **sbb monitor**: Add sbb monitor widget
|
||||
([`3d59c25`](https://github.com/bec-project/bec_widgets/commit/3d59c25aa93590a62ab4d31a4ab08589402bf407))
|
||||
|
||||
|
||||
## v2.20.1 (2025-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **signal input base**: Unregister callback to avoid accessing deleted qt objects
|
||||
([`7ba8863`](https://github.com/bec-project/bec_widgets/commit/7ba8863d6a0c21f772e4ef8a5d4180c2a7ab49cb))
|
||||
|
||||
|
||||
## v2.20.0 (2025-06-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **curve_settings**: Larger minimalWidth for the x device combobox selection
|
||||
([`48a0e58`](https://github.com/bec-project/bec_widgets/commit/48a0e5831feccd30f24218821bbc9d73f8c47933))
|
||||
|
||||
### Features
|
||||
|
||||
- **waveform**: Move x axis selection to a combobox
|
||||
([`d10328c`](https://github.com/bec-project/bec_widgets/commit/d10328cb5c775a9b7b40ed4e9f2889e63eb039ff))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **curve settings**: Move signal logic to SignalCombobox
|
||||
([`e841468`](https://github.com/bec-project/bec_widgets/commit/e84146889210165de1c4e63eb20b39f30cc5c623))
|
||||
|
||||
### Testing
|
||||
|
||||
- **curve settings**: Add curve tree elements to the dialog test
|
||||
([`1e9dd4c`](https://github.com/bec-project/bec_widgets/commit/1e9dd4cd2561d37bdda1cd86b511295c259b2831))
|
||||
|
||||
|
||||
## v2.19.4 (2025-06-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **curve tree**: Remove manual interception of the close event; call parent cleanup
|
||||
([`bc3085a`](https://github.com/bec-project/bec_widgets/commit/bc3085ab8cb6688da358df4a7c07fc213a99f2df))
|
||||
|
||||
- **waveform**: Curve tree elements must clean up signal combobox
|
||||
([`9cba696`](https://github.com/bec-project/bec_widgets/commit/9cba696afd3300a76678dfdc4226604696cc3696))
|
||||
|
||||
|
||||
## v2.19.3 (2025-06-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scan_control**: Safeguard against empty history; reversed history to fetch the newest scan
|
||||
([`29a26b1`](https://github.com/bec-project/bec_widgets/commit/29a26b19f9ab829b0d877c3233613a0936db0a12))
|
||||
|
||||
|
||||
## v2.19.2 (2025-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scan_control**: Scan parameters fetched from the scan_history, fix #707
|
||||
([`4456297`](https://github.com/bec-project/bec_widgets/commit/4456297beb940b147882f96caee6fb19aaf93c73))
|
||||
|
||||
### Build System
|
||||
|
||||
- Bec_lib 3.44 required
|
||||
([`9f3dcc3`](https://github.com/bec-project/bec_widgets/commit/9f3dcc3ab30a2c238ffffa8d594735ccaf6f1ca4))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **scan_control**: Request_last_executed_scan_parameters logic adjusted
|
||||
([`57f75bd`](https://github.com/bec-project/bec_widgets/commit/57f75bd4d506ca4d8dc982f3051d0d4c29b0d41c))
|
||||
|
||||
|
||||
## v2.19.1 (2025-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **launch_window**: Number of remaining connections extended to 4
|
||||
([`7484f51`](https://github.com/bec-project/bec_widgets/commit/7484f5160c8c6d632fd27996035ff6c0dda2e657))
|
||||
|
||||
|
||||
## v2.19.0 (2025-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **ci**: Extend check for pyside import to tests
|
||||
([`d5a40da`](https://github.com/bec-project/bec_widgets/commit/d5a40dabc74753acad05e3eb6b121499fc1e03d7))
|
||||
|
||||
### Features
|
||||
|
||||
- (#494) add signal display to device browser
|
||||
([`f3da6e9`](https://github.com/bec-project/bec_widgets/commit/f3da6e959e0416827ee5d02e34e6ad0ecfc8e5e7))
|
||||
|
||||
- (#494) add tabbed layout for device item
|
||||
([`3378051`](https://github.com/bec-project/bec_widgets/commit/337805125098c3e028a17b74ef6d9ae4b9ba3d6d))
|
||||
|
||||
- (#494) display device signals
|
||||
([`3a10341`](https://github.com/bec-project/bec_widgets/commit/3a103410e7448256a56b59bb3276fee056ec42a0))
|
||||
|
||||
|
||||
## v2.18.0 (2025-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -27,11 +27,11 @@ 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
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
from bec_widgets.utils.ui_loader import UILoader
|
||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, UILaunchWindow
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
|
||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
@@ -395,20 +395,24 @@ class LaunchWindow(BECMainWindow):
|
||||
if isinstance(result_widget, BECMainWindow):
|
||||
result_widget.show()
|
||||
else:
|
||||
window = BECMainWindow()
|
||||
window = BECMainWindowNoRPC()
|
||||
window.setCentralWidget(result_widget)
|
||||
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
|
||||
window.show()
|
||||
return result_widget
|
||||
|
||||
def _launch_custom_ui_file(self, ui_file: str | None) -> BECMainWindow:
|
||||
# Load the custom UI file
|
||||
"""
|
||||
Load a custom .ui file. If the top-level widget is a MainWindow subclass,
|
||||
instantiate it directly; otherwise, embed it in a UILaunchWindow.
|
||||
"""
|
||||
if ui_file is None:
|
||||
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)
|
||||
|
||||
# Parse the UI to detect top-level widget class
|
||||
tree = ET.parse(ui_file)
|
||||
root = tree.getroot()
|
||||
# Check if the top-level widget is a QMainWindow
|
||||
@@ -416,19 +420,22 @@ class LaunchWindow(BECMainWindow):
|
||||
if widget is None:
|
||||
raise ValueError("No widget found in the UI file.")
|
||||
|
||||
if widget.attrib.get("class") == "QMainWindow":
|
||||
raise ValueError(
|
||||
"Loading a QMainWindow from a UI file is currently not supported. "
|
||||
"If you need this, please contact the BEC team or create a ticket on gitlab.psi.ch/bec/bec_widgets."
|
||||
)
|
||||
# Load the UI into a widget
|
||||
loader = UILoader(None)
|
||||
loaded = loader.loader(ui_file)
|
||||
|
||||
# Display the UI in a BECMainWindow
|
||||
if isinstance(loaded, BECMainWindow):
|
||||
window = loaded
|
||||
window.object_name = filename
|
||||
else:
|
||||
window = BECMainWindow(object_name=filename)
|
||||
window.setCentralWidget(loaded)
|
||||
|
||||
window = UILaunchWindow(object_name=filename)
|
||||
QApplication.processEvents()
|
||||
result_widget = UILoader(window).loader(ui_file)
|
||||
window.setCentralWidget(result_widget)
|
||||
window.setWindowTitle(f"BEC - {window.object_name}")
|
||||
window.setWindowTitle(f"BEC - {filename}")
|
||||
window.show()
|
||||
logger.info(f"Object name of new instance: {result_widget.objectName()}, {window.gui_id}")
|
||||
logger.info(f"Launched custom UI: {filename}, type: {type(window).__name__}")
|
||||
return window
|
||||
|
||||
def _launch_auto_update(self, auto_update: str) -> AutoUpdates:
|
||||
@@ -451,7 +458,7 @@ class LaunchWindow(BECMainWindow):
|
||||
|
||||
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||
|
||||
window = BECMainWindow()
|
||||
window = BECMainWindowNoRPC()
|
||||
|
||||
widget_instance = widget(root_widget=True, object_name=name)
|
||||
assert isinstance(widget_instance, QWidget)
|
||||
@@ -542,7 +549,7 @@ class LaunchWindow(BECMainWindow):
|
||||
remaining_connections = [
|
||||
connection for connection in connections.values() if connection.parent_id != self.gui_id
|
||||
]
|
||||
return len(remaining_connections) <= 2
|
||||
return len(remaining_connections) <= 4
|
||||
|
||||
def _turn_off_the_lights(self, connections: dict):
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import Literal, Optional
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
||||
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -29,6 +29,7 @@ class _WidgetsEnumType(str, enum.Enum):
|
||||
_Widgets = {
|
||||
"AbortButton": "AbortButton",
|
||||
"BECDockArea": "BECDockArea",
|
||||
"BECMainWindow": "BECMainWindow",
|
||||
"BECProgressBar": "BECProgressBar",
|
||||
"BECQueue": "BECQueue",
|
||||
"BECStatusBox": "BECStatusBox",
|
||||
@@ -37,19 +38,24 @@ _Widgets = {
|
||||
"DeviceBrowser": "DeviceBrowser",
|
||||
"DeviceComboBox": "DeviceComboBox",
|
||||
"DeviceLineEdit": "DeviceLineEdit",
|
||||
"Heatmap": "Heatmap",
|
||||
"Image": "Image",
|
||||
"LogPanel": "LogPanel",
|
||||
"Minesweeper": "Minesweeper",
|
||||
"MonacoWidget": "MonacoWidget",
|
||||
"MotorMap": "MotorMap",
|
||||
"MultiWaveform": "MultiWaveform",
|
||||
"PositionIndicator": "PositionIndicator",
|
||||
"PositionerBox": "PositionerBox",
|
||||
"PositionerBox2D": "PositionerBox2D",
|
||||
"PositionerControlLine": "PositionerControlLine",
|
||||
"PositionerGroup": "PositionerGroup",
|
||||
"ResetButton": "ResetButton",
|
||||
"ResumeButton": "ResumeButton",
|
||||
"RingProgressBar": "RingProgressBar",
|
||||
"SBBMonitor": "SBBMonitor",
|
||||
"ScanControl": "ScanControl",
|
||||
"ScanProgressBar": "ScanProgressBar",
|
||||
"ScatterWaveform": "ScatterWaveform",
|
||||
"SignalComboBox": "SignalComboBox",
|
||||
"SignalLabel": "SignalLabel",
|
||||
@@ -408,6 +414,13 @@ class BECDockArea(RPCBase):
|
||||
dict: The state of the dock area.
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def restore_state(
|
||||
self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom"
|
||||
@@ -422,6 +435,14 @@ class BECDockArea(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECMainWindow(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class BECProgressBar(RPCBase):
|
||||
"""A custom progress bar with smooth transitions. The displayed text can be customized using a template."""
|
||||
|
||||
@@ -1180,6 +1201,551 @@ class EllipticalROI(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class Heatmap(RPCBase):
|
||||
"""Heatmap widget for visualizing 2d grid data with color mapping for the z-axis."""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_toolbar(self) -> "bool":
|
||||
"""
|
||||
Show Toolbar.
|
||||
"""
|
||||
|
||||
@enable_toolbar.setter
|
||||
@rpc_call
|
||||
def enable_toolbar(self) -> "bool":
|
||||
"""
|
||||
Show Toolbar.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_side_panel(self) -> "bool":
|
||||
"""
|
||||
Show Side Panel
|
||||
"""
|
||||
|
||||
@enable_side_panel.setter
|
||||
@rpc_call
|
||||
def enable_side_panel(self) -> "bool":
|
||||
"""
|
||||
Show Side Panel
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self) -> "bool":
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@enable_fps_monitor.setter
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self) -> "bool":
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def title(self) -> "str":
|
||||
"""
|
||||
Set title of the plot.
|
||||
"""
|
||||
|
||||
@title.setter
|
||||
@rpc_call
|
||||
def title(self) -> "str":
|
||||
"""
|
||||
Set title of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_label(self) -> "str":
|
||||
"""
|
||||
The set label for the x-axis.
|
||||
"""
|
||||
|
||||
@x_label.setter
|
||||
@rpc_call
|
||||
def x_label(self) -> "str":
|
||||
"""
|
||||
The set label for the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_label(self) -> "str":
|
||||
"""
|
||||
The set label for the y-axis.
|
||||
"""
|
||||
|
||||
@y_label.setter
|
||||
@rpc_call
|
||||
def y_label(self) -> "str":
|
||||
"""
|
||||
The set label for the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the x limits of the plot.
|
||||
"""
|
||||
|
||||
@x_limits.setter
|
||||
@rpc_call
|
||||
def x_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the x limits of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the y limits of the plot.
|
||||
"""
|
||||
|
||||
@y_limits.setter
|
||||
@rpc_call
|
||||
def y_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the y limits of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the x-axis.
|
||||
"""
|
||||
|
||||
@x_grid.setter
|
||||
@rpc_call
|
||||
def x_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@y_grid.setter
|
||||
@rpc_call
|
||||
def y_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def inner_axes(self) -> "bool":
|
||||
"""
|
||||
Show inner axes of the plot widget.
|
||||
"""
|
||||
|
||||
@inner_axes.setter
|
||||
@rpc_call
|
||||
def inner_axes(self) -> "bool":
|
||||
"""
|
||||
Show inner axes of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def outer_axes(self) -> "bool":
|
||||
"""
|
||||
Show the outer axes of the plot widget.
|
||||
"""
|
||||
|
||||
@outer_axes.setter
|
||||
@rpc_call
|
||||
def outer_axes(self) -> "bool":
|
||||
"""
|
||||
Show the outer axes of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def auto_range_x(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the x-axis.
|
||||
"""
|
||||
|
||||
@auto_range_x.setter
|
||||
@rpc_call
|
||||
def auto_range_x(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def auto_range_y(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@auto_range_y.setter
|
||||
@rpc_call
|
||||
def auto_range_y(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def minimal_crosshair_precision(self) -> "int":
|
||||
"""
|
||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||
"""
|
||||
|
||||
@minimal_crosshair_precision.setter
|
||||
@rpc_call
|
||||
def minimal_crosshair_precision(self) -> "int":
|
||||
"""
|
||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color_map(self) -> "str":
|
||||
"""
|
||||
Set the color map of the image.
|
||||
"""
|
||||
|
||||
@color_map.setter
|
||||
@rpc_call
|
||||
def color_map(self) -> "str":
|
||||
"""
|
||||
Set the color map of the image.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def v_range(self) -> "QPointF":
|
||||
"""
|
||||
Set the v_range of the main image.
|
||||
"""
|
||||
|
||||
@v_range.setter
|
||||
@rpc_call
|
||||
def v_range(self) -> "QPointF":
|
||||
"""
|
||||
Set the v_range of the main image.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def v_min(self) -> "float":
|
||||
"""
|
||||
Get the minimum value of the v_range.
|
||||
"""
|
||||
|
||||
@v_min.setter
|
||||
@rpc_call
|
||||
def v_min(self) -> "float":
|
||||
"""
|
||||
Get the minimum value of the v_range.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def v_max(self) -> "float":
|
||||
"""
|
||||
Get the maximum value of the v_range.
|
||||
"""
|
||||
|
||||
@v_max.setter
|
||||
@rpc_call
|
||||
def v_max(self) -> "float":
|
||||
"""
|
||||
Get the maximum value of the v_range.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self) -> "bool":
|
||||
"""
|
||||
Whether the aspect ratio is locked.
|
||||
"""
|
||||
|
||||
@lock_aspect_ratio.setter
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self) -> "bool":
|
||||
"""
|
||||
Whether the aspect ratio is locked.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def autorange(self) -> "bool":
|
||||
"""
|
||||
Whether autorange is enabled.
|
||||
"""
|
||||
|
||||
@autorange.setter
|
||||
@rpc_call
|
||||
def autorange(self) -> "bool":
|
||||
"""
|
||||
Whether autorange is enabled.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def autorange_mode(self) -> "str":
|
||||
"""
|
||||
Autorange mode.
|
||||
|
||||
Options:
|
||||
- "max": Use the maximum value of the image for autoranging.
|
||||
- "mean": Use the mean value of the image for autoranging.
|
||||
"""
|
||||
|
||||
@autorange_mode.setter
|
||||
@rpc_call
|
||||
def autorange_mode(self) -> "str":
|
||||
"""
|
||||
Autorange mode.
|
||||
|
||||
Options:
|
||||
- "max": Use the maximum value of the image for autoranging.
|
||||
- "mean": Use the mean value of the image for autoranging.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_colorbar(
|
||||
self,
|
||||
enabled: "bool",
|
||||
style: "Literal['full', 'simple']" = "full",
|
||||
vrange: "tuple[int, int] | None" = None,
|
||||
):
|
||||
"""
|
||||
Enable the colorbar and switch types of colorbars.
|
||||
|
||||
Args:
|
||||
enabled(bool): Whether to enable the colorbar.
|
||||
style(Literal["full", "simple"]): The type of colorbar to enable.
|
||||
vrange(tuple): The range of values to use for the colorbar.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_simple_colorbar(self) -> "bool":
|
||||
"""
|
||||
Enable the simple colorbar.
|
||||
"""
|
||||
|
||||
@enable_simple_colorbar.setter
|
||||
@rpc_call
|
||||
def enable_simple_colorbar(self) -> "bool":
|
||||
"""
|
||||
Enable the simple colorbar.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_full_colorbar(self) -> "bool":
|
||||
"""
|
||||
Enable the full colorbar.
|
||||
"""
|
||||
|
||||
@enable_full_colorbar.setter
|
||||
@rpc_call
|
||||
def enable_full_colorbar(self) -> "bool":
|
||||
"""
|
||||
Enable the full colorbar.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def interpolation_method(self) -> "str":
|
||||
"""
|
||||
The interpolation method used for the heatmap.
|
||||
"""
|
||||
|
||||
@interpolation_method.setter
|
||||
@rpc_call
|
||||
def interpolation_method(self) -> "str":
|
||||
"""
|
||||
The interpolation method used for the heatmap.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def oversampling_factor(self) -> "float":
|
||||
"""
|
||||
The oversampling factor for grid resolution.
|
||||
"""
|
||||
|
||||
@oversampling_factor.setter
|
||||
@rpc_call
|
||||
def oversampling_factor(self) -> "float":
|
||||
"""
|
||||
The oversampling factor for grid resolution.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enforce_interpolation(self) -> "bool":
|
||||
"""
|
||||
Whether to enforce interpolation even for grid scans.
|
||||
"""
|
||||
|
||||
@enforce_interpolation.setter
|
||||
@rpc_call
|
||||
def enforce_interpolation(self) -> "bool":
|
||||
"""
|
||||
Whether to enforce interpolation even for grid scans.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def fft(self) -> "bool":
|
||||
"""
|
||||
Whether FFT postprocessing is enabled.
|
||||
"""
|
||||
|
||||
@fft.setter
|
||||
@rpc_call
|
||||
def fft(self) -> "bool":
|
||||
"""
|
||||
Whether FFT postprocessing is enabled.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def log(self) -> "bool":
|
||||
"""
|
||||
Whether logarithmic scaling is applied.
|
||||
"""
|
||||
|
||||
@log.setter
|
||||
@rpc_call
|
||||
def log(self) -> "bool":
|
||||
"""
|
||||
Whether logarithmic scaling is applied.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def main_image(self) -> "ImageItem":
|
||||
"""
|
||||
Access the main image item.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_roi(
|
||||
self,
|
||||
kind: "Literal['rect', 'circle', 'ellipse']" = "rect",
|
||||
name: "str | None" = None,
|
||||
line_width: "int | None" = 5,
|
||||
pos: "tuple[float, float] | None" = (10, 10),
|
||||
size: "tuple[float, float] | None" = (50, 50),
|
||||
movable: "bool" = True,
|
||||
**pg_kwargs,
|
||||
) -> "RectangularROI | CircularROI":
|
||||
"""
|
||||
Add a ROI to the image.
|
||||
|
||||
Args:
|
||||
kind(str): The type of ROI to add. Options are "rect" or "circle".
|
||||
name(str): The name of the ROI.
|
||||
line_width(int): The line width of the ROI.
|
||||
pos(tuple): The position of the ROI.
|
||||
size(tuple): The size of the ROI.
|
||||
movable(bool): Whether the ROI is movable.
|
||||
**pg_kwargs: Additional arguments for the ROI.
|
||||
|
||||
Returns:
|
||||
RectangularROI | CircularROI: The created ROI object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove_roi(self, roi: "int | str"):
|
||||
"""
|
||||
Remove an ROI by index or label via the ROIController.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rois(self) -> "list[BaseROI]":
|
||||
"""
|
||||
Get the list of ROIs.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def plot(
|
||||
self,
|
||||
x_name: "str",
|
||||
y_name: "str",
|
||||
z_name: "str",
|
||||
x_entry: "None | str" = None,
|
||||
y_entry: "None | str" = None,
|
||||
z_entry: "None | str" = None,
|
||||
color_map: "str | None" = "plasma",
|
||||
validate_bec: "bool" = True,
|
||||
interpolation: "Literal['linear', 'nearest'] | None" = None,
|
||||
enforce_interpolation: "bool | None" = None,
|
||||
oversampling_factor: "float | None" = None,
|
||||
lock_aspect_ratio: "bool | None" = None,
|
||||
show_config_label: "bool | None" = None,
|
||||
reload: "bool" = False,
|
||||
):
|
||||
"""
|
||||
Plot the heatmap with the given x, y, and z data.
|
||||
|
||||
Args:
|
||||
x_name (str): The name of the x-axis signal.
|
||||
y_name (str): The name of the y-axis signal.
|
||||
z_name (str): The name of the z-axis signal.
|
||||
x_entry (str | None): The entry for the x-axis signal.
|
||||
y_entry (str | None): The entry for the y-axis signal.
|
||||
z_entry (str | None): The entry for the z-axis signal.
|
||||
color_map (str | None): The color map to use for the heatmap.
|
||||
validate_bec (bool): Whether to validate the entries against BEC signals.
|
||||
interpolation (Literal["linear", "nearest"] | None): The interpolation method to use.
|
||||
enforce_interpolation (bool | None): Whether to enforce interpolation even for grid scans.
|
||||
oversampling_factor (float | None): Factor to oversample the grid resolution.
|
||||
lock_aspect_ratio (bool | None): Whether to lock the aspect ratio of the image.
|
||||
show_config_label (bool | None): Whether to show the configuration label in the heatmap.
|
||||
reload (bool): Whether to reload the heatmap with new data.
|
||||
"""
|
||||
|
||||
|
||||
class Image(RPCBase):
|
||||
"""Image widget for displaying 2D data."""
|
||||
|
||||
@@ -1412,6 +1978,13 @@ class Image(RPCBase):
|
||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color_map(self) -> "str":
|
||||
@@ -1878,6 +2451,146 @@ class LogPanel(RPCBase):
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class MonacoWidget(RPCBase):
|
||||
"""A simple Monaco editor widget"""
|
||||
|
||||
@rpc_call
|
||||
def set_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the text in the Monaco editor.
|
||||
|
||||
Args:
|
||||
text (str): The text to set in the editor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_text(self) -> str:
|
||||
"""
|
||||
Get the current text from the Monaco editor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def insert_text(self, text: str, line: int | None = None, column: int | None = None) -> None:
|
||||
"""
|
||||
Insert text at the current cursor position or at a specified line and column.
|
||||
|
||||
Args:
|
||||
text (str): The text to insert.
|
||||
line (int, optional): The line number (1-based) to insert the text at. Defaults to None.
|
||||
column (int, optional): The column number (1-based) to insert the text at. Defaults to None.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def delete_line(self, line: int | None = None) -> None:
|
||||
"""
|
||||
Delete a line in the Monaco editor.
|
||||
|
||||
Args:
|
||||
line (int, optional): The line number (1-based) to delete. If None, the current line will be deleted.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_language(self, language: str) -> None:
|
||||
"""
|
||||
Set the programming language for syntax highlighting in the Monaco editor.
|
||||
|
||||
Args:
|
||||
language (str): The programming language to set (e.g., "python", "javascript").
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_language(self) -> str:
|
||||
"""
|
||||
Get the current programming language set in the Monaco editor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""
|
||||
Set the theme for the Monaco editor.
|
||||
|
||||
Args:
|
||||
theme (str): The theme to set (e.g., "vs-dark", "light").
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_theme(self) -> str:
|
||||
"""
|
||||
Get the current theme of the Monaco editor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_readonly(self, read_only: bool) -> None:
|
||||
"""
|
||||
Set the Monaco editor to read-only mode.
|
||||
|
||||
Args:
|
||||
read_only (bool): If True, the editor will be read-only.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_cursor(
|
||||
self,
|
||||
line: int,
|
||||
column: int = 1,
|
||||
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set the cursor position in the Monaco editor.
|
||||
|
||||
Args:
|
||||
line (int): Line number (1-based).
|
||||
column (int): Column number (1-based), defaults to 1.
|
||||
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def current_cursor(self) -> dict[str, int]:
|
||||
"""
|
||||
Get the current cursor position in the Monaco editor.
|
||||
|
||||
Returns:
|
||||
dict[str, int]: A dictionary with keys 'line' and 'column' representing the cursor position.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_minimap_enabled(self, enabled: bool) -> None:
|
||||
"""
|
||||
Enable or disable the minimap in the Monaco editor.
|
||||
|
||||
Args:
|
||||
enabled (bool): If True, the minimap will be enabled; otherwise, it will be disabled.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_vim_mode_enabled(self, enabled: bool) -> None:
|
||||
"""
|
||||
Enable or disable Vim mode in the Monaco editor.
|
||||
|
||||
Args:
|
||||
enabled (bool): If True, Vim mode will be enabled; otherwise, it will be disabled.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_lsp_header(self, header: str) -> None:
|
||||
"""
|
||||
Set the LSP (Language Server Protocol) header for the Monaco editor.
|
||||
The header is used to provide context for language servers but is not displayed in the editor.
|
||||
|
||||
Args:
|
||||
header (str): The LSP header to set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_lsp_header(self) -> str:
|
||||
"""
|
||||
Get the current LSP header set in the Monaco editor.
|
||||
|
||||
Returns:
|
||||
str: The LSP header.
|
||||
"""
|
||||
|
||||
|
||||
class MotorMap(RPCBase):
|
||||
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
||||
|
||||
@@ -2152,6 +2865,13 @@ class MotorMap(RPCBase):
|
||||
The font size of the legend font.
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color(self) -> "tuple":
|
||||
@@ -2557,6 +3277,13 @@ class MultiWaveform(RPCBase):
|
||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def highlighted_index(self):
|
||||
@@ -2771,6 +3498,13 @@ class PositionerBox(RPCBase):
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox2D(RPCBase):
|
||||
"""Simple Widget to control two positioners in box form"""
|
||||
@@ -2793,6 +3527,13 @@ class PositionerBox2D(RPCBase):
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
|
||||
class PositionerControlLine(RPCBase):
|
||||
"""A widget that controls a single device."""
|
||||
@@ -2806,6 +3547,13 @@ class PositionerControlLine(RPCBase):
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
|
||||
class PositionerGroup(RPCBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
@@ -3249,6 +3997,12 @@ class RingProgressBar(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class SBBMonitor(RPCBase):
|
||||
"""A widget to display the SBB monitor website."""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class ScanControl(RPCBase):
|
||||
"""Widget to submit new scans to the queue."""
|
||||
|
||||
@@ -3258,6 +4012,13 @@ class ScanControl(RPCBase):
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
|
||||
class ScanProgressBar(RPCBase):
|
||||
"""Widget to display a progress bar that is hooked up to the scan progress of a scan."""
|
||||
@@ -3566,6 +4327,13 @@ class ScatterWaveform(RPCBase):
|
||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def main_curve(self) -> "ScatterCurve":
|
||||
@@ -3739,6 +4507,76 @@ class SignalLabel(RPCBase):
|
||||
Show the button to select the signal to display
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def show_hinted_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show hinted signals
|
||||
"""
|
||||
|
||||
@show_hinted_signals.setter
|
||||
@rpc_call
|
||||
def show_hinted_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show hinted signals
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def show_normal_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show normal signals
|
||||
"""
|
||||
|
||||
@show_normal_signals.setter
|
||||
@rpc_call
|
||||
def show_normal_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show normal signals
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def show_config_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show config signals
|
||||
"""
|
||||
|
||||
@show_config_signals.setter
|
||||
@rpc_call
|
||||
def show_config_signals(self) -> "bool":
|
||||
"""
|
||||
In the signal selection menu, show config signals
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def display_array_data(self) -> "bool":
|
||||
"""
|
||||
Displays the full data from array signals if set to True.
|
||||
"""
|
||||
|
||||
@display_array_data.setter
|
||||
@rpc_call
|
||||
def display_array_data(self) -> "bool":
|
||||
"""
|
||||
Displays the full data from array signals if set to True.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def max_list_display_len(self) -> "int":
|
||||
"""
|
||||
For small lists, the max length to display
|
||||
"""
|
||||
|
||||
@max_list_display_len.setter
|
||||
@rpc_call
|
||||
def max_list_display_len(self) -> "int":
|
||||
"""
|
||||
For small lists, the max length to display
|
||||
"""
|
||||
|
||||
|
||||
class SignalLineEdit(RPCBase):
|
||||
"""Line edit widget for device input with autocomplete for device names."""
|
||||
@@ -3814,14 +4652,6 @@ class TextBox(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class UILaunchWindow(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class VSCodeEditor(RPCBase):
|
||||
"""A widget to display the VSCode editor."""
|
||||
|
||||
@@ -4070,6 +4900,15 @@ class Waveform(RPCBase):
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def auto_range(self, value: "bool" = True):
|
||||
"""
|
||||
On demand apply autorange to the plot item based on the visible curves.
|
||||
|
||||
Args:
|
||||
value(bool): If True, apply autorange to the visible curves.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_log(self) -> "bool":
|
||||
@@ -4126,6 +4965,13 @@ class Waveform(RPCBase):
|
||||
Minimum decimal places for crosshair when dynamic precision is enabled.
|
||||
"""
|
||||
|
||||
@rpc_timeout(None)
|
||||
@rpc_call
|
||||
def screenshot(self, file_name: "str | None" = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def curves(self) -> "list[Curve]":
|
||||
|
||||
@@ -51,7 +51,7 @@ def _filter_output(output: str) -> str:
|
||||
|
||||
|
||||
def _get_output(process, logger) -> None:
|
||||
log_func = {process.stdout: logger.debug, process.stderr: logger.error}
|
||||
log_func = {process.stdout: logger.debug, process.stderr: logger.info}
|
||||
stream_buffer = {process.stdout: [], process.stderr: []}
|
||||
try:
|
||||
os.set_blocking(process.stdout.fileno(), False)
|
||||
@@ -151,8 +151,10 @@ def wait_for_server(client: BECGuiClient):
|
||||
raise RuntimeError("GUI is not alive")
|
||||
try:
|
||||
if client._gui_started_event.wait(timeout=timeout):
|
||||
client._gui_started_timer.cancel()
|
||||
client._gui_started_timer.join()
|
||||
if client._gui_started_timer is not None:
|
||||
# cancel the timer, we are done
|
||||
client._gui_started_timer.cancel()
|
||||
client._gui_started_timer.join()
|
||||
else:
|
||||
raise TimeoutError("Could not connect to GUI server")
|
||||
finally:
|
||||
@@ -261,13 +263,20 @@ class BECGuiClient(RPCBase):
|
||||
|
||||
def start(self, wait: bool = False) -> None:
|
||||
"""Start the GUI server."""
|
||||
logger.warning("Using <gui>.start() is deprecated, use <gui>.show() instead.")
|
||||
return self._start(wait=wait)
|
||||
|
||||
def show(self):
|
||||
"""Show the GUI window."""
|
||||
def show(self, wait=True) -> None:
|
||||
"""
|
||||
Show the GUI window.
|
||||
If the GUI server is not running, it will be started.
|
||||
|
||||
Args:
|
||||
wait(bool): Whether to wait for the server to start. Defaults to True.
|
||||
"""
|
||||
if self._check_if_server_is_alive():
|
||||
return self._show_all()
|
||||
return self.start(wait=True)
|
||||
return self._start(wait=wait)
|
||||
|
||||
def hide(self):
|
||||
"""Hide the GUI window."""
|
||||
@@ -382,6 +391,9 @@ class BECGuiClient(RPCBase):
|
||||
"""
|
||||
Start the GUI server, and execute callback when it is launched
|
||||
"""
|
||||
if self._gui_is_alive():
|
||||
self._gui_started_event.set()
|
||||
return
|
||||
if self._process is None or self._process.poll() is not None:
|
||||
logger.success("GUI starting...")
|
||||
self._startup_timeout = 5
|
||||
@@ -524,7 +536,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
# Test the client_utils.py module
|
||||
gui = BECGuiClient()
|
||||
|
||||
gui.start(wait=True)
|
||||
gui.show(wait=True)
|
||||
gui.new().new(widget="Waveform")
|
||||
time.sleep(10)
|
||||
finally:
|
||||
|
||||
@@ -53,7 +53,7 @@ from __future__ import annotations
|
||||
{base_imports}
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
||||
{"from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module" if self._base else ""}
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -180,7 +180,10 @@ class {class_name}(RPCBase):"""
|
||||
f"Method {method} not found in class {cls.__name__}. "
|
||||
f"Please check the USER_ACCESS list."
|
||||
)
|
||||
|
||||
if hasattr(obj, "__rpc_timeout__"):
|
||||
timeout = {"value": obj.__rpc_timeout__}
|
||||
else:
|
||||
timeout = {}
|
||||
if isinstance(obj, (property, QtProperty)):
|
||||
# for the cli, we can map qt properties to regular properties
|
||||
if is_property_setter:
|
||||
@@ -205,14 +208,26 @@ class {class_name}(RPCBase):"""
|
||||
def {method}{str(sig_overload)}: ...
|
||||
"""
|
||||
|
||||
self.content += """
|
||||
@rpc_call"""
|
||||
self.content += f"""
|
||||
{self._rpc_call(timeout)}"""
|
||||
self.content += f"""
|
||||
def {method}{str(sig)}:
|
||||
\"\"\"
|
||||
{doc}
|
||||
\"\"\""""
|
||||
|
||||
def _rpc_call(self, timeout_info: dict[str, float | None]):
|
||||
"""
|
||||
Decorator to mark a method as an RPC call.
|
||||
This is used to generate the client code for the method.
|
||||
"""
|
||||
if not timeout_info:
|
||||
return "@rpc_call"
|
||||
timeout = timeout_info.get("value", None)
|
||||
return f"""
|
||||
@rpc_timeout({timeout})
|
||||
@rpc_call"""
|
||||
|
||||
def write(self, file_name: str):
|
||||
"""
|
||||
Write the content to a file, automatically formatted with black.
|
||||
|
||||
@@ -7,6 +7,7 @@ from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.device import DeviceBaseWithConfig
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
|
||||
|
||||
@@ -24,6 +25,43 @@ else:
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
def _name_arg(arg):
|
||||
if isinstance(arg, DeviceBaseWithConfig):
|
||||
# if dev.<device> is passed to GUI, it passes full_name
|
||||
if hasattr(arg, "full_name"):
|
||||
return arg.full_name
|
||||
elif hasattr(arg, "name"):
|
||||
return arg.name
|
||||
return arg
|
||||
|
||||
|
||||
def _transform_args_kwargs(args, kwargs) -> tuple[tuple, dict]:
|
||||
return tuple(_name_arg(arg) for arg in args), {k: _name_arg(v) for k, v in kwargs.items()}
|
||||
|
||||
|
||||
def rpc_timeout(timeout):
|
||||
"""
|
||||
A decorator to set a timeout for an RPC call.
|
||||
|
||||
Args:
|
||||
timeout: The timeout in seconds.
|
||||
|
||||
Returns:
|
||||
The decorated function.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if "timeout" not in kwargs:
|
||||
kwargs["timeout"] = timeout
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def rpc_call(func):
|
||||
"""
|
||||
A decorator for calling a function on the server.
|
||||
@@ -47,15 +85,7 @@ def rpc_call(func):
|
||||
return None # func(*args, **kwargs)
|
||||
caller_frame = caller_frame.f_back
|
||||
|
||||
out = []
|
||||
for arg in args:
|
||||
if hasattr(arg, "name"):
|
||||
arg = arg.name
|
||||
out.append(arg)
|
||||
args = tuple(out)
|
||||
for key, val in kwargs.items():
|
||||
if hasattr(val, "name"):
|
||||
kwargs[key] = val.name
|
||||
args, kwargs = _transform_args_kwargs(args, kwargs)
|
||||
if not self._root._gui_is_alive():
|
||||
raise RuntimeError("GUI is not alive")
|
||||
return self._run_rpc(func.__name__, *args, **kwargs)
|
||||
|
||||
@@ -9,6 +9,7 @@ from contextlib import redirect_stderr, redirect_stdout
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from qtmonaco.pylsp_provider import pylsp_server
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication
|
||||
@@ -142,6 +143,8 @@ class GUIServer:
|
||||
"""
|
||||
Shutdown the GUI server.
|
||||
"""
|
||||
if pylsp_server.is_running():
|
||||
pylsp_server.stop()
|
||||
if self.dispatcher:
|
||||
self.dispatcher.stop_cli_server()
|
||||
self.dispatcher.disconnect_all()
|
||||
|
||||
@@ -161,8 +161,6 @@ 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)
|
||||
|
||||
# Error popups
|
||||
self.error_utility = ErrorPopupUtility()
|
||||
@@ -186,25 +184,6 @@ class BECConnector:
|
||||
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.
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import darkdetect
|
||||
import shiboken6
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QObject, Slot
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtWidgets import QApplication, QFileDialog, QWidget
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_widgets.widgets.containers.dock import BECDock
|
||||
@@ -87,7 +91,7 @@ class BECWidget(BECConnector):
|
||||
theme = "dark"
|
||||
self.apply_theme(theme)
|
||||
|
||||
@Slot(str)
|
||||
@SafeSlot(str)
|
||||
def apply_theme(self, theme: str):
|
||||
"""
|
||||
Apply the theme to the widget.
|
||||
@@ -96,12 +100,43 @@ class BECWidget(BECConnector):
|
||||
theme(str, optional): The theme to be applied.
|
||||
"""
|
||||
|
||||
@SafeSlot()
|
||||
@SafeSlot(str)
|
||||
@rpc_timeout(None)
|
||||
def screenshot(self, file_name: str | None = None):
|
||||
"""
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
if not isinstance(self, QWidget):
|
||||
logger.error("Cannot take screenshot of non-QWidget instance")
|
||||
return
|
||||
|
||||
screenshot = self.grab()
|
||||
if file_name is None:
|
||||
file_name, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save Screenshot",
|
||||
f"bec_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png",
|
||||
"PNG Files (*.png);;JPEG Files (*.jpg *.jpeg);;All Files (*)",
|
||||
)
|
||||
if not file_name:
|
||||
return
|
||||
screenshot.save(file_name)
|
||||
logger.info(f"Screenshot saved to {file_name}")
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
with RPCRegister.delayed_broadcast():
|
||||
# All widgets need to call super().cleanup() in their cleanup method
|
||||
logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
|
||||
self.rpc_register.remove_rpc(self)
|
||||
children = self.findChildren(BECWidget)
|
||||
for child in children:
|
||||
if not shiboken6.isValid(child):
|
||||
# If the child is not valid, it means it has already been deleted
|
||||
continue
|
||||
child.close()
|
||||
child.deleteLater()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
||||
|
||||
@@ -259,12 +259,3 @@ class CompactPopupWidget(QWidget):
|
||||
@expand_popup.setter
|
||||
def expand_popup(self, popup: bool):
|
||||
self._expand_popup = popup
|
||||
|
||||
def closeEvent(self, event):
|
||||
# Called by Qt, on closing - since the children widgets can be
|
||||
# BECWidgets, it is good to explicitely call 'close' on them,
|
||||
# to ensure proper resources cleanup
|
||||
for child in self.container.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
|
||||
child.close()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
@@ -5,9 +5,13 @@ from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QObject, Qt, Signal, Slot
|
||||
from qtpy.QtCore import QObject, QPointF, Qt, Signal
|
||||
from qtpy.QtGui import QCursor, QTransform
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||
|
||||
|
||||
class CrosshairScatterItem(pg.ScatterPlotItem):
|
||||
def setDownsampling(self, ds=None, auto=None, method=None):
|
||||
@@ -160,7 +164,7 @@ class Crosshair(QObject):
|
||||
qapp.theme_signal.theme_updated.connect(self._update_theme)
|
||||
self._update_theme()
|
||||
|
||||
@Slot(str)
|
||||
@SafeSlot(str)
|
||||
def _update_theme(self, theme: str | None = None):
|
||||
"""Update the theme."""
|
||||
if theme is None:
|
||||
@@ -187,7 +191,7 @@ class Crosshair(QObject):
|
||||
self.coord_label.fill = pg.mkBrush(label_bg_color)
|
||||
self.coord_label.border = pg.mkPen(None)
|
||||
|
||||
@Slot(int)
|
||||
@SafeSlot(int)
|
||||
def update_highlighted_curve(self, curve_index: int):
|
||||
"""
|
||||
Update the highlighted curve in the case of multiple curves in a plot item.
|
||||
@@ -265,15 +269,47 @@ class Crosshair(QObject):
|
||||
[0, 0], size=[item.image.shape[0], 1], pen=pg.mkPen("r", width=2), movable=False
|
||||
)
|
||||
self.marker_2d_row.skip_auto_range = True
|
||||
if item.image_transform is not None:
|
||||
self.marker_2d_row.setTransform(item.image_transform)
|
||||
self.plot_item.addItem(self.marker_2d_row)
|
||||
|
||||
# Create vertical ROI for column highlighting
|
||||
self.marker_2d_col = pg.ROI(
|
||||
[0, 0], size=[1, item.image.shape[1]], pen=pg.mkPen("r", width=2), movable=False
|
||||
)
|
||||
if item.image_transform is not None:
|
||||
self.marker_2d_col.setTransform(item.image_transform)
|
||||
self.marker_2d_col.skip_auto_range = True
|
||||
self.plot_item.addItem(self.marker_2d_col)
|
||||
|
||||
@SafeSlot()
|
||||
def update_markers_on_image_change(self):
|
||||
"""
|
||||
Update markers when the image changes, e.g. when the
|
||||
image shape or transformation changes.
|
||||
"""
|
||||
for item in self.items:
|
||||
if not isinstance(item, pg.ImageItem):
|
||||
continue
|
||||
if self.marker_2d_row is not None:
|
||||
self.marker_2d_row.setSize([item.image.shape[0], 1])
|
||||
self.marker_2d_row.setTransform(item.image_transform)
|
||||
if self.marker_2d_col is not None:
|
||||
self.marker_2d_col.setSize([1, item.image.shape[1]])
|
||||
self.marker_2d_col.setTransform(item.image_transform)
|
||||
# Get the current mouse position
|
||||
views = self.plot_item.vb.scene().views()
|
||||
if not views:
|
||||
return
|
||||
view = views[0]
|
||||
global_pos = QCursor.pos()
|
||||
view_pos = view.mapFromGlobal(global_pos)
|
||||
scene_pos = view.mapToScene(view_pos)
|
||||
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(scene_pos):
|
||||
plot_pt = self.plot_item.vb.mapSceneToView(scene_pos)
|
||||
self.mouse_moved(manual_pos=(plot_pt.x(), plot_pt.y()))
|
||||
|
||||
def snap_to_data(
|
||||
self, x: float, y: float
|
||||
) -> tuple[None, None] | tuple[defaultdict[Any, list], defaultdict[Any, list]]:
|
||||
@@ -316,9 +352,25 @@ class Crosshair(QObject):
|
||||
image_2d = item.image
|
||||
if image_2d is None:
|
||||
continue
|
||||
# Clip the x and y values to the image dimensions to avoid out of bounds errors
|
||||
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||
x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||
# Map scene coordinates (plot units) back to image pixel coordinates
|
||||
if item.image_transform is not None:
|
||||
inv_transform, _ = item.image_transform.inverted()
|
||||
xy_trans = inv_transform.map(QPointF(x, y))
|
||||
else:
|
||||
xy_trans = QPointF(x, y)
|
||||
|
||||
# Define valid pixel coordinate bounds
|
||||
min_x_px, min_y_px = 0, 0
|
||||
max_x_px = image_2d.shape[0] - 1 # columns
|
||||
max_y_px = image_2d.shape[1] - 1 # rows
|
||||
|
||||
# Clip the mapped coordinates to the image bounds
|
||||
px = int(np.clip(xy_trans.x(), min_x_px, max_x_px))
|
||||
py = int(np.clip(xy_trans.y(), min_y_px, max_y_px))
|
||||
|
||||
# Store snapped pixel positions
|
||||
x_values[name] = px
|
||||
y_values[name] = py
|
||||
|
||||
if x_values and y_values:
|
||||
if all(v is None for v in x_values.values()) or all(
|
||||
@@ -358,60 +410,74 @@ class Crosshair(QObject):
|
||||
|
||||
return list_x[original_index], list_y[original_index]
|
||||
|
||||
def mouse_moved(self, event):
|
||||
"""Handles the mouse moved event, updating the crosshair position and emitting signals.
|
||||
@SafeSlot(object, tuple)
|
||||
def mouse_moved(self, event=None, manual_pos=None):
|
||||
"""
|
||||
Handles the mouse moved event, updating the crosshair position and emitting signals.
|
||||
|
||||
Args:
|
||||
event: The mouse moved event
|
||||
event(object): The mouse moved event, which contains the scene position.
|
||||
manual_pos(tuple, optional): A tuple containing the (x, y) coordinates to manually set the crosshair position.
|
||||
"""
|
||||
pos = event[0]
|
||||
# Determine target (x, y) in *plot* coordinates
|
||||
if manual_pos is not None:
|
||||
x, y = manual_pos
|
||||
else:
|
||||
if event is None:
|
||||
return # nothing to do
|
||||
scene_pos = event[0] # SignalProxy bundle
|
||||
if not self.plot_item.vb.sceneBoundingRect().contains(scene_pos):
|
||||
return
|
||||
view_pos = self.plot_item.vb.mapSceneToView(scene_pos)
|
||||
x, y = view_pos.x(), view_pos.y()
|
||||
|
||||
# Update cross‑hair visuals
|
||||
self.v_line.setPos(x)
|
||||
self.h_line.setPos(y)
|
||||
|
||||
self.update_markers()
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
||||
x, y = mouse_point.x(), mouse_point.y()
|
||||
self.v_line.setPos(x)
|
||||
self.h_line.setPos(y)
|
||||
scaled_x, scaled_y = self.scale_emitted_coordinates(mouse_point.x(), mouse_point.y())
|
||||
self.crosshairChanged.emit((scaled_x, scaled_y))
|
||||
self.positionChanged.emit((x, y))
|
||||
scaled_x, scaled_y = self.scale_emitted_coordinates(x, y)
|
||||
self.crosshairChanged.emit((scaled_x, scaled_y))
|
||||
self.positionChanged.emit((x, y))
|
||||
|
||||
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
||||
if x_snap_values is None or y_snap_values is None:
|
||||
return
|
||||
if all(v is None for v in x_snap_values.values()) or all(
|
||||
v is None for v in y_snap_values.values()
|
||||
):
|
||||
# not sure how we got here, but just to be safe...
|
||||
return
|
||||
snap_x_vals, snap_y_vals = self.snap_to_data(x, y)
|
||||
if snap_x_vals is None or snap_y_vals is None:
|
||||
return
|
||||
if all(v is None for v in snap_x_vals.values()) or all(
|
||||
v is None for v in snap_y_vals.values()
|
||||
):
|
||||
return
|
||||
|
||||
precision = self._current_precision()
|
||||
for item in self.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
name = item.name() or str(id(item))
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_moved_1d[name].setData([x], [y])
|
||||
x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
|
||||
coordinate_to_emit = (
|
||||
name,
|
||||
round(x_snapped_scaled, precision),
|
||||
round(y_snapped_scaled, precision),
|
||||
)
|
||||
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
name = item.objectName() or str(id(item))
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
# Set position of horizontal ROI (row)
|
||||
self.marker_2d_row.setPos([0, y])
|
||||
# Set position of vertical ROI (column)
|
||||
self.marker_2d_col.setPos([x, 0])
|
||||
coordinate_to_emit = (name, x, y)
|
||||
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
||||
else:
|
||||
precision = self._current_precision()
|
||||
|
||||
for item in self.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
name = item.name() or str(id(item))
|
||||
sx, sy = snap_x_vals[name], snap_y_vals[name]
|
||||
if sx is None or sy is None:
|
||||
continue
|
||||
self.marker_moved_1d[name].setData([sx], [sy])
|
||||
sx_s, sy_s = self.scale_emitted_coordinates(sx, sy)
|
||||
self.coordinatesChanged1D.emit(
|
||||
(name, round(sx_s, precision), round(sy_s, precision))
|
||||
)
|
||||
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
name = item.objectName() or str(id(item))
|
||||
px, py = snap_x_vals[name], snap_y_vals[name]
|
||||
if px is None or py is None:
|
||||
continue
|
||||
|
||||
# Respect image transforms
|
||||
if isinstance(item, ImageItem) and item.image_transform is not None:
|
||||
row, col = self._get_transformed_position(px, py, item.image_transform)
|
||||
self.marker_2d_row.setPos(row)
|
||||
self.marker_2d_col.setPos(col)
|
||||
else:
|
||||
self.marker_2d_row.setPos([0, py])
|
||||
self.marker_2d_col.setPos([px, 0])
|
||||
|
||||
self.coordinatesChanged2D.emit((name, px, py))
|
||||
|
||||
def mouse_clicked(self, event):
|
||||
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
|
||||
@@ -462,15 +528,35 @@ class Crosshair(QObject):
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
# Set position of horizontal ROI (row)
|
||||
self.marker_2d_row.setPos([0, y])
|
||||
# Set position of vertical ROI (column)
|
||||
self.marker_2d_col.setPos([x, 0])
|
||||
|
||||
if isinstance(item, ImageItem) and item.image_transform is not None:
|
||||
row, col = self._get_transformed_position(x, y, item.image_transform)
|
||||
self.marker_2d_row.setPos(row)
|
||||
self.marker_2d_col.setPos(col)
|
||||
else:
|
||||
self.marker_2d_row.setPos([0, y])
|
||||
self.marker_2d_col.setPos([x, 0])
|
||||
|
||||
coordinate_to_emit = (name, x, y)
|
||||
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
||||
else:
|
||||
continue
|
||||
|
||||
def _get_transformed_position(
|
||||
self, x: float, y: float, transform: QTransform
|
||||
) -> tuple[QPointF, QPointF]:
|
||||
"""
|
||||
Maps the given x and y coordinates to the transformed position using the provided transform.
|
||||
Args:
|
||||
x (float): The x-coordinate to transform.
|
||||
y (float): The y-coordinate to transform.
|
||||
transform (QTransform): The transformation to apply.
|
||||
"""
|
||||
origin = transform.map(QPointF(0, 0))
|
||||
row = transform.map(QPointF(0, y)) - origin
|
||||
col = transform.map(QPointF(x, 0)) - origin
|
||||
return row, col
|
||||
|
||||
def clear_markers(self):
|
||||
"""Clears the markers from the plot."""
|
||||
for marker in self.marker_moved_1d.values():
|
||||
@@ -512,8 +598,18 @@ class Crosshair(QObject):
|
||||
image = item.image
|
||||
if image is None:
|
||||
continue
|
||||
ix = int(np.clip(x, 0, image.shape[0] - 1))
|
||||
iy = int(np.clip(y, 0, image.shape[1] - 1))
|
||||
|
||||
if item.image_transform is not None:
|
||||
inv_transform, _ = item.image_transform.inverted()
|
||||
pt = inv_transform.map(QPointF(x, y))
|
||||
px, py = pt.x(), pt.y()
|
||||
else:
|
||||
px, py = x, y
|
||||
|
||||
# Clip to valid pixel indices
|
||||
ix = int(np.clip(px, 0, image.shape[0] - 1)) # column
|
||||
iy = int(np.clip(py, 0, image.shape[1] - 1)) # row
|
||||
|
||||
intensity = image[ix, iy]
|
||||
text += f"\nIntensity: {intensity:.{precision}f}"
|
||||
break
|
||||
@@ -533,15 +629,19 @@ class Crosshair(QObject):
|
||||
self.is_derivative = self.plot_item.ctrl.derivativeCheck.isChecked()
|
||||
self.clear_markers()
|
||||
|
||||
def cleanup(self):
|
||||
@SafeSlot()
|
||||
def reset(self):
|
||||
"""Resets the crosshair to its initial state."""
|
||||
if self.marker_2d_row is not None:
|
||||
self.plot_item.removeItem(self.marker_2d_row)
|
||||
self.marker_2d_row = None
|
||||
if self.marker_2d_col is not None:
|
||||
self.plot_item.removeItem(self.marker_2d_col)
|
||||
self.marker_2d_col = None
|
||||
self.clear_markers()
|
||||
|
||||
def cleanup(self):
|
||||
self.reset()
|
||||
self.plot_item.removeItem(self.v_line)
|
||||
self.plot_item.removeItem(self.h_line)
|
||||
self.plot_item.removeItem(self.coord_label)
|
||||
|
||||
self.clear_markers()
|
||||
|
||||
@@ -28,6 +28,10 @@ class EntryValidator:
|
||||
if not available_entries:
|
||||
available_entries = [name]
|
||||
|
||||
# edge case for if name is passed instead of full_name, should not happen
|
||||
if entry in signals_dict:
|
||||
entry = signals_dict[entry].get("obj_name", entry)
|
||||
|
||||
if entry is None or entry == "":
|
||||
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
|
||||
if entry not in available_entries:
|
||||
|
||||
@@ -171,8 +171,9 @@ class TypedForm(BECWidget, QWidget):
|
||||
|
||||
|
||||
class PydanticModelForm(TypedForm):
|
||||
metadata_updated = Signal(dict)
|
||||
metadata_cleared = Signal(NoneType)
|
||||
form_data_updated = Signal(dict)
|
||||
form_data_cleared = Signal(NoneType)
|
||||
validity_proc = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -204,7 +205,7 @@ class PydanticModelForm(TypedForm):
|
||||
|
||||
self._validity = CompactPopupWidget()
|
||||
self._validity.compact_view = True # type: ignore
|
||||
self._validity.label = "Metadata validity" # type: ignore
|
||||
self._validity.label = "Validity" # type: ignore
|
||||
self._validity.compact_show_popup.setIcon(
|
||||
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
|
||||
)
|
||||
@@ -264,16 +265,18 @@ class PydanticModelForm(TypedForm):
|
||||
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."""
|
||||
Otherwise, emits on form_data_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)
|
||||
self.form_data_updated.emit(metadata_dict)
|
||||
self.validity_proc.emit(True)
|
||||
return True
|
||||
except ValidationError as e:
|
||||
self._validity.set_global_state("emergency")
|
||||
self._validity_message.setText(str(e))
|
||||
self.metadata_cleared.emit(None)
|
||||
self.form_data_cleared.emit(None)
|
||||
self.validity_proc.emit(False)
|
||||
return False
|
||||
|
||||
@@ -390,17 +390,29 @@ class ListFormItem(DynamicFormItem):
|
||||
|
||||
def _add_buttons(self):
|
||||
self._button_holder = QWidget()
|
||||
self._button_holder.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
|
||||
self._buttons = QVBoxLayout()
|
||||
self._buttons.setContentsMargins(0, 0, 0, 0)
|
||||
self._button_holder.setLayout(self._buttons)
|
||||
self._layout.addWidget(self._button_holder)
|
||||
|
||||
self._add_remove_button_holder = QWidget()
|
||||
self._add_remove_button_layout = QHBoxLayout()
|
||||
self._add_remove_button_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._add_remove_button_holder.setLayout(self._add_remove_button_layout)
|
||||
|
||||
self._add_button = QPushButton("+")
|
||||
self._add_button.setMinimumHeight(15)
|
||||
self._add_button.setToolTip("add a new row")
|
||||
self._remove_button = QPushButton("-")
|
||||
self._remove_button.setMinimumHeight(15)
|
||||
self._remove_button.setToolTip("delete the focused row (if any)")
|
||||
self._add_button.clicked.connect(self._add_row)
|
||||
self._remove_button.clicked.connect(self._delete_row)
|
||||
self._buttons.addWidget(self._add_button)
|
||||
self._buttons.addWidget(self._remove_button)
|
||||
|
||||
self._buttons.addWidget(self._add_remove_button_holder)
|
||||
self._add_remove_button_layout.addWidget(self._add_button)
|
||||
self._add_remove_button_layout.addWidget(self._remove_button)
|
||||
|
||||
def _set_pretty_display(self):
|
||||
super()._set_pretty_display()
|
||||
|
||||
@@ -7,7 +7,7 @@ from qtpy.QtCore import QObject
|
||||
|
||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||
|
||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
|
||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDock"]
|
||||
_PARENT_ARG_REGEX = r".__init__\(\s*(?:parent\)|parent=parent,?|parent,?)"
|
||||
_SELF_PARENT_ARG_REGEX = r".__init__\(\s*self,\s*(?:parent\)|parent=parent,?|parent,?)"
|
||||
SUPER_INIT_REGEX = re.compile(r"super\(\)" + _PARENT_ARG_REGEX, re.MULTILINE)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
{widget_import}
|
||||
@@ -20,6 +21,8 @@ class {plugin_name_pascal}Plugin(QDesignerCustomWidgetInterface): # pragma: no
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = {plugin_name_pascal}(parent)
|
||||
return t
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ def get_custom_classes(repo_name: str) -> BECClassContainer:
|
||||
class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj)
|
||||
if issubclass(obj, BECConnector):
|
||||
class_info.is_connector = True
|
||||
if issubclass(obj, BECWidget):
|
||||
if issubclass(obj, QWidget) or issubclass(obj, BECWidget):
|
||||
class_info.is_widget = True
|
||||
if len(subs) == 1 and (
|
||||
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
|
||||
|
||||
@@ -13,3 +13,17 @@ def register_rpc_methods(cls):
|
||||
if getattr(method, "rpc_public", False):
|
||||
cls.USER_ACCESS.add(name)
|
||||
return cls
|
||||
|
||||
|
||||
def rpc_timeout(timeout: float | None):
|
||||
"""
|
||||
Decorator to set a timeout for RPC methods.
|
||||
The actual implementation of timeout handling is within the cli module. This decorator
|
||||
is solely to inform the generate-cli command about the timeout value.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
func.__rpc_timeout__ = timeout # Store the timeout value in the function
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -16,7 +16,8 @@ from qtpy.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
||||
|
||||
|
||||
class SidePanel(QWidget):
|
||||
@@ -61,7 +62,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(parent=self, orientation="vertical")
|
||||
|
||||
self.container = QWidget()
|
||||
self.container.layout = QVBoxLayout(self.container)
|
||||
@@ -92,7 +93,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(parent=self, orientation="horizontal")
|
||||
|
||||
self.container = QWidget()
|
||||
self.container.layout = QVBoxLayout(self.container)
|
||||
@@ -288,8 +289,16 @@ class SidePanel(QWidget):
|
||||
|
||||
# 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)
|
||||
action = MaterialIconAction(
|
||||
icon_name=icon_name, tooltip=tooltip, checkable=True, parent=self
|
||||
)
|
||||
self.toolbar.components.add_safe(action_id, action)
|
||||
bundle = ToolbarBundle(action_id, self.toolbar.components)
|
||||
bundle.add_action(action_id)
|
||||
self.toolbar.add_bundle(bundle)
|
||||
shown_bundles = self.toolbar.shown_bundles
|
||||
shown_bundles.append(action_id)
|
||||
self.toolbar.show_bundles(shown_bundles)
|
||||
|
||||
def on_action_toggled(checked: bool):
|
||||
if self.switching_actions:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
524
bec_widgets/utils/toolbars/actions.py
Normal file
524
bec_widgets/utils/toolbars/actions.py
Normal file
@@ -0,0 +1,524 @@
|
||||
# pylint: disable=no-name-in-module
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict, Literal
|
||||
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import QSize, Qt, QTimer
|
||||
from qtpy.QtGui import QAction, QColor, QIcon
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMenu,
|
||||
QSizePolicy,
|
||||
QStyledItemDelegate,
|
||||
QToolBar,
|
||||
QToolButton,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class NoCheckDelegate(QStyledItemDelegate):
|
||||
"""To reduce space in combo boxes by removing the checkmark."""
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super().initStyleOption(option, index)
|
||||
# Remove any check indicator
|
||||
option.checkState = Qt.Unchecked
|
||||
|
||||
|
||||
class LongPressToolButton(QToolButton):
|
||||
def __init__(self, *args, long_press_threshold=500, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.long_press_threshold = long_press_threshold
|
||||
self._long_press_timer = QTimer(self)
|
||||
self._long_press_timer.setSingleShot(True)
|
||||
self._long_press_timer.timeout.connect(self.handleLongPress)
|
||||
self._pressed = False
|
||||
self._longPressed = False
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self._pressed = True
|
||||
self._longPressed = False
|
||||
self._long_press_timer.start(self.long_press_threshold)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._pressed = False
|
||||
if self._longPressed:
|
||||
self._longPressed = False
|
||||
self._long_press_timer.stop()
|
||||
event.accept() # Prevent normal click action after a long press
|
||||
return
|
||||
self._long_press_timer.stop()
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def handleLongPress(self):
|
||||
if self._pressed:
|
||||
self._longPressed = True
|
||||
self.showMenu()
|
||||
|
||||
|
||||
class ToolBarAction(ABC):
|
||||
"""
|
||||
Abstract base class for toolbar actions.
|
||||
|
||||
Args:
|
||||
icon_path (str, optional): The name of the icon file from `assets/toolbar_icons`. Defaults to None.
|
||||
tooltip (str, optional): The tooltip for the action. Defaults to None.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(self, icon_path: str = None, tooltip: str = None, checkable: bool = False):
|
||||
self.icon_path = (
|
||||
os.path.join(MODULE_PATH, "assets", "toolbar_icons", icon_path) if icon_path else None
|
||||
)
|
||||
self.tooltip = tooltip
|
||||
self.checkable = checkable
|
||||
self.action = None
|
||||
|
||||
@abstractmethod
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
"""Adds an action or widget to a toolbar.
|
||||
|
||||
Args:
|
||||
toolbar (QToolBar): The toolbar to add the action or widget to.
|
||||
target (QWidget): The target widget for the action.
|
||||
"""
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleans up the action, if necessary."""
|
||||
pass
|
||||
|
||||
|
||||
class SeparatorAction(ToolBarAction):
|
||||
"""Separator action for the toolbar."""
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
toolbar.addSeparator()
|
||||
|
||||
|
||||
class QtIconAction(ToolBarAction):
|
||||
def __init__(self, standard_icon, tooltip=None, checkable=False, parent=None):
|
||||
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.setCheckable(self.checkable)
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
def get_icon(self):
|
||||
return self.icon
|
||||
|
||||
|
||||
class MaterialIconAction(ToolBarAction):
|
||||
"""
|
||||
Action with a Material icon for the toolbar.
|
||||
|
||||
Args:
|
||||
icon_name (str, optional): The name of the Material icon. Defaults to None.
|
||||
tooltip (str, optional): The tooltip for the action. Defaults to None.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
filled (bool, optional): Whether the icon is filled. Defaults to False.
|
||||
color (str | tuple | QColor | dict[Literal["dark", "light"], str] | None, optional): The color of the icon.
|
||||
Defaults to None.
|
||||
parent (QWidget or None, optional): Parent widget for the underlying QAction.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
icon_name: str = None,
|
||||
tooltip: str = None,
|
||||
checkable: bool = False,
|
||||
filled: bool = False,
|
||||
color: str | tuple | QColor | dict[Literal["dark", "light"], str] | None = None,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||
self.icon_name = icon_name
|
||||
self.filled = filled
|
||||
self.color = color
|
||||
# Generate the icon using the material_icon helper
|
||||
self.icon = material_icon(
|
||||
self.icon_name,
|
||||
size=(20, 20),
|
||||
convert_to_pixmap=False,
|
||||
filled=self.filled,
|
||||
color=self.color,
|
||||
)
|
||||
if parent is None:
|
||||
logger.warning(
|
||||
"MaterialIconAction was created without a parent. Please consider adding one. Using None as parent may cause issues."
|
||||
)
|
||||
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
||||
self.action.setCheckable(self.checkable)
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
"""
|
||||
Adds the action to the toolbar.
|
||||
|
||||
Args:
|
||||
toolbar(QToolBar): The toolbar to add the action to.
|
||||
target(QWidget): The target widget for the action.
|
||||
"""
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
def get_icon(self):
|
||||
"""
|
||||
Returns the icon for the action.
|
||||
|
||||
Returns:
|
||||
QIcon: The icon for the action.
|
||||
"""
|
||||
return self.icon
|
||||
|
||||
|
||||
class DeviceSelectionAction(ToolBarAction):
|
||||
"""
|
||||
Action for selecting a device in a combobox.
|
||||
|
||||
Args:
|
||||
label (str): The label for the combobox.
|
||||
device_combobox (DeviceComboBox): The combobox for selecting the device.
|
||||
"""
|
||||
|
||||
def __init__(self, label: str | None = None, device_combobox=None):
|
||||
super().__init__()
|
||||
self.label = label
|
||||
self.device_combobox = device_combobox
|
||||
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
widget = QWidget(parent=target)
|
||||
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)
|
||||
layout.addWidget(label)
|
||||
if self.device_combobox is not None:
|
||||
layout.addWidget(self.device_combobox)
|
||||
toolbar.addWidget(widget)
|
||||
|
||||
def set_combobox_style(self, color: str):
|
||||
self.device_combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
|
||||
|
||||
|
||||
class SwitchableToolBarAction(ToolBarAction):
|
||||
"""
|
||||
A split toolbar action that combines a main action and a drop-down menu for additional actions.
|
||||
|
||||
The main button displays the currently selected action's icon and tooltip. Clicking on the main button
|
||||
triggers that action. Clicking on the drop-down arrow displays a menu with alternative actions. When an
|
||||
alternative action is selected, it becomes the new default and its callback is immediately executed.
|
||||
|
||||
This design mimics the behavior seen in Adobe Photoshop or Affinity Designer toolbars.
|
||||
|
||||
Args:
|
||||
actions (dict): A dictionary mapping a unique key to a ToolBarAction instance.
|
||||
initial_action (str, optional): The key of the initial default action. If not provided, the first action is used.
|
||||
tooltip (str, optional): An optional tooltip for the split action; if provided, it overrides the default action's tooltip.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to True.
|
||||
parent (QWidget, optional): Parent widget for the underlying QAction.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
actions: Dict[str, ToolBarAction],
|
||||
initial_action: str = None,
|
||||
tooltip: str = None,
|
||||
checkable: bool = True,
|
||||
default_state_checked: bool = False,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||
self.actions = actions
|
||||
self.current_key = initial_action if initial_action is not None else next(iter(actions))
|
||||
self.parent = parent
|
||||
self.checkable = checkable
|
||||
self.default_state_checked = default_state_checked
|
||||
self.main_button = None
|
||||
self.menu_actions: Dict[str, QAction] = {}
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
"""
|
||||
Adds the split action to the toolbar.
|
||||
|
||||
Args:
|
||||
toolbar (QToolBar): The toolbar to add the action to.
|
||||
target (QWidget): The target widget for the action.
|
||||
"""
|
||||
self.main_button = LongPressToolButton(toolbar)
|
||||
self.main_button.setPopupMode(QToolButton.MenuButtonPopup)
|
||||
self.main_button.setCheckable(self.checkable)
|
||||
default_action = self.actions[self.current_key]
|
||||
self.main_button.setIcon(default_action.get_icon())
|
||||
self.main_button.setToolTip(default_action.tooltip)
|
||||
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.setIconVisibleInMenu(True)
|
||||
menu_action.setCheckable(self.checkable)
|
||||
menu_action.setChecked(key == self.current_key)
|
||||
menu_action.triggered.connect(lambda checked, k=key: self.set_default_action(k))
|
||||
menu.addAction(menu_action)
|
||||
self.main_button.setMenu(menu)
|
||||
if self.default_state_checked:
|
||||
self.main_button.setChecked(True)
|
||||
self.action = toolbar.addWidget(self.main_button)
|
||||
|
||||
def _trigger_current_action(self):
|
||||
"""
|
||||
Triggers the current action associated with the main button.
|
||||
"""
|
||||
action_obj = self.actions[self.current_key]
|
||||
action_obj.action.trigger()
|
||||
|
||||
def set_default_action(self, key: str):
|
||||
"""
|
||||
Sets the default action for the split action.
|
||||
|
||||
Args:
|
||||
key(str): The key of the action to set as default.
|
||||
"""
|
||||
if self.main_button is None:
|
||||
return
|
||||
self.current_key = key
|
||||
new_action = self.actions[self.current_key]
|
||||
self.main_button.setIcon(new_action.get_icon())
|
||||
self.main_button.setToolTip(new_action.tooltip)
|
||||
# Update check state of menu items
|
||||
for k, menu_act in self.actions.items():
|
||||
menu_act.action.setChecked(False)
|
||||
new_action.action.trigger()
|
||||
# Active action chosen from menu is always checked, uncheck through main button
|
||||
if self.checkable:
|
||||
new_action.action.setChecked(True)
|
||||
self.main_button.setChecked(True)
|
||||
|
||||
def block_all_signals(self, block: bool = True):
|
||||
"""
|
||||
Blocks or unblocks all signals for the actions in the toolbar.
|
||||
|
||||
Args:
|
||||
block (bool): Whether to block signals. Defaults to True.
|
||||
"""
|
||||
if self.main_button is not None:
|
||||
self.main_button.blockSignals(block)
|
||||
|
||||
for action in self.actions.values():
|
||||
action.action.blockSignals(block)
|
||||
|
||||
@contextmanager
|
||||
def signal_blocker(self):
|
||||
"""
|
||||
Context manager to block signals for all actions in the toolbar.
|
||||
"""
|
||||
self.block_all_signals(True)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.block_all_signals(False)
|
||||
|
||||
def set_state_all(self, state: bool):
|
||||
"""
|
||||
Uncheck all actions in the toolbar.
|
||||
"""
|
||||
for action in self.actions.values():
|
||||
action.action.setChecked(state)
|
||||
if self.main_button is None:
|
||||
return
|
||||
self.main_button.setChecked(state)
|
||||
|
||||
def get_icon(self) -> QIcon:
|
||||
return self.actions[self.current_key].get_icon()
|
||||
|
||||
|
||||
class WidgetAction(ToolBarAction):
|
||||
"""
|
||||
Action for adding any widget to the toolbar.
|
||||
Please note that the injected widget must be life-cycled by the parent widget,
|
||||
i.e., the widget must be properly cleaned up outside of this action. The WidgetAction
|
||||
will not perform any cleanup on the widget itself, only on the container that holds it.
|
||||
|
||||
Args:
|
||||
label (str|None): The label for the widget.
|
||||
widget (QWidget): The widget to be added to the toolbar.
|
||||
adjust_size (bool): Whether to adjust the size of the widget based on its contents. Defaults to True.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
label: str | None = None,
|
||||
widget: QWidget = None,
|
||||
adjust_size: bool = True,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(icon_path=None, tooltip=label, checkable=False)
|
||||
self.label = label
|
||||
self.widget = widget
|
||||
self.container = None
|
||||
self.adjust_size = adjust_size
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
"""
|
||||
Adds the widget to the toolbar.
|
||||
|
||||
Args:
|
||||
toolbar (QToolBar): The toolbar to add the widget to.
|
||||
target (QWidget): The target widget for the action.
|
||||
"""
|
||||
self.container = QWidget(parent=target)
|
||||
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.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
layout.addWidget(label_widget)
|
||||
|
||||
if isinstance(self.widget, QComboBox) and self.adjust_size:
|
||||
self.widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
||||
|
||||
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.widget.setSizePolicy(size_policy)
|
||||
|
||||
self.widget.setMinimumWidth(self.calculate_minimum_width(self.widget))
|
||||
|
||||
layout.addWidget(self.widget)
|
||||
|
||||
toolbar.addWidget(self.container)
|
||||
# Store the container as the action to allow toggling visibility.
|
||||
self.action = self.container
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleans up the action by closing and deleting the container widget.
|
||||
This method will be called automatically when the toolbar is cleaned up.
|
||||
"""
|
||||
if self.container is not None:
|
||||
self.container.close()
|
||||
self.container.deleteLater()
|
||||
return super().cleanup()
|
||||
|
||||
@staticmethod
|
||||
def calculate_minimum_width(combo_box: QComboBox) -> int:
|
||||
font_metrics = combo_box.fontMetrics()
|
||||
max_width = max(font_metrics.width(combo_box.itemText(i)) for i in range(combo_box.count()))
|
||||
return max_width + 60
|
||||
|
||||
|
||||
class ExpandableMenuAction(ToolBarAction):
|
||||
"""
|
||||
Action for an expandable menu in the toolbar.
|
||||
|
||||
Args:
|
||||
label (str): The label for the menu.
|
||||
actions (dict): A dictionary of actions to populate the menu.
|
||||
icon_path (str, optional): The path to the icon file. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(self, label: str, actions: dict, icon_path: str = None):
|
||||
super().__init__(icon_path, label)
|
||||
self.actions = actions
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
button = QToolButton(toolbar)
|
||||
if self.icon_path:
|
||||
button.setIcon(QIcon(self.icon_path))
|
||||
button.setText(self.tooltip)
|
||||
button.setPopupMode(QToolButton.InstantPopup)
|
||||
button.setStyleSheet(
|
||||
"""
|
||||
QToolButton {
|
||||
font-size: 14px;
|
||||
}
|
||||
QMenu {
|
||||
font-size: 14px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
menu = QMenu(button)
|
||||
for action_container in self.actions.values():
|
||||
action: QAction = action_container.action
|
||||
action.setIconVisibleInMenu(True)
|
||||
if action_container.icon_path:
|
||||
icon = QIcon()
|
||||
icon.addFile(action_container.icon_path, size=QSize(20, 20))
|
||||
action.setIcon(icon)
|
||||
elif hasattr(action, "get_icon") and callable(action_container.get_icon):
|
||||
sub_icon = action_container.get_icon()
|
||||
if sub_icon and not sub_icon.isNull():
|
||||
action.setIcon(sub_icon)
|
||||
action.setCheckable(action_container.checkable)
|
||||
menu.addAction(action)
|
||||
button.setMenu(menu)
|
||||
toolbar.addWidget(button)
|
||||
|
||||
|
||||
class DeviceComboBoxAction(WidgetAction):
|
||||
"""
|
||||
Action for a device selection combobox in the toolbar.
|
||||
|
||||
Args:
|
||||
label (str): The label for the combobox.
|
||||
device_combobox (QComboBox): The combobox for selecting the device.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_widget: QWidget,
|
||||
device_filter: list[BECDeviceFilter] | None = None,
|
||||
readout_priority_filter: (
|
||||
str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
|
||||
) = None,
|
||||
tooltip: str | None = None,
|
||||
add_empty_item: bool = False,
|
||||
no_check_delegate: bool = False,
|
||||
):
|
||||
self.combobox = DeviceComboBox(
|
||||
parent=target_widget,
|
||||
device_filter=device_filter,
|
||||
readout_priority_filter=readout_priority_filter,
|
||||
)
|
||||
super().__init__(widget=self.combobox, adjust_size=False)
|
||||
|
||||
if add_empty_item:
|
||||
self.combobox.addItem("", None)
|
||||
self.combobox.setCurrentText("")
|
||||
if tooltip is not None:
|
||||
self.combobox.setToolTip(tooltip)
|
||||
if no_check_delegate:
|
||||
self.combobox.setItemDelegate(NoCheckDelegate(self.combobox))
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleans up the action by closing and deleting the combobox widget.
|
||||
This method will be called automatically when the toolbar is cleaned up.
|
||||
"""
|
||||
if self.combobox is not None:
|
||||
self.combobox.close()
|
||||
self.combobox.deleteLater()
|
||||
return super().cleanup()
|
||||
244
bec_widgets/utils/toolbars/bundles.py
Normal file
244
bec_widgets/utils/toolbars/bundles.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, DefaultDict
|
||||
from weakref import ReferenceType
|
||||
|
||||
import louie
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from bec_widgets.utils.toolbars.actions import SeparatorAction, ToolBarAction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ActionInfo(BaseModel):
|
||||
action: ToolBarAction
|
||||
toolbar_bundle: ToolbarBundle | None = None
|
||||
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
|
||||
class ToolbarComponents:
|
||||
def __init__(self, toolbar: ModularToolBar):
|
||||
"""
|
||||
Initializes the toolbar components.
|
||||
|
||||
Args:
|
||||
toolbar (ModularToolBar): The toolbar to which the components will be added.
|
||||
"""
|
||||
self.toolbar = toolbar
|
||||
|
||||
self._components: dict[str, ActionInfo] = {}
|
||||
self.add("separator", SeparatorAction())
|
||||
|
||||
def add(self, name: str, component: ToolBarAction):
|
||||
"""
|
||||
Adds a component to the toolbar.
|
||||
|
||||
Args:
|
||||
component (ToolBarAction): The component to add.
|
||||
"""
|
||||
if name in self._components:
|
||||
raise ValueError(f"Component with name '{name}' already exists.")
|
||||
self._components[name] = ActionInfo(action=component, toolbar_bundle=None)
|
||||
|
||||
def add_safe(self, name: str, component: ToolBarAction):
|
||||
"""
|
||||
Adds a component to the toolbar, ensuring it does not already exist.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component.
|
||||
component (ToolBarAction): The component to add.
|
||||
"""
|
||||
if self.exists(name):
|
||||
logger.info(f"Component with name '{name}' already exists. Skipping addition.")
|
||||
return
|
||||
self.add(name, component)
|
||||
|
||||
def exists(self, name: str) -> bool:
|
||||
"""
|
||||
Checks if a component exists in the toolbar.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the component exists, False otherwise.
|
||||
"""
|
||||
return name in self._components
|
||||
|
||||
def get_action_reference(self, name: str) -> ReferenceType[ToolBarAction]:
|
||||
"""
|
||||
Retrieves a component by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component to retrieve.
|
||||
|
||||
"""
|
||||
if not self.exists(name):
|
||||
raise KeyError(f"Component with name '{name}' does not exist.")
|
||||
return louie.saferef.safe_ref(self._components[name].action)
|
||||
|
||||
def get_action(self, name: str) -> ToolBarAction:
|
||||
"""
|
||||
Retrieves a component by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component to retrieve.
|
||||
|
||||
Returns:
|
||||
ToolBarAction: The action associated with the given name.
|
||||
"""
|
||||
if not self.exists(name):
|
||||
raise KeyError(
|
||||
f"Component with name '{name}' does not exist. The following components are available: {list(self._components.keys())}"
|
||||
)
|
||||
return self._components[name].action
|
||||
|
||||
def set_bundle(self, name: str, bundle: ToolbarBundle):
|
||||
"""
|
||||
Sets the bundle for a component.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component.
|
||||
bundle (ToolbarBundle): The bundle to set.
|
||||
"""
|
||||
if not self.exists(name):
|
||||
raise KeyError(f"Component with name '{name}' does not exist.")
|
||||
comp = self._components[name]
|
||||
if comp.toolbar_bundle is not None:
|
||||
logger.info(
|
||||
f"Component '{name}' already has a bundle ({comp.toolbar_bundle.name}). Setting it to {bundle.name}."
|
||||
)
|
||||
comp.toolbar_bundle.bundle_actions.pop(name, None)
|
||||
comp.toolbar_bundle = bundle
|
||||
|
||||
def remove_action(self, name: str):
|
||||
"""
|
||||
Removes a component from the toolbar.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component to remove.
|
||||
"""
|
||||
if not self.exists(name):
|
||||
raise KeyError(f"Action with ID '{name}' does not exist.")
|
||||
action_info = self._components.pop(name)
|
||||
if action_info.toolbar_bundle:
|
||||
action_info.toolbar_bundle.bundle_actions.pop(name, None)
|
||||
self.toolbar.refresh()
|
||||
action_info.toolbar_bundle = None
|
||||
if hasattr(action_info.action, "cleanup"):
|
||||
# Call cleanup if the action has a cleanup method
|
||||
action_info.action.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleans up the toolbar components by removing all actions and bundles.
|
||||
"""
|
||||
for action_info in self._components.values():
|
||||
if hasattr(action_info.action, "cleanup"):
|
||||
# Call cleanup if the action has a cleanup method
|
||||
action_info.action.cleanup()
|
||||
self._components.clear()
|
||||
|
||||
|
||||
class ToolbarBundle:
|
||||
def __init__(self, name: str, components: ToolbarComponents):
|
||||
"""
|
||||
Initializes a new bundle component.
|
||||
|
||||
Args:
|
||||
bundle_name (str): Unique identifier for the bundle.
|
||||
"""
|
||||
self.name = name
|
||||
self.components = components
|
||||
self.bundle_actions: DefaultDict[str, ReferenceType[ToolBarAction]] = defaultdict()
|
||||
self._connections: dict[str, BundleConnection] = {}
|
||||
|
||||
def add_action(self, name: str):
|
||||
"""
|
||||
Adds an action to the bundle.
|
||||
|
||||
Args:
|
||||
name (str): Unique identifier for the action.
|
||||
action (ToolBarAction): The action to add.
|
||||
"""
|
||||
if name in self.bundle_actions:
|
||||
raise ValueError(f"Action with name '{name}' already exists in bundle '{self.name}'.")
|
||||
if not self.components.exists(name):
|
||||
raise ValueError(
|
||||
f"Component with name '{name}' does not exist in the toolbar. Please add it first using the `ToolbarComponents.add` method."
|
||||
)
|
||||
self.bundle_actions[name] = self.components.get_action_reference(name)
|
||||
self.components.set_bundle(name, self)
|
||||
|
||||
def remove_action(self, name: str):
|
||||
"""
|
||||
Removes an action from the bundle.
|
||||
|
||||
Args:
|
||||
name (str): The name of the action to remove.
|
||||
"""
|
||||
if name not in self.bundle_actions:
|
||||
raise KeyError(f"Action with name '{name}' does not exist in bundle '{self.name}'.")
|
||||
del self.bundle_actions[name]
|
||||
|
||||
def add_separator(self):
|
||||
"""
|
||||
Adds a separator action to the bundle.
|
||||
"""
|
||||
self.add_action("separator")
|
||||
|
||||
def add_connection(self, name: str, connection):
|
||||
"""
|
||||
Adds a connection to the bundle.
|
||||
|
||||
Args:
|
||||
name (str): Unique identifier for the connection.
|
||||
connection: The connection to add.
|
||||
"""
|
||||
if name in self._connections:
|
||||
raise ValueError(
|
||||
f"Connection with name '{name}' already exists in bundle '{self.name}'."
|
||||
)
|
||||
self._connections[name] = connection
|
||||
|
||||
def remove_connection(self, name: str):
|
||||
"""
|
||||
Removes a connection from the bundle.
|
||||
|
||||
Args:
|
||||
name (str): The name of the connection to remove.
|
||||
"""
|
||||
if name not in self._connections:
|
||||
raise KeyError(f"Connection with name '{name}' does not exist in bundle '{self.name}'.")
|
||||
self._connections[name].disconnect()
|
||||
del self._connections[name]
|
||||
|
||||
def get_connection(self, name: str):
|
||||
"""
|
||||
Retrieves a connection by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the connection to retrieve.
|
||||
|
||||
Returns:
|
||||
The connection associated with the given name.
|
||||
"""
|
||||
if name not in self._connections:
|
||||
raise KeyError(f"Connection with name '{name}' does not exist in bundle '{self.name}'.")
|
||||
return self._connections[name]
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnects all connections in the bundle.
|
||||
"""
|
||||
for connection in self._connections.values():
|
||||
connection.disconnect()
|
||||
self._connections.clear()
|
||||
23
bec_widgets/utils/toolbars/connections.py
Normal file
23
bec_widgets/utils/toolbars/connections.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
|
||||
class BundleConnection(QObject):
|
||||
bundle_name: str
|
||||
|
||||
@abstractmethod
|
||||
def connect(self):
|
||||
"""
|
||||
Connects the bundle to the target widget or application.
|
||||
This method should be implemented by subclasses to define how the bundle interacts with the target.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnects the bundle from the target widget or application.
|
||||
This method should be implemented by subclasses to define how to clean up connections.
|
||||
"""
|
||||
64
bec_widgets/utils/toolbars/performance.py
Normal file
64
bec_widgets/utils/toolbars/performance.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.utils.toolbars.toolbar import ToolbarComponents
|
||||
|
||||
|
||||
def performance_bundle(components: ToolbarComponents) -> ToolbarBundle:
|
||||
"""
|
||||
Creates a performance toolbar bundle.
|
||||
|
||||
Args:
|
||||
components (ToolbarComponents): The components to be added to the bundle.
|
||||
|
||||
Returns:
|
||||
ToolbarBundle: The performance toolbar bundle.
|
||||
"""
|
||||
components.add_safe(
|
||||
"fps_monitor",
|
||||
MaterialIconAction(
|
||||
icon_name="speed", tooltip="Show FPS Monitor", checkable=True, parent=components.toolbar
|
||||
),
|
||||
)
|
||||
bundle = ToolbarBundle("performance", components)
|
||||
bundle.add_action("fps_monitor")
|
||||
return bundle
|
||||
|
||||
|
||||
class PerformanceConnection(BundleConnection):
|
||||
|
||||
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||
self.bundle_name = "performance"
|
||||
self.components = components
|
||||
self.target_widget = target_widget
|
||||
if not hasattr(self.target_widget, "enable_fps_monitor"):
|
||||
raise AttributeError("Target widget must implement 'enable_fps_monitor'.")
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
|
||||
@SafeSlot(bool)
|
||||
def set_fps_monitor(self, enabled: bool):
|
||||
setattr(self.target_widget, "enable_fps_monitor", enabled)
|
||||
|
||||
def connect(self):
|
||||
self._connected = True
|
||||
# Connect the action to the target widget's method
|
||||
self.components.get_action_reference("fps_monitor")().action.toggled.connect(
|
||||
self.set_fps_monitor
|
||||
)
|
||||
|
||||
def disconnect(self):
|
||||
if not self._connected:
|
||||
return
|
||||
# Disconnect the action from the target widget's method
|
||||
self.components.get_action_reference("fps_monitor")().action.toggled.disconnect(
|
||||
self.set_fps_monitor
|
||||
)
|
||||
self._connected = False
|
||||
513
bec_widgets/utils/toolbars/toolbar.py
Normal file
513
bec_widgets/utils/toolbars/toolbar.py
Normal file
@@ -0,0 +1,513 @@
|
||||
# pylint: disable=no-name-in-module
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import DefaultDict, Literal
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QAction, QColor
|
||||
from qtpy.QtWidgets import QApplication, QLabel, QMainWindow, QMenu, QToolBar, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import get_theme_name, set_theme
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction, ToolBarAction
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents
|
||||
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
# Ensure that icons are shown in menus (especially on macOS)
|
||||
QApplication.setAttribute(Qt.AA_DontShowIconsInMenus, False)
|
||||
|
||||
|
||||
class ModularToolBar(QToolBar):
|
||||
"""Modular toolbar with optional automatic initialization.
|
||||
|
||||
Args:
|
||||
parent (QWidget, optional): The parent widget of the toolbar. Defaults to None.
|
||||
actions (dict, optional): A dictionary of action creators to populate the toolbar. Defaults to None.
|
||||
target_widget (QWidget, optional): The widget that the actions will target. Defaults to None.
|
||||
orientation (Literal["horizontal", "vertical"], optional): The initial orientation of the toolbar. Defaults to "horizontal".
|
||||
background_color (str, optional): The background color of the toolbar. Defaults to "rgba(0, 0, 0, 0)".
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
orientation: Literal["horizontal", "vertical"] = "horizontal",
|
||||
background_color: str = "rgba(0, 0, 0, 0)",
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.background_color = background_color
|
||||
self.set_background_color(self.background_color)
|
||||
|
||||
# Set the initial orientation
|
||||
self.set_orientation(orientation)
|
||||
|
||||
self.components = ToolbarComponents(self)
|
||||
|
||||
# Initialize bundles
|
||||
self.bundles: dict[str, ToolbarBundle] = {}
|
||||
self.shown_bundles: list[str] = []
|
||||
|
||||
#########################
|
||||
# outdated items... remove
|
||||
self.available_widgets: DefaultDict[str, ToolBarAction] = defaultdict()
|
||||
########################
|
||||
|
||||
def new_bundle(self, name: str) -> ToolbarBundle:
|
||||
"""
|
||||
Creates a new bundle component.
|
||||
|
||||
Args:
|
||||
name (str): Unique identifier for the bundle.
|
||||
|
||||
Returns:
|
||||
ToolbarBundle: The new bundle component.
|
||||
"""
|
||||
if name in self.bundles:
|
||||
raise ValueError(f"Bundle with name '{name}' already exists.")
|
||||
bundle = ToolbarBundle(name=name, components=self.components)
|
||||
self.bundles[name] = bundle
|
||||
return bundle
|
||||
|
||||
def add_bundle(self, bundle: ToolbarBundle):
|
||||
"""
|
||||
Adds a bundle component to the toolbar.
|
||||
|
||||
Args:
|
||||
bundle (ToolbarBundle): The bundle component to add.
|
||||
"""
|
||||
if bundle.name in self.bundles:
|
||||
raise ValueError(f"Bundle with name '{bundle.name}' already exists.")
|
||||
self.bundles[bundle.name] = bundle
|
||||
if not bundle.bundle_actions:
|
||||
logger.warning(f"Bundle '{bundle.name}' has no actions.")
|
||||
|
||||
def remove_bundle(self, name: str):
|
||||
"""
|
||||
Removes a bundle component by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the bundle to remove.
|
||||
"""
|
||||
if name not in self.bundles:
|
||||
raise KeyError(f"Bundle with name '{name}' does not exist.")
|
||||
del self.bundles[name]
|
||||
if name in self.shown_bundles:
|
||||
self.shown_bundles.remove(name)
|
||||
logger.info(f"Bundle '{name}' removed from the toolbar.")
|
||||
|
||||
def get_bundle(self, name: str) -> ToolbarBundle:
|
||||
"""
|
||||
Retrieves a bundle component by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the bundle to retrieve.
|
||||
|
||||
Returns:
|
||||
ToolbarBundle: The bundle component.
|
||||
"""
|
||||
if name not in self.bundles:
|
||||
raise KeyError(
|
||||
f"Bundle with name '{name}' does not exist. Available bundles: {list(self.bundles.keys())}"
|
||||
)
|
||||
return self.bundles[name]
|
||||
|
||||
def show_bundles(self, bundle_names: list[str]):
|
||||
"""
|
||||
Sets the bundles to be shown for the toolbar.
|
||||
|
||||
Args:
|
||||
bundle_names (list[str]): A list of bundle names to show. If a bundle is not in this list, its actions will be hidden.
|
||||
"""
|
||||
self.clear()
|
||||
for requested_bundle in bundle_names:
|
||||
bundle = self.get_bundle(requested_bundle)
|
||||
for bundle_action in bundle.bundle_actions.values():
|
||||
action = bundle_action()
|
||||
if action is None:
|
||||
logger.warning(
|
||||
f"Action for bundle '{requested_bundle}' has been deleted. Skipping."
|
||||
)
|
||||
continue
|
||||
action.add_to_toolbar(self, self.parent())
|
||||
separator = self.components.get_action_reference("separator")()
|
||||
if separator is not None:
|
||||
separator.add_to_toolbar(self, self.parent())
|
||||
self.update_separators() # Ensure separators are updated after showing bundles
|
||||
self.shown_bundles = bundle_names
|
||||
|
||||
def add_action(self, action_name: str, action: ToolBarAction):
|
||||
"""
|
||||
Adds a single action to the toolbar. It will create a new bundle
|
||||
with the same name as the action.
|
||||
|
||||
Args:
|
||||
action_name (str): Unique identifier for the action.
|
||||
action (ToolBarAction): The action to add.
|
||||
"""
|
||||
self.components.add_safe(action_name, action)
|
||||
bundle = ToolbarBundle(name=action_name, components=self.components)
|
||||
bundle.add_action(action_name)
|
||||
self.add_bundle(bundle)
|
||||
|
||||
def hide_action(self, action_name: str):
|
||||
"""
|
||||
Hides a specific action in the toolbar.
|
||||
|
||||
Args:
|
||||
action_name (str): Unique identifier for the action to hide.
|
||||
"""
|
||||
action = self.components.get_action(action_name)
|
||||
if hasattr(action, "action") and action.action is not None:
|
||||
action.action.setVisible(False)
|
||||
self.update_separators()
|
||||
|
||||
def show_action(self, action_name: str):
|
||||
"""
|
||||
Shows a specific action in the toolbar.
|
||||
|
||||
Args:
|
||||
action_name (str): Unique identifier for the action to show.
|
||||
"""
|
||||
action = self.components.get_action(action_name)
|
||||
if hasattr(action, "action") and action.action is not None:
|
||||
action.action.setVisible(True)
|
||||
self.update_separators()
|
||||
|
||||
@property
|
||||
def toolbar_actions(self) -> list[ToolBarAction]:
|
||||
"""
|
||||
Returns a list of all actions currently in the toolbar.
|
||||
|
||||
Returns:
|
||||
list[ToolBarAction]: List of actions in the toolbar.
|
||||
"""
|
||||
actions = []
|
||||
for bundle in self.shown_bundles:
|
||||
if bundle not in self.bundles:
|
||||
continue
|
||||
for action in self.bundles[bundle].bundle_actions.values():
|
||||
action_instance = action()
|
||||
if action_instance is not None:
|
||||
actions.append(action_instance)
|
||||
return actions
|
||||
|
||||
def refresh(self):
|
||||
"""Refreshes the toolbar by clearing and re-populating it."""
|
||||
self.clear()
|
||||
self.show_bundles(self.shown_bundles)
|
||||
|
||||
def connect_bundle(self, connection_name: str, connector: BundleConnection):
|
||||
"""
|
||||
Connects a bundle to a target widget or application.
|
||||
|
||||
Args:
|
||||
bundle_name (str): The name of the bundle to connect.
|
||||
connector (BundleConnection): The connector instance that implements the connection logic.
|
||||
"""
|
||||
bundle_name = connector.bundle_name
|
||||
if bundle_name not in self.bundles:
|
||||
raise KeyError(f"Bundle with name '{bundle_name}' does not exist.")
|
||||
connector.connect()
|
||||
self.bundles[bundle_name].add_connection(connection_name, connector)
|
||||
|
||||
def disconnect_bundle(self, bundle_name: str, connection_name: str | None = None):
|
||||
"""
|
||||
Disconnects a bundle connection.
|
||||
|
||||
Args:
|
||||
bundle_name (str): The name of the bundle to disconnect.
|
||||
connection_name (str): The name of the connection to disconnect. If None, disconnects all connections for the bundle.
|
||||
"""
|
||||
if bundle_name not in self.bundles:
|
||||
raise KeyError(f"Bundle with name '{bundle_name}' does not exist.")
|
||||
bundle = self.bundles[bundle_name]
|
||||
if connection_name is None:
|
||||
# Disconnect all connections in the bundle
|
||||
bundle.disconnect()
|
||||
else:
|
||||
bundle.remove_connection(name=connection_name)
|
||||
|
||||
def set_background_color(self, color: str = "rgba(0, 0, 0, 0)"):
|
||||
"""
|
||||
Sets the background color and other appearance settings.
|
||||
|
||||
Args:
|
||||
color (str): The background color of the toolbar.
|
||||
"""
|
||||
self.setIconSize(QSize(20, 20))
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.background_color = color
|
||||
self.setStyleSheet(f"QToolBar {{ background-color: {color}; border: none; }}")
|
||||
|
||||
def set_orientation(self, orientation: Literal["horizontal", "vertical"]):
|
||||
"""Sets the orientation of the toolbar.
|
||||
|
||||
Args:
|
||||
orientation (Literal["horizontal", "vertical"]): The desired orientation of the toolbar.
|
||||
"""
|
||||
if orientation == "horizontal":
|
||||
self.setOrientation(Qt.Horizontal)
|
||||
elif orientation == "vertical":
|
||||
self.setOrientation(Qt.Vertical)
|
||||
else:
|
||||
raise ValueError("Orientation must be 'horizontal' or 'vertical'.")
|
||||
|
||||
def update_material_icon_colors(self, new_color: str | tuple | QColor):
|
||||
"""
|
||||
Updates the color of all MaterialIconAction icons.
|
||||
|
||||
Args:
|
||||
new_color (str | tuple | QColor): The new color.
|
||||
"""
|
||||
for action in self.available_widgets.values():
|
||||
if isinstance(action, MaterialIconAction):
|
||||
action.color = new_color
|
||||
updated_icon = action.get_icon()
|
||||
action.action.setIcon(updated_icon)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""
|
||||
Overrides the context menu event to show toolbar actions with checkboxes and icons.
|
||||
|
||||
Args:
|
||||
event (QContextMenuEvent): The context menu event.
|
||||
"""
|
||||
menu = QMenu(self)
|
||||
theme = get_theme_name()
|
||||
if theme == "dark":
|
||||
menu.setStyleSheet(
|
||||
"""
|
||||
QMenu {
|
||||
background-color: rgba(50, 50, 50, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: rgba(0, 0, 255, 0.2);
|
||||
}
|
||||
"""
|
||||
)
|
||||
else:
|
||||
# Light theme styling
|
||||
menu.setStyleSheet(
|
||||
"""
|
||||
QMenu {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: rgba(0, 0, 255, 0.2);
|
||||
}
|
||||
"""
|
||||
)
|
||||
for ii, bundle in enumerate(self.shown_bundles):
|
||||
self.handle_bundle_context_menu(menu, bundle)
|
||||
if ii < len(self.shown_bundles) - 1:
|
||||
menu.addSeparator()
|
||||
menu.triggered.connect(self.handle_menu_triggered)
|
||||
menu.exec_(event.globalPos())
|
||||
|
||||
def handle_bundle_context_menu(self, menu: QMenu, bundle_id: str):
|
||||
"""
|
||||
Adds bundle actions to the context menu.
|
||||
|
||||
Args:
|
||||
menu (QMenu): The context menu.
|
||||
bundle_id (str): The bundle identifier.
|
||||
"""
|
||||
bundle = self.bundles.get(bundle_id)
|
||||
if not bundle:
|
||||
return
|
||||
for act_id in bundle.bundle_actions:
|
||||
toolbar_action = self.components.get_action(act_id)
|
||||
if not isinstance(toolbar_action, ToolBarAction) or not hasattr(
|
||||
toolbar_action, "action"
|
||||
):
|
||||
continue
|
||||
qaction = toolbar_action.action
|
||||
if not isinstance(qaction, QAction):
|
||||
continue
|
||||
self._add_qaction_to_menu(menu, qaction, toolbar_action, act_id)
|
||||
|
||||
def _add_qaction_to_menu(
|
||||
self, menu: QMenu, qaction: QAction, toolbar_action: ToolBarAction, act_id: str
|
||||
):
|
||||
display_name = qaction.text() or toolbar_action.tooltip or act_id
|
||||
menu_action = QAction(display_name, self)
|
||||
menu_action.setCheckable(True)
|
||||
menu_action.setChecked(qaction.isVisible())
|
||||
menu_action.setData(act_id) # Store the action_id
|
||||
|
||||
# Set the icon if available
|
||||
if qaction.icon() and not qaction.icon().isNull():
|
||||
menu_action.setIcon(qaction.icon())
|
||||
menu.addAction(menu_action)
|
||||
|
||||
def handle_action_context_menu(self, menu: QMenu, action_id: str):
|
||||
"""
|
||||
Adds a single toolbar action to the context menu.
|
||||
|
||||
Args:
|
||||
menu (QMenu): The context menu to which the action is added.
|
||||
action_id (str): Unique identifier for the action.
|
||||
"""
|
||||
toolbar_action = self.available_widgets.get(action_id)
|
||||
if not isinstance(toolbar_action, ToolBarAction) or not hasattr(toolbar_action, "action"):
|
||||
return
|
||||
qaction = toolbar_action.action
|
||||
if not isinstance(qaction, QAction):
|
||||
return
|
||||
display_name = qaction.text() or toolbar_action.tooltip or action_id
|
||||
menu_action = QAction(display_name, self)
|
||||
menu_action.setCheckable(True)
|
||||
menu_action.setChecked(qaction.isVisible())
|
||||
menu_action.setIconVisibleInMenu(True)
|
||||
menu_action.setData(action_id) # Store the action_id
|
||||
|
||||
# Set the icon if available
|
||||
if qaction.icon() and not qaction.icon().isNull():
|
||||
menu_action.setIcon(qaction.icon())
|
||||
|
||||
menu.addAction(menu_action)
|
||||
|
||||
def handle_menu_triggered(self, action):
|
||||
"""
|
||||
Handles the triggered signal from the context menu.
|
||||
|
||||
Args:
|
||||
action: Action triggered.
|
||||
"""
|
||||
action_id = action.data()
|
||||
if action_id:
|
||||
self.toggle_action_visibility(action_id)
|
||||
|
||||
def toggle_action_visibility(self, action_id: str, visible: bool | None = None):
|
||||
"""
|
||||
Toggles the visibility of a specific action.
|
||||
|
||||
Args:
|
||||
action_id (str): Unique identifier.
|
||||
visible (bool): Whether the action should be visible. If None, toggles the current visibility.
|
||||
"""
|
||||
if not self.components.exists(action_id):
|
||||
return
|
||||
tool_action = self.components.get_action(action_id)
|
||||
if hasattr(tool_action, "action") and tool_action.action is not None:
|
||||
if visible is None:
|
||||
visible = not tool_action.action.isVisible()
|
||||
tool_action.action.setVisible(visible)
|
||||
self.update_separators()
|
||||
|
||||
def update_separators(self):
|
||||
"""
|
||||
Hide separators that are adjacent to another separator or have no non-separator actions between them.
|
||||
"""
|
||||
toolbar_actions = self.actions()
|
||||
# First pass: set visibility based on surrounding non-separator actions.
|
||||
for i, action in enumerate(toolbar_actions):
|
||||
if not action.isSeparator():
|
||||
continue
|
||||
prev_visible = None
|
||||
for j in range(i - 1, -1, -1):
|
||||
if toolbar_actions[j].isVisible():
|
||||
prev_visible = toolbar_actions[j]
|
||||
break
|
||||
next_visible = None
|
||||
for j in range(i + 1, len(toolbar_actions)):
|
||||
if toolbar_actions[j].isVisible():
|
||||
next_visible = toolbar_actions[j]
|
||||
break
|
||||
if (prev_visible is None or prev_visible.isSeparator()) and (
|
||||
next_visible is None or next_visible.isSeparator()
|
||||
):
|
||||
action.setVisible(False)
|
||||
else:
|
||||
action.setVisible(True)
|
||||
# Second pass: ensure no two visible separators are adjacent.
|
||||
prev = None
|
||||
for action in toolbar_actions:
|
||||
if action.isVisible() and action.isSeparator():
|
||||
if prev and prev.isSeparator():
|
||||
action.setVisible(False)
|
||||
else:
|
||||
prev = action
|
||||
else:
|
||||
if action.isVisible():
|
||||
prev = action
|
||||
|
||||
if not toolbar_actions:
|
||||
return
|
||||
|
||||
# Make sure the first visible action is not a separator
|
||||
for i, action in enumerate(toolbar_actions):
|
||||
if action.isVisible():
|
||||
if action.isSeparator():
|
||||
action.setVisible(False)
|
||||
break
|
||||
|
||||
# Make sure the last visible action is not a separator
|
||||
for i, action in enumerate(reversed(toolbar_actions)):
|
||||
if action.isVisible():
|
||||
if action.isSeparator():
|
||||
action.setVisible(False)
|
||||
break
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleans up the toolbar by removing all actions and bundles.
|
||||
"""
|
||||
# First, disconnect all bundles
|
||||
for bundle_name in list(self.bundles.keys()):
|
||||
self.disconnect_bundle(bundle_name)
|
||||
|
||||
# Clear all components
|
||||
self.components.cleanup()
|
||||
self.bundles.clear()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from bec_widgets.utils.toolbars.performance import PerformanceConnection, performance_bundle
|
||||
from bec_widgets.widgets.plots.toolbar_components.plot_export import plot_export_bundle
|
||||
|
||||
class MainWindow(QMainWindow): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Toolbar / ToolbarBundle Demo")
|
||||
self.central_widget = QWidget()
|
||||
self.setCentralWidget(self.central_widget)
|
||||
self.test_label = QLabel(text="This is a test label.")
|
||||
self.central_widget.layout = QVBoxLayout(self.central_widget)
|
||||
self.central_widget.layout.addWidget(self.test_label)
|
||||
|
||||
self.toolbar = ModularToolBar(parent=self)
|
||||
self.addToolBar(self.toolbar)
|
||||
self.toolbar.add_bundle(performance_bundle(self.toolbar.components))
|
||||
self.toolbar.add_bundle(plot_export_bundle(self.toolbar.components))
|
||||
self.toolbar.connect_bundle(
|
||||
"base", PerformanceConnection(self.toolbar.components, self)
|
||||
)
|
||||
self.toolbar.show_bundles(["performance", "plot_export"])
|
||||
self.toolbar.get_bundle("performance").add_action("save")
|
||||
self.toolbar.refresh()
|
||||
|
||||
def enable_fps_monitor(self, enabled: bool):
|
||||
"""
|
||||
Example method to enable or disable FPS monitoring.
|
||||
This method should be implemented in the target widget.
|
||||
"""
|
||||
if enabled:
|
||||
self.test_label.setText("FPS Monitor Enabled")
|
||||
else:
|
||||
self.test_label.setText("FPS Monitor Disabled")
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("light")
|
||||
main_window = MainWindow()
|
||||
main_window.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -264,6 +264,48 @@ class WidgetIO:
|
||||
return WidgetIO._handlers[base]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_widgets(widget_class: QWidget | str, recursive: bool = True) -> list[QWidget]:
|
||||
"""
|
||||
Return widgets matching the given class (or class-name string).
|
||||
|
||||
Args:
|
||||
widget_class: Either a QWidget subclass or its class-name as a string.
|
||||
recursive: If True (default), traverse all top-level widgets and their children;
|
||||
if False, scan app.allWidgets() for a flat list.
|
||||
|
||||
Returns:
|
||||
List of QWidget instances matching the class or class-name.
|
||||
"""
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
raise RuntimeError("No QApplication instance found.")
|
||||
|
||||
# Match by class-name string
|
||||
if isinstance(widget_class, str):
|
||||
name = widget_class
|
||||
if recursive:
|
||||
result: list[QWidget] = []
|
||||
for top in app.topLevelWidgets():
|
||||
if top.__class__.__name__ == name:
|
||||
result.append(top)
|
||||
result.extend(
|
||||
w for w in top.findChildren(QWidget) if w.__class__.__name__ == name
|
||||
)
|
||||
return result
|
||||
return [w for w in app.allWidgets() if w.__class__.__name__ == name]
|
||||
|
||||
# Match by actual class
|
||||
if recursive:
|
||||
result: list[QWidget] = []
|
||||
for top in app.topLevelWidgets():
|
||||
if isinstance(top, widget_class):
|
||||
result.append(top)
|
||||
result.extend(top.findChildren(widget_class))
|
||||
return result
|
||||
|
||||
return [w for w in app.allWidgets() if isinstance(w, widget_class)]
|
||||
|
||||
|
||||
################## for exporting and importing widget hierarchies ##################
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class AutoUpdates(BECMainWindow):
|
||||
_default_dock: BECDock
|
||||
USER_ACCESS = ["enabled", "enabled.setter", "selected_device", "selected_device.setter"]
|
||||
RPC = True
|
||||
PLUGIN = False
|
||||
|
||||
# enforce that subclasses have the same rpc widget class
|
||||
rpc_widget_class = "AutoUpdates"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['dock_area.py']}
|
||||
@@ -1,22 +1,19 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECDockArea' name='dock_area'>
|
||||
<widget class='BECDockArea' name='bec_dock_area'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -24,6 +21,8 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = BECDockArea(parent)
|
||||
return t
|
||||
|
||||
@@ -31,13 +30,13 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Plots"
|
||||
return "BEC Containers"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(BECDockArea.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "dock_area"
|
||||
return "bec_dock_area"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
@@ -52,7 +51,7 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BECDockArea"
|
||||
|
||||
def toolTip(self):
|
||||
return "BECDockArea"
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -389,6 +389,7 @@ class BECDock(BECWidget, Dock):
|
||||
if widget in self.widgets:
|
||||
self.widgets.remove(widget)
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
|
||||
def delete_all(self):
|
||||
"""
|
||||
|
||||
@@ -15,18 +15,20 @@ 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 (
|
||||
from bec_widgets.utils.toolbars.actions import (
|
||||
ExpandableMenuAction,
|
||||
MaterialIconAction,
|
||||
ModularToolBar,
|
||||
SeparatorAction,
|
||||
WidgetAction,
|
||||
)
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
||||
from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
|
||||
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
|
||||
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
|
||||
@@ -69,6 +71,7 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"detach_dock",
|
||||
"attach_all",
|
||||
"save_state",
|
||||
"screenshot",
|
||||
"restore_state",
|
||||
]
|
||||
|
||||
@@ -104,145 +107,239 @@ class BECDockArea(BECWidget, QWidget):
|
||||
|
||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||
self.dock_area = DockArea(parent=self)
|
||||
self.toolbar = ModularToolBar(
|
||||
parent=self,
|
||||
actions={
|
||||
"menu_plots": ExpandableMenuAction(
|
||||
label="Add Plot ",
|
||||
actions={
|
||||
"waveform": MaterialIconAction(
|
||||
icon_name=Waveform.ICON_NAME, tooltip="Add Waveform", filled=True
|
||||
),
|
||||
"scatter_waveform": MaterialIconAction(
|
||||
icon_name=ScatterWaveform.ICON_NAME,
|
||||
tooltip="Add Scatter Waveform",
|
||||
filled=True,
|
||||
),
|
||||
"multi_waveform": MaterialIconAction(
|
||||
icon_name=MultiWaveform.ICON_NAME,
|
||||
tooltip="Add Multi Waveform",
|
||||
filled=True,
|
||||
),
|
||||
"image": MaterialIconAction(
|
||||
icon_name=Image.ICON_NAME, tooltip="Add Image", filled=True
|
||||
),
|
||||
"motor_map": MaterialIconAction(
|
||||
icon_name=MotorMap.ICON_NAME, tooltip="Add Motor Map", filled=True
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_0": SeparatorAction(),
|
||||
"menu_devices": ExpandableMenuAction(
|
||||
label="Add Device Control ",
|
||||
actions={
|
||||
"scan_control": MaterialIconAction(
|
||||
icon_name=ScanControl.ICON_NAME, tooltip="Add Scan Control", filled=True
|
||||
),
|
||||
"positioner_box": MaterialIconAction(
|
||||
icon_name=PositionerBox.ICON_NAME, tooltip="Add Device Box", filled=True
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_1": SeparatorAction(),
|
||||
"menu_utils": ExpandableMenuAction(
|
||||
label="Add Utils ",
|
||||
actions={
|
||||
"queue": MaterialIconAction(
|
||||
icon_name=BECQueue.ICON_NAME, tooltip="Add Scan Queue", filled=True
|
||||
),
|
||||
"vs_code": MaterialIconAction(
|
||||
icon_name=VSCodeEditor.ICON_NAME, tooltip="Add VS Code", filled=True
|
||||
),
|
||||
"status": MaterialIconAction(
|
||||
icon_name=BECStatusBox.ICON_NAME,
|
||||
tooltip="Add BEC Status Box",
|
||||
filled=True,
|
||||
),
|
||||
"progress_bar": MaterialIconAction(
|
||||
icon_name=RingProgressBar.ICON_NAME,
|
||||
tooltip="Add Circular ProgressBar",
|
||||
filled=True,
|
||||
),
|
||||
# FIXME temporarily disabled -> issue #644
|
||||
"log_panel": MaterialIconAction(
|
||||
icon_name=LogPanel.ICON_NAME,
|
||||
tooltip="Add LogPanel - Disabled",
|
||||
filled=True,
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_2": SeparatorAction(),
|
||||
"attach_all": MaterialIconAction(
|
||||
icon_name="zoom_in_map", tooltip="Attach all floating docks"
|
||||
),
|
||||
"save_state": MaterialIconAction(icon_name="bookmark", tooltip="Save Dock State"),
|
||||
"restore_state": MaterialIconAction(
|
||||
icon_name="frame_reload", tooltip="Restore Dock State"
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
)
|
||||
self.toolbar = ModularToolBar(parent=self)
|
||||
self._setup_toolbar()
|
||||
|
||||
self.layout.addWidget(self.toolbar)
|
||||
self.layout.addWidget(self.dock_area)
|
||||
self.spacer = QWidget(parent=self)
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar.addWidget(self.spacer)
|
||||
self.toolbar.addWidget(self.dark_mode_button)
|
||||
|
||||
self._hook_toolbar()
|
||||
self.toolbar.show_bundles(
|
||||
["menu_plots", "menu_devices", "menu_utils", "dock_actions", "dark_mode"]
|
||||
)
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return QSize(800, 600)
|
||||
|
||||
def _setup_toolbar(self):
|
||||
|
||||
# Add plot menu
|
||||
self.toolbar.components.add_safe(
|
||||
"menu_plots",
|
||||
ExpandableMenuAction(
|
||||
label="Add Plot ",
|
||||
actions={
|
||||
"waveform": MaterialIconAction(
|
||||
icon_name=Waveform.ICON_NAME,
|
||||
tooltip="Add Waveform",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"scatter_waveform": MaterialIconAction(
|
||||
icon_name=ScatterWaveform.ICON_NAME,
|
||||
tooltip="Add Scatter Waveform",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"multi_waveform": MaterialIconAction(
|
||||
icon_name=MultiWaveform.ICON_NAME,
|
||||
tooltip="Add Multi Waveform",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"image": MaterialIconAction(
|
||||
icon_name=Image.ICON_NAME, tooltip="Add Image", filled=True, parent=self
|
||||
),
|
||||
"motor_map": MaterialIconAction(
|
||||
icon_name=MotorMap.ICON_NAME,
|
||||
tooltip="Add Motor Map",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"heatmap": MaterialIconAction(
|
||||
icon_name=Heatmap.ICON_NAME, tooltip="Add Heatmap", filled=True, parent=self
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
bundle = ToolbarBundle("menu_plots", self.toolbar.components)
|
||||
bundle.add_action("menu_plots")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
# Add control menu
|
||||
self.toolbar.components.add_safe(
|
||||
"menu_devices",
|
||||
ExpandableMenuAction(
|
||||
label="Add Device Control ",
|
||||
actions={
|
||||
"scan_control": MaterialIconAction(
|
||||
icon_name=ScanControl.ICON_NAME,
|
||||
tooltip="Add Scan Control",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"positioner_box": MaterialIconAction(
|
||||
icon_name=PositionerBox.ICON_NAME,
|
||||
tooltip="Add Device Box",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
bundle = ToolbarBundle("menu_devices", self.toolbar.components)
|
||||
bundle.add_action("menu_devices")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
# Add utils menu
|
||||
self.toolbar.components.add_safe(
|
||||
"menu_utils",
|
||||
ExpandableMenuAction(
|
||||
label="Add Utils ",
|
||||
actions={
|
||||
"queue": MaterialIconAction(
|
||||
icon_name=BECQueue.ICON_NAME,
|
||||
tooltip="Add Scan Queue",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"vs_code": MaterialIconAction(
|
||||
icon_name=VSCodeEditor.ICON_NAME,
|
||||
tooltip="Add VS Code",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"status": MaterialIconAction(
|
||||
icon_name=BECStatusBox.ICON_NAME,
|
||||
tooltip="Add BEC Status Box",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"progress_bar": MaterialIconAction(
|
||||
icon_name=RingProgressBar.ICON_NAME,
|
||||
tooltip="Add Circular ProgressBar",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
# FIXME temporarily disabled -> issue #644
|
||||
"log_panel": MaterialIconAction(
|
||||
icon_name=LogPanel.ICON_NAME,
|
||||
tooltip="Add LogPanel - Disabled",
|
||||
filled=True,
|
||||
parent=self,
|
||||
),
|
||||
"sbb_monitor": MaterialIconAction(
|
||||
icon_name="train", tooltip="Add SBB Monitor", filled=True, parent=self
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
bundle = ToolbarBundle("menu_utils", self.toolbar.components)
|
||||
bundle.add_action("menu_utils")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
########## Dock Actions ##########
|
||||
spacer = QWidget(parent=self)
|
||||
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar.components.add_safe("spacer", WidgetAction(widget=spacer, adjust_size=False))
|
||||
|
||||
self.toolbar.components.add_safe(
|
||||
"dark_mode", WidgetAction(widget=self.dark_mode_button, adjust_size=False)
|
||||
)
|
||||
|
||||
bundle = ToolbarBundle("dark_mode", self.toolbar.components)
|
||||
bundle.add_action("spacer")
|
||||
bundle.add_action("dark_mode")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
self.toolbar.components.add_safe(
|
||||
"attach_all",
|
||||
MaterialIconAction(
|
||||
icon_name="zoom_in_map", tooltip="Attach all floating docks", parent=self
|
||||
),
|
||||
)
|
||||
|
||||
self.toolbar.components.add_safe(
|
||||
"save_state",
|
||||
MaterialIconAction(icon_name="bookmark", tooltip="Save Dock State", parent=self),
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"restore_state",
|
||||
MaterialIconAction(icon_name="frame_reload", tooltip="Restore Dock State", parent=self),
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"screenshot",
|
||||
MaterialIconAction(icon_name="photo_camera", tooltip="Take Screenshot", parent=self),
|
||||
)
|
||||
|
||||
bundle = ToolbarBundle("dock_actions", self.toolbar.components)
|
||||
bundle.add_action("attach_all")
|
||||
bundle.add_action("save_state")
|
||||
bundle.add_action("restore_state")
|
||||
bundle.add_action("screenshot")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
def _hook_toolbar(self):
|
||||
# Menu Plot
|
||||
self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect(
|
||||
menu_plots = self.toolbar.components.get_action("menu_plots")
|
||||
menu_devices = self.toolbar.components.get_action("menu_devices")
|
||||
menu_utils = self.toolbar.components.get_action("menu_utils")
|
||||
|
||||
menu_plots.actions["waveform"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="Waveform")
|
||||
)
|
||||
self.toolbar.widgets["menu_plots"].widgets["scatter_waveform"].triggered.connect(
|
||||
|
||||
menu_plots.actions["scatter_waveform"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="ScatterWaveform")
|
||||
)
|
||||
self.toolbar.widgets["menu_plots"].widgets["multi_waveform"].triggered.connect(
|
||||
menu_plots.actions["multi_waveform"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="MultiWaveform")
|
||||
)
|
||||
self.toolbar.widgets["menu_plots"].widgets["image"].triggered.connect(
|
||||
menu_plots.actions["image"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="Image")
|
||||
)
|
||||
self.toolbar.widgets["menu_plots"].widgets["motor_map"].triggered.connect(
|
||||
menu_plots.actions["motor_map"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="MotorMap")
|
||||
)
|
||||
menu_plots.actions["heatmap"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="Heatmap")
|
||||
)
|
||||
|
||||
# Menu Devices
|
||||
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect(
|
||||
menu_devices.actions["scan_control"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="ScanControl")
|
||||
)
|
||||
self.toolbar.widgets["menu_devices"].widgets["positioner_box"].triggered.connect(
|
||||
menu_devices.actions["positioner_box"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="PositionerBox")
|
||||
)
|
||||
|
||||
# Menu Utils
|
||||
self.toolbar.widgets["menu_utils"].widgets["queue"].triggered.connect(
|
||||
menu_utils.actions["queue"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="BECQueue")
|
||||
)
|
||||
self.toolbar.widgets["menu_utils"].widgets["status"].triggered.connect(
|
||||
menu_utils.actions["status"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="BECStatusBox")
|
||||
)
|
||||
self.toolbar.widgets["menu_utils"].widgets["vs_code"].triggered.connect(
|
||||
menu_utils.actions["vs_code"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="VSCodeEditor")
|
||||
)
|
||||
self.toolbar.widgets["menu_utils"].widgets["progress_bar"].triggered.connect(
|
||||
menu_utils.actions["progress_bar"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="RingProgressBar")
|
||||
)
|
||||
# FIXME temporarily disabled -> issue #644
|
||||
self.toolbar.widgets["menu_utils"].widgets["log_panel"].setEnabled(False)
|
||||
# self.toolbar.widgets["menu_utils"].widgets["log_panel"].triggered.connect(
|
||||
# lambda: self._create_widget_from_toolbar(widget_name="LogPanel")
|
||||
# )
|
||||
menu_utils.actions["log_panel"].action.setEnabled(False)
|
||||
|
||||
menu_utils.actions["sbb_monitor"].action.triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="SBBMonitor")
|
||||
)
|
||||
|
||||
# Icons
|
||||
self.toolbar.widgets["attach_all"].action.triggered.connect(self.attach_all)
|
||||
self.toolbar.widgets["save_state"].action.triggered.connect(self.save_state)
|
||||
self.toolbar.widgets["restore_state"].action.triggered.connect(self.restore_state)
|
||||
self.toolbar.components.get_action("attach_all").action.triggered.connect(self.attach_all)
|
||||
self.toolbar.components.get_action("save_state").action.triggered.connect(self.save_state)
|
||||
self.toolbar.components.get_action("restore_state").action.triggered.connect(
|
||||
self.restore_state
|
||||
)
|
||||
self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot)
|
||||
|
||||
@SafeSlot()
|
||||
def _create_widget_from_toolbar(self, widget_name: str) -> None:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['dock_area.py','dock.py']}
|
||||
@@ -6,7 +6,7 @@ def main(): # pragma: no cover
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.containers.dock.dock_area_plugin import BECDockAreaPlugin
|
||||
from bec_widgets.widgets.containers.dock.bec_dock_area_plugin import BECDockAreaPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECDockAreaPlugin())
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QPoint, Qt
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QProgressBar, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class WidgetTooltip(QWidget):
|
||||
"""Frameless, always-on-top window that behaves like a tooltip."""
|
||||
|
||||
def __init__(self, content: QWidget) -> None:
|
||||
super().__init__(None, Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating)
|
||||
self.setMouseTracking(True)
|
||||
self.content = content
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(6, 6, 6, 6)
|
||||
layout.addWidget(self.content)
|
||||
self.adjustSize()
|
||||
|
||||
def leaveEvent(self, _event) -> None:
|
||||
self.hide()
|
||||
|
||||
def show_above(self, global_pos: QPoint, offset: int = 8) -> None:
|
||||
self.adjustSize()
|
||||
screen = QApplication.screenAt(global_pos) or QApplication.primaryScreen()
|
||||
screen_geo = screen.availableGeometry()
|
||||
geom = self.geometry()
|
||||
|
||||
x = global_pos.x() - geom.width() // 2
|
||||
y = global_pos.y() - geom.height() - offset
|
||||
|
||||
x = max(screen_geo.left(), min(x, screen_geo.right() - geom.width()))
|
||||
y = max(screen_geo.top(), min(y, screen_geo.bottom() - geom.height()))
|
||||
|
||||
self.move(x, y)
|
||||
self.show()
|
||||
|
||||
|
||||
class HoverWidget(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, *, simple: QWidget, full: QWidget):
|
||||
super().__init__(parent)
|
||||
self._simple = simple
|
||||
self._full = full
|
||||
self._full.setVisible(False)
|
||||
self._tooltip = None
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.addWidget(simple)
|
||||
|
||||
def enterEvent(self, event):
|
||||
# suppress empty-label tooltips for labels
|
||||
if isinstance(self._full, QLabel) and not self._full.text():
|
||||
return
|
||||
|
||||
if self._tooltip is None: # first time only
|
||||
self._tooltip = WidgetTooltip(self._full)
|
||||
self._full.setVisible(True)
|
||||
|
||||
centre = self.mapToGlobal(self.rect().center())
|
||||
self._tooltip.show_above(centre)
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
if self._tooltip and self._tooltip.isVisible():
|
||||
self._tooltip.hide()
|
||||
super().leaveEvent(event)
|
||||
|
||||
def close(self):
|
||||
if self._tooltip:
|
||||
self._tooltip.close()
|
||||
self._tooltip.deleteLater()
|
||||
self._tooltip = None
|
||||
super().close()
|
||||
|
||||
|
||||
################################################################################
|
||||
# Demo
|
||||
# Just a simple example to show how the HoverWidget can be used to display
|
||||
# a tooltip with a full widget inside (two different widgets are used
|
||||
# for the simple and full versions).
|
||||
################################################################################
|
||||
|
||||
|
||||
class DemoSimpleWidget(QLabel): # pragma: no cover
|
||||
"""A simple widget to be used as a trigger for the tooltip."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setText("Hover me for a preview!")
|
||||
|
||||
|
||||
class DemoFullWidget(QProgressBar): # pragma: no cover
|
||||
"""A full widget to be shown in the tooltip."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setRange(0, 100)
|
||||
self.setValue(75)
|
||||
self.setFixedWidth(320)
|
||||
self.setFixedHeight(30)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
window = QWidget()
|
||||
window.layout = QHBoxLayout(window)
|
||||
hover_widget = HoverWidget(simple=DemoSimpleWidget(), full=DemoFullWidget())
|
||||
window.layout.addWidget(hover_widget)
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{'files': ['main_window.py']}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECMainWindow' name='bec_main_window'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class BECMainWindowPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
# We want to initialize BECMainWindow upon starting designer
|
||||
t = BECMainWindow(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Containers"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(BECMainWindow.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_main_window"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
import os
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
import bec_widgets
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
QApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
|
||||
app = QApplication.instance()
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "BEC-General-App.png"),
|
||||
size=QSize(48, 48),
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return True
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "BECMainWindow"
|
||||
|
||||
def toolTip(self):
|
||||
return "BECMainWindow"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -3,15 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import (
|
||||
QAbstractAnimation,
|
||||
QEasingCurve,
|
||||
QEvent,
|
||||
QPropertyAnimation,
|
||||
QSize,
|
||||
Qt,
|
||||
QTimer,
|
||||
)
|
||||
from qtpy.QtCore import QEasingCurve, QEvent, QPropertyAnimation, QSize, Qt, QTimer
|
||||
from qtpy.QtGui import QAction, QActionGroup, QIcon
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -27,19 +19,28 @@ from qtpy.QtWidgets import (
|
||||
import bec_widgets
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.colors import apply_theme, set_theme
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.containers.main_window.addons.hover_widget import HoverWidget
|
||||
from bec_widgets.widgets.containers.main_window.addons.notification_center.notification_banner import (
|
||||
BECNotificationBroker,
|
||||
NotificationCentre,
|
||||
NotificationIndicator,
|
||||
)
|
||||
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
|
||||
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||||
from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
# Ensure the application does not use the native menu bar on macOS to be consistent with linux development.
|
||||
QApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
|
||||
|
||||
|
||||
class BECMainWindow(BECWidget, QMainWindow):
|
||||
RPC = False
|
||||
PLUGIN = False
|
||||
RPC = True
|
||||
PLUGIN = True
|
||||
SCAN_PROGRESS_WIDTH = 100 # px
|
||||
STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds
|
||||
|
||||
@@ -57,6 +58,14 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self.app = QApplication.instance()
|
||||
self.status_bar = self.statusBar()
|
||||
self.setWindowTitle(window_title)
|
||||
|
||||
# Notification Centre overlay
|
||||
self.notification_centre = NotificationCentre(parent=self) # Notification layer
|
||||
self.notification_broker = BECNotificationBroker()
|
||||
self._nc_margin = 16
|
||||
self._position_notification_centre()
|
||||
|
||||
# Init ui
|
||||
self._init_ui()
|
||||
self._connect_to_theme_change()
|
||||
|
||||
@@ -65,6 +74,34 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self.display_client_message, MessageEndpoints.client_info()
|
||||
)
|
||||
|
||||
def setCentralWidget(self, widget: QWidget, qt_default: bool = False): # type: ignore[override]
|
||||
"""
|
||||
Re‑implement QMainWindow.setCentralWidget so that the *main content*
|
||||
widget always lives on the lower layer of the stacked layout that
|
||||
hosts our notification overlays.
|
||||
|
||||
Args:
|
||||
widget: The widget that should become the new central content.
|
||||
qt_default: When *True* the call is forwarded to the base class so
|
||||
that Qt behaves exactly as the original implementation (used
|
||||
during __init__ when we first install ``self._full_content``).
|
||||
"""
|
||||
super().setCentralWidget(widget)
|
||||
self.notification_centre.raise_()
|
||||
self.statusBar().raise_()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self._position_notification_centre()
|
||||
|
||||
def _position_notification_centre(self):
|
||||
"""Keep the notification panel at a fixed margin top-right."""
|
||||
if not hasattr(self, "notification_centre"):
|
||||
return
|
||||
margin = getattr(self, "_nc_margin", 16) # px
|
||||
nc = self.notification_centre
|
||||
nc.move(self.width() - nc.width() - margin, margin)
|
||||
|
||||
################################################################################
|
||||
# MainWindow Elements Initialization
|
||||
################################################################################
|
||||
@@ -96,33 +133,80 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self._add_separator()
|
||||
|
||||
# Centre: Client‑info label (stretch=1 so it expands)
|
||||
self._client_info_label = ScrollLabel()
|
||||
self._add_client_info_label()
|
||||
|
||||
# Add scan_progress bar with display logic
|
||||
self._add_scan_progress_bar()
|
||||
|
||||
# Setup NotificationIndicator to bottom right of the status bar
|
||||
self._add_notification_indicator()
|
||||
|
||||
################################################################################
|
||||
# Notification indicator and Notification Centre helpers
|
||||
|
||||
def _add_notification_indicator(self):
|
||||
"""
|
||||
Add the notification indicator to the status bar and hook the signals.
|
||||
"""
|
||||
# Add the notification indicator to the status bar
|
||||
self.notification_indicator = NotificationIndicator(self)
|
||||
self.status_bar.addPermanentWidget(self.notification_indicator)
|
||||
|
||||
# Connect the notification broker to the indicator
|
||||
self.notification_centre.counts_updated.connect(self.notification_indicator.update_counts)
|
||||
self.notification_indicator.filter_changed.connect(self.notification_centre.apply_filter)
|
||||
self.notification_indicator.show_all_requested.connect(self.notification_centre.show_all)
|
||||
self.notification_indicator.hide_all_requested.connect(self.notification_centre.hide_all)
|
||||
|
||||
################################################################################
|
||||
# Client message status bar widget helpers
|
||||
|
||||
def _add_client_info_label(self):
|
||||
"""
|
||||
Add a client info label to the status bar.
|
||||
This label will display messages from the BEC dispatcher.
|
||||
"""
|
||||
|
||||
# Scroll label for client info in Status Bar
|
||||
self._client_info_label = ScrollLabel(self)
|
||||
self._client_info_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.status_bar.addWidget(self._client_info_label, 1)
|
||||
# Full label used in the hover widget
|
||||
self._client_info_label_full = QLabel(self)
|
||||
self._client_info_label_full.setAlignment(
|
||||
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
# Hover widget to show the full client info label
|
||||
self._client_info_hover = HoverWidget(
|
||||
self, simple=self._client_info_label, full=self._client_info_label_full
|
||||
)
|
||||
self.status_bar.addWidget(self._client_info_hover, 1)
|
||||
|
||||
# Timer to automatically clear client messages once they expire
|
||||
self._client_info_expire_timer = QTimer(self)
|
||||
self._client_info_expire_timer.setSingleShot(True)
|
||||
self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText(""))
|
||||
|
||||
# Add scan_progress bar with display logic
|
||||
self._add_scan_progress_bar()
|
||||
self._client_info_expire_timer.timeout.connect(
|
||||
lambda: self._client_info_label_full.setText("")
|
||||
)
|
||||
|
||||
################################################################################
|
||||
# Progress‑bar helpers
|
||||
def _add_scan_progress_bar(self):
|
||||
|
||||
# --- Progress bar -------------------------------------------------
|
||||
# Scan progress bar minimalistic design setup
|
||||
self._scan_progress_bar = ScanProgressBar(self, one_line_design=True)
|
||||
self._scan_progress_bar.show_elapsed_time = False
|
||||
self._scan_progress_bar.show_remaining_time = False
|
||||
self._scan_progress_bar.show_source_label = False
|
||||
self._scan_progress_bar.progressbar.label_template = ""
|
||||
self._scan_progress_bar.progressbar.setFixedHeight(8)
|
||||
self._scan_progress_bar.progressbar.setFixedWidth(80)
|
||||
# Setting HoverWidget for the scan progress bar - minimal and full version
|
||||
self._scan_progress_bar_simple = ScanProgressBar(self, one_line_design=True)
|
||||
self._scan_progress_bar_simple.show_elapsed_time = False
|
||||
self._scan_progress_bar_simple.show_remaining_time = False
|
||||
self._scan_progress_bar_simple.show_source_label = False
|
||||
self._scan_progress_bar_simple.progressbar.label_template = ""
|
||||
self._scan_progress_bar_simple.progressbar.setFixedHeight(8)
|
||||
self._scan_progress_bar_simple.progressbar.setFixedWidth(80)
|
||||
self._scan_progress_bar_full = ScanProgressBar(self)
|
||||
self._scan_progress_hover = HoverWidget(
|
||||
self, simple=self._scan_progress_bar_simple, full=self._scan_progress_bar_full
|
||||
)
|
||||
|
||||
# Bundle the progress bar with a separator
|
||||
separator = self._add_separator(separate_object=True)
|
||||
@@ -133,7 +217,7 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self._scan_progress_bar_with_separator.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._scan_progress_bar_with_separator.layout.setSpacing(0)
|
||||
self._scan_progress_bar_with_separator.layout.addWidget(separator)
|
||||
self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_bar)
|
||||
self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_hover)
|
||||
|
||||
# Set Size
|
||||
self._scan_progress_bar_target_width = self.SCAN_PROGRESS_WIDTH
|
||||
@@ -152,8 +236,8 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self._scan_progress_hide_timer.timeout.connect(self._animate_hide_scan_progress_bar)
|
||||
|
||||
# Show / hide behaviour
|
||||
self._scan_progress_bar.progress_started.connect(self._show_scan_progress_bar)
|
||||
self._scan_progress_bar.progress_finished.connect(self._delay_hide_scan_progress_bar)
|
||||
self._scan_progress_bar_simple.progress_started.connect(self._show_scan_progress_bar)
|
||||
self._scan_progress_bar_simple.progress_finished.connect(self._delay_hide_scan_progress_bar)
|
||||
|
||||
def _show_scan_progress_bar(self):
|
||||
if self._scan_progress_hide_timer.isActive():
|
||||
@@ -342,10 +426,10 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
msg(dict): The message to display, should contain:
|
||||
meta(dict): Metadata about the message, usually empty.
|
||||
"""
|
||||
# self._client_info_label.setText("")
|
||||
message = msg.get("message", "")
|
||||
expiration = msg.get("expire", 0) # 0 → never expire
|
||||
self._client_info_label.setText(message)
|
||||
self._client_info_label_full.setText(message)
|
||||
|
||||
# Restart the expiration timer if necessary
|
||||
if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
|
||||
@@ -359,12 +443,12 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
@SafeSlot(str)
|
||||
def change_theme(self, theme: str):
|
||||
"""
|
||||
Change the theme of the application.
|
||||
Change the theme of the application and propagate it to widgets.
|
||||
|
||||
Args:
|
||||
theme(str): The theme to apply, either "light" or "dark".
|
||||
theme(str): Either "light" or "dark".
|
||||
"""
|
||||
apply_theme(theme)
|
||||
set_theme(theme) # emits theme_updated and applies palette globally
|
||||
|
||||
def event(self, event):
|
||||
if event.type() == QEvent.Type.StatusTip:
|
||||
@@ -393,22 +477,33 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
if hasattr(self, "_scan_progress_hide_timer") and self._scan_progress_hide_timer.isActive():
|
||||
self._scan_progress_hide_timer.stop()
|
||||
|
||||
########################################
|
||||
# Status bar widgets cleanup
|
||||
|
||||
# Client info label cleanup
|
||||
self._client_info_label.cleanup()
|
||||
self._scan_progress_bar.close()
|
||||
self._scan_progress_bar.deleteLater()
|
||||
self._client_info_hover.close()
|
||||
self._client_info_hover.deleteLater()
|
||||
# Scan progress bar cleanup
|
||||
self._scan_progress_bar_simple.close()
|
||||
self._scan_progress_bar_simple.deleteLater()
|
||||
self._scan_progress_bar_full.close()
|
||||
self._scan_progress_bar_full.deleteLater()
|
||||
self._scan_progress_hover.close()
|
||||
self._scan_progress_hover.deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
class UILaunchWindow(BECMainWindow):
|
||||
RPC = True
|
||||
class BECMainWindowNoRPC(BECMainWindow):
|
||||
RPC = False
|
||||
PLUGIN = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
main_window = UILaunchWindow()
|
||||
main_window = BECMainWindow()
|
||||
main_window.show()
|
||||
main_window.resize(800, 600)
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.containers.main_window.bec_main_window_plugin import (
|
||||
BECMainWindowPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECMainWindowPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
|
||||
@@ -20,6 +21,8 @@ class AbortButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = AbortButton(parent)
|
||||
return t
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.buttons.button_reset.button_reset import ResetButton
|
||||
@@ -20,6 +21,8 @@ class ResetButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = ResetButton(parent)
|
||||
return t
|
||||
|
||||
@@ -48,7 +51,7 @@ class ResetButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "ResetButton"
|
||||
|
||||
def toolTip(self):
|
||||
return "A button that reset the scan queue."
|
||||
return "A button that resets the scan queue."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.buttons.button_resume.button_resume import ResumeButton
|
||||
@@ -20,6 +21,8 @@ class ResumeButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = ResumeButton(parent)
|
||||
return t
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
||||
|
||||
@@ -15,8 +14,6 @@ DOM_XML = """
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -24,6 +21,8 @@ class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = StopButton(parent)
|
||||
return t
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
|
||||
PositionIndicator,
|
||||
@@ -17,8 +16,6 @@ DOM_XML = """
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -26,6 +23,8 @@ class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = PositionIndicator(parent)
|
||||
return t
|
||||
|
||||
@@ -54,7 +53,7 @@ class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
||||
return "PositionIndicator"
|
||||
|
||||
def toolTip(self):
|
||||
return "PositionIndicator"
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -138,7 +138,11 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
signals = msg_content.get("signals", {})
|
||||
# pylint: disable=protected-access
|
||||
hinted_signals = self.dev[device]._hints
|
||||
precision = self.dev[device].precision
|
||||
precision = getattr(self.dev[device], "precision", 8)
|
||||
try:
|
||||
precision = int(precision)
|
||||
except (TypeError, ValueError):
|
||||
precision = int(8)
|
||||
|
||||
spinner = ui_components["spinner"]
|
||||
position_indicator = ui_components["position_indicator"]
|
||||
@@ -178,11 +182,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
spinner.setVisible(False)
|
||||
|
||||
if readback_val is not None:
|
||||
readback.setText(f"{readback_val:.{precision}f}")
|
||||
text = f"{readback_val:.{precision}f}"
|
||||
readback.setText(text)
|
||||
position_emit(readback_val)
|
||||
|
||||
if setpoint_val is not None:
|
||||
setpoint.setText(f"{setpoint_val:.{precision}f}")
|
||||
text = f"{setpoint_val:.{precision}f}"
|
||||
setpoint.setText(text)
|
||||
|
||||
limits = self.dev[device].limits
|
||||
limit_update(limits)
|
||||
@@ -205,10 +211,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
ui["readback"].setToolTip(f"{device} readback")
|
||||
ui["setpoint"].setToolTip(f"{device} setpoint")
|
||||
ui["step_size"].setToolTip(f"Step size for {device}")
|
||||
precision = self.dev[device].precision
|
||||
if precision is not None:
|
||||
ui["step_size"].setDecimals(precision)
|
||||
ui["step_size"].setValue(10**-precision * 10)
|
||||
precision = getattr(self.dev[device], "precision", 8)
|
||||
try:
|
||||
precision = int(precision)
|
||||
except (TypeError, ValueError):
|
||||
precision = int(8)
|
||||
ui["step_size"].setDecimals(precision)
|
||||
ui["step_size"].setValue(10**-precision * 10)
|
||||
|
||||
def _swap_readback_signal_connection(self, slot, old_device, new_device):
|
||||
self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))
|
||||
|
||||
@@ -33,7 +33,7 @@ class PositionerBox(PositionerBoxBase):
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
|
||||
USER_ACCESS = ["set_positioner"]
|
||||
USER_ACCESS = ["set_positioner", "screenshot"]
|
||||
device_changed = Signal(str, str)
|
||||
# Signal emitted to inform listeners about a position update
|
||||
position_update = Signal(float)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import (
|
||||
PositionerBox,
|
||||
)
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
@@ -14,7 +15,6 @@ DOM_XML = """
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
@@ -23,6 +23,8 @@ class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = PositionerBox(parent)
|
||||
return t
|
||||
|
||||
@@ -30,7 +32,7 @@ class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
return "BEC Device Control"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(PositionerBox.ICON_NAME)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
|
||||
@@ -22,6 +23,8 @@ class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = PositionerBox2D(parent)
|
||||
return t
|
||||
|
||||
@@ -29,7 +32,7 @@ class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
return "BEC Device Control"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(PositionerBox2D.ICON_NAME)
|
||||
|
||||
@@ -34,7 +34,7 @@ class PositionerBox2D(PositionerBoxBase):
|
||||
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
|
||||
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver", "screenshot"]
|
||||
|
||||
device_changed_hor = Signal(str, str)
|
||||
device_changed_ver = Signal(str, str)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerControlLine
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import (
|
||||
PositionerControlLine,
|
||||
)
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
@@ -14,7 +15,6 @@ DOM_XML = """
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
@@ -23,6 +23,8 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = PositionerControlLine(parent)
|
||||
return t
|
||||
|
||||
@@ -30,7 +32,7 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
return "BEC Device Control"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(PositionerControlLine.ICON_NAME)
|
||||
@@ -51,7 +53,7 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
|
||||
return "PositionerControlLine"
|
||||
|
||||
def toolTip(self):
|
||||
return "A widget that controls a single positioner in line form."
|
||||
return "A widget that controls a single device."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = bec_logger.logger
|
||||
|
||||
|
||||
class PositionerGroupBox(QGroupBox):
|
||||
PLUGIN = True
|
||||
|
||||
position_update = Signal(float)
|
||||
|
||||
def __init__(self, parent, dev_name):
|
||||
@@ -45,7 +45,12 @@ class PositionerGroupBox(QGroupBox):
|
||||
|
||||
def _on_position_update(self, pos: float):
|
||||
self.position_update.emit(pos)
|
||||
self.widget.label = f"%.{self.widget.dev[self.widget.device].precision}f" % pos
|
||||
precision = getattr(self.widget.dev[self.widget.device], "precision", 8)
|
||||
try:
|
||||
precision = int(precision)
|
||||
except (TypeError, ValueError):
|
||||
precision = int(8)
|
||||
self.widget.label = f"{pos:.{precision}f}"
|
||||
|
||||
def close(self):
|
||||
self.widget.close()
|
||||
@@ -55,6 +60,7 @@ class PositionerGroupBox(QGroupBox):
|
||||
class PositionerGroup(BECWidget, QWidget):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "grid_view"
|
||||
USER_ACCESS = ["set_positioners"]
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{'files': ['positioner_group.py']}
|
||||
{'files': ['positioner_group.py']}
|
||||
@@ -1,9 +1,8 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_control.positioner_group.positioner_group import (
|
||||
@@ -16,7 +15,6 @@ DOM_XML = """
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
@@ -25,6 +23,8 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = PositionerGroup(parent)
|
||||
return t
|
||||
|
||||
@@ -32,7 +32,7 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
return "BEC Device Control"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(PositionerGroup.ICON_NAME)
|
||||
@@ -53,7 +53,7 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "PositionerGroup"
|
||||
|
||||
def toolTip(self):
|
||||
return "Container Widget to control positioners in compact form, in a grid"
|
||||
return "Simple Widget to control a positioner in box form"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -112,7 +112,9 @@ class DeviceInputBase(BECWidget):
|
||||
WidgetIO.set_value(widget=self, value=device)
|
||||
self.config.default = device
|
||||
else:
|
||||
logger.warning(f"Device {device} is not in the filtered selection.")
|
||||
logger.warning(
|
||||
f"Device {device} is not in the filtered selection of {self}: {self.devices}."
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def update_devices_from_filters(self):
|
||||
@@ -131,7 +133,8 @@ class DeviceInputBase(BECWidget):
|
||||
# Filter based on readout priority
|
||||
devs = [dev for dev in devs if self._check_readout_filter(dev)]
|
||||
self.devices = [device.name for device in devs]
|
||||
self.set_device(current_device)
|
||||
if current_device != "":
|
||||
self.set_device(current_device)
|
||||
|
||||
@SafeSlot(list)
|
||||
def set_available_devices(self, devices: list[str]):
|
||||
|
||||
@@ -55,7 +55,7 @@ class DeviceSignalInputBase(BECWidget):
|
||||
self._hinted_signals = []
|
||||
self._normal_signals = []
|
||||
self._config_signals = []
|
||||
self.bec_dispatcher.client.callbacks.register(
|
||||
self._device_update_register = self.bec_dispatcher.client.callbacks.register(
|
||||
EventType.DEVICE_UPDATE, self.update_signals_from_filters
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ class DeviceSignalInputBase(BECWidget):
|
||||
Args:
|
||||
signal (str): signal name.
|
||||
"""
|
||||
if self.validate_signal(signal) is True:
|
||||
if self.validate_signal(signal):
|
||||
WidgetIO.set_value(widget=self, value=signal)
|
||||
self.config.default = signal
|
||||
else:
|
||||
@@ -289,3 +289,10 @@ class DeviceSignalInputBase(BECWidget):
|
||||
if config is None:
|
||||
return DeviceSignalInputBaseConfig(widget_class=self.__class__.__name__)
|
||||
return DeviceSignalInputBaseConfig.model_validate(config)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the widget.
|
||||
"""
|
||||
self.bec_dispatcher.client.callbacks.remove(self._device_update_register)
|
||||
super().cleanup()
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
{
|
||||
"files": ["device_combobox.py"]
|
||||
}
|
||||
{'files': ['device_combobox.py']}
|
||||
@@ -1,22 +1,19 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='DeviceComboBox' name='device_combobox'>
|
||||
<widget class='DeviceComboBox' name='device_combo_box'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -24,6 +21,8 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = DeviceComboBox(parent)
|
||||
return t
|
||||
|
||||
@@ -37,7 +36,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return designer_material_icon(DeviceComboBox.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "device_combobox"
|
||||
return "device_combo_box"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
@@ -52,7 +51,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "DeviceComboBox"
|
||||
|
||||
def toolTip(self):
|
||||
return "Device ComboBox Example for BEC Widgets"
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -5,6 +5,7 @@ from qtpy.QtGui import QPainter, QPaintEvent, QPen
|
||||
from qtpy.QtWidgets import QComboBox, QSizePolicy
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.utils.error_popups import SafeProperty
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import (
|
||||
BECDeviceFilter,
|
||||
DeviceInputBase,
|
||||
@@ -61,6 +62,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
self._callback_id = None
|
||||
self._is_valid_input = False
|
||||
self._accent_colors = get_accent_colors()
|
||||
self._set_first_element_as_empty = False
|
||||
# We do not consider the config that is passed here, this produced problems
|
||||
# with QtDesigner, since config and input arguments may differ and resolve properly
|
||||
# Implementing this logic and config recoverage is postponed.
|
||||
@@ -93,6 +95,31 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
self.currentTextChanged.connect(self.check_validity)
|
||||
self.check_validity(self.currentText())
|
||||
|
||||
@SafeProperty(bool)
|
||||
def set_first_element_as_empty(self) -> bool:
|
||||
"""
|
||||
Whether the first element in the combobox should be empty.
|
||||
This is useful to allow the user to select a device from the list.
|
||||
"""
|
||||
return self._set_first_element_as_empty
|
||||
|
||||
@set_first_element_as_empty.setter
|
||||
def set_first_element_as_empty(self, value: bool) -> None:
|
||||
"""
|
||||
Set whether the first element in the combobox should be empty.
|
||||
This is useful to allow the user to select a device from the list.
|
||||
|
||||
Args:
|
||||
value (bool): True if the first element should be empty, False otherwise.
|
||||
"""
|
||||
self._set_first_element_as_empty = value
|
||||
if self._set_first_element_as_empty:
|
||||
self.insertItem(0, "")
|
||||
self.setCurrentIndex(0)
|
||||
else:
|
||||
if self.count() > 0 and self.itemText(0) == "":
|
||||
self.removeItem(0)
|
||||
|
||||
def on_device_update(self, action: str, content: dict) -> None:
|
||||
"""
|
||||
Callback for device update events. Triggers the device_update signal.
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
{
|
||||
"files": ["device_line_edit.py"]
|
||||
}
|
||||
{'files': ['device_line_edit.py']}
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
@@ -17,8 +16,6 @@ DOM_XML = """
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -26,6 +23,8 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = DeviceLineEdit(parent)
|
||||
return t
|
||||
|
||||
@@ -54,7 +53,7 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "DeviceLineEdit"
|
||||
|
||||
def toolTip(self):
|
||||
return "Device LineEdit Example for BEC Widgets with autocomplete."
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||
@@ -20,6 +21,8 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = SignalComboBox(parent)
|
||||
return t
|
||||
|
||||
@@ -48,7 +51,7 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "SignalComboBox"
|
||||
|
||||
def toolTip(self):
|
||||
return "Signal ComboBox Example for BEC Widgets with autocomplete."
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
from qtpy.QtCore import QSize, Signal
|
||||
from qtpy.QtWidgets import QComboBox, QSizePolicy
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.filter_io import ComboBoxFilterHandler, FilterIO
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_signal_input_base import (
|
||||
@@ -54,6 +56,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.setMinimumSize(QSize(100, 0))
|
||||
self._set_first_element_as_empty = True
|
||||
# We do not consider the config that is passed here, this produced problems
|
||||
# with QtDesigner, since config and input arguments may differ and resolve properly
|
||||
# Implementing this logic and config recoverage is postponed.
|
||||
@@ -90,6 +93,61 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
|
||||
self.insertItem(0, "Hinted Signals")
|
||||
self.model().item(0).setEnabled(False)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def set_first_element_as_empty(self) -> bool:
|
||||
"""
|
||||
Whether the first element in the combobox should be empty.
|
||||
This is useful to allow the user to select a device from the list.
|
||||
"""
|
||||
return self._set_first_element_as_empty
|
||||
|
||||
@set_first_element_as_empty.setter
|
||||
def set_first_element_as_empty(self, value: bool) -> None:
|
||||
"""
|
||||
Set whether the first element in the combobox should be empty.
|
||||
This is useful to allow the user to select a device from the list.
|
||||
|
||||
Args:
|
||||
value (bool): True if the first element should be empty, False otherwise.
|
||||
"""
|
||||
self._set_first_element_as_empty = value
|
||||
if self._set_first_element_as_empty:
|
||||
self.insertItem(0, "")
|
||||
self.setCurrentIndex(0)
|
||||
else:
|
||||
if self.count() > 0 and self.itemText(0) == "":
|
||||
self.removeItem(0)
|
||||
|
||||
def set_to_obj_name(self, obj_name: str) -> bool:
|
||||
"""
|
||||
Set the combobox to the object name of the signal.
|
||||
|
||||
Args:
|
||||
obj_name (str): Object name of the signal.
|
||||
|
||||
Returns:
|
||||
bool: True if the object name was found and set, False otherwise.
|
||||
"""
|
||||
for i in range(self.count()):
|
||||
signal_data = self.itemData(i)
|
||||
if signal_data and signal_data.get("obj_name") == obj_name:
|
||||
self.setCurrentIndex(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_to_first_enabled(self) -> bool:
|
||||
"""
|
||||
Set the combobox to the first enabled item.
|
||||
|
||||
Returns:
|
||||
bool: True if an enabled item was found and set, False otherwise.
|
||||
"""
|
||||
for i in range(self.count()):
|
||||
if self.model().item(i).isEnabled():
|
||||
self.setCurrentIndex(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
@SafeSlot()
|
||||
def reset_selection(self):
|
||||
"""Reset the selection of the combobox."""
|
||||
@@ -112,6 +170,10 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
|
||||
return
|
||||
self.device_signal_changed.emit(text)
|
||||
|
||||
@property
|
||||
def selected_signal_comp_name(self) -> str:
|
||||
return dict(self.signals).get(self.currentText(), {}).get("component_name", "")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit import (
|
||||
@@ -22,6 +23,8 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = SignalLineEdit(parent)
|
||||
return t
|
||||
|
||||
@@ -50,7 +53,7 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "SignalLineEdit"
|
||||
|
||||
def toolTip(self):
|
||||
return "Signal LineEdit Example for BEC Widgets with autocomplete."
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -45,6 +45,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
Widget to submit new scans to the queue.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["remove", "screenshot"]
|
||||
PLUGIN = True
|
||||
ICON_NAME = "tune"
|
||||
ARG_BOX_POSITION: int = 2
|
||||
@@ -169,8 +170,8 @@ class ScanControl(BECWidget, QWidget):
|
||||
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)
|
||||
self._metadata_form.metadata_updated.connect(self.update_scan_metadata)
|
||||
self._metadata_form.metadata_cleared.connect(self.update_scan_metadata)
|
||||
self._metadata_form.form_data_updated.connect(self.update_scan_metadata)
|
||||
self._metadata_form.form_data_cleared.connect(self.update_scan_metadata)
|
||||
self._metadata_form.validate_form()
|
||||
|
||||
def populate_scans(self):
|
||||
@@ -203,35 +204,40 @@ class ScanControl(BECWidget, QWidget):
|
||||
"""
|
||||
Requests the last executed scan parameters from BEC and restores them to the scan control widget.
|
||||
"""
|
||||
enabled = self.toggle.checked
|
||||
current_scan = self.comboBox_scan_selection.currentText()
|
||||
if enabled:
|
||||
history = self.client.connector.lrange(MessageEndpoints.scan_queue_history(), 0, -1)
|
||||
self.last_scan_found = False
|
||||
if not self.toggle.checked:
|
||||
return
|
||||
|
||||
for scan in history:
|
||||
scan_name = scan.content["info"]["request_blocks"][-1]["msg"].content["scan_type"]
|
||||
if scan_name == current_scan:
|
||||
args_dict = scan.content["info"]["request_blocks"][-1]["msg"].content[
|
||||
"parameter"
|
||||
]["args"]
|
||||
args_list = []
|
||||
for key, value in args_dict.items():
|
||||
args_list.append(key)
|
||||
args_list.extend(value)
|
||||
if len(args_list) > 1 and self.arg_box is not None:
|
||||
self.arg_box.set_parameters(args_list)
|
||||
kwargs = scan.content["info"]["request_blocks"][-1]["msg"].content["parameter"][
|
||||
"kwargs"
|
||||
]
|
||||
if kwargs and self.kwarg_boxes:
|
||||
for box in self.kwarg_boxes:
|
||||
box.set_parameters(kwargs)
|
||||
self.last_scan_found = True
|
||||
break
|
||||
else:
|
||||
self.last_scan_found = False
|
||||
else:
|
||||
self.last_scan_found = False
|
||||
current_scan = self.comboBox_scan_selection.currentText()
|
||||
history = (
|
||||
self.client.connector.xread(
|
||||
MessageEndpoints.scan_history(), from_start=True, user_id=self.object_name
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
for scan in reversed(history):
|
||||
scan_data = scan.get("data")
|
||||
if not scan_data:
|
||||
continue
|
||||
|
||||
if scan_data.scan_name != current_scan:
|
||||
continue
|
||||
|
||||
ri = getattr(scan_data, "request_inputs", {}) or {}
|
||||
args_list = ri.get("arg_bundle", [])
|
||||
if args_list and self.arg_box:
|
||||
self.arg_box.set_parameters(args_list)
|
||||
|
||||
inputs = ri.get("inputs", {})
|
||||
kwargs = ri.get("kwargs", {})
|
||||
merged = {**inputs, **kwargs}
|
||||
if merged and self.kwarg_boxes:
|
||||
for box in self.kwarg_boxes:
|
||||
box.set_parameters(merged)
|
||||
|
||||
self.last_scan_found = True
|
||||
break
|
||||
|
||||
@SafeProperty(str)
|
||||
def current_scan(self):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
||||
@@ -20,6 +21,8 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = ScanControl(parent)
|
||||
return t
|
||||
|
||||
@@ -27,7 +30,7 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
return "BEC Device Control"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(ScanControl.ICON_NAME)
|
||||
@@ -48,7 +51,7 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "ScanControl"
|
||||
|
||||
def toolTip(self):
|
||||
return "ScanControl"
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||
@@ -20,6 +21,8 @@ class DapComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = DapComboBox(parent)
|
||||
return t
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||
@@ -20,6 +21,8 @@ class LMFitDialogPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = LMFitDialog(parent)
|
||||
return t
|
||||
|
||||
@@ -48,7 +51,7 @@ class LMFitDialogPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "LMFitDialog"
|
||||
|
||||
def toolTip(self):
|
||||
return "LMFitDialog"
|
||||
return "Dialog for displaying the fit summary and params for LMFit DAP processes"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
244
bec_widgets/widgets/editors/monaco/monaco_widget.py
Normal file
244
bec_widgets/widgets/editors/monaco/monaco_widget.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from typing import Literal
|
||||
|
||||
import qtmonaco
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_theme_name
|
||||
|
||||
|
||||
class MonacoWidget(BECWidget, QWidget):
|
||||
"""
|
||||
A simple Monaco editor widget
|
||||
"""
|
||||
|
||||
text_changed = Signal(str)
|
||||
PLUGIN = True
|
||||
ICON_NAME = "code"
|
||||
USER_ACCESS = [
|
||||
"set_text",
|
||||
"get_text",
|
||||
"insert_text",
|
||||
"delete_line",
|
||||
"set_language",
|
||||
"get_language",
|
||||
"set_theme",
|
||||
"get_theme",
|
||||
"set_readonly",
|
||||
"set_cursor",
|
||||
"current_cursor",
|
||||
"set_minimap_enabled",
|
||||
"set_vim_mode_enabled",
|
||||
"set_lsp_header",
|
||||
"get_lsp_header",
|
||||
]
|
||||
|
||||
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
||||
super().__init__(
|
||||
parent=parent, client=client, gui_id=gui_id, config=config, theme_update=True, **kwargs
|
||||
)
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.editor = qtmonaco.Monaco(self)
|
||||
layout.addWidget(self.editor)
|
||||
self.setLayout(layout)
|
||||
self.editor.text_changed.connect(self.text_changed.emit)
|
||||
self.editor.initialized.connect(self.apply_theme)
|
||||
|
||||
def apply_theme(self, theme: str | None = None) -> None:
|
||||
"""
|
||||
Apply the current theme to the Monaco editor.
|
||||
|
||||
Args:
|
||||
theme (str, optional): The theme to apply. If None, the current theme will be used.
|
||||
"""
|
||||
if theme is None:
|
||||
theme = get_theme_name()
|
||||
editor_theme = "vs" if theme == "light" else "vs-dark"
|
||||
self.set_theme(editor_theme)
|
||||
|
||||
def set_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the text in the Monaco editor.
|
||||
|
||||
Args:
|
||||
text (str): The text to set in the editor.
|
||||
"""
|
||||
self.editor.set_text(text)
|
||||
|
||||
def get_text(self) -> str:
|
||||
"""
|
||||
Get the current text from the Monaco editor.
|
||||
"""
|
||||
return self.editor.get_text()
|
||||
|
||||
def insert_text(self, text: str, line: int | None = None, column: int | None = None) -> None:
|
||||
"""
|
||||
Insert text at the current cursor position or at a specified line and column.
|
||||
|
||||
Args:
|
||||
text (str): The text to insert.
|
||||
line (int, optional): The line number (1-based) to insert the text at. Defaults to None.
|
||||
column (int, optional): The column number (1-based) to insert the text at. Defaults to None.
|
||||
"""
|
||||
self.editor.insert_text(text, line, column)
|
||||
|
||||
def delete_line(self, line: int | None = None) -> None:
|
||||
"""
|
||||
Delete a line in the Monaco editor.
|
||||
|
||||
Args:
|
||||
line (int, optional): The line number (1-based) to delete. If None, the current line will be deleted.
|
||||
"""
|
||||
self.editor.delete_line(line)
|
||||
|
||||
def set_cursor(
|
||||
self,
|
||||
line: int,
|
||||
column: int = 1,
|
||||
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set the cursor position in the Monaco editor.
|
||||
|
||||
Args:
|
||||
line (int): Line number (1-based).
|
||||
column (int): Column number (1-based), defaults to 1.
|
||||
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||
"""
|
||||
self.editor.set_cursor(line, column, move_to_position)
|
||||
|
||||
def current_cursor(self) -> dict[str, int]:
|
||||
"""
|
||||
Get the current cursor position in the Monaco editor.
|
||||
|
||||
Returns:
|
||||
dict[str, int]: A dictionary with keys 'line' and 'column' representing the cursor position.
|
||||
"""
|
||||
return self.editor.current_cursor
|
||||
|
||||
def set_language(self, language: str) -> None:
|
||||
"""
|
||||
Set the programming language for syntax highlighting in the Monaco editor.
|
||||
|
||||
Args:
|
||||
language (str): The programming language to set (e.g., "python", "javascript").
|
||||
"""
|
||||
self.editor.set_language(language)
|
||||
|
||||
def get_language(self) -> str:
|
||||
"""
|
||||
Get the current programming language set in the Monaco editor.
|
||||
"""
|
||||
return self.editor.get_language()
|
||||
|
||||
def set_readonly(self, read_only: bool) -> None:
|
||||
"""
|
||||
Set the Monaco editor to read-only mode.
|
||||
|
||||
Args:
|
||||
read_only (bool): If True, the editor will be read-only.
|
||||
"""
|
||||
self.editor.set_readonly(read_only)
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""
|
||||
Set the theme for the Monaco editor.
|
||||
|
||||
Args:
|
||||
theme (str): The theme to set (e.g., "vs-dark", "light").
|
||||
"""
|
||||
self.editor.set_theme(theme)
|
||||
|
||||
def get_theme(self) -> str:
|
||||
"""
|
||||
Get the current theme of the Monaco editor.
|
||||
"""
|
||||
return self.editor.get_theme()
|
||||
|
||||
def set_minimap_enabled(self, enabled: bool) -> None:
|
||||
"""
|
||||
Enable or disable the minimap in the Monaco editor.
|
||||
|
||||
Args:
|
||||
enabled (bool): If True, the minimap will be enabled; otherwise, it will be disabled.
|
||||
"""
|
||||
self.editor.set_minimap_enabled(enabled)
|
||||
|
||||
def set_highlighted_lines(self, start_line: int, end_line: int) -> None:
|
||||
"""
|
||||
Highlight a range of lines in the Monaco editor.
|
||||
|
||||
Args:
|
||||
start_line (int): The starting line number (1-based).
|
||||
end_line (int): The ending line number (1-based).
|
||||
"""
|
||||
self.editor.set_highlighted_lines(start_line, end_line)
|
||||
|
||||
def clear_highlighted_lines(self) -> None:
|
||||
"""
|
||||
Clear any highlighted lines in the Monaco editor.
|
||||
"""
|
||||
self.editor.clear_highlighted_lines()
|
||||
|
||||
def set_vim_mode_enabled(self, enabled: bool) -> None:
|
||||
"""
|
||||
Enable or disable Vim mode in the Monaco editor.
|
||||
|
||||
Args:
|
||||
enabled (bool): If True, Vim mode will be enabled; otherwise, it will be disabled.
|
||||
"""
|
||||
self.editor.set_vim_mode_enabled(enabled)
|
||||
|
||||
def set_lsp_header(self, header: str) -> None:
|
||||
"""
|
||||
Set the LSP (Language Server Protocol) header for the Monaco editor.
|
||||
The header is used to provide context for language servers but is not displayed in the editor.
|
||||
|
||||
Args:
|
||||
header (str): The LSP header to set.
|
||||
"""
|
||||
self.editor.set_lsp_header(header)
|
||||
|
||||
def get_lsp_header(self) -> str:
|
||||
"""
|
||||
Get the current LSP header set in the Monaco editor.
|
||||
|
||||
Returns:
|
||||
str: The LSP header.
|
||||
"""
|
||||
return self.editor.get_lsp_header()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
qapp = QApplication([])
|
||||
widget = MonacoWidget()
|
||||
# set the default size
|
||||
widget.resize(800, 600)
|
||||
widget.set_language("python")
|
||||
widget.set_theme("vs-dark")
|
||||
widget.editor.set_minimap_enabled(False)
|
||||
widget.set_text(
|
||||
"""
|
||||
import numpy as np
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.devicemanager import DeviceContainer
|
||||
from bec_lib.scans import Scans
|
||||
dev: DeviceContainer
|
||||
scans: Scans
|
||||
|
||||
#######################################
|
||||
########## User Script #####################
|
||||
#######################################
|
||||
|
||||
# This is a comment
|
||||
def hello_world():
|
||||
print("Hello, world!")
|
||||
"""
|
||||
)
|
||||
widget.set_highlighted_lines(1, 3)
|
||||
widget.show()
|
||||
qapp.exec_()
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['monaco_widget.py']}
|
||||
57
bec_widgets/widgets/editors/monaco/monaco_widget_plugin.py
Normal file
57
bec_widgets/widgets/editors/monaco/monaco_widget_plugin.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='MonacoWidget' name='monaco_widget'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class MonacoWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = MonacoWidget(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Developer"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(MonacoWidget.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "monaco_widget"
|
||||
|
||||
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 "MonacoWidget"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
15
bec_widgets/widgets/editors/monaco/register_monaco_widget.py
Normal file
15
bec_widgets/widgets/editors/monaco/register_monaco_widget.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget_plugin import MonacoWidgetPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(MonacoWidgetPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.editors.sbb_monitor.sbb_monitor_plugin import SBBMonitorPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(SBBMonitorPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
15
bec_widgets/widgets/editors/sbb_monitor/sbb_monitor.py
Normal file
15
bec_widgets/widgets/editors/sbb_monitor/sbb_monitor.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from bec_widgets.widgets.editors.website.website import WebsiteWidget
|
||||
|
||||
|
||||
class SBBMonitor(WebsiteWidget):
|
||||
"""
|
||||
A widget to display the SBB monitor website.
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "train"
|
||||
USER_ACCESS = []
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
url = "https://free.oevplus.ch/monitor/?viewType=splitView&layout=1&showClock=true&showPerron=true&stationGroup1Title=Villigen%2C%20PSI%20West&stationGroup2Title=Siggenthal-Würenlingen&station_1_id=85%3A3592&station_1_name=Villigen%2C%20PSI%20West&station_1_quantity=5&station_1_group=1&station_2_id=85%3A3502&station_2_name=Siggenthal-Würenlingen&station_2_quantity=5&station_2_group=2"
|
||||
super().__init__(parent=parent, url=url, **kwargs)
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['sbb_monitor.py']}
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.sbb_monitor.sbb_monitor import SBBMonitor
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='SBBMonitor' name='sbb_monitor'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class SBBMonitorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = SBBMonitor(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(SBBMonitor.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "sbb_monitor"
|
||||
|
||||
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 "SBBMonitor"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
||||
@@ -20,6 +21,8 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = ScanMetadata(parent)
|
||||
return t
|
||||
|
||||
@@ -27,7 +30,7 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
return "BEC Input Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(ScanMetadata.ICON_NAME)
|
||||
@@ -48,7 +51,7 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "ScanMetadata"
|
||||
|
||||
def toolTip(self):
|
||||
return "Dynamically generates a form for inclusion of metadata for a scan."
|
||||
return "ScanMetadata"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.text_box.text_box import TextBox
|
||||
|
||||
@@ -14,7 +13,6 @@ DOM_XML = """
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
@@ -23,6 +21,8 @@ class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = TextBox(parent)
|
||||
return t
|
||||
|
||||
@@ -51,7 +51,7 @@ class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "TextBox"
|
||||
|
||||
def toolTip(self):
|
||||
return "TextBox"
|
||||
return "A widget that displays text in plain and HTML format"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
|
||||
|
||||
@@ -15,8 +14,6 @@ DOM_XML = """
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -24,6 +21,8 @@ class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = VSCodeEditor(parent)
|
||||
return t
|
||||
|
||||
|
||||
0
bec_widgets/widgets/editors/web_console/__init__.py
Normal file
0
bec_widgets/widgets/editors/web_console/__init__.py
Normal file
@@ -6,11 +6,12 @@ import time
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from louie.saferef import safe_ref
|
||||
from qtpy.QtCore import QUrl, qInstallMessageHandler
|
||||
from qtpy.QtCore import QTimer, QUrl, Signal, qInstallMessageHandler
|
||||
from qtpy.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -165,11 +166,16 @@ class WebConsole(BECWidget, QWidget):
|
||||
A simple widget to display a website
|
||||
"""
|
||||
|
||||
_js_callback = Signal(bool)
|
||||
initialized = Signal()
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "terminal"
|
||||
|
||||
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self._startup_cmd = "bec --nogui"
|
||||
self._is_initialized = False
|
||||
_web_console_registry.register(self)
|
||||
self._token = _web_console_registry._token
|
||||
layout = QVBoxLayout()
|
||||
@@ -181,6 +187,48 @@ class WebConsole(BECWidget, QWidget):
|
||||
layout.addWidget(self.browser)
|
||||
self.setLayout(layout)
|
||||
self.page.setUrl(QUrl(f"http://localhost:{_web_console_registry._server_port}"))
|
||||
self._startup_timer = QTimer()
|
||||
self._startup_timer.setInterval(500)
|
||||
self._startup_timer.timeout.connect(self._check_page_ready)
|
||||
self._startup_timer.start()
|
||||
self._js_callback.connect(self._on_js_callback)
|
||||
|
||||
def _check_page_ready(self):
|
||||
"""
|
||||
Check if the page is ready and stop the timer if it is.
|
||||
"""
|
||||
if self.page.isLoading():
|
||||
return
|
||||
|
||||
self.page.runJavaScript("window.term !== undefined", self._js_callback.emit)
|
||||
|
||||
def _on_js_callback(self, ready: bool):
|
||||
"""
|
||||
Callback for when the JavaScript is ready.
|
||||
"""
|
||||
if not ready:
|
||||
return
|
||||
self._is_initialized = True
|
||||
self._startup_timer.stop()
|
||||
if self._startup_cmd:
|
||||
self.write(self._startup_cmd)
|
||||
self.initialized.emit()
|
||||
|
||||
@SafeProperty(str)
|
||||
def startup_cmd(self):
|
||||
"""
|
||||
Get the startup command for the web console.
|
||||
"""
|
||||
return self._startup_cmd
|
||||
|
||||
@startup_cmd.setter
|
||||
def startup_cmd(self, cmd: str):
|
||||
"""
|
||||
Set the startup command for the web console.
|
||||
"""
|
||||
if not isinstance(cmd, str):
|
||||
raise ValueError("Startup command must be a string.")
|
||||
self._startup_cmd = cmd
|
||||
|
||||
def write(self, data: str, send_return: bool = True):
|
||||
"""
|
||||
@@ -213,10 +261,19 @@ class WebConsole(BECWidget, QWidget):
|
||||
"document.querySelector('textarea.xterm-helper-textarea').dispatchEvent(new KeyboardEvent('keypress', {charCode: 3}))"
|
||||
)
|
||||
|
||||
def set_readonly(self, readonly: bool):
|
||||
"""
|
||||
Set the web console to read-only mode.
|
||||
"""
|
||||
if not isinstance(readonly, bool):
|
||||
raise ValueError("Readonly must be a boolean.")
|
||||
self.setEnabled(not readonly)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up the registry by removing any instances that are no longer valid.
|
||||
"""
|
||||
self._startup_timer.stop()
|
||||
_web_console_registry.unregister(self)
|
||||
super().cleanup()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.web_console.web_console import WebConsole
|
||||
@@ -20,6 +21,8 @@ class WebConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = WebConsole(parent)
|
||||
return t
|
||||
|
||||
@@ -27,7 +30,7 @@ class WebConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Console"
|
||||
return "BEC Developer"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(WebConsole.ICON_NAME)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.website.website import WebsiteWidget
|
||||
|
||||
@@ -14,7 +13,6 @@ DOM_XML = """
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
@@ -23,6 +21,8 @@ class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = WebsiteWidget(parent)
|
||||
return t
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.games.minesweeper import Minesweeper
|
||||
@@ -20,6 +21,8 @@ class MinesweeperPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = Minesweeper(parent)
|
||||
return t
|
||||
|
||||
|
||||
0
bec_widgets/widgets/plots/heatmap/__init__.py
Normal file
0
bec_widgets/widgets/plots/heatmap/__init__.py
Normal file
990
bec_widgets/widgets/plots/heatmap/heatmap.py
Normal file
990
bec_widgets/widgets/plots/heatmap/heatmap.py
Normal file
@@ -0,0 +1,990 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy.QtCore import QTimer, Signal
|
||||
from qtpy.QtGui import QTransform
|
||||
from scipy.interpolate import (
|
||||
CloughTocher2DInterpolator,
|
||||
LinearNDInterpolator,
|
||||
NearestNDInterpolator,
|
||||
)
|
||||
from scipy.spatial import cKDTree
|
||||
from toolz import partition
|
||||
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||
from bec_widgets.widgets.plots.heatmap.settings.heatmap_setting import HeatmapSettings
|
||||
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class HeatmapDeviceSignal(BaseModel):
|
||||
"""The configuration of a signal in the scatter waveform widget."""
|
||||
|
||||
name: str
|
||||
entry: str
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class HeatmapConfig(ConnectionConfig):
|
||||
parent_id: str | None = Field(None, description="The parent plot of the curve.")
|
||||
color_map: str | None = Field(
|
||||
"plasma", description="The color palette of the heatmap widget.", validate_default=True
|
||||
)
|
||||
color_bar: Literal["full", "simple"] | None = Field(
|
||||
None, description="The type of the color bar."
|
||||
)
|
||||
interpolation: Literal["linear", "nearest", "clough"] = Field(
|
||||
"linear", description="The interpolation method for the heatmap."
|
||||
)
|
||||
oversampling_factor: float = Field(
|
||||
1.0,
|
||||
description="Factor to oversample the grid resolution (1.0 = no oversampling, 2.0 = 2x resolution).",
|
||||
)
|
||||
show_config_label: bool = Field(
|
||||
True, description="Whether to show the configuration label in the heatmap."
|
||||
)
|
||||
enforce_interpolation: bool = Field(
|
||||
False, description="Whether to use the interpolation mode even for grid scans."
|
||||
)
|
||||
lock_aspect_ratio: bool = Field(
|
||||
False, description="Whether to lock the aspect ratio of the image."
|
||||
)
|
||||
x_device: HeatmapDeviceSignal | None = Field(
|
||||
None, description="The x device signal of the heatmap."
|
||||
)
|
||||
y_device: HeatmapDeviceSignal | None = Field(
|
||||
None, description="The y device signal of the heatmap."
|
||||
)
|
||||
z_device: HeatmapDeviceSignal | None = Field(
|
||||
None, description="The z device signal of the heatmap."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
_validate_color_palette = field_validator("color_map")(Colors.validate_color_map)
|
||||
|
||||
|
||||
class Heatmap(ImageBase):
|
||||
"""
|
||||
Heatmap widget for visualizing 2d grid data with color mapping for the z-axis.
|
||||
"""
|
||||
|
||||
USER_ACCESS = [
|
||||
# General PlotBase Settings
|
||||
"enable_toolbar",
|
||||
"enable_toolbar.setter",
|
||||
"enable_side_panel",
|
||||
"enable_side_panel.setter",
|
||||
"enable_fps_monitor",
|
||||
"enable_fps_monitor.setter",
|
||||
"set",
|
||||
"title",
|
||||
"title.setter",
|
||||
"x_label",
|
||||
"x_label.setter",
|
||||
"y_label",
|
||||
"y_label.setter",
|
||||
"x_limits",
|
||||
"x_limits.setter",
|
||||
"y_limits",
|
||||
"y_limits.setter",
|
||||
"x_grid",
|
||||
"x_grid.setter",
|
||||
"y_grid",
|
||||
"y_grid.setter",
|
||||
"inner_axes",
|
||||
"inner_axes.setter",
|
||||
"outer_axes",
|
||||
"outer_axes.setter",
|
||||
"auto_range_x",
|
||||
"auto_range_x.setter",
|
||||
"auto_range_y",
|
||||
"auto_range_y.setter",
|
||||
"minimal_crosshair_precision",
|
||||
"minimal_crosshair_precision.setter",
|
||||
"screenshot",
|
||||
# ImageView Specific Settings
|
||||
"color_map",
|
||||
"color_map.setter",
|
||||
"v_range",
|
||||
"v_range.setter",
|
||||
"v_min",
|
||||
"v_min.setter",
|
||||
"v_max",
|
||||
"v_max.setter",
|
||||
"lock_aspect_ratio",
|
||||
"lock_aspect_ratio.setter",
|
||||
"autorange",
|
||||
"autorange.setter",
|
||||
"autorange_mode",
|
||||
"autorange_mode.setter",
|
||||
"enable_colorbar",
|
||||
"enable_simple_colorbar",
|
||||
"enable_simple_colorbar.setter",
|
||||
"enable_full_colorbar",
|
||||
"enable_full_colorbar.setter",
|
||||
"interpolation_method",
|
||||
"interpolation_method.setter",
|
||||
"oversampling_factor",
|
||||
"oversampling_factor.setter",
|
||||
"enforce_interpolation",
|
||||
"enforce_interpolation.setter",
|
||||
"fft",
|
||||
"fft.setter",
|
||||
"log",
|
||||
"log.setter",
|
||||
"main_image",
|
||||
"add_roi",
|
||||
"remove_roi",
|
||||
"rois",
|
||||
"plot",
|
||||
]
|
||||
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
ICON_NAME = "dataset"
|
||||
|
||||
new_scan = Signal()
|
||||
new_scan_id = Signal(str)
|
||||
sync_signal_update = Signal()
|
||||
heatmap_property_changed = Signal()
|
||||
|
||||
def __init__(self, parent=None, config: HeatmapConfig | None = None, **kwargs):
|
||||
if config is None:
|
||||
config = HeatmapConfig(
|
||||
widget_class=self.__class__.__name__,
|
||||
parent_id=None,
|
||||
color_map="plasma",
|
||||
color_bar=None,
|
||||
interpolation="linear",
|
||||
oversampling_factor=1.0,
|
||||
lock_aspect_ratio=False,
|
||||
x_device=None,
|
||||
y_device=None,
|
||||
z_device=None,
|
||||
)
|
||||
super().__init__(parent=parent, config=config, theme_update=True, **kwargs)
|
||||
self._image_config = config
|
||||
self.scan_id = None
|
||||
self.old_scan_id = None
|
||||
self.scan_item = None
|
||||
self.status_message = None
|
||||
self._grid_index = None
|
||||
self.heatmap_dialog = None
|
||||
bg_color = pg.mkColor((240, 240, 240, 150))
|
||||
self.config_label = pg.LegendItem(
|
||||
labelTextColor=(0, 0, 0), offset=(-30, 1), brush=pg.mkBrush(bg_color), horSpacing=0
|
||||
)
|
||||
self.config_label.setParentItem(self.plot_item.vb)
|
||||
self.config_label.setVisible(False)
|
||||
self.reload = False
|
||||
self.bec_dispatcher.connect_slot(self.on_scan_status, MessageEndpoints.scan_status())
|
||||
self.bec_dispatcher.connect_slot(self.on_scan_progress, MessageEndpoints.scan_progress())
|
||||
self.heatmap_property_changed.connect(lambda: self.sync_signal_update.emit())
|
||||
|
||||
self.proxy_update_sync = pg.SignalProxy(
|
||||
self.sync_signal_update, rateLimit=5, slot=self.update_plot
|
||||
)
|
||||
self._init_toolbar_heatmap()
|
||||
self.toolbar.show_bundles(
|
||||
[
|
||||
"heatmap_settings",
|
||||
"plot_export",
|
||||
"image_crosshair",
|
||||
"mouse_interaction",
|
||||
"image_autorange",
|
||||
"image_colorbar",
|
||||
"image_processing",
|
||||
"axis_popup",
|
||||
"interpolation_info",
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def main_image(self) -> ImageItem:
|
||||
"""Access the main image item."""
|
||||
return self.layer_manager["main"].image
|
||||
|
||||
################################################################################
|
||||
# Widget Specific GUI interactions
|
||||
################################################################################
|
||||
|
||||
@SafeSlot(str)
|
||||
def apply_theme(self, theme: str):
|
||||
"""
|
||||
Apply the current theme to the heatmap widget.
|
||||
"""
|
||||
super().apply_theme(theme)
|
||||
if theme == "dark":
|
||||
brush = pg.mkBrush(pg.mkColor(50, 50, 50, 150))
|
||||
color = pg.mkColor(255, 255, 255)
|
||||
else:
|
||||
brush = pg.mkBrush(pg.mkColor(240, 240, 240, 150))
|
||||
color = pg.mkColor(0, 0, 0)
|
||||
if hasattr(self, "config_label"):
|
||||
self.config_label.setBrush(brush)
|
||||
self.config_label.setLabelTextColor(color)
|
||||
self.redraw_config_label()
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def plot(
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
z_name: str,
|
||||
x_entry: None | str = None,
|
||||
y_entry: None | str = None,
|
||||
z_entry: None | str = None,
|
||||
color_map: str | None = "plasma",
|
||||
validate_bec: bool = True,
|
||||
interpolation: Literal["linear", "nearest"] | None = None,
|
||||
enforce_interpolation: bool | None = None,
|
||||
oversampling_factor: float | None = None,
|
||||
lock_aspect_ratio: bool | None = None,
|
||||
show_config_label: bool | None = None,
|
||||
reload: bool = False,
|
||||
):
|
||||
"""
|
||||
Plot the heatmap with the given x, y, and z data.
|
||||
|
||||
Args:
|
||||
x_name (str): The name of the x-axis signal.
|
||||
y_name (str): The name of the y-axis signal.
|
||||
z_name (str): The name of the z-axis signal.
|
||||
x_entry (str | None): The entry for the x-axis signal.
|
||||
y_entry (str | None): The entry for the y-axis signal.
|
||||
z_entry (str | None): The entry for the z-axis signal.
|
||||
color_map (str | None): The color map to use for the heatmap.
|
||||
validate_bec (bool): Whether to validate the entries against BEC signals.
|
||||
interpolation (Literal["linear", "nearest"] | None): The interpolation method to use.
|
||||
enforce_interpolation (bool | None): Whether to enforce interpolation even for grid scans.
|
||||
oversampling_factor (float | None): Factor to oversample the grid resolution.
|
||||
lock_aspect_ratio (bool | None): Whether to lock the aspect ratio of the image.
|
||||
show_config_label (bool | None): Whether to show the configuration label in the heatmap.
|
||||
reload (bool): Whether to reload the heatmap with new data.
|
||||
"""
|
||||
if validate_bec:
|
||||
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
|
||||
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
|
||||
z_entry = self.entry_validator.validate_signal(z_name, z_entry)
|
||||
|
||||
if x_entry is None or y_entry is None or z_entry is None:
|
||||
raise ValueError("x, y, and z entries must be provided.")
|
||||
if x_name is None or y_name is None or z_name is None:
|
||||
raise ValueError("x, y, and z names must be provided.")
|
||||
|
||||
if interpolation is None:
|
||||
interpolation = self._image_config.interpolation
|
||||
|
||||
if oversampling_factor is None:
|
||||
oversampling_factor = self._image_config.oversampling_factor
|
||||
|
||||
if enforce_interpolation is None:
|
||||
enforce_interpolation = self._image_config.enforce_interpolation
|
||||
|
||||
if lock_aspect_ratio is None:
|
||||
lock_aspect_ratio = self._image_config.lock_aspect_ratio
|
||||
|
||||
if show_config_label is None:
|
||||
show_config_label = self._image_config.show_config_label
|
||||
|
||||
self._image_config = HeatmapConfig(
|
||||
parent_id=self.gui_id,
|
||||
x_device=HeatmapDeviceSignal(name=x_name, entry=x_entry),
|
||||
y_device=HeatmapDeviceSignal(name=y_name, entry=y_entry),
|
||||
z_device=HeatmapDeviceSignal(name=z_name, entry=z_entry),
|
||||
color_map=color_map,
|
||||
color_bar=None,
|
||||
interpolation=interpolation,
|
||||
oversampling_factor=oversampling_factor,
|
||||
enforce_interpolation=enforce_interpolation,
|
||||
lock_aspect_ratio=lock_aspect_ratio,
|
||||
show_config_label=show_config_label,
|
||||
)
|
||||
self.color_map = color_map
|
||||
self.reload = reload
|
||||
self.update_labels()
|
||||
|
||||
self._fetch_running_scan()
|
||||
self.sync_signal_update.emit()
|
||||
|
||||
def _fetch_running_scan(self):
|
||||
scan = self.client.queue.scan_storage.current_scan
|
||||
if scan is not None:
|
||||
self.scan_item = scan
|
||||
self.scan_id = scan.scan_id
|
||||
elif self.client.history and len(self.client.history) > 0:
|
||||
self.scan_item = self.client.history[-1]
|
||||
self.scan_id = self.client.history._scan_ids[-1]
|
||||
self.old_scan_id = None
|
||||
|
||||
def update_labels(self):
|
||||
"""
|
||||
Update the labels of the x, y, and z axes.
|
||||
"""
|
||||
if self._image_config is None:
|
||||
return
|
||||
x_name = self._image_config.x_device.name
|
||||
y_name = self._image_config.y_device.name
|
||||
z_name = self._image_config.z_device.name
|
||||
|
||||
if x_name is not None:
|
||||
self.x_label = x_name # type: ignore
|
||||
x_dev = self.dev.get(x_name)
|
||||
if x_dev and hasattr(x_dev, "egu"):
|
||||
self.x_label_units = x_dev.egu()
|
||||
if y_name is not None:
|
||||
self.y_label = y_name # type: ignore
|
||||
y_dev = self.dev.get(y_name)
|
||||
if y_dev and hasattr(y_dev, "egu"):
|
||||
self.y_label_units = y_dev.egu()
|
||||
if z_name is not None:
|
||||
self.title = z_name
|
||||
|
||||
def _init_toolbar_heatmap(self):
|
||||
"""
|
||||
Initialize the toolbar for the heatmap widget, adding actions for heatmap settings.
|
||||
"""
|
||||
self.toolbar.add_action(
|
||||
"heatmap_settings",
|
||||
MaterialIconAction(
|
||||
icon_name="scatter_plot",
|
||||
tooltip="Show Heatmap Settings",
|
||||
checkable=True,
|
||||
parent=self,
|
||||
),
|
||||
)
|
||||
|
||||
self.toolbar.components.get_action("heatmap_settings").action.triggered.connect(
|
||||
self.show_heatmap_settings
|
||||
)
|
||||
|
||||
# disable all processing actions except for the fft and log
|
||||
bundle = self.toolbar.get_bundle("image_processing")
|
||||
for name, action in bundle.bundle_actions.items():
|
||||
if name not in ["image_processing_fft", "image_processing_log"]:
|
||||
action().action.setVisible(False)
|
||||
|
||||
self.toolbar.add_action(
|
||||
"interpolation_info",
|
||||
MaterialIconAction(
|
||||
icon_name="info", tooltip="Show Interpolation Info", checkable=True, parent=self
|
||||
),
|
||||
)
|
||||
self.toolbar.components.get_action("interpolation_info").action.triggered.connect(
|
||||
self.toggle_interpolation_info
|
||||
)
|
||||
self.toolbar.components.get_action("interpolation_info").action.setChecked(
|
||||
self._image_config.show_config_label
|
||||
)
|
||||
|
||||
def show_heatmap_settings(self):
|
||||
"""
|
||||
Show the heatmap settings dialog.
|
||||
"""
|
||||
heatmap_settings_action = self.toolbar.components.get_action("heatmap_settings").action
|
||||
if self.heatmap_dialog is None or not self.heatmap_dialog.isVisible():
|
||||
heatmap_settings = HeatmapSettings(parent=self, target_widget=self, popup=True)
|
||||
self.heatmap_dialog = SettingsDialog(
|
||||
self, settings_widget=heatmap_settings, window_title="Heatmap Settings", modal=False
|
||||
)
|
||||
self.heatmap_dialog.resize(700, 350)
|
||||
# When the dialog is closed, update the toolbar icon and clear the reference
|
||||
self.heatmap_dialog.finished.connect(self._heatmap_dialog_closed)
|
||||
self.heatmap_dialog.show()
|
||||
heatmap_settings_action.setChecked(True)
|
||||
else:
|
||||
# If already open, bring it to the front
|
||||
self.heatmap_dialog.raise_()
|
||||
self.heatmap_dialog.activateWindow()
|
||||
heatmap_settings_action.setChecked(True) # keep it toggled
|
||||
|
||||
def toggle_interpolation_info(self):
|
||||
"""
|
||||
Toggle the visibility of the interpolation info label.
|
||||
"""
|
||||
self._image_config.show_config_label = not self._image_config.show_config_label
|
||||
self.toolbar.components.get_action("interpolation_info").action.setChecked(
|
||||
self._image_config.show_config_label
|
||||
)
|
||||
self.redraw_config_label()
|
||||
|
||||
def _heatmap_dialog_closed(self):
|
||||
"""
|
||||
Slot for when the heatmap settings dialog is closed.
|
||||
"""
|
||||
self.heatmap_dialog = None
|
||||
self.toolbar.components.get_action("heatmap_settings").action.setChecked(False)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_scan_status(self, msg: dict, meta: dict):
|
||||
"""
|
||||
Initial scan status message handler, which is triggered at the begging and end of scan.
|
||||
|
||||
Args:
|
||||
msg(dict): The message content.
|
||||
meta(dict): The message metadata.
|
||||
"""
|
||||
current_scan_id = msg.get("scan_id", None)
|
||||
if current_scan_id is None:
|
||||
return
|
||||
if current_scan_id != self.scan_id:
|
||||
self.reset()
|
||||
self.new_scan.emit()
|
||||
self.new_scan_id.emit(current_scan_id)
|
||||
self.old_scan_id = self.scan_id
|
||||
self.scan_id = current_scan_id
|
||||
self.scan_item = self.queue.scan_storage.find_scan_by_ID(self.scan_id) # type: ignore
|
||||
|
||||
# First trigger to update the scan curves
|
||||
self.sync_signal_update.emit()
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_scan_progress(self, msg: dict, meta: dict):
|
||||
self.sync_signal_update.emit()
|
||||
status = msg.get("done")
|
||||
if status:
|
||||
QTimer.singleShot(100, self.update_plot)
|
||||
QTimer.singleShot(300, self.update_plot)
|
||||
|
||||
@SafeSlot(verify_sender=True)
|
||||
def update_plot(self, _=None) -> None:
|
||||
"""
|
||||
Update the plot with the current data.
|
||||
"""
|
||||
if self.scan_item is None:
|
||||
logger.info("No scan executed so far; skipping update.")
|
||||
return
|
||||
data, access_key = self._fetch_scan_data_and_access()
|
||||
if data == "none":
|
||||
logger.info("No scan executed so far; skipping update.")
|
||||
return
|
||||
|
||||
if self._image_config is None:
|
||||
return
|
||||
try:
|
||||
x_name = self._image_config.x_device.name
|
||||
x_entry = self._image_config.x_device.entry
|
||||
y_name = self._image_config.y_device.name
|
||||
y_entry = self._image_config.y_device.entry
|
||||
z_name = self._image_config.z_device.name
|
||||
z_entry = self._image_config.z_device.entry
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
if access_key == "val":
|
||||
x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, None)
|
||||
y_data = data.get(y_name, {}).get(y_entry, {}).get(access_key, None)
|
||||
z_data = data.get(z_name, {}).get(z_entry, {}).get(access_key, None)
|
||||
else:
|
||||
x_data = data.get(x_name, {}).get(x_entry, {}).read().get("value", None)
|
||||
y_data = data.get(y_name, {}).get(y_entry, {}).read().get("value", None)
|
||||
z_data = data.get(z_name, {}).get(z_entry, {}).read().get("value", None)
|
||||
|
||||
if not isinstance(x_data, list):
|
||||
x_data = x_data.tolist() if isinstance(x_data, np.ndarray) else None
|
||||
if not isinstance(y_data, list):
|
||||
y_data = y_data.tolist() if isinstance(y_data, np.ndarray) else None
|
||||
if not isinstance(z_data, list):
|
||||
z_data = z_data.tolist() if isinstance(z_data, np.ndarray) else None
|
||||
|
||||
if x_data is None or y_data is None or z_data is None:
|
||||
logger.warning("x, y, or z data is None; skipping update.")
|
||||
return
|
||||
if len(x_data) != len(y_data) or len(x_data) != len(z_data):
|
||||
logger.warning(
|
||||
"x, y, and z data lengths do not match; skipping update. "
|
||||
f"Lengths: x={len(x_data)}, y={len(y_data)}, z={len(z_data)}"
|
||||
)
|
||||
return
|
||||
|
||||
if hasattr(self.scan_item, "status_message"):
|
||||
scan_msg = self.scan_item.status_message
|
||||
elif hasattr(self.scan_item, "metadata"):
|
||||
metadata = self.scan_item.metadata["bec"]
|
||||
status = metadata["exit_status"]
|
||||
scan_id = metadata["scan_id"]
|
||||
scan_name = metadata["scan_name"]
|
||||
scan_type = metadata["scan_type"]
|
||||
scan_number = metadata["scan_number"]
|
||||
request_inputs = metadata["request_inputs"]
|
||||
if "arg_bundle" in request_inputs and isinstance(request_inputs["arg_bundle"], str):
|
||||
# Convert the arg_bundle from a JSON string to a dictionary
|
||||
request_inputs["arg_bundle"] = json.loads(request_inputs["arg_bundle"])
|
||||
positions = metadata.get("positions", [])
|
||||
positions = positions.tolist() if isinstance(positions, np.ndarray) else positions
|
||||
|
||||
scan_msg = messages.ScanStatusMessage(
|
||||
status=status,
|
||||
scan_id=scan_id,
|
||||
scan_name=scan_name,
|
||||
scan_number=scan_number,
|
||||
scan_type=scan_type,
|
||||
request_inputs=request_inputs,
|
||||
info={"positions": positions},
|
||||
)
|
||||
else:
|
||||
scan_msg = None
|
||||
|
||||
if scan_msg is None:
|
||||
logger.warning("Scan message is None; skipping update.")
|
||||
return
|
||||
self.status_message = scan_msg
|
||||
|
||||
if self._image_config.show_config_label:
|
||||
self.redraw_config_label()
|
||||
|
||||
img, transform = self.get_image_data(x_data=x_data, y_data=y_data, z_data=z_data)
|
||||
if img is None:
|
||||
logger.warning("Image data is None; skipping update.")
|
||||
return
|
||||
|
||||
if self._color_bar is not None:
|
||||
self._color_bar.blockSignals(True)
|
||||
self.main_image.set_data(img, transform=transform)
|
||||
if self._color_bar is not None:
|
||||
self._color_bar.blockSignals(False)
|
||||
self.image_updated.emit()
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.update_markers_on_image_change()
|
||||
|
||||
def redraw_config_label(self):
|
||||
scan_msg = self.status_message
|
||||
if scan_msg is None:
|
||||
return
|
||||
if not self._image_config.show_config_label:
|
||||
self.config_label.setVisible(False)
|
||||
return
|
||||
|
||||
self.config_label.setOffset((-30, 1))
|
||||
self.config_label.setVisible(True)
|
||||
self.config_label.clear()
|
||||
self.config_label.addItem(self.plot_item, f"Scan: {scan_msg.scan_number}")
|
||||
self.config_label.addItem(self.plot_item, f"Scan Name: {scan_msg.scan_name}")
|
||||
if scan_msg.scan_name != "grid_scan" or self._image_config.enforce_interpolation:
|
||||
self.config_label.addItem(
|
||||
self.plot_item, f"Interpolation: {self._image_config.interpolation}"
|
||||
)
|
||||
self.config_label.addItem(
|
||||
self.plot_item, f"Oversampling: {self._image_config.oversampling_factor}x"
|
||||
)
|
||||
|
||||
def get_image_data(
|
||||
self,
|
||||
x_data: list[float] | None = None,
|
||||
y_data: list[float] | None = None,
|
||||
z_data: list[float] | None = None,
|
||||
) -> tuple[np.ndarray | None, QTransform | None]:
|
||||
"""
|
||||
Get the image data for the heatmap. Depending on the scan type, it will
|
||||
either pre-allocate the grid (grid_scan) or interpolate the data (step scan).
|
||||
|
||||
Args:
|
||||
x_data (np.ndarray): The x data.
|
||||
y_data (np.ndarray): The y data.
|
||||
z_data (np.ndarray): The z data.
|
||||
msg (messages.ScanStatusMessage): The scan status message.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, QTransform]: The image data and the QTransform.
|
||||
"""
|
||||
msg = self.status_message
|
||||
if x_data is None or y_data is None or z_data is None or msg is None:
|
||||
logger.warning("x, y, or z data is None; skipping update.")
|
||||
return None, None
|
||||
|
||||
if msg.scan_name == "grid_scan" and not self._image_config.enforce_interpolation:
|
||||
# We only support the grid scan mode if both scanning motors
|
||||
# are configured in the heatmap config.
|
||||
device_x = self._image_config.x_device.entry
|
||||
device_y = self._image_config.y_device.entry
|
||||
if (
|
||||
device_x in msg.request_inputs["arg_bundle"]
|
||||
and device_y in msg.request_inputs["arg_bundle"]
|
||||
):
|
||||
return self.get_grid_scan_image(z_data, msg)
|
||||
if len(z_data) < 4:
|
||||
# LinearNDInterpolator requires at least 4 points to interpolate
|
||||
return None, None
|
||||
return self.get_step_scan_image(x_data, y_data, z_data, msg)
|
||||
|
||||
def get_grid_scan_image(
|
||||
self, z_data: list[float], msg: messages.ScanStatusMessage
|
||||
) -> tuple[np.ndarray, QTransform]:
|
||||
"""
|
||||
Get the image data for a grid scan.
|
||||
Args:
|
||||
z_data (np.ndarray): The z data.
|
||||
msg (messages.ScanStatusMessage): The scan status message.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, QTransform]: The image data and the QTransform.
|
||||
"""
|
||||
|
||||
args = self.arg_bundle_to_dict(4, msg.request_inputs["arg_bundle"])
|
||||
|
||||
shape = (
|
||||
args[self._image_config.x_device.entry][-1],
|
||||
args[self._image_config.y_device.entry][-1],
|
||||
)
|
||||
|
||||
data = self.main_image.raw_data
|
||||
|
||||
if data is None or data.shape != shape:
|
||||
data = np.empty(shape)
|
||||
data.fill(np.nan)
|
||||
|
||||
def _get_grid_data(axis, snaked=True):
|
||||
x_grid, y_grid = np.meshgrid(axis[0], axis[1])
|
||||
if snaked:
|
||||
y_grid.T[::2] = np.fliplr(y_grid.T[::2])
|
||||
x_flat = x_grid.T.ravel()
|
||||
y_flat = y_grid.T.ravel()
|
||||
positions = np.vstack((x_flat, y_flat)).T
|
||||
return positions
|
||||
|
||||
snaked = msg.request_inputs["kwargs"].get("snaked", True)
|
||||
|
||||
# If the scan's fast axis is x, we need to swap the x and y axes
|
||||
swap = bool(msg.request_inputs["arg_bundle"][4] == self._image_config.x_device.entry)
|
||||
|
||||
# calculate the QTransform to put (0,0) at the axis origin
|
||||
scan_pos = np.asarray(msg.info["positions"])
|
||||
x_min = min(scan_pos[:, 0])
|
||||
x_max = max(scan_pos[:, 0])
|
||||
y_min = min(scan_pos[:, 1])
|
||||
y_max = max(scan_pos[:, 1])
|
||||
|
||||
x_range = x_max - x_min
|
||||
y_range = y_max - y_min
|
||||
|
||||
pixel_size_x = x_range / (shape[0] - 1)
|
||||
pixel_size_y = y_range / (shape[1] - 1)
|
||||
|
||||
transform = QTransform()
|
||||
if swap:
|
||||
transform.scale(pixel_size_y, pixel_size_x)
|
||||
transform.translate(y_min / pixel_size_y - 0.5, x_min / pixel_size_x - 0.5)
|
||||
else:
|
||||
transform.scale(pixel_size_x, pixel_size_y)
|
||||
transform.translate(x_min / pixel_size_x - 0.5, y_min / pixel_size_y - 0.5)
|
||||
|
||||
target_positions = _get_grid_data(
|
||||
(np.arange(shape[int(swap)]), np.arange(shape[int(not swap)])), snaked=snaked
|
||||
)
|
||||
|
||||
# Fill the data array with the z values
|
||||
if self._grid_index is None or self.reload:
|
||||
self._grid_index = 0
|
||||
self.reload = False
|
||||
|
||||
for i in range(self._grid_index, len(z_data)):
|
||||
data[target_positions[i, int(swap)], target_positions[i, int(not swap)]] = z_data[i]
|
||||
self._grid_index = len(z_data)
|
||||
return data, transform
|
||||
|
||||
def get_step_scan_image(
|
||||
self,
|
||||
x_data: list[float],
|
||||
y_data: list[float],
|
||||
z_data: list[float],
|
||||
msg: messages.ScanStatusMessage,
|
||||
) -> tuple[np.ndarray, QTransform]:
|
||||
"""
|
||||
Get the image data for an arbitrary step scan.
|
||||
|
||||
Args:
|
||||
x_data (list[float]): The x data.
|
||||
y_data (list[float]): The y data.
|
||||
z_data (list[float]): The z data.
|
||||
msg (messages.ScanStatusMessage): The scan status message.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, QTransform]: The image data and the QTransform.
|
||||
"""
|
||||
xy_data = np.column_stack((x_data, y_data))
|
||||
grid_x, grid_y, transform = self.get_image_grid(xy_data)
|
||||
|
||||
# Interpolate the z data onto the grid
|
||||
if self._image_config.interpolation == "linear":
|
||||
interp = LinearNDInterpolator(xy_data, z_data)
|
||||
elif self._image_config.interpolation == "nearest":
|
||||
interp = NearestNDInterpolator(xy_data, z_data)
|
||||
elif self._image_config.interpolation == "clough":
|
||||
interp = CloughTocher2DInterpolator(xy_data, z_data)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Interpolation method must be either 'linear', 'nearest', or 'clough'."
|
||||
)
|
||||
grid_z = interp(grid_x, grid_y)
|
||||
|
||||
return grid_z, transform
|
||||
|
||||
def get_image_grid(self, positions) -> tuple[np.ndarray, np.ndarray, QTransform]:
|
||||
"""
|
||||
LRU-cached calculation of the grid for the image. The lru cache is indexed by the scan_id
|
||||
to avoid recalculating the grid for the same scan.
|
||||
|
||||
Args:
|
||||
_scan_id (str): The scan ID. Needed for caching but not used in the function.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, np.ndarray, QTransform]: The grid x and y coordinates and the QTransform.
|
||||
"""
|
||||
base_width, base_height = self.estimate_image_resolution(positions)
|
||||
|
||||
# Apply oversampling factor
|
||||
factor = self._image_config.oversampling_factor
|
||||
|
||||
# Apply oversampling
|
||||
width = int(base_width * factor)
|
||||
height = int(base_height * factor)
|
||||
|
||||
# Create grid
|
||||
grid_x, grid_y = np.mgrid[
|
||||
min(positions[:, 0]) : max(positions[:, 0]) : width * 1j,
|
||||
min(positions[:, 1]) : max(positions[:, 1]) : height * 1j,
|
||||
]
|
||||
|
||||
# Calculate transform
|
||||
x_min, x_max = min(positions[:, 0]), max(positions[:, 0])
|
||||
y_min, y_max = min(positions[:, 1]), max(positions[:, 1])
|
||||
x_range = x_max - x_min
|
||||
y_range = y_max - y_min
|
||||
x_scale = x_range / width
|
||||
y_scale = y_range / height
|
||||
|
||||
transform = QTransform()
|
||||
transform.scale(x_scale, y_scale)
|
||||
transform.translate(x_min / x_scale - 0.5, y_min / y_scale - 0.5)
|
||||
|
||||
return grid_x, grid_y, transform
|
||||
|
||||
@staticmethod
|
||||
def estimate_image_resolution(coords: np.ndarray) -> tuple[int, int]:
|
||||
"""
|
||||
Estimate the number of pixels needed for the image based on the coordinates.
|
||||
|
||||
Args:
|
||||
coords (np.ndarray): The coordinates of the points.
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: The estimated width and height of the image."""
|
||||
if coords.ndim != 2 or coords.shape[1] != 2:
|
||||
raise ValueError("Input must be an (m x 2) array of (x, y) coordinates.")
|
||||
|
||||
x_min, x_max = coords[:, 0].min(), coords[:, 0].max()
|
||||
y_min, y_max = coords[:, 1].min(), coords[:, 1].max()
|
||||
|
||||
tree = cKDTree(coords)
|
||||
distances, _ = tree.query(coords, k=2)
|
||||
distances = distances[:, 1] # Get the second nearest neighbor distance
|
||||
avg_distance = np.mean(distances)
|
||||
|
||||
width_extent = x_max - x_min
|
||||
height_extent = y_max - y_min
|
||||
|
||||
# Calculate the number of pixels needed based on the average distance
|
||||
width_pixels = int(np.ceil(width_extent / avg_distance))
|
||||
height_pixels = int(np.ceil(height_extent / avg_distance))
|
||||
|
||||
return max(1, width_pixels), max(1, height_pixels)
|
||||
|
||||
def arg_bundle_to_dict(self, bundle_size: int, args: list) -> dict:
|
||||
"""
|
||||
Convert the argument bundle to a dictionary.
|
||||
|
||||
Args:
|
||||
args (list): The argument bundle.
|
||||
|
||||
Returns:
|
||||
dict: The dictionary representation of the argument bundle.
|
||||
"""
|
||||
params = {}
|
||||
for cmds in partition(bundle_size, args):
|
||||
params[cmds[0]] = list(cmds[1:])
|
||||
return params
|
||||
|
||||
def _fetch_scan_data_and_access(self):
|
||||
"""
|
||||
Decide whether the widget is in live or historical mode
|
||||
and return the appropriate data dict and access key.
|
||||
|
||||
Returns:
|
||||
data_dict (dict): The data structure for the current scan.
|
||||
access_key (str): Either 'val' (live) or 'value' (history).
|
||||
"""
|
||||
if self.scan_item is None:
|
||||
# Optionally fetch the latest from history if nothing is set
|
||||
# self.update_with_scan_history(-1)
|
||||
if self.scan_item is None:
|
||||
logger.info("No scan executed so far; skipping update.")
|
||||
return "none", "none"
|
||||
|
||||
if hasattr(self.scan_item, "live_data"):
|
||||
# Live scan
|
||||
return self.scan_item.live_data, "val"
|
||||
|
||||
# Historical
|
||||
scan_devices = self.scan_item.devices
|
||||
return scan_devices, "value"
|
||||
|
||||
def reset(self):
|
||||
self._grid_index = None
|
||||
self.main_image.clear()
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.reset()
|
||||
super().reset()
|
||||
|
||||
@SafeProperty(str)
|
||||
def interpolation_method(self) -> str:
|
||||
"""
|
||||
The interpolation method used for the heatmap.
|
||||
"""
|
||||
return self._image_config.interpolation
|
||||
|
||||
@interpolation_method.setter
|
||||
def interpolation_method(self, value: str):
|
||||
"""
|
||||
Set the interpolation method for the heatmap.
|
||||
Args:
|
||||
value(str): The interpolation method, either 'linear' or 'nearest'.
|
||||
"""
|
||||
if value not in ["linear", "nearest"]:
|
||||
raise ValueError("Interpolation method must be either 'linear' or 'nearest'.")
|
||||
self._image_config.interpolation = value
|
||||
self.heatmap_property_changed.emit()
|
||||
|
||||
@SafeProperty(float)
|
||||
def oversampling_factor(self) -> float:
|
||||
"""
|
||||
The oversampling factor for grid resolution.
|
||||
"""
|
||||
return self._image_config.oversampling_factor
|
||||
|
||||
@oversampling_factor.setter
|
||||
def oversampling_factor(self, value: float):
|
||||
"""
|
||||
Set the oversampling factor for grid resolution.
|
||||
Args:
|
||||
value(float): The oversampling factor (1.0 = no oversampling, 2.0 = 2x resolution).
|
||||
"""
|
||||
if value <= 0:
|
||||
raise ValueError("Oversampling factor must be greater than 0.")
|
||||
self._image_config.oversampling_factor = value
|
||||
self.heatmap_property_changed.emit()
|
||||
|
||||
@SafeProperty(bool)
|
||||
def enforce_interpolation(self) -> bool:
|
||||
"""
|
||||
Whether to enforce interpolation even for grid scans.
|
||||
"""
|
||||
return self._image_config.enforce_interpolation
|
||||
|
||||
@enforce_interpolation.setter
|
||||
def enforce_interpolation(self, value: bool):
|
||||
"""
|
||||
Set whether to enforce interpolation even for grid scans.
|
||||
|
||||
Args:
|
||||
value(bool): Whether to enforce interpolation.
|
||||
"""
|
||||
self._image_config.enforce_interpolation = value
|
||||
self.heatmap_property_changed.emit()
|
||||
|
||||
################################################################################
|
||||
# Post Processing
|
||||
################################################################################
|
||||
|
||||
@SafeProperty(bool)
|
||||
def fft(self) -> bool:
|
||||
"""
|
||||
Whether FFT postprocessing is enabled.
|
||||
"""
|
||||
return self.main_image.fft
|
||||
|
||||
@fft.setter
|
||||
def fft(self, enable: bool):
|
||||
"""
|
||||
Set FFT postprocessing.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to enable FFT postprocessing.
|
||||
"""
|
||||
self.main_image.fft = enable
|
||||
|
||||
@SafeProperty(bool)
|
||||
def log(self) -> bool:
|
||||
"""
|
||||
Whether logarithmic scaling is applied.
|
||||
"""
|
||||
return self.main_image.log
|
||||
|
||||
@log.setter
|
||||
def log(self, enable: bool):
|
||||
"""
|
||||
Set logarithmic scaling.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to enable logarithmic scaling.
|
||||
"""
|
||||
self.main_image.log = enable
|
||||
|
||||
@SafeProperty(int)
|
||||
def num_rotation_90(self) -> int:
|
||||
"""
|
||||
The number of 90° rotations to apply counterclockwise.
|
||||
"""
|
||||
return self.main_image.num_rotation_90
|
||||
|
||||
@num_rotation_90.setter
|
||||
def num_rotation_90(self, value: int):
|
||||
"""
|
||||
Set the number of 90° rotations to apply counterclockwise.
|
||||
|
||||
Args:
|
||||
value(int): The number of 90° rotations to apply.
|
||||
"""
|
||||
self.main_image.num_rotation_90 = value
|
||||
|
||||
@SafeProperty(bool)
|
||||
def transpose(self) -> bool:
|
||||
"""
|
||||
Whether the image is transposed.
|
||||
"""
|
||||
return self.main_image.transpose
|
||||
|
||||
@transpose.setter
|
||||
def transpose(self, enable: bool):
|
||||
"""
|
||||
Set the image to be transposed.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to enable transposing the image.
|
||||
"""
|
||||
self.main_image.transpose = enable
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
heatmap = Heatmap()
|
||||
heatmap.plot(x_name="samx", y_name="samy", z_name="bpm4i", oversampling_factor=5.0)
|
||||
heatmap.show()
|
||||
sys.exit(app.exec_())
|
||||
1
bec_widgets/widgets/plots/heatmap/heatmap.pyproject
Normal file
1
bec_widgets/widgets/plots/heatmap/heatmap.pyproject
Normal file
@@ -0,0 +1 @@
|
||||
{'files': ['heatmap.py']}
|
||||
57
bec_widgets/widgets/plots/heatmap/heatmap_plugin.py
Normal file
57
bec_widgets/widgets/plots/heatmap/heatmap_plugin.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='Heatmap' name='heatmap'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class HeatmapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = Heatmap(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Plots"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(Heatmap.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "heatmap"
|
||||
|
||||
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 "Heatmap"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
15
bec_widgets/widgets/plots/heatmap/register_heatmap.py
Normal file
15
bec_widgets/widgets/plots/heatmap/register_heatmap.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.plots.heatmap.heatmap_plugin import HeatmapPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(HeatmapPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
188
bec_widgets/widgets/plots/heatmap/settings/heatmap_setting.py
Normal file
188
bec_widgets/widgets/plots/heatmap/settings/heatmap_setting.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import (
|
||||
SignalComboBox,
|
||||
)
|
||||
|
||||
|
||||
class HeatmapSettings(SettingWidget):
|
||||
def __init__(self, parent=None, target_widget=None, popup=False, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
# This is a settings widget that depends on the target widget
|
||||
# and should mirror what is in the target widget.
|
||||
# Saving settings for this widget could result in recursively setting the target widget.
|
||||
self.setProperty("skip_settings", True)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
if popup:
|
||||
form = UILoader().load_ui(
|
||||
os.path.join(current_path, "heatmap_settings_horizontal.ui"), self
|
||||
)
|
||||
else:
|
||||
form = UILoader().load_ui(
|
||||
os.path.join(current_path, "heatmap_settings_vertical.ui"), self
|
||||
)
|
||||
|
||||
self.target_widget = target_widget
|
||||
self.popup = popup
|
||||
|
||||
# # Scroll area
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShape(QFrame.NoFrame)
|
||||
self.scroll_area.setWidget(form)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.scroll_area)
|
||||
self.ui = form
|
||||
|
||||
self.fetch_all_properties()
|
||||
|
||||
self.target_widget.heatmap_property_changed.connect(self.fetch_all_properties)
|
||||
if popup is False:
|
||||
self.ui.button_apply.clicked.connect(self.accept_changes)
|
||||
|
||||
self.ui.x_name.setFocus()
|
||||
|
||||
@SafeSlot()
|
||||
def fetch_all_properties(self):
|
||||
"""
|
||||
Fetch all properties from the target widget and update the settings widget.
|
||||
"""
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
# Get properties from the target widget
|
||||
color_map = getattr(self.target_widget, "color_map", None)
|
||||
|
||||
# Default values for device properties
|
||||
x_name, x_entry = None, None
|
||||
y_name, y_entry = None, None
|
||||
z_name, z_entry = None, None
|
||||
|
||||
# Safely access device properties
|
||||
if hasattr(self.target_widget, "_image_config") and self.target_widget._image_config:
|
||||
config = self.target_widget._image_config
|
||||
|
||||
if hasattr(config, "x_device") and config.x_device:
|
||||
x_name = getattr(config.x_device, "name", None)
|
||||
x_entry = getattr(config.x_device, "entry", None)
|
||||
|
||||
if hasattr(config, "y_device") and config.y_device:
|
||||
y_name = getattr(config.y_device, "name", None)
|
||||
y_entry = getattr(config.y_device, "entry", None)
|
||||
|
||||
if hasattr(config, "z_device") and config.z_device:
|
||||
z_name = getattr(config.z_device, "name", None)
|
||||
z_entry = getattr(config.z_device, "entry", None)
|
||||
|
||||
# Apply the properties to the settings widget
|
||||
if hasattr(self.ui, "color_map"):
|
||||
self.ui.color_map.colormap = color_map
|
||||
|
||||
if hasattr(self.ui, "x_name"):
|
||||
self.ui.x_name.set_device(x_name)
|
||||
if hasattr(self.ui, "x_entry") and x_entry is not None:
|
||||
self.ui.x_entry.set_to_obj_name(x_entry)
|
||||
|
||||
if hasattr(self.ui, "y_name"):
|
||||
self.ui.y_name.set_device(y_name)
|
||||
if hasattr(self.ui, "y_entry") and y_entry is not None:
|
||||
self.ui.y_entry.set_to_obj_name(y_entry)
|
||||
|
||||
if hasattr(self.ui, "z_name"):
|
||||
self.ui.z_name.set_device(z_name)
|
||||
if hasattr(self.ui, "z_entry") and z_entry is not None:
|
||||
self.ui.z_entry.set_to_obj_name(z_entry)
|
||||
|
||||
if hasattr(self.ui, "interpolation"):
|
||||
self.ui.interpolation.setCurrentText(
|
||||
getattr(self.target_widget._image_config, "interpolation", "linear")
|
||||
)
|
||||
if hasattr(self.ui, "oversampling_factor"):
|
||||
self.ui.oversampling_factor.setValue(
|
||||
getattr(self.target_widget._image_config, "oversampling_factor", 1.0)
|
||||
)
|
||||
if hasattr(self.ui, "enforce_interpolation"):
|
||||
self.ui.enforce_interpolation.setChecked(
|
||||
getattr(self.target_widget._image_config, "enforce_interpolation", False)
|
||||
)
|
||||
|
||||
def _get_signal_name(self, signal: SignalComboBox) -> str:
|
||||
"""
|
||||
Get the signal name from the signal combobox.
|
||||
Args:
|
||||
signal (SignalComboBox): The signal combobox to get the name from.
|
||||
Returns:
|
||||
str: The signal name.
|
||||
"""
|
||||
device_entry = signal.currentText()
|
||||
index = signal.findText(device_entry)
|
||||
if index == -1:
|
||||
return device_entry
|
||||
|
||||
device_entry_info = signal.itemData(index)
|
||||
if device_entry_info:
|
||||
device_entry = device_entry_info.get("obj_name", device_entry)
|
||||
|
||||
return device_entry if device_entry else ""
|
||||
|
||||
@SafeSlot()
|
||||
def accept_changes(self):
|
||||
"""
|
||||
Apply all properties from the settings widget to the target widget.
|
||||
"""
|
||||
x_name = self.ui.x_name.currentText()
|
||||
x_entry = self._get_signal_name(self.ui.x_entry)
|
||||
y_name = self.ui.y_name.currentText()
|
||||
y_entry = self._get_signal_name(self.ui.y_entry)
|
||||
z_name = self.ui.z_name.currentText()
|
||||
z_entry = self._get_signal_name(self.ui.z_entry)
|
||||
validate_bec = self.ui.validate_bec.checked
|
||||
color_map = self.ui.color_map.colormap
|
||||
interpolation = self.ui.interpolation.currentText()
|
||||
oversampling_factor = self.ui.oversampling_factor.value()
|
||||
enforce_interpolation = self.ui.enforce_interpolation.isChecked()
|
||||
|
||||
self.target_widget.plot(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color_map=color_map,
|
||||
validate_bec=validate_bec,
|
||||
interpolation=interpolation,
|
||||
oversampling_factor=oversampling_factor,
|
||||
enforce_interpolation=enforce_interpolation,
|
||||
reload=True,
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
self.ui.x_name.close()
|
||||
self.ui.x_name.deleteLater()
|
||||
self.ui.x_entry.close()
|
||||
self.ui.x_entry.deleteLater()
|
||||
self.ui.y_name.close()
|
||||
self.ui.y_name.deleteLater()
|
||||
self.ui.y_entry.close()
|
||||
self.ui.y_entry.deleteLater()
|
||||
self.ui.z_name.close()
|
||||
self.ui.z_name.deleteLater()
|
||||
self.ui.z_entry.close()
|
||||
self.ui.z_entry.deleteLater()
|
||||
self.ui.interpolation.close()
|
||||
self.ui.interpolation.deleteLater()
|
||||
@@ -0,0 +1,433 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>826</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="title">
|
||||
<string>Interpolation</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="1" column="2" alignment="Qt::AlignmentFlag::AlignRight">
|
||||
<widget class="ToggleSwitch" name="enforce_interpolation">
|
||||
<property name="toolTip">
|
||||
<string>Use the interpolation mode even for grid scans</string>
|
||||
</property>
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="toolTip">
|
||||
<string>Use the interpolation mode even for grid scans</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Enforce Interpolation</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="QDoubleSpinBox" name="oversampling_factor">
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>10.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="interpolation">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>nearest</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>clough</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>Oversampling</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Interpolation Method</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item alignment="Qt::AlignmentFlag::AlignRight">
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="title">
|
||||
<string>General</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Validate BEC</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3" alignment="Qt::AlignmentFlag::AlignRight">
|
||||
<widget class="ToggleSwitch" name="validate_bec"/>
|
||||
</item>
|
||||
<item row="3" column="3" alignment="Qt::AlignmentFlag::AlignRight">
|
||||
<widget class="BECColorMapWidget" name="color_map">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>Colormap</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>X Device</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="DeviceComboBox" name="x_name">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="set_first_element_as_empty" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="SignalComboBox" name="x_entry">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Y Device</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="DeviceComboBox" name="y_name">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="set_first_element_as_empty" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="SignalComboBox" name="y_entry">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="title">
|
||||
<string>Z Device</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="DeviceComboBox" name="z_name">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="set_first_element_as_empty" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="SignalComboBox" name="z_entry">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DeviceComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>device_combobox</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SignalComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>signal_combo_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECColorMapWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_color_map_widget</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>x_name</tabstop>
|
||||
<tabstop>y_name</tabstop>
|
||||
<tabstop>z_name</tabstop>
|
||||
<tabstop>x_entry</tabstop>
|
||||
<tabstop>y_entry</tabstop>
|
||||
<tabstop>z_entry</tabstop>
|
||||
<tabstop>interpolation</tabstop>
|
||||
<tabstop>oversampling_factor</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>x_name</sender>
|
||||
<signal>device_reset()</signal>
|
||||
<receiver>x_entry</receiver>
|
||||
<slot>reset_selection()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>254</x>
|
||||
<y>226</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>254</x>
|
||||
<y>267</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>x_name</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>x_entry</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>254</x>
|
||||
<y>226</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>254</x>
|
||||
<y>267</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>y_name</sender>
|
||||
<signal>device_reset()</signal>
|
||||
<receiver>y_entry</receiver>
|
||||
<slot>reset_selection()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>526</x>
|
||||
<y>226</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>526</x>
|
||||
<y>267</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>y_name</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>y_entry</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>526</x>
|
||||
<y>226</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>526</x>
|
||||
<y>267</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>z_name</sender>
|
||||
<signal>device_reset()</signal>
|
||||
<receiver>z_entry</receiver>
|
||||
<slot>reset_selection()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>798</x>
|
||||
<y>226</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>798</x>
|
||||
<y>267</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>z_name</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>z_entry</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>798</x>
|
||||
<y>226</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>798</x>
|
||||
<y>267</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,374 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>305</width>
|
||||
<height>629</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>629</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_apply">
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECColorMapWidget" name="color_map"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Validate BEC</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ToggleSwitch" name="validate_bec">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>X Device</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="DeviceComboBox" name="x_name">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="set_first_element_as_empty" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="SignalComboBox" name="x_entry">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Y Device</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="DeviceComboBox" name="y_name">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="set_first_element_as_empty" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="SignalComboBox" name="y_entry">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="title">
|
||||
<string>Z Device</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="DeviceComboBox" name="z_name">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="set_first_element_as_empty" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="SignalComboBox" name="z_entry">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="title">
|
||||
<string>Interpolation</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Interpolation Method</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="interpolation">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>nearest</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Enforce Interpolation</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" alignment="Qt::AlignmentFlag::AlignRight">
|
||||
<widget class="ToggleSwitch" name="enforce_interpolation">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>Oversampling</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QDoubleSpinBox" name="oversampling_factor">
|
||||
<property name="minimum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>10.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DeviceComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>device_combobox</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SignalComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>signal_combo_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECColorMapWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_color_map_widget</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>x_name</tabstop>
|
||||
<tabstop>y_name</tabstop>
|
||||
<tabstop>z_name</tabstop>
|
||||
<tabstop>button_apply</tabstop>
|
||||
<tabstop>x_entry</tabstop>
|
||||
<tabstop>y_entry</tabstop>
|
||||
<tabstop>z_entry</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>x_name</sender>
|
||||
<signal>device_reset()</signal>
|
||||
<receiver>x_entry</receiver>
|
||||
<slot>reset_selection()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>113</x>
|
||||
<y>178</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>110</x>
|
||||
<y>183</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>x_name</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>x_entry</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>160</x>
|
||||
<y>178</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>159</x>
|
||||
<y>188</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>y_name</sender>
|
||||
<signal>device_reset()</signal>
|
||||
<receiver>y_entry</receiver>
|
||||
<slot>reset_selection()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>92</x>
|
||||
<y>278</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>92</x>
|
||||
<y>287</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>y_name</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>y_entry</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>136</x>
|
||||
<y>277</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>135</x>
|
||||
<y>290</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>z_name</sender>
|
||||
<signal>device_reset()</signal>
|
||||
<receiver>z_entry</receiver>
|
||||
<slot>reset_selection()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>106</x>
|
||||
<y>376</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>112</x>
|
||||
<y>397</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>z_name</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>z_entry</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>164</x>
|
||||
<y>376</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>168</x>
|
||||
<y>389</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user