mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-09 18:20:55 +02:00
Compare commits
91 Commits
feat/dap_t
...
fix/wavefo
| Author | SHA1 | Date | |
|---|---|---|---|
| 15eebd500b | |||
|
|
bc0e277332 | ||
| 75a2780fe0 | |||
| a6c479e42e | |||
| 64a4824054 | |||
| 1619446ec9 | |||
| 37f002427a | |||
|
|
50cb70dcc6 | ||
| 55f7efc4f5 | |||
| be72c9f270 | |||
| c8cedc0124 | |||
|
|
3fdbe4031e | ||
| c16b9dce9c | |||
| 9387275851 | |||
| 94463afdba | |||
| 02563b10f3 | |||
| fff4af2489 | |||
| 452124b528 | |||
|
|
9c84e158ba | ||
| 58a0bc7974 | |||
| 770dbd4b63 | |||
| d22035f897 | |||
|
|
fe21b39b7f | ||
| 1b78840fd8 | |||
|
|
46519342b6 | ||
| 9079ddd727 | |||
|
|
205745cc72 | ||
| 717017e69e | |||
| a3de1f0a31 | |||
| 8eef4253b0 | |||
| 1f2db927f5 | |||
| 98f159b25f | |||
| 061f3481da | |||
| f35f4c4b29 | |||
| c36852b2ef | |||
| 4eaadd1545 | |||
|
|
d04770fe91 | ||
| 23fee22ef8 | |||
| 6e7920c119 | |||
| e3d0d5566c | |||
| e5b532274e | |||
| eb0323b989 | |||
| 60852e228f | |||
| b3dbe922de | |||
| fde912005d | |||
| 5e4965fe1f | |||
| aff5a51f4c | |||
| b4af2cc77a | |||
| 25bd905cef | |||
| 2f0d213e32 | |||
| b6695b45d0 | |||
| 77f9d42576 | |||
| 8cca510fa1 | |||
| 06a4954d3d | |||
| 4acf5befb1 | |||
| 99d76236ca | |||
| afc818bf7d | |||
| 8e846d4499 | |||
| a1c859c743 | |||
| 75cc45d767 | |||
| 1d091071e1 | |||
| 8e64b65c2d | |||
| 27ea92d120 | |||
| 3ddfeaa49f | |||
| 074bbbc166 | |||
| 3709cdc866 | |||
| 9d6d0b406a | |||
| 6318b2d822 | |||
| f89e74b199 | |||
| 0ac14a74b8 | |||
| 1910993b2b | |||
| 7c303d0129 | |||
| 113938e71a | |||
| e0f146beeb | |||
| fc1cdc814f | |||
| a13de45131 | |||
| 8ff2063bc8 | |||
| cdc613b6e7 | |||
| 1fc6125369 | |||
| fef07ac8e1 | |||
| 86647b9b7e | |||
| 36dc174bfe | |||
| a06f0600c1 | |||
| f88dfc8f1b | |||
| c70cd9d6e8 | |||
| 8fbd54c3aa | |||
| ef4a52cc17 | |||
| b460ea9955 | |||
| 1fe052e9da | |||
| f2d5b57e86 | |||
| 6630ba1c42 |
@@ -13,7 +13,7 @@ variables:
|
||||
value: main
|
||||
CHILD_PIPELINE_BRANCH: $CI_DEFAULT_BRANCH
|
||||
CHECK_PKG_VERSIONS:
|
||||
description: Whether to run additional tests against min/max/random selection of dependencies. Set to 1 for running.
|
||||
description: Whether to run additional tests against min/max/random selection of dependencies. Set to 1 for running.
|
||||
value: 0
|
||||
|
||||
workflow:
|
||||
@@ -77,7 +77,7 @@ formatter:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
script:
|
||||
- pip install bec_lib[dev]
|
||||
- pip install -e ./[dev]
|
||||
- isort --check --diff --line-length=100 --profile=black --multi-line=3 --trailing-comma ./
|
||||
- black --check --diff --color --line-length=100 --skip-magic-trailing-comma ./
|
||||
rules:
|
||||
@@ -162,6 +162,20 @@ tests:
|
||||
- tests/reference_failures/
|
||||
when: always
|
||||
|
||||
generate-client-check:
|
||||
stage: test
|
||||
needs: []
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- *install-repos
|
||||
- pip install -e .[dev,pyside6]
|
||||
- bw-generate-cli --target bec_widgets
|
||||
# if there are changes in the generated files, fail the job
|
||||
- git diff --exit-code
|
||||
|
||||
test-matrix:
|
||||
parallel:
|
||||
matrix:
|
||||
@@ -189,7 +203,7 @@ test-matrix:
|
||||
end-2-end-conda:
|
||||
stage: End2End
|
||||
needs: []
|
||||
image: continuumio/miniconda3
|
||||
image: continuumio/miniconda3:25.1.1-2
|
||||
allow_failure: false
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
@@ -216,7 +230,7 @@ end-2-end-conda:
|
||||
- pip install -e ./ophyd_devices
|
||||
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pytest -v --files-path ./ --start-servers --flush-redis --random-order ./tests/end-2-end
|
||||
- pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end
|
||||
|
||||
artifacts:
|
||||
when: on_failure
|
||||
@@ -231,7 +245,7 @@ end-2-end-conda:
|
||||
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^pre_release.*$/'
|
||||
- if: "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^pre_release.*$/"
|
||||
|
||||
semver:
|
||||
stage: Deploy
|
||||
|
||||
731
CHANGELOG.md
731
CHANGELOG.md
@@ -1,6 +1,737 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v2.1.2 (2025-05-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **waveform**: Ignore callbacks for on_async_readback from QtSender objects that are already
|
||||
destroyed; closes #497
|
||||
([`64a4824`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/64a48240546846fdf4541c2adf3a0a5a0829f948))
|
||||
|
||||
### Build System
|
||||
|
||||
- Remove flush-redis from ci job
|
||||
([`a6c479e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a6c479e42ea2a47c45e5a323bb3072bab503ecf1))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **bec-progressbar**: Add private method for bec_progressbar, udate client file
|
||||
([`37f0024`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37f002427ad5da01164ae3b0f4983695fe61c243))
|
||||
|
||||
- **bec-status-box**: Add get_server_state user_access method to BECStatusBox
|
||||
([`1619446`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1619446ec9839cfa1c666a3790a0c2abc449c4a8))
|
||||
|
||||
|
||||
## v2.1.1 (2025-05-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Import add operator in client
|
||||
([`55f7efc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/55f7efc4f586128dfb66fc6a8eb5d3a9f32bf61e))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Supply bec designer filename to function
|
||||
([`be72c9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be72c9f2708c93dab24d4383f5622e38cf1dc8a2))
|
||||
|
||||
|
||||
## v2.1.0 (2025-05-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Ensure rpc object do not collide with protected names
|
||||
([`94463af`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/94463afdba11fe2da5958a371ef49572889b8622))
|
||||
|
||||
### Chores
|
||||
|
||||
- **formatter**: Upgrade to black v25
|
||||
([`452124b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/452124b528c41db14d1e34ab98db95f6f7230ad6))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Install dev dependencies for formatter
|
||||
([`fff4af2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fff4af2489bdea0cf4f6f8db68db59fba411c25e))
|
||||
|
||||
### Features
|
||||
|
||||
- **SafeSlot**: Slot parameters can be overridden with kwarg; add option to raise
|
||||
([`9387275`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/93872758517177503b1f868376a6095670131844))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **colormap_widget**: Widget is rounded
|
||||
([`02563b1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/02563b10f3c90bddc069446dfe4137aa5a9727cb))
|
||||
|
||||
### Testing
|
||||
|
||||
- **Dock**: Add validation for new dock creation with invalid name
|
||||
([`c16b9dc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c16b9dce9ce629b794d731cd7f3282a59f8b8c59))
|
||||
|
||||
|
||||
## v2.0.3 (2025-05-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **generate_cli**: Apply isort config
|
||||
([`770dbd4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/770dbd4b63baba588871a4d4ffa77d44872d085b))
|
||||
|
||||
- **image_item**: Wrong user access name for rotation
|
||||
([`58a0bc7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58a0bc79742e7e7578988711a9840ed6041d9a69))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- Add job to test that the generated client is up to date
|
||||
([`d22035f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d22035f8974ac51ae1b6efc0e2b3749ca0a674ff))
|
||||
|
||||
|
||||
## v2.0.2 (2025-05-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **plot_base**: No content margin for plot_widget window
|
||||
([`1b78840`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b78840fd87ea0f156c73beeb57c6c06f685f7b1))
|
||||
|
||||
|
||||
## v2.0.1 (2025-04-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **dock_area**: Restore state safeguard to not pass none to pyqtgraph restoreState
|
||||
([`9079ddd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9079ddd7278ede7a9a12d7b39797154e83659c20))
|
||||
|
||||
|
||||
## v2.0.0 (2025-04-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add designer plugin for ScanMetadata
|
||||
([`43e1aa9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43e1aa9505cfa6e87b4fce1d065efb48b4111190))
|
||||
|
||||
- Add support for 'add_slice', add downsampling for performance improvements. add tests
|
||||
([`7f7891d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7f7891dfa54588f5d902448b760f141b183a7fa1))
|
||||
|
||||
- Broadcast context manager to emit registry changes just once
|
||||
([`a5f06c8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a5f06c8f8380156a763445a69df29ee0e62e434c))
|
||||
|
||||
- Bugfix in cleanup of ScatterWaveform ScatterCurve; closes #520
|
||||
([`1d09107`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1d091071e1179821bb1dcd47fb97f3d0959b972f))
|
||||
|
||||
- Change default colormap to plasma
|
||||
([`074bbbc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/074bbbc16648849bdfcfc28b2c520b0e38dd07c2))
|
||||
|
||||
- Create widget enum programatically
|
||||
([`7726d83`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7726d83b6834b8145e48e709e2f839fb0ec1b971))
|
||||
|
||||
- Ensure provided dock and dock_area names are valid and defaults are snake_case
|
||||
([`0ac14a7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0ac14a74b851578fff668fb8c6722f990130831d))
|
||||
|
||||
- Expose common classes from bec_widgets package
|
||||
([`28ae0d2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/28ae0d2b577d7c926ee54690898fe8e327e1229f))
|
||||
|
||||
- Forward parent to children
|
||||
([`1fc6125`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fc61253699425a2bf64a0f8b560f8474549b841))
|
||||
|
||||
- Import from qtpy instead of PySide6
|
||||
([`fef07ac`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fef07ac8e12399e7e49bcd673a5fc7cbf713bc50))
|
||||
|
||||
- Proper cleanup of progressbar
|
||||
([`8ff2063`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ff2063bc8978c5b2a97f720d5da055e8ec08f0c))
|
||||
|
||||
- Rpc access enabled for certain widgets.
|
||||
([`ef4a52c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef4a52cc17748f35ed627170b1025e6e028d70b8))
|
||||
|
||||
- Server shutdown widgets
|
||||
([`75b2446`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/75b24467def65284ea6b6114b25098437e31ec95))
|
||||
|
||||
- Support auto_range_x/y for viewAll during measurement
|
||||
([`af28e2e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af28e2e433c9b0233436da850be97cd63df90a74))
|
||||
|
||||
- Unique name for widgets, fix new method for docks; closes #534
|
||||
([`77f9d42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77f9d425765061f137c997062a3bf769a939bc64))
|
||||
|
||||
- Warning in logpanel
|
||||
([`1d7b423`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1d7b423bb307b6aae3879987776310c14380895d))
|
||||
|
||||
- chain a signal to the child BecLogsQueue rather than passing the signal instance in
|
||||
|
||||
- Wrap fetching plugin widgets in case of errors
|
||||
([`ef14831`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef148317dea9c7ff985b2a3ff06ccdb37258153f))
|
||||
|
||||
- **auto_updates**: Fix condition to skip auto update
|
||||
([`18e4ba6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/18e4ba6cfe9f67512efbd3989156de5670aab3fe))
|
||||
|
||||
- **bec_connector**: Add assertion to ensure BECConnector is used with a QObject; closes #475
|
||||
([`1921444`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1921444e152e06c4decc790452f3c496cf8ee961))
|
||||
|
||||
- **bec_connector**: Add setObjectName method to update object name and broadcast if registered;
|
||||
closes #472
|
||||
([`064343a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/064343acf2631e4ae62b2a5e08bc08087246570c))
|
||||
|
||||
- **bec_connector**: Call cleanup on widgets if the parent was deleted
|
||||
([`fc1cdc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc1cdc814fc3c44a571c20986bc627935f90ff91))
|
||||
|
||||
- **bec_connector**: Improve cleanup handling on deleted parent to prevent errors
|
||||
([`3709cdc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3709cdc86671e5219afca7a8e11bdd01f03dd30e))
|
||||
|
||||
- **bec_connector**: Move RPC registration into single shot method to ensure the rpc name is in sync
|
||||
([`3b16c9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3b16c9f5a2f7f16b23f25560b1e8fb4e42359ef0))
|
||||
|
||||
- **bec_queue**: Set parent for toolbar buttons
|
||||
([`cdc613b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cdc613b6e7d7eac806d458515321590e9344244a))
|
||||
|
||||
- **becconnector**: Widgets can be flagged as root widget, skipping the BECMainWindow in CLI usage
|
||||
([`061f348`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/061f3481daae6844a83c44e9caca7ed56a1bb100))
|
||||
|
||||
- **becconnector,widgets**: Parent_id is always fetched from the real bec widget parent; all widgets
|
||||
adjusted; hardcoded parent_ids removed
|
||||
([`f35f4c4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f35f4c4b295139b99a2dad9e8241f900d2565aeb))
|
||||
|
||||
- **BECGuiClient**: Add launch_script parameter to dock area creation
|
||||
([`06a4954`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06a4954d3da44c6805232d34e47e242b28ba7fd1))
|
||||
|
||||
- **cleanup**: Prevent double cleanup by tracking object destruction state
|
||||
([`fde9120`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fde912005db61a60707e7181c3425a4557bdc011))
|
||||
|
||||
- **cli**: Add type ignore comment to generated files
|
||||
([`d171255`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d1712552ffd1118845dc7121218df86ce10e8750))
|
||||
|
||||
- **client**: Import reduce
|
||||
([`8cca510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8cca510fa1cbda00a07edbef9d36fdd74e63d201))
|
||||
|
||||
- **client**: Regenerated client
|
||||
([`c97db6a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c97db6aaae81d08019a13c344414c16c42691654))
|
||||
|
||||
- **client**: Rpc API adjusted for DockArea, ImageItem and Waveform
|
||||
([`6ca4aa0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ca4aa0f9b9d5ace9fb1e174219f4da5617ebbac))
|
||||
|
||||
- **client_utils**: Simplify RPC client instantiation in BECGuiClient
|
||||
([`96b31a4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96b31a450998aca2b7ac94138b07223418d2bacd))
|
||||
|
||||
- **colormap_widget**: Size policy fixed
|
||||
([`1cc2a98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1cc2a9848906a7013e86687976d42d4b9676b25f))
|
||||
|
||||
- **compact_popup**: Forward close event
|
||||
([`e0f146b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e0f146beeb34367a4d3454a7012af4728d594b9b))
|
||||
|
||||
- **crosshair**: Adapted for 2D image
|
||||
([`a85402d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a85402dde1af1d9c4a154892c46422ac3e1f22f9))
|
||||
|
||||
- **curve**: Fix unique names for custom curves
|
||||
([`8e846d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e846d449955ded3cb8090e44ea36d26efccb80e))
|
||||
|
||||
- **dark-mode-button**: Fix parent passed to QObjects in various classes
|
||||
([`a06f060`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a06f0600c1c9a80436f01533a82905a6f3633895))
|
||||
|
||||
- **designer**: Avoid touching deleted widgets during init as QtDesigner will segfault
|
||||
([`4381fcc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4381fcc4c212cd03ce91f1638dc361c3315f8c45))
|
||||
|
||||
- **designer-plugin-generator**: Enhance super constructor validation for new style classes
|
||||
([`6318b2d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6318b2d822be0ded561a1afd0d485158614e2406))
|
||||
|
||||
- **device_input_base**: Removed enums from Pydantic models to make them serialisable
|
||||
([`43b747e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43b747ec8a761530d78b26650b0ec2ee4581ffaf))
|
||||
|
||||
- **dock_area**: Close BECMainWindow if dock area is central widget
|
||||
([`e725de3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e725de3c4504d43fbcad25d69c5cb8cbe7a70867))
|
||||
|
||||
- **docs**: Update copyright year to be dynamic
|
||||
([`f2d5b57`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f2d5b57e86d0c9d690b8d9f988035427608f0b4c))
|
||||
|
||||
- **entry_validator**: Validator reports list of signal if user chooses the wrong one
|
||||
([`da05877`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/da05877dd04fa618cdb45268cb62df602a5e808f))
|
||||
|
||||
- **image**: Imageitem remove adjusted to disconnect and remove current displayed image
|
||||
([`98f159b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98f159b25f6bf7e1f2dd76726d7ab66a0baf88de))
|
||||
|
||||
- **launch_window**: Redesign
|
||||
([`7e65d4f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7e65d4f2d6d840d3895e023f5cd090a56ea6e5f3))
|
||||
|
||||
- **launch_window**: Return None when cancelling the ui file launcher
|
||||
([`b3dbe92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b3dbe922dea2cea9190d1583bd6b69f1a45d6b90))
|
||||
|
||||
- **launch_window**: Update LaunchTile icon to use new UI loader tile image
|
||||
([`3cd6e05`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3cd6e05b2478654210049ca8e1756ad592f1da81))
|
||||
|
||||
- **launcher**: Hide launcher when launcher is closed even though it is not the last widget
|
||||
([`6e7920c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6e7920c119824650006e7357ca2f4ff95d413e13))
|
||||
|
||||
- **lmfit_dialog_vertical**: Vertical sizePolicy fixed
|
||||
([`584b945`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/584b94500565a33e5daed86b7552ec54f1135cf6))
|
||||
|
||||
- **main_window**: Connected to theme change
|
||||
([`11feeff`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/11feeff37ce0b02fcbc8e506c67c14e1fc5e0cb6))
|
||||
|
||||
- **main_window**: Show app id only when connected to redis
|
||||
([`be72268`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be722683a7cc7b215c572f9c2e996839b010b64e))
|
||||
|
||||
- **moduar-toolbar**: Fix cleanup of modular toolbar and dock_area
|
||||
([`c70cd9d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c70cd9d6e8f7ea9d5f81b10ac437cdcc9ee900e9))
|
||||
|
||||
- **motor_map**: Limit map creating optimized
|
||||
([`9f2a083`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9f2a083abbcfb465ebea9acee8263dcc9a6da5d9))
|
||||
|
||||
- **plot_base**: Ability to set y label suffix
|
||||
([`890b501`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/890b50115fef845c2a77242fdb05863d2eec4a00))
|
||||
|
||||
- **plot_base**: Aspect ratio removed from the PlotBase
|
||||
([`19d8aeb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/19d8aeb16249a1093cfec124d0ebdf6af11d94a8))
|
||||
|
||||
- **plot_base**: Axis setting filter for relevant properties
|
||||
([`0204d9c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0204d9c86f9665dcefdcbe7f49ac23918d74dd66))
|
||||
|
||||
- **plot_base**: Do not enable inner axes when label is changed
|
||||
([`98eda03`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98eda03f4d6d449605d5559a1db44c900d93cb79))
|
||||
|
||||
- **plot_base**: Enable popup property fixed
|
||||
([`30db183`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30db18367e9c6d6375fda970a1bb255d966cba5a))
|
||||
|
||||
- **plot_base**: Fix cleanup of popups if popups are still open when PlotBase is closed
|
||||
([`39cf4dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39cf4ddd5a033ee7f589d508f765669186e776bc))
|
||||
|
||||
- **plot_base**: Improved handling of matplotlib exporter errors
|
||||
([`4f9514f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f9514fbd1ff0059248d3b7b5b4fcd85c3eb9c72))
|
||||
|
||||
- **plot_base**: Inner and outer axis setting in popup mode
|
||||
([`055b968`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/055b96818aa69d66119caee9a3e8c24575ce60b4))
|
||||
|
||||
- **plot_base**: Update mouse mode state on mode change
|
||||
([`fc24c8b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc24c8b3a5f9cd55fb3d49f753b53a65a2a0fa26))
|
||||
|
||||
- **plot_framework**: All widgets, popups and side menus cleanups adjusted
|
||||
([`337a332`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/337a332ed123f99729b8cf6869f7fe4b056c2b16))
|
||||
|
||||
- **plot_indicators**: Cleanup adjusted
|
||||
([`4865341`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/48653410101a2a38d5067fbfca7712d255d89625))
|
||||
|
||||
- **plot_indicators**: Plot indicators added to the PlotBase
|
||||
([`42e3b9c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/42e3b9c13786e67220874a1275a3d9ee9515541a))
|
||||
|
||||
- **positioner-indicator**: Fix property setters for position indicator
|
||||
([`1910993`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1910993b2b3d30ecb8e4977b4a362f46adae3c75))
|
||||
|
||||
- **progress-ring-bar**: Fix parent inheritance and cleanup of ring objects; closes #496
|
||||
([`b460ea9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b460ea9955318879ddfe4f9ae963249ba342bbb5))
|
||||
|
||||
- **ring-progress-bar**: Fix bug in disconnect slot of rings, enable 'scan' mode as default for init
|
||||
with first ring
|
||||
([`7c303d0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7c303d01294493a55fd26db8c1475e8c58b3e492))
|
||||
|
||||
- **ring_progress_bar**: Replaced hard-coded endpoints by MessageEndpoints
|
||||
([`e4e9feb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e4e9febc98268a4b6b9774b253419e88ea044811))
|
||||
|
||||
- **round_frame**: Orientation can be vertical
|
||||
([`c1bbb16`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c1bbb16dad481c628e7680180d7250ba8a560c46))
|
||||
|
||||
- **round_frame**: Roundframe removed from BECWidget inheritance
|
||||
([`b58a098`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b58a098ed4afbe62721fd2bf8497f363deecbfa6))
|
||||
|
||||
- **rpc**: Call close on container widget if needed
|
||||
([`a13de45`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a13de45131309771c1438407f3733a8c0897d495))
|
||||
|
||||
- **rpc-base**: Deprecate widget_name in favor of object_name; closes #499
|
||||
([`86647b9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/86647b9b7e2fa111105ae483808883a624fa4cd6))
|
||||
|
||||
- **rpc_base**: Ensure message wait event is set after processing RPC response
|
||||
([`4dc59aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4dc59aa5e9b15e5ec40401e80e7965acd88e2fce))
|
||||
|
||||
- **rpc_base**: Timeout run_rpc 3s
|
||||
([`8558b46`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8558b46114760a9434eaa827f81d5fd9d047112f))
|
||||
|
||||
- **rpc_register**: _lock and _skip_broad_cast moved to instance attributes
|
||||
([`8d17f7e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d17f7e32f81894294d7da472268e8d9eb3bb74b))
|
||||
|
||||
- **rpc_register**: Change add_rpc parameter type to BECConnector and add object_is_registered
|
||||
method
|
||||
([`82b8265`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/82b82659b7919b15d629375866302624b5b6e457))
|
||||
|
||||
- **rpc_register**: Lock changed to RLock
|
||||
([`6c90ca3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6c90ca31078d97124a3ad535ffe83da138558d67))
|
||||
|
||||
- **rpc_server**: Broadcasted data check
|
||||
([`c36852b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c36852b2ef762cdae3fde569bbd0d5f2f6f2725b))
|
||||
|
||||
- **rpc_server**: Enhance serialization logic for BECConnector objects and fix return types
|
||||
([`125afc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/125afc89073b4fc69a3f42650b3d4f6fa6ccaa47))
|
||||
|
||||
- **rpc_server**: Update _serialize_bec_connector to include wait parameter for registration check
|
||||
([`d6fccd1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d6fccd10f5d600ea67cf7b2a5ebb42295d15cdfe))
|
||||
|
||||
- **RPCReference**: Setattr added
|
||||
([`a2128ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a2128ad8d688995551c5e26974396fd0588b6804))
|
||||
|
||||
- **scan_control**: Restore scan parameters always regenerate the arg box, preventing infinite loop
|
||||
([`1f2db92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f2db927f50f4f30d43ebe52e39118c7d79994d4))
|
||||
|
||||
- **scan_matadata**: Parent passing
|
||||
([`4eaadd1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4eaadd1545885b111fce3f8cab527a77b8633ff3))
|
||||
|
||||
- **scatter_waveform,waveform**: Added QTimer to fetch the last data points after 500ms
|
||||
([`e6795dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6795dd87ccd93cfd53e22cd94d71bffe1ef54dd))
|
||||
|
||||
- **serialization**: Add serialization for qpointf
|
||||
([`3ddfeaa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3ddfeaa49fd4a7fdbff7cae47b90c25720f6dca0))
|
||||
|
||||
- **server**: Becdockarea type added
|
||||
([`4a74891`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4a74891184f112751258866b6bc9d800dbc5ed05))
|
||||
|
||||
- **server**: Remove window.hide() since widgets will be teared down on kill_server before siginit
|
||||
signals is sent
|
||||
([`58b0c7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58b0c7ddc1d0b85b35e7e18434c0b83aac01a735))
|
||||
|
||||
- **server**: Turn_off_the_lights cleanup fixed for parent_id widgets
|
||||
([`20a86ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/20a86ad325d36aa5aec73aeda7ff43ea9cc6c1f7))
|
||||
|
||||
- **setting_widget**: Added parent kwarg into all settings widgets in plotting framework
|
||||
([`94c2e2d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/94c2e2db6518402207b2a1077bb16403a8e61cee))
|
||||
|
||||
- **side_panel**: Side panel menu can be initialized without a title
|
||||
([`112eed6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/112eed694c0ef6eb80ec7a7cfdfbaacf732d5b9f))
|
||||
|
||||
- **toolbar**: Update action check handling logic for SwitchableToolBarAction
|
||||
([`ac08bdf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac08bdfab2162ac8fd103e60779a76d36e9a3765))
|
||||
|
||||
- **type hints**: Add future import to prevent sphinx from crashing
|
||||
([`aff5a51`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aff5a51f4c059ce21ec72cefc263f37df2491480))
|
||||
|
||||
- **waveform**: Dap curve flickering
|
||||
([`b03d2ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b03d2eaeed4263846c470bc45eba9208ced2370b))
|
||||
|
||||
- **waveform**: Error where scan history is empty
|
||||
([`288ea4d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/288ea4dbbde6d5f770c37f4daf377da9ec8fe729))
|
||||
|
||||
- **waveform**: Fix dap curve categorization logic
|
||||
([`b91f1fe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b91f1fe4879e43e71a1be49ce5a206efbae19315))
|
||||
|
||||
- **waveform**: Legend is correctly updated when changed from curve dialog
|
||||
([`c2d2c48`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c2d2c484cd1f133f45fe7147616c22c0b5fd5611))
|
||||
|
||||
- **waveform**: Signals for x device can be defined from gui
|
||||
([`39164fe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39164feb18f9e996d97814f58157892e8db816ae))
|
||||
|
||||
- **waveform, rpc_reference**: __getitem__ removed form waveform and rpc_reference
|
||||
([`3a82c95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3a82c95f60cb2b7f0b29a1ea5cdcbfa5bf602af8))
|
||||
|
||||
- **website-widget**: Add super().cleanup() in website widget
|
||||
([`8fbd54c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8fbd54c3aa864623e42b39a1ebf92ba098ba437d))
|
||||
|
||||
- **widgets**: Becconnector resolves hierarchy including objectName, parent, parent_id upon init;
|
||||
all widgets adjusted
|
||||
([`a1bec75`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a1bec7511549277da231928d989b16ecad0eed1b))
|
||||
|
||||
### Build System
|
||||
|
||||
- Pyside6 capped to 6.9
|
||||
([`9dabf2c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9dabf2c66c8023194964b9ad308e06197471f89f))
|
||||
|
||||
- **bec_lib**: Raised required version to 3.28.1
|
||||
([`a5f1f47`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a5f1f4781ed9787148053d71e0d12fefe42e142a))
|
||||
|
||||
- **dependencies**: Update min bec_lib version to 3.29
|
||||
([`eb0323b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb0323b989e96e89d2eb1ff7b648edb43f5fe198))
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- **e2e**: E2e tests are saving logs
|
||||
([`d4106c5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d4106c548e2373463a48268fd991ded7f554e3a6))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add docs on widget plugins
|
||||
([`52a9f29`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/52a9f29bdcb20a9339a8970508bc0a93ba8bef5f))
|
||||
|
||||
- Add missing class doc strings for rpc-enabled widgets
|
||||
([`cfc8272`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cfc8272ac288541d1e20c0840bd2ce6fa930897c))
|
||||
|
||||
- Better document logpanel code
|
||||
([`d2c9075`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2c90757c21e040940a378325cad75c4d94470f9))
|
||||
|
||||
- Grammar improvement
|
||||
([`1fe052e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fe052e9da9e44bf9872db0d42218843a8e6d275))
|
||||
|
||||
- Remove BECFigure
|
||||
([`75cc45d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/75cc45d767970e985c566fd4aeccd4394f48dfa3))
|
||||
|
||||
- Remove BECFigure from docs, fix wrong api for docs of plotting widgets
|
||||
([`a1c859c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a1c859c7434357e6fb82f0912314c203fb73e890))
|
||||
|
||||
- Replaces instances of QtDesigner with BEC Designer for improved clarity
|
||||
([`60852e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/60852e228f80f0d2e74813f82bd30f1ba83ff154))
|
||||
|
||||
- Review quick_start
|
||||
([`4acf5be`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4acf5befb1dbcc69e8cc7da70ebf5663b9ec15f2))
|
||||
|
||||
- Update docs for v2
|
||||
([`25bd905`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/25bd905cef987a713e24aca178c04aef1ab59656))
|
||||
|
||||
- Update docs for various widgets
|
||||
([`b6695b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b6695b45d076dbf3896e94eedcf73d542022d764))
|
||||
|
||||
- Update quick_start
|
||||
([`afc818b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/afc818bf7d17f42703c3cebafd2b292b8444647a))
|
||||
|
||||
- Updated docs for v2 ([#531](https://gitlab.psi.ch/bec/bec_widgets/-/merge_requests/531),
|
||||
[`b4af2cc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4af2cc77aa0013f4547cd98345b0c77abb7101b))
|
||||
|
||||
- **auto_updates**: Update documentation for auto updates functionality and add launcher image
|
||||
([`6630ba1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6630ba1c421e566bf86ac38701a86eff624395d2))
|
||||
|
||||
- **lmfit**: Fix links
|
||||
([`5e4965f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5e4965fe1f88d18fcc7e6875777ff3eb01ab08ec))
|
||||
|
||||
- **plot_base**: Update docstrings for properties and setters
|
||||
([`b085ef6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b085ef6e730d529149bcb696b1ad4cd9c5220a83))
|
||||
|
||||
- **position-indicator**: Update docs for positioner indicator
|
||||
([`2f0d213`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2f0d213e32fd662bfffd4df73b9281fa30cef6e3))
|
||||
|
||||
### Features
|
||||
|
||||
- Add loader/helper for widget plugins
|
||||
([`ca2bb4f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca2bb4f9b42ebaac2fc544d3da36267d93e9903d))
|
||||
|
||||
- Add rpc broadcast
|
||||
([`2ba9b4c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2ba9b4cb236a2182261dfb88398d5ece733ba393))
|
||||
|
||||
- Add support for auto updates
|
||||
([`2511056`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2511056557daf0b5dd78d3e85ac4befb8bf8c316))
|
||||
|
||||
- Delete bec_app
|
||||
([`8e64b65`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e64b65c2d3b8a8f3c6e5376e694369b41733da4))
|
||||
|
||||
- Deprecated and delete alignment_1d gui
|
||||
([`27ea92d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/27ea92d120cc8ef01fff10341ce0954b4f7fed5d))
|
||||
|
||||
- Namespace update for gui, dock_area and docks.
|
||||
([`ac3c5a3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac3c5a38e449c2c3e4a1c61d5f9a59acfbf0cab5))
|
||||
|
||||
- **auto_update**: Add GUI highlight management for auto updates status
|
||||
([`5f272a6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5f272a66a4d4f65273d7b2a6709336cd3582d695))
|
||||
|
||||
- **auto_updates**: Enforce rpc widget class for subclasses of auto updates
|
||||
([`778230b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/778230b5edf5d24df8a10c78c90ea065510e8344))
|
||||
|
||||
- **image**: New Image widget based on new PlotBase
|
||||
([`cb39ff3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cb39ff3fbde99f4e4bed49dee8a5e5987d257b23))
|
||||
|
||||
- **launch_window**: Add custom UI file launching functionality and UI tile
|
||||
([`3089ca1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3089ca15ec4a8c110d11c57aff2da42f4af5bd08))
|
||||
|
||||
- **launch_window**: Add user access permissions
|
||||
([`8efa93d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8efa93d2d2c5e6c28008e1bbde89e5cc8a01d139))
|
||||
|
||||
- **launch_window**: Enhance auto update functionality with selector and dynamic loading
|
||||
([`2965323`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/29653239c5cf43313224cc5123d066fcba4b831b))
|
||||
|
||||
- **launcher**: Add option for launching with auto updates
|
||||
([`20a1c5d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/20a1c5ddb3cd0763ce69bba5a893f54c56678706))
|
||||
|
||||
- **main_window**: Add launcher menu and functionality to show launcher
|
||||
([`55baa84`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/55baa84eb6723b30b407092bc36f826b826cc934))
|
||||
|
||||
- **motor_map**: New MotorMap widget based on PlotBase
|
||||
([`fec26d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fec26d793e14965a719a4d038838418b9a7603bb))
|
||||
|
||||
- **multi_waveform**: Multi-waveform widget based on new PlotBase
|
||||
([`77f9616`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77f96160ab348c1a65ceb55986ea4ea75f8be04a))
|
||||
|
||||
- **plugin_utils**: Add functionality to retrieve auto update classes from plugins
|
||||
([`c434af9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c434af9b92d68d08da87112ef424738e5e42ae6e))
|
||||
|
||||
- **positioner_box**: Add units QLabel to device UI components and update visibility logic
|
||||
([`f653fc5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f653fc5f7ebf8ad5297facd739a8a49ea0a06c95))
|
||||
|
||||
- **scatter_waveform**: Scatter waveform widget based on new Plotbase
|
||||
([`95fcf01`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/95fcf016c32c52330acfd5900a3996c99c4ee01f))
|
||||
|
||||
- **server,launcher**: Rpc server separated with the launcher window introduced
|
||||
([`5f27a90`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5f27a9098903ffd8ec27c1b45565f1c113892cca))
|
||||
|
||||
- **slot**: Add 'verify_sender' argument to SafeSlot for sender verification
|
||||
([`8eef425`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8eef4253b0507f60f50c06ed48b59a1b19b29644))
|
||||
|
||||
- **ui_launch_window**: Add UILaunchWindow class
|
||||
([`45cd82e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/45cd82e6354c72e1e35cd6366aa7aad93f8b12ca))
|
||||
|
||||
- **waveform**: New Waveform widget based on NextGen PlotBase
|
||||
([`4bec181`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4bec181f3aff34d9de7d3f9ec012b641c125a661))
|
||||
|
||||
- **widget_io**: Added handler for Sliders
|
||||
([`1a0097e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1a0097e02728b6470217d3a574260f376776d81f))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Add fallback to 'index' plotting in case of missmatch in length
|
||||
([`515d7ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/515d7ad05584086a8e8ac626b476d629e27aacf3))
|
||||
|
||||
- Add pragma no cover to various TYPE_CHECKING
|
||||
([`f88dfc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f88dfc8f1bbc0819736a4f32bf21682366fd3437))
|
||||
|
||||
- Add support to plot against x_data
|
||||
([`0e276d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0e276d4c09cddb459688aecf28684a963d8f6613))
|
||||
|
||||
- Add template for debugging the cli generator
|
||||
([`f89e74b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f89e74b199d007cf47f355a1c5e1f582daeea90a))
|
||||
|
||||
- Autoupdate disabled
|
||||
([`4e29291`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4e29291b3a0891657a8d2011bcaf1d6e65de125a))
|
||||
|
||||
- Cleanup MR
|
||||
([`0b00cd2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0b00cd24fd43dbc87c81f7dbfae816343f7da4c4))
|
||||
|
||||
- Cleanup rpc reference tracking, fix appquit, fix namespace updates edge cases
|
||||
([`7ba93ce`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7ba93ce934cc644ad6340f141a6a0888bd1d3d98))
|
||||
|
||||
- Cleanup, fix tests and _top_level dict/windows
|
||||
([`5872253`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58722531232b2290f9fd974bae24877c9d5451f4))
|
||||
|
||||
- Fix cleanup bug for BECConnector items, renamed _registry_state to _server_registry
|
||||
([`be83c7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be83c7d5f4bc04d110734b491727dc60d8dd61ef))
|
||||
|
||||
- Fix cleanup for various widgets, including RoundedFrame
|
||||
([`d05179a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d05179a519d6419c9631ffdf4fa6aa262966c2ed))
|
||||
|
||||
- Improve plotting behaviour from history
|
||||
([`ed2d958`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ed2d958de62223cd796c869b6c8b9b75170e66f5))
|
||||
|
||||
- Rearrange base of metadata forms for generic use
|
||||
([`d04770f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d04770fe913474ec9d4e06b056c85e720d1470c4))
|
||||
|
||||
- Set downsampling to auto=True, method 'peak', activate clipToView for (Async)-Curves and fix
|
||||
ViewAll hook from pg.view_box menu
|
||||
([`25820a1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/25820a1cdec2cff99ab0d6085aece0e3e7dd9092))
|
||||
|
||||
- Tidy client generation and add options
|
||||
([`b492591`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4925918f7acf31e40971814639be8a6c55d46df))
|
||||
|
||||
- **assets**: New icon for ui loader
|
||||
([`e5b5322`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e5b532274ede281456a14b02a99855302603490a))
|
||||
|
||||
- **auto_update**: Auto_update changed to be BECMainWindow; removed auto update logic from
|
||||
BECDockArea
|
||||
([`56c2827`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56c282714037f733bcfd8a659f34baadcd1aa223))
|
||||
|
||||
- **auto_updates**: Move cleanup method from user section to internal section
|
||||
([`ac9224e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac9224e5f2d3edcb5b1cc1cbc1a8583f81d0b912))
|
||||
|
||||
- **bec_connector**: Replace pyqtSlot with SafeSlot for consistency
|
||||
([`9d6d0b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d6d0b406a812e08ca8417415b5def98b40bdf92))
|
||||
|
||||
- **bec_figure**: Becfigure removed
|
||||
([`f76d931`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f76d9319bd13bb52b1ae2524c1c5e44a167cc330))
|
||||
|
||||
- **client_utils**: Remove unused auto update attributes from BECGuiClient
|
||||
([`b7795b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b7795b4d0ae21641bead0f1f1541f920ae95702a))
|
||||
|
||||
- **image_widget**: Old BECImageWidget removed
|
||||
([`de10609`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/de10609b3c714b80a14bf6940e86763d0779402b))
|
||||
|
||||
- **launch_window**: Remove cleanup method
|
||||
([`9a940bb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9a940bb8d58f37d1fc24ce4fdb38282d02349efb))
|
||||
|
||||
- **launcher,main_window**: Launcher window moved to inherit from BECMainWindow
|
||||
([`99383b7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99383b77150ca7c74c19c899a0e6a7879b770376))
|
||||
|
||||
- **motor_map_widget**: Becmotormapwidget removed
|
||||
([`f878e87`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f878e87ad545e0fe68292030d9f06dee693e0da2))
|
||||
|
||||
- **multi_waveform_widget**: Becmultiwaveformwidget removed
|
||||
([`7c31bbd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7c31bbd9c2e0230e54f4dca0f1e5c4d2cd6e7674))
|
||||
|
||||
- **plots**: Plot_next_gen module renamed to plots
|
||||
([`9fb9a1c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9fb9a1cfd2a94efd5e2a9fcbaa05d65c7b7105ee))
|
||||
|
||||
- **plots**: Waveform and image rpc api review
|
||||
([`a3de1f0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3de1f0a31dfb9048493fb61983167960577fb97))
|
||||
|
||||
- **rpc_reference**: Refactor rpc reference tracking
|
||||
([`bd5e251`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bd5e251ee9396633f419732e43411821726250aa))
|
||||
|
||||
- **rpc_server**: Add type hint for _get_becwidget_ancestor method parameter; minor cleanup of
|
||||
imports
|
||||
([`cb91ebc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cb91ebc0c34ae29b6b293996199f4624d36a3cc0))
|
||||
|
||||
- **rpc_server**: Add type hints and docstrings for heartbeat and registry update methods
|
||||
([`08168f2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/08168f28d3c5375b9ace9df0d7aa31e33adb97e9))
|
||||
|
||||
- **rpc_server**: Cli_server renamed to rpc_server
|
||||
([`6082e7a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6082e7a6907c2fe15e4e5ebca857fbf8f222d192))
|
||||
|
||||
- **tests**: Create dummy scan item moved to client_mocks.py
|
||||
([`0dd9617`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0dd9617e6e5ea756edc344c324451480a62bdae2))
|
||||
|
||||
- **ui_loader**: Remove unnecessary parent_id handling
|
||||
([`d60cf6c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d60cf6c843ecc135a6065d1e913f9f6abb1a483d))
|
||||
|
||||
- **ui_loader**: Remove unused import
|
||||
([`a6ce312`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a6ce312f7c60c2babcc37127f7c69d54c1b32573))
|
||||
|
||||
- **utils**: Qt_utils moved to utils
|
||||
([`be552d3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be552d3ece97e7f472c4534b4af8438b95c518aa))
|
||||
|
||||
- **waveform_widget**: Removed and replaced by Waveform
|
||||
([`96cff49`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96cff49cd4453fa70d8802653d5afe62d71c6b2a))
|
||||
|
||||
### Testing
|
||||
|
||||
- Add function scoped rpc_widgets e2e test; closes #510
|
||||
([`36dc174`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/36dc174bfedf212532658b84f8ab64971863d292))
|
||||
|
||||
- Add IPython client GUI object test module with tab completion
|
||||
([`e3d0d55`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e3d0d5566c50f6a80ba861d4e3e0789f17785a46))
|
||||
|
||||
- Add tests for name creation of custom curves, and object name handling
|
||||
([`99d7623`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99d76236cac63042f0d7d1db580dde8aa7cfd214))
|
||||
|
||||
- Disable test_bec_dock_rpc_e2e module, issue to fix this created #450
|
||||
([`17f2dda`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17f2dda977025bc422e26289293d3fcbd224a6f6))
|
||||
|
||||
- Fix rpc widgets e2e test
|
||||
([`113938e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/113938e71a6dacba37164069e2c795cc9db168d4))
|
||||
|
||||
- Fix tests for launcher close / hide behavior
|
||||
([`23fee22`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/23fee22ef8f2c22f191dfc1da57b921484ede6cd))
|
||||
|
||||
- Fix tests for namespace updates
|
||||
([`f3d3c94`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f3d3c9425d3ed619b978427cea782137beedfb59))
|
||||
|
||||
- Qapp must shutdown cli server before checking for leaked QTimer
|
||||
([`d066051`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d06605122e5c2e225650b44ebfc047daa5aa6f55))
|
||||
|
||||
- **bec_connector**: Becconnector requires a QObject
|
||||
([`23bdd95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/23bdd95d8c6311d989cb3b807921e3fb2a3d62a0))
|
||||
|
||||
- **device_signal_input**: Fix init of device input widget
|
||||
([`31c3b64`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/31c3b64d7b157e5e26d44e5288afabef343c5e13))
|
||||
|
||||
- **e2e**: E2e tests adjusted for new plotting framework
|
||||
([`378398a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/378398a29b34e43f0cca0a49b08adfcb144e4777))
|
||||
|
||||
- **generate_cli**: Fix reference output
|
||||
([`a8adb06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8adb064f5011e1708ee2dc0090326f533407260))
|
||||
|
||||
- **launch_window**: Add test for launching UI file that raises ValueError for QMainWindow
|
||||
([`33a8a76`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33a8a767f31a57bcfda624d503ed39e0e4578dcb))
|
||||
|
||||
- **launch_window**: Add unit tests for LaunchWindow initialization and custom UI file launching
|
||||
([`d5e422c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d5e422c7fc0e169c35d7f206937e8c7902fbf123))
|
||||
|
||||
- **launch_window**: Tests for default and plugin auto updates
|
||||
([`e10f5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e10f5ec088c6937beb26ec468f510a209c7cc782))
|
||||
|
||||
- **plot_base**: Test for plot base re-enabled
|
||||
([`b51d637`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b51d637c5ff3418420801cd9b457fc073fa98adc))
|
||||
|
||||
- **plot_indicators**: Tests adapted to not be dependent on BECWaveformWidget
|
||||
([`360fe4c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/360fe4c9c3b5c3c1f26e97cb795aef8f4aba3b46))
|
||||
|
||||
- **setting_dialog**: Test that settings reject calls cleanup
|
||||
([`8914f1d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8914f1d50600cab588a6cbecb08d85bfd1a715a1))
|
||||
|
||||
- **unit_tests**: Unit tests adjusted to use a modern plotting framework instead of BECFigure
|
||||
([`6ade934`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ade93435632fa66fb012d92f9b8b548d96e718f))
|
||||
|
||||
|
||||
## v1.25.1 (2025-03-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
""" This module contains the GUI for the 1D alignment application.
|
||||
It is a preliminary version of the GUI, which will be added to the main branch and steadily updated to be improved.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from bec_lib.device import Signal as BECSignal
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
||||
from bec_widgets.widgets.control.device_control.positioner_group.positioner_group import (
|
||||
PositionerGroup,
|
||||
)
|
||||
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
||||
from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import BECProgressBar
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# FIXME BECWaveFormWidget is gone, this app will not work until adapted to new Waveform
|
||||
class Alignment1D:
|
||||
"""Alignment GUI to perform 1D scans"""
|
||||
|
||||
def __init__(self, client=None, gui_id: Optional[str] = None) -> None:
|
||||
"""Initialization
|
||||
|
||||
Args:
|
||||
config: Configuration of the application.
|
||||
client: BEC client object.
|
||||
gui_id: GUI ID.
|
||||
"""
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
QApplication.instance().aboutToQuit.connect(self.close)
|
||||
self.dev = self.client.device_manager.devices
|
||||
|
||||
self._accent_colors = get_accent_colors()
|
||||
self.ui_file = "alignment_1d.ui"
|
||||
self.ui = None
|
||||
self.progress_bar = None
|
||||
self.waveform = None
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialise the UI from QT Designer file"""
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader(None).loader(os.path.join(current_path, self.ui_file))
|
||||
# Customize the plotting widget
|
||||
self.waveform = self.ui.findChild(BECWaveformWidget, "bec_waveform_widget")
|
||||
self._customise_bec_waveform_widget()
|
||||
# Setup comboboxes for motor and signal selection
|
||||
# FIXME after changing the filtering in the combobox
|
||||
self._setup_signal_combobox()
|
||||
# Setup motor indicator
|
||||
self._setup_motor_indicator()
|
||||
# Setup progress bar
|
||||
self._setup_progress_bar()
|
||||
# Add actions buttons
|
||||
self._customise_buttons()
|
||||
# Hook scaninfo updates
|
||||
self.bec_dispatcher.connect_slot(self.scan_status_callback, MessageEndpoints.scan_status())
|
||||
|
||||
def show(self):
|
||||
return self.ui.show()
|
||||
|
||||
##############################
|
||||
############ SLOTS ###########
|
||||
##############################
|
||||
|
||||
@Slot(dict, dict)
|
||||
def scan_status_callback(self, content: dict, _) -> None:
|
||||
"""This slot allows to enable/disable the UI critical components when a scan is running"""
|
||||
if content["status"] in ["open"]:
|
||||
self.enable_ui(False)
|
||||
elif content["status"] in ["aborted", "halted", "closed"]:
|
||||
self.enable_ui(True)
|
||||
|
||||
@Slot(tuple)
|
||||
def move_to_center(self, move_request: tuple) -> None:
|
||||
"""Move the selected motor to the center"""
|
||||
motor = self.ui.device_combobox.currentText()
|
||||
if move_request[0] in ["center", "center1", "center2"]:
|
||||
pos = move_request[1]
|
||||
self.dev.get(motor).move(float(pos), relative=False)
|
||||
|
||||
@Slot()
|
||||
def reset_progress_bar(self) -> None:
|
||||
"""Reset the progress bar"""
|
||||
self.progress_bar.set_value(0)
|
||||
self.progress_bar.set_minimum(0)
|
||||
|
||||
@Slot(dict, dict)
|
||||
def update_progress_bar(self, content: dict, _) -> None:
|
||||
"""Hook to update the progress bar
|
||||
|
||||
Args:
|
||||
content: Content of the scan progress message.
|
||||
metadata: Metadata of the message.
|
||||
"""
|
||||
if content["max_value"] == 0:
|
||||
self.progress_bar.set_value(0)
|
||||
return
|
||||
self.progress_bar.set_maximum(content["max_value"])
|
||||
self.progress_bar.set_value(content["value"])
|
||||
|
||||
@Slot()
|
||||
def clear_queue(self) -> None:
|
||||
"""Clear the scan queue"""
|
||||
self.queue.request_queue_reset()
|
||||
|
||||
##############################
|
||||
######## END OF SLOTS ########
|
||||
##############################
|
||||
|
||||
def enable_ui(self, enable: bool) -> None:
|
||||
"""Enable or disable the UI components"""
|
||||
# Enable/disable motor and signal selection
|
||||
self.ui.device_combobox_2.setEnabled(enable)
|
||||
# Enable/disable DAP selection
|
||||
self.ui.dap_combo_box.setEnabled(enable)
|
||||
# Enable/disable Scan Button
|
||||
# self.ui.scan_button.setEnabled(enable)
|
||||
# Disable move to buttons in LMFitDialog
|
||||
self.ui.findChild(LMFitDialog).set_actions_enabled(enable)
|
||||
|
||||
def _customise_buttons(self) -> None:
|
||||
"""Add action buttons for the Action Control.
|
||||
In addition, we are adding a callback to also clear the queue to the stop button
|
||||
to ensure that upon clicking the button, no scans from another client may be queued
|
||||
which would be confusing without the queue widget.
|
||||
"""
|
||||
fit_dialog = self.ui.findChild(LMFitDialog)
|
||||
fit_dialog.active_action_list = ["center", "center1", "center2"]
|
||||
fit_dialog.move_action.connect(self.move_to_center)
|
||||
stop_button = self.ui.findChild(StopButton)
|
||||
stop_button.button.setText("Stop and Clear Queue")
|
||||
stop_button.button.clicked.connect(self.clear_queue)
|
||||
|
||||
def _customise_bec_waveform_widget(self) -> None:
|
||||
"""Customise the BEC Waveform Widget, i.e. clear the toolbar"""
|
||||
self.waveform.toolbar.clear()
|
||||
|
||||
def _setup_motor_indicator(self) -> None:
|
||||
"""Setup the arrow item"""
|
||||
self.waveform.waveform.tick_item.add_to_plot()
|
||||
positioner_box = self.ui.findChild(PositionerGroup)
|
||||
positioner_box.position_update.connect(self.waveform.waveform.tick_item.set_position)
|
||||
self.waveform.waveform.tick_item.set_position(0)
|
||||
|
||||
def _setup_signal_combobox(self) -> None:
|
||||
"""Setup signal selection"""
|
||||
# FIXME after changing the filtering in the combobox
|
||||
signals = [name for name in self.dev if isinstance(self.dev.get(name), BECSignal)]
|
||||
self.ui.device_combobox_2.setCurrentText(signals[0])
|
||||
self.ui.device_combobox_2.set_device_filter("Signal")
|
||||
|
||||
def _setup_progress_bar(self) -> None:
|
||||
"""Setup progress bar"""
|
||||
# FIXME once the BECScanProgressBar is implemented
|
||||
self.progress_bar = self.ui.findChild(BECProgressBar, "bec_progress_bar")
|
||||
self.progress_bar.set_value(0)
|
||||
self.ui.bec_waveform_widget.new_scan.connect(self.reset_progress_bar)
|
||||
self.bec_dispatcher.connect_slot(self.update_progress_bar, MessageEndpoints.scan_progress())
|
||||
|
||||
def close(self):
|
||||
logger.info("Disconnecting", repr(self.bec_dispatcher))
|
||||
self.bec_dispatcher.disconnect_all()
|
||||
logger.info("Shutting down BEC Client", repr(self.client))
|
||||
self.client.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "alignment_1d.png"), size=QSize(48, 48)
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
window = Alignment1D()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,615 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>mainWindow</class>
|
||||
<widget class="QMainWindow" name="mainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1611</width>
|
||||
<height>1019</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Alignment tool</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="widget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="DarkModeButton" name="dark_mode_button"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECStatusBox" name="bec_status_box">
|
||||
<property name="compact_view" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="label" stdset="0">
|
||||
<string>BEC Servers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECQueue" name="bec_queue">
|
||||
<property name="compact_view" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radioButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>SLS Light On</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radioButton_3">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BEAMLINE Checks</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="StopButton" name="stop_button">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECProgressBar" name="bec_progress_bar">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="ControlTab">
|
||||
<attribute name="title">
|
||||
<string>Alignment Control</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_4" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="ScanControl" name="scan_control">
|
||||
<property name="current_scan" stdset="0">
|
||||
<string>line_scan</string>
|
||||
</property>
|
||||
<property name="hide_arg_box" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="hide_scan_selection_combobox" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_add_remove_buttons" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PositionerGroup" name="positioner_group"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>4</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_2" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Monitor</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DeviceComboBox" name="device_combobox_2"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>LMFit Model</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DapComboBox" name="dap_combo_box"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Enable ROI</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ToggleSwitch" name="toggle_switch">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>3</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Activate linear region select for LMFit</string>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LayoutDirection::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_8">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECWaveformWidget" name="bec_waveform_widget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="clear_curves_on_plot_update" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="LMFitDialog" name="lm_fit_dialog">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>190</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="always_show_latest" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_curve_selection" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_summary" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Logbook</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="WebsiteWidget" name="website_widget">
|
||||
<property name="url" stdset="0">
|
||||
<string>https://scilog.psi.ch/login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DapComboBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dap_combo_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>StopButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>stop_button</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WebsiteWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>website_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECQueue</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_queue</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ScanControl</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>scan_control</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECProgressBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_progress_bar</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DarkModeButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dark_mode_button</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PositionerGroup</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>positioner_group</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECWaveformWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_waveform_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DeviceComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>device_combobox</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>LMFitDialog</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>lm_fit_dialog</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECStatusBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_status_box</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>toggle_switch</sender>
|
||||
<signal>enabled(bool)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>toogle_roi_select(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1042</x>
|
||||
<y>212</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>322</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>bec_waveform_widget</sender>
|
||||
<signal>dap_summary_update(QVariantMap,QVariantMap)</signal>
|
||||
<receiver>lm_fit_dialog</receiver>
|
||||
<slot>update_summary_tree(QVariantMap,QVariantMap)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1099</x>
|
||||
<y>258</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1157</x>
|
||||
<y>929</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>device_combobox_2</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>plot(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>577</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>427</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>device_combobox_2</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>dap_combo_box</receiver>
|
||||
<slot>select_y_axis(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>577</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>909</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>dap_combo_box</sender>
|
||||
<signal>new_dap_config(QString,QString,QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>add_dap(QString,QString,QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>909</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>447</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>positioner_group</receiver>
|
||||
<slot>set_positioners(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>230</x>
|
||||
<y>306</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>187</x>
|
||||
<y>926</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>set_x(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>187</x>
|
||||
<y>356</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>972</x>
|
||||
<y>509</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>dap_combo_box</receiver>
|
||||
<slot>select_x_axis(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>187</x>
|
||||
<y>356</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>794</x>
|
||||
<y>202</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -1,84 +0,0 @@
|
||||
"""
|
||||
Launcher for BEC GUI Applications
|
||||
|
||||
Application must be located in bec_widgets/applications ;
|
||||
in order for the launcher to find the application, it has to be put in
|
||||
a subdirectory with the same name as the main Python module:
|
||||
|
||||
/bec_widgets/applications
|
||||
├── alignment
|
||||
│ └── alignment_1d
|
||||
│ └── alignment_1d.py
|
||||
├── other_app
|
||||
└── other_app.py
|
||||
|
||||
The tree above would contain 2 applications, alignment_1d and other_app.
|
||||
|
||||
The Python module for the application must have `if __name__ == "__main__":`
|
||||
in order for the launcher to execute it (it is run with `python -m`).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
MODULE_PATH = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def find_apps(base_dir: str) -> list[str]:
|
||||
matching_modules = []
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
parent_dir = os.path.basename(root)
|
||||
|
||||
for file in files:
|
||||
if file.endswith(".py") and file != "__init__.py":
|
||||
file_name_without_ext = os.path.splitext(file)[0]
|
||||
|
||||
if file_name_without_ext == parent_dir:
|
||||
rel_path = os.path.relpath(root, base_dir)
|
||||
module_path = rel_path.replace(os.sep, ".")
|
||||
|
||||
module_name = f"{module_path}.{file_name_without_ext}"
|
||||
matching_modules.append((file_name_without_ext, module_name))
|
||||
|
||||
return matching_modules
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="BEC application launcher")
|
||||
|
||||
parser.add_argument("-m", "--module", type=str, help="The module to run (string argument).")
|
||||
|
||||
# Add a positional argument for the module, which acts as a fallback if -m is not provided
|
||||
parser.add_argument(
|
||||
"positional_module",
|
||||
nargs="?", # This makes the positional argument optional
|
||||
help="Positional argument that is treated as module if -m is not specified.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
# If the -m/--module is not provided, fallback to the positional argument
|
||||
module = args.module if args.module else args.positional_module
|
||||
|
||||
if module:
|
||||
for app_name, app_module in find_apps(MODULE_PATH):
|
||||
if module in (app_name, app_module):
|
||||
print("Starting:", app_name)
|
||||
python_executable = sys.executable
|
||||
|
||||
# Replace the current process with the new Python module
|
||||
os.execvp(
|
||||
python_executable,
|
||||
[python_executable, "-m", f"bec_widgets.applications.{app_module}"],
|
||||
)
|
||||
print(f"Error: cannot find application {module}")
|
||||
|
||||
# display list of apps
|
||||
print("Available applications:")
|
||||
for app, _ in find_apps(MODULE_PATH):
|
||||
print(f" - {app}")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
|
||||
|
||||
def dock_area(object_name: str | None = None) -> BECDockArea:
|
||||
_dock_area = BECDockArea(object_name=object_name)
|
||||
_dock_area = BECDockArea(object_name=object_name, root_widget=True)
|
||||
return _dock_area
|
||||
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ class LaunchWindow(BECMainWindow):
|
||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||
self.toolbar = ModularToolBar(parent=self)
|
||||
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
|
||||
self.spacer = QWidget()
|
||||
self.spacer = QWidget(self)
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar.addWidget(self.spacer)
|
||||
self.toolbar.addWidget(self.dark_mode_button)
|
||||
@@ -204,13 +204,17 @@ class LaunchWindow(BECMainWindow):
|
||||
list(self.available_auto_updates.keys()) + ["Default"]
|
||||
)
|
||||
|
||||
self.register = RPCRegister()
|
||||
self.register.callbacks.append(self._turn_off_the_lights)
|
||||
self.register.broadcast()
|
||||
|
||||
def launch(
|
||||
self,
|
||||
launch_script: str,
|
||||
name: str | None = None,
|
||||
geometry: tuple[int, int, int, int] | None = None,
|
||||
**kwargs,
|
||||
) -> QWidget:
|
||||
) -> QWidget | None:
|
||||
"""Launch the specified script. If the launch script creates a QWidget, it will be
|
||||
embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown
|
||||
as a separate window.
|
||||
@@ -231,6 +235,8 @@ class LaunchWindow(BECMainWindow):
|
||||
raise ValueError(
|
||||
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
|
||||
)
|
||||
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||
|
||||
else:
|
||||
name = "dock_area"
|
||||
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
||||
@@ -242,6 +248,8 @@ class LaunchWindow(BECMainWindow):
|
||||
|
||||
if launch_script == "custom_ui_file":
|
||||
ui_file = kwargs.pop("ui_file", None)
|
||||
if not ui_file:
|
||||
return None
|
||||
return self._launch_custom_ui_file(ui_file)
|
||||
|
||||
if launch_script == "auto_update":
|
||||
@@ -274,6 +282,8 @@ class LaunchWindow(BECMainWindow):
|
||||
raise ValueError("UI file must be provided for custom UI file launch.")
|
||||
filename = os.path.basename(ui_file).split(".")[0]
|
||||
|
||||
WidgetContainerUtils.raise_for_invalid_name(filename)
|
||||
|
||||
tree = ET.parse(ui_file)
|
||||
root = tree.getroot()
|
||||
# Check if the top-level widget is a QMainWindow
|
||||
@@ -371,6 +381,45 @@ class LaunchWindow(BECMainWindow):
|
||||
super().showEvent(event)
|
||||
self.setFixedSize(self.size())
|
||||
|
||||
def _launcher_is_last_widget(self, connections: dict) -> bool:
|
||||
"""
|
||||
Check if the launcher is the last widget in the application.
|
||||
"""
|
||||
|
||||
remaining_connections = [
|
||||
connection for connection in connections.values() if connection.parent_id != self.gui_id
|
||||
]
|
||||
return len(remaining_connections) <= 1
|
||||
|
||||
def _turn_off_the_lights(self, connections: dict):
|
||||
"""
|
||||
If there is only one connection remaining, it is the launcher, so we show it.
|
||||
Once the launcher is closed as the last window, we quit the application.
|
||||
"""
|
||||
if self._launcher_is_last_widget(connections):
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
self.raise_()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(True) # type: ignore
|
||||
return
|
||||
|
||||
self.hide()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(False) # type: ignore
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""
|
||||
Close the launcher window.
|
||||
"""
|
||||
connections = self.register.list_all_connections()
|
||||
if self._launcher_is_last_widget(connections):
|
||||
event.accept()
|
||||
return
|
||||
|
||||
event.ignore()
|
||||
self.hide()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 1.7 MiB |
@@ -6,6 +6,8 @@ from __future__ import annotations
|
||||
import enum
|
||||
import inspect
|
||||
import traceback
|
||||
from functools import reduce
|
||||
from operator import add
|
||||
from typing import Literal, Optional
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
@@ -25,22 +27,31 @@ class _WidgetsEnumType(str, enum.Enum):
|
||||
|
||||
|
||||
_Widgets = {
|
||||
"AbortButton": "AbortButton",
|
||||
"BECDockArea": "BECDockArea",
|
||||
"BECProgressBar": "BECProgressBar",
|
||||
"BECQueue": "BECQueue",
|
||||
"BECStatusBox": "BECStatusBox",
|
||||
"DapComboBox": "DapComboBox",
|
||||
"DarkModeButton": "DarkModeButton",
|
||||
"DeviceBrowser": "DeviceBrowser",
|
||||
"DeviceComboBox": "DeviceComboBox",
|
||||
"DeviceLineEdit": "DeviceLineEdit",
|
||||
"Image": "Image",
|
||||
"LogPanel": "LogPanel",
|
||||
"Minesweeper": "Minesweeper",
|
||||
"MotorMap": "MotorMap",
|
||||
"MultiWaveform": "MultiWaveform",
|
||||
"PositionIndicator": "PositionIndicator",
|
||||
"PositionerBox": "PositionerBox",
|
||||
"PositionerBox2D": "PositionerBox2D",
|
||||
"PositionerControlLine": "PositionerControlLine",
|
||||
"ResetButton": "ResetButton",
|
||||
"ResumeButton": "ResumeButton",
|
||||
"RingProgressBar": "RingProgressBar",
|
||||
"ScanControl": "ScanControl",
|
||||
"ScatterWaveform": "ScatterWaveform",
|
||||
"StopButton": "StopButton",
|
||||
"TextBox": "TextBox",
|
||||
"VSCodeEditor": "VSCodeEditor",
|
||||
"Waveform": "Waveform",
|
||||
@@ -76,6 +87,16 @@ except ImportError as e:
|
||||
logger.error(f"Failed loading plugins: \n{reduce(add, traceback.format_exception(e))}")
|
||||
|
||||
|
||||
class AbortButton(RPCBase):
|
||||
"""A button that abort the scan."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class AutoUpdates(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -449,6 +470,12 @@ class BECProgressBar(RPCBase):
|
||||
>>> progressbar.label_template = "$value / $percentage %"
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_label(self) -> str:
|
||||
"""
|
||||
Return the label text. mostly used for testing rpc.
|
||||
"""
|
||||
|
||||
|
||||
class BECQueue(RPCBase):
|
||||
"""Widget to display the BEC queue."""
|
||||
@@ -463,6 +490,12 @@ class BECQueue(RPCBase):
|
||||
class BECStatusBox(RPCBase):
|
||||
"""An autonomous widget to display the status of BEC services."""
|
||||
|
||||
@rpc_call
|
||||
def get_server_state(self) -> "str":
|
||||
"""
|
||||
Get the state ("RUNNING", "BUSY", "IDLE", "ERROR") of the BEC server
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
@@ -665,6 +698,14 @@ class DapComboBox(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class DarkModeButton(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class DeviceBrowser(RPCBase):
|
||||
"""DeviceBrowser is a widget that displays all available devices in the current BEC session."""
|
||||
|
||||
@@ -923,48 +964,6 @@ class Image(RPCBase):
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_log(self) -> "bool":
|
||||
"""
|
||||
Set X-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@x_log.setter
|
||||
@rpc_call
|
||||
def x_log(self) -> "bool":
|
||||
"""
|
||||
Set X-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_log(self) -> "bool":
|
||||
"""
|
||||
Set Y-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@y_log.setter
|
||||
@rpc_call
|
||||
def y_log(self) -> "bool":
|
||||
"""
|
||||
Set Y-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def legend_label_size(self) -> "int":
|
||||
"""
|
||||
The font size of the legend font.
|
||||
"""
|
||||
|
||||
@legend_label_size.setter
|
||||
@rpc_call
|
||||
def legend_label_size(self) -> "int":
|
||||
"""
|
||||
The font size of the legend font.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color_map(self) -> "str":
|
||||
@@ -1159,16 +1158,16 @@ class Image(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rotation(self) -> "int":
|
||||
def num_rotation_90(self) -> "int":
|
||||
"""
|
||||
The number of 90° rotations to apply.
|
||||
The number of 90° rotations to apply counterclockwise.
|
||||
"""
|
||||
|
||||
@rotation.setter
|
||||
@num_rotation_90.setter
|
||||
@rpc_call
|
||||
def rotation(self) -> "int":
|
||||
def num_rotation_90(self) -> "int":
|
||||
"""
|
||||
The number of 90° rotations to apply.
|
||||
The number of 90° rotations to apply counterclockwise.
|
||||
"""
|
||||
|
||||
@property
|
||||
@@ -1331,14 +1330,14 @@ class ImageItem(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rotation(self) -> "Optional[int]":
|
||||
def num_rotation_90(self) -> "Optional[int]":
|
||||
"""
|
||||
Get or set the number of 90° rotations to apply.
|
||||
"""
|
||||
|
||||
@rotation.setter
|
||||
@num_rotation_90.setter
|
||||
@rpc_call
|
||||
def rotation(self) -> "Optional[int]":
|
||||
def num_rotation_90(self) -> "Optional[int]":
|
||||
"""
|
||||
Get or set the number of 90° rotations to apply.
|
||||
"""
|
||||
@@ -1388,6 +1387,9 @@ class LogPanel(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class MotorMap(RPCBase):
|
||||
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
||||
|
||||
@@ -2166,7 +2168,7 @@ class MultiWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def plot(self, monitor: "str", color_palette: "str | None" = "magma"):
|
||||
def plot(self, monitor: "str", color_palette: "str | None" = "plasma"):
|
||||
"""
|
||||
Create a plot for the given monitor.
|
||||
Args:
|
||||
@@ -2196,7 +2198,10 @@ class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
"""
|
||||
None
|
||||
Set the position of the indicator
|
||||
|
||||
Args:
|
||||
position: The new position of the indicator
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -2216,6 +2221,13 @@ class PositionIndicator(RPCBase):
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
|
||||
@vertical.setter
|
||||
@rpc_call
|
||||
def vertical(self):
|
||||
"""
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def indicator_width(self):
|
||||
@@ -2223,6 +2235,13 @@ class PositionIndicator(RPCBase):
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
|
||||
@indicator_width.setter
|
||||
@rpc_call
|
||||
def indicator_width(self):
|
||||
"""
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rounded_corners(self):
|
||||
@@ -2230,6 +2249,61 @@ class PositionIndicator(RPCBase):
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
|
||||
@rounded_corners.setter
|
||||
@rpc_call
|
||||
def rounded_corners(self):
|
||||
"""
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox(RPCBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox2D(RPCBase):
|
||||
"""Simple Widget to control two positioners in box form"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner_hor(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner_ver(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerControlLine(RPCBase):
|
||||
"""A widget that controls a single device."""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerGroup(RPCBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
@@ -2243,6 +2317,26 @@ class PositionerGroup(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class ResetButton(RPCBase):
|
||||
"""A button that resets the scan queue."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class ResumeButton(RPCBase):
|
||||
"""A button that continue scan queue."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class Ring(RPCBase):
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
@@ -2371,9 +2465,9 @@ class RingProgressBar(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rings(self):
|
||||
def rings(self) -> "list[Ring]":
|
||||
"""
|
||||
None
|
||||
Returns a list of all rings in the progress bar.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -2847,7 +2941,7 @@ class ScatterWaveform(RPCBase):
|
||||
x_entry: "None | str" = None,
|
||||
y_entry: "None | str" = None,
|
||||
z_entry: "None | str" = None,
|
||||
color_map: "str | None" = "magma",
|
||||
color_map: "str | None" = "plasma",
|
||||
label: "str | None" = None,
|
||||
validate_bec: "bool" = True,
|
||||
) -> "ScatterCurve":
|
||||
@@ -2887,6 +2981,16 @@ class ScatterWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class StopButton(RPCBase):
|
||||
"""A button that stops the current scan."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class TextBox(RPCBase):
|
||||
"""A widget that displays text in plain and HTML format"""
|
||||
|
||||
@@ -3231,6 +3335,20 @@ class Waveform(RPCBase):
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_entry(self) -> "str | None":
|
||||
"""
|
||||
The x signal name.
|
||||
"""
|
||||
|
||||
@x_entry.setter
|
||||
@rpc_call
|
||||
def x_entry(self) -> "str | None":
|
||||
"""
|
||||
The x signal name.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color_palette(self) -> "str":
|
||||
|
||||
@@ -20,6 +20,7 @@ from rich.table import Table
|
||||
|
||||
import bec_widgets.cli.client as client
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
||||
from bec_widgets.utils.serialization import register_serializer_extension
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.messages import GUIRegistryStateMessage
|
||||
@@ -31,7 +32,8 @@ logger = bec_logger.logger
|
||||
IGNORE_WIDGETS = ["LaunchWindow"]
|
||||
|
||||
RegistryState: TypeAlias = dict[
|
||||
Literal["gui_id", "name", "widget_class", "config", "__rpc__"], str | bool | dict
|
||||
Literal["gui_id", "name", "widget_class", "config", "__rpc__", "container_proxy"],
|
||||
str | bool | dict,
|
||||
]
|
||||
|
||||
# pylint: disable=redefined-outer-scope
|
||||
@@ -214,6 +216,7 @@ class BECGuiClient(RPCBase):
|
||||
self._server_registry: dict[str, RegistryState] = {}
|
||||
self._ipython_registry: dict[str, RPCReference] = {}
|
||||
self.available_widgets = AvailableWidgetsNamespace()
|
||||
register_serializer_extension()
|
||||
|
||||
####################
|
||||
#### Client API ####
|
||||
@@ -275,6 +278,8 @@ class BECGuiClient(RPCBase):
|
||||
name: str | None = None,
|
||||
wait: bool = True,
|
||||
geometry: tuple[int, int, int, int] | None = None,
|
||||
launch_script: str = "dock_area",
|
||||
**kwargs,
|
||||
) -> client.BECDockArea:
|
||||
"""Create a new top-level dock area.
|
||||
|
||||
@@ -290,11 +295,11 @@ class BECGuiClient(RPCBase):
|
||||
if wait:
|
||||
with wait_for_server(self):
|
||||
widget = self.launcher._run_rpc(
|
||||
"launch", "dock_area", name, geometry
|
||||
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
||||
) # pylint: disable=protected-access
|
||||
return widget
|
||||
widget = self.launcher._run_rpc(
|
||||
"new_dock_area", name, geometry
|
||||
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
||||
) # pylint: disable=protected-access
|
||||
return widget
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ class ClientGenerator:
|
||||
"""import enum
|
||||
import inspect
|
||||
import traceback
|
||||
from functools import reduce
|
||||
from operator import add
|
||||
from typing import Literal, Optional
|
||||
"""
|
||||
if self._base
|
||||
@@ -221,18 +223,18 @@ class {class_name}(RPCBase):"""
|
||||
# Combine header and content, then format with black
|
||||
full_content = self.header + "\n" + self.content
|
||||
try:
|
||||
formatted_content = black.format_str(full_content, mode=black.FileMode(line_length=100))
|
||||
formatted_content = black.format_str(full_content, mode=black.Mode(line_length=100))
|
||||
except black.NothingChanged:
|
||||
formatted_content = full_content
|
||||
|
||||
isort.Config(
|
||||
config = isort.Config(
|
||||
profile="black",
|
||||
line_length=100,
|
||||
multi_line_output=3,
|
||||
include_trailing_comma=True,
|
||||
include_trailing_comma=False,
|
||||
known_first_party=["bec_widgets"],
|
||||
)
|
||||
formatted_content = isort.code(formatted_content)
|
||||
formatted_content = isort.code(formatted_content, config=config)
|
||||
|
||||
with open(file_name, "w", encoding="utf-8") as file:
|
||||
file.write(formatted_content)
|
||||
@@ -315,4 +317,7 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
sys.argv = ["bw-generate-cli", "--target", "bec_widgets"]
|
||||
main()
|
||||
|
||||
@@ -130,6 +130,7 @@ class RPCBase:
|
||||
config: dict | None = None,
|
||||
object_name: str | None = None,
|
||||
parent=None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
|
||||
self._config = config if config is not None else {}
|
||||
@@ -144,21 +145,21 @@ class RPCBase:
|
||||
def __repr__(self):
|
||||
type_ = type(self)
|
||||
qualname = type_.__qualname__
|
||||
return f"<{qualname} with name: {self.widget_name}>"
|
||||
return f"<{qualname} with name: {self.object_name}>"
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the widget.
|
||||
"""
|
||||
obj = self._root._server_registry.get(self._gui_id)
|
||||
if obj is None:
|
||||
raise ValueError(f"Widget {self._gui_id} not found.")
|
||||
if proxy := obj.get("container_proxy"):
|
||||
assert isinstance(proxy, str)
|
||||
self._run_rpc("remove", gui_id=proxy)
|
||||
return
|
||||
self._run_rpc("remove")
|
||||
|
||||
@property
|
||||
def widget_name(self):
|
||||
"""
|
||||
Get the widget name.
|
||||
"""
|
||||
return self.object_name
|
||||
|
||||
@property
|
||||
def _root(self) -> BECGuiClient:
|
||||
"""
|
||||
@@ -171,7 +172,15 @@ class RPCBase:
|
||||
parent = parent._parent
|
||||
return parent # type: ignore
|
||||
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=5, **kwargs) -> Any:
|
||||
def _run_rpc(
|
||||
self,
|
||||
method,
|
||||
*args,
|
||||
wait_for_rpc_response=True,
|
||||
timeout=5,
|
||||
gui_id: str | None = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
"""
|
||||
Run the RPC call.
|
||||
|
||||
@@ -179,6 +188,8 @@ class RPCBase:
|
||||
method: The method to call.
|
||||
args: The arguments to pass to the method.
|
||||
wait_for_rpc_response: Whether to wait for the RPC response.
|
||||
timeout: The timeout for the RPC response.
|
||||
gui_id: The GUI ID to use for the RPC call. If None, the default GUI ID is used.
|
||||
kwargs: The keyword arguments to pass to the method.
|
||||
|
||||
Returns:
|
||||
@@ -187,7 +198,7 @@ class RPCBase:
|
||||
request_id = str(uuid.uuid4())
|
||||
rpc_msg = messages.GUIInstructionMessage(
|
||||
action=method,
|
||||
parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
|
||||
parameter={"args": args, "kwargs": kwargs, "gui_id": gui_id or self._gui_id},
|
||||
metadata={"request_id": request_id},
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@@ -83,29 +83,6 @@ class GUIServer:
|
||||
service_config = ServiceConfig()
|
||||
return service_config
|
||||
|
||||
def _turn_off_the_lights(self, connections: dict):
|
||||
"""
|
||||
If there is only one connection remaining, it is the launcher, so we show it.
|
||||
Once the launcher is closed as the last window, we quit the application.
|
||||
"""
|
||||
self.launcher_window = cast(LaunchWindow, self.launcher_window)
|
||||
|
||||
remaining_connections = [
|
||||
connection
|
||||
for connection in connections.values()
|
||||
if connection.parent_id != self.launcher_window.gui_id
|
||||
]
|
||||
if len(remaining_connections) <= 1:
|
||||
self.launcher_window.show()
|
||||
self.launcher_window.activateWindow()
|
||||
self.launcher_window.raise_()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(True)
|
||||
else:
|
||||
self.launcher_window.hide()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
def _run(self):
|
||||
"""
|
||||
Run the GUI server.
|
||||
@@ -125,10 +102,6 @@ class GUIServer:
|
||||
self.app.aboutToQuit.connect(self.shutdown)
|
||||
self.app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
register = RPCRegister()
|
||||
register.callbacks.append(self._turn_off_the_lights)
|
||||
register.broadcast()
|
||||
|
||||
if self.gui_class:
|
||||
# If the server is started with a specific gui class, we launch it.
|
||||
# This will automatically hide the launcher.
|
||||
|
||||
@@ -43,22 +43,22 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"pg": pg,
|
||||
"wh": wh,
|
||||
"dock": self.dock,
|
||||
"im": self.im,
|
||||
"mi": self.mi,
|
||||
"mm": self.mm,
|
||||
"lm": self.lm,
|
||||
"btn1": self.btn1,
|
||||
"btn2": self.btn2,
|
||||
"btn3": self.btn3,
|
||||
"btn4": self.btn4,
|
||||
"btn5": self.btn5,
|
||||
"btn6": self.btn6,
|
||||
"pb": self.pb,
|
||||
"pi": self.pi,
|
||||
"wf": self.wf,
|
||||
"scatter": self.scatter,
|
||||
"scatter_mi": self.scatter,
|
||||
"mwf": self.mwf,
|
||||
# "im": self.im,
|
||||
# "mi": self.mi,
|
||||
# "mm": self.mm,
|
||||
# "lm": self.lm,
|
||||
# "btn1": self.btn1,
|
||||
# "btn2": self.btn2,
|
||||
# "btn3": self.btn3,
|
||||
# "btn4": self.btn4,
|
||||
# "btn5": self.btn5,
|
||||
# "btn6": self.btn6,
|
||||
# "pb": self.pb,
|
||||
# "pi": self.pi,
|
||||
# "wf": self.wf,
|
||||
# "scatter": self.scatter,
|
||||
# "scatter_mi": self.scatter,
|
||||
# "mwf": self.mwf,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -77,76 +77,76 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
first_tab_layout.addWidget(self.dock)
|
||||
tab_widget.addTab(first_tab, "Dock Area")
|
||||
|
||||
third_tab = QWidget()
|
||||
third_tab_layout = QVBoxLayout(third_tab)
|
||||
self.lm = LayoutManagerWidget()
|
||||
third_tab_layout.addWidget(self.lm)
|
||||
tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||
|
||||
fourth_tab = QWidget()
|
||||
fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||
self.pb = PlotBase()
|
||||
self.pi = self.pb.plot_item
|
||||
fourth_tab_layout.addWidget(self.pb)
|
||||
tab_widget.addTab(fourth_tab, "PlotBase")
|
||||
|
||||
tab_widget.setCurrentIndex(3)
|
||||
|
||||
# third_tab = QWidget()
|
||||
# third_tab_layout = QVBoxLayout(third_tab)
|
||||
# self.lm = LayoutManagerWidget()
|
||||
# third_tab_layout.addWidget(self.lm)
|
||||
# tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||
#
|
||||
# fourth_tab = QWidget()
|
||||
# fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||
# self.pb = PlotBase()
|
||||
# self.pi = self.pb.plot_item
|
||||
# fourth_tab_layout.addWidget(self.pb)
|
||||
# tab_widget.addTab(fourth_tab, "PlotBase")
|
||||
#
|
||||
# tab_widget.setCurrentIndex(3)
|
||||
#
|
||||
group_box = QGroupBox("Jupyter Console", splitter)
|
||||
group_box_layout = QVBoxLayout(group_box)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
group_box_layout.addWidget(self.console)
|
||||
|
||||
# Some buttons for layout testing
|
||||
self.btn1 = QPushButton("Button 1")
|
||||
self.btn2 = QPushButton("Button 2")
|
||||
self.btn3 = QPushButton("Button 3")
|
||||
self.btn4 = QPushButton("Button 4")
|
||||
self.btn5 = QPushButton("Button 5")
|
||||
self.btn6 = QPushButton("Button 6")
|
||||
|
||||
fifth_tab = QWidget()
|
||||
fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
self.wf = Waveform()
|
||||
fifth_tab_layout.addWidget(self.wf)
|
||||
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
tab_widget.setCurrentIndex(4)
|
||||
|
||||
sixth_tab = QWidget()
|
||||
sixth_tab_layout = QVBoxLayout(sixth_tab)
|
||||
self.im = Image()
|
||||
self.mi = self.im.main_image
|
||||
sixth_tab_layout.addWidget(self.im)
|
||||
tab_widget.addTab(sixth_tab, "Image Next Gen")
|
||||
tab_widget.setCurrentIndex(5)
|
||||
|
||||
seventh_tab = QWidget()
|
||||
seventh_tab_layout = QVBoxLayout(seventh_tab)
|
||||
self.scatter = ScatterWaveform()
|
||||
self.scatter_mi = self.scatter.main_curve
|
||||
self.scatter.plot("samx", "samy", "bpm4i")
|
||||
seventh_tab_layout.addWidget(self.scatter)
|
||||
tab_widget.addTab(seventh_tab, "Scatter Waveform")
|
||||
tab_widget.setCurrentIndex(6)
|
||||
|
||||
eighth_tab = QWidget()
|
||||
eighth_tab_layout = QVBoxLayout(eighth_tab)
|
||||
self.mm = MotorMap()
|
||||
eighth_tab_layout.addWidget(self.mm)
|
||||
tab_widget.addTab(eighth_tab, "Motor Map")
|
||||
tab_widget.setCurrentIndex(7)
|
||||
|
||||
ninth_tab = QWidget()
|
||||
ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||
self.mwf = MultiWaveform()
|
||||
ninth_tab_layout.addWidget(self.mwf)
|
||||
tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||
tab_widget.setCurrentIndex(8)
|
||||
|
||||
# add stuff to the new Waveform widget
|
||||
self._init_waveform()
|
||||
|
||||
self.setWindowTitle("Jupyter Console Window")
|
||||
#
|
||||
# # Some buttons for layout testing
|
||||
# self.btn1 = QPushButton("Button 1")
|
||||
# self.btn2 = QPushButton("Button 2")
|
||||
# self.btn3 = QPushButton("Button 3")
|
||||
# self.btn4 = QPushButton("Button 4")
|
||||
# self.btn5 = QPushButton("Button 5")
|
||||
# self.btn6 = QPushButton("Button 6")
|
||||
#
|
||||
# fifth_tab = QWidget()
|
||||
# fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
# self.wf = Waveform()
|
||||
# fifth_tab_layout.addWidget(self.wf)
|
||||
# tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
# tab_widget.setCurrentIndex(4)
|
||||
#
|
||||
# sixth_tab = QWidget()
|
||||
# sixth_tab_layout = QVBoxLayout(sixth_tab)
|
||||
# self.im = Image()
|
||||
# self.mi = self.im.main_image
|
||||
# sixth_tab_layout.addWidget(self.im)
|
||||
# tab_widget.addTab(sixth_tab, "Image Next Gen")
|
||||
# tab_widget.setCurrentIndex(5)
|
||||
#
|
||||
# seventh_tab = QWidget()
|
||||
# seventh_tab_layout = QVBoxLayout(seventh_tab)
|
||||
# self.scatter = ScatterWaveform()
|
||||
# self.scatter_mi = self.scatter.main_curve
|
||||
# self.scatter.plot("samx", "samy", "bpm4i")
|
||||
# seventh_tab_layout.addWidget(self.scatter)
|
||||
# tab_widget.addTab(seventh_tab, "Scatter Waveform")
|
||||
# tab_widget.setCurrentIndex(6)
|
||||
#
|
||||
# eighth_tab = QWidget()
|
||||
# eighth_tab_layout = QVBoxLayout(eighth_tab)
|
||||
# self.mm = MotorMap()
|
||||
# eighth_tab_layout.addWidget(self.mm)
|
||||
# tab_widget.addTab(eighth_tab, "Motor Map")
|
||||
# tab_widget.setCurrentIndex(7)
|
||||
#
|
||||
# ninth_tab = QWidget()
|
||||
# ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||
# self.mwf = MultiWaveform()
|
||||
# ninth_tab_layout.addWidget(self.mwf)
|
||||
# tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||
# tab_widget.setCurrentIndex(8)
|
||||
#
|
||||
# # add stuff to the new Waveform widget
|
||||
# self._init_waveform()
|
||||
#
|
||||
# self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
def _init_waveform(self):
|
||||
self.wf.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
|
||||
@@ -174,7 +174,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
icon = material_icon("terminal", color=(255, 255, 255, 255), filled=True)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
bec_dispatcher = BECDispatcher(gui_id="jupyter_console")
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
@@ -14,8 +15,7 @@ from qtpy.QtCore import QObject, QRunnable, QThreadPool, QTimer, Signal
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.utils.error_popups import SafeSlot as pyqtSlot
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility, SafeSlot
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
||||
|
||||
@@ -85,9 +85,21 @@ class BECConnector:
|
||||
gui_id: str | None = None,
|
||||
object_name: str | None = None,
|
||||
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
|
||||
parent_id: str | None = None,
|
||||
root_widget: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
BECConnector mixin class to handle BEC client and device manager.
|
||||
|
||||
Args:
|
||||
client(BECClient, optional): The BEC client.
|
||||
config(ConnectionConfig, optional): The connection configuration with specific gui id.
|
||||
gui_id(str, optional): The GUI ID.
|
||||
object_name(str, optional): The object name.
|
||||
parent_dock(BECDock, optional): The parent dock.# TODO should go away -> issue created #473
|
||||
root_widget(bool, optional): If set to True, the parent_id will be always set to None, thus enforcing that the widget is accessible as a root widget of the BECGuiClient object.
|
||||
**kwargs:
|
||||
"""
|
||||
# Extract object_name from kwargs to not pass it to Qt class
|
||||
object_name = object_name or kwargs.pop("objectName", None)
|
||||
# Ensure the parent is always the first argument for QObject
|
||||
@@ -99,6 +111,9 @@ class BECConnector:
|
||||
self, QObject
|
||||
), "BECConnector must be used with a QObject or any qt related class."
|
||||
|
||||
# flag to check if the object was destroyed and its cleanup was called
|
||||
self._destroyed = False
|
||||
|
||||
# BEC related connections
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
@@ -108,7 +123,7 @@ class BECConnector:
|
||||
if not self.client in BECConnector.EXIT_HANDLERS:
|
||||
# register function to clean connections at exit;
|
||||
# the function depends on BECClient, and BECDispatcher
|
||||
@pyqtSlot()
|
||||
@SafeSlot()
|
||||
def terminate(client=self.client, dispatcher=self.bec_dispatcher):
|
||||
logger.info("Disconnecting", repr(dispatcher))
|
||||
dispatcher.disconnect_all()
|
||||
@@ -128,7 +143,6 @@ class BECConnector:
|
||||
)
|
||||
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
|
||||
self.parent_id = parent_id
|
||||
# If the gui_id is passed, it should be respected. However, this should be revisted since
|
||||
# the gui_id has to be unique, and may no longer be.
|
||||
if gui_id:
|
||||
@@ -147,10 +161,8 @@ class BECConnector:
|
||||
|
||||
# 2) Enforce unique objectName among siblings with the same BECConnector parent
|
||||
self.setParent(parent)
|
||||
if parent_id is None:
|
||||
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||
if connector_parent is not None:
|
||||
self.parent_id = connector_parent.gui_id
|
||||
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()
|
||||
@@ -159,8 +171,40 @@ class BECConnector:
|
||||
# Store references to running workers so they're not garbage collected prematurely.
|
||||
self._workers = []
|
||||
|
||||
# If set to True, the parent_id will be always set to None, thus enforcing that the widget is accessible as a root widget of the BECGuiClient object.
|
||||
self.root_widget = root_widget
|
||||
|
||||
QTimer.singleShot(0, self._update_object_name)
|
||||
|
||||
@property
|
||||
def parent_id(self) -> str | None:
|
||||
try:
|
||||
if self.root_widget:
|
||||
return None
|
||||
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||
return connector_parent.gui_id if connector_parent else None
|
||||
except:
|
||||
logger.error(f"Error getting parent_id for {self.__class__.__name__}")
|
||||
|
||||
@SafeSlot()
|
||||
def _run_cleanup_on_deleted_parent(self) -> None:
|
||||
"""
|
||||
Run cleanup on the deleted parent.
|
||||
This method is called when the parent is deleted.
|
||||
"""
|
||||
if not hasattr(self, "cleanup"):
|
||||
return
|
||||
try:
|
||||
if not self._destroyed:
|
||||
self.cleanup()
|
||||
self._destroyed = True
|
||||
except Exception:
|
||||
content = traceback.format_exc()
|
||||
logger.info(
|
||||
"Failed to run cleanup on deleted parent. "
|
||||
f"This is not necessarily an error as the parent may be deleted before the child and includes already a cleanup. The following exception was raised:\n{content}"
|
||||
)
|
||||
|
||||
def _update_object_name(self) -> None:
|
||||
"""
|
||||
Enforce a unique object name among siblings and register the object for RPC.
|
||||
@@ -228,7 +272,7 @@ class BECConnector:
|
||||
if self.rpc_register.object_is_registered(self):
|
||||
self.rpc_register.broadcast()
|
||||
|
||||
def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker:
|
||||
def submit_task(self, fn, *args, on_complete: SafeSlot = None, **kwargs) -> Worker:
|
||||
"""
|
||||
Submit a task to run in a separate thread. The task will run the specified
|
||||
function with the provided arguments and emit the completed signal when done.
|
||||
@@ -356,7 +400,7 @@ class BECConnector:
|
||||
file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
|
||||
save_yaml(file_path, self._config_dict)
|
||||
|
||||
# @pyqtSlot(str)
|
||||
# @SafeSlot(str)
|
||||
def _set_gui_id(self, gui_id: str) -> None:
|
||||
"""
|
||||
Set the GUI ID for the widget.
|
||||
@@ -388,7 +432,7 @@ class BECConnector:
|
||||
self.client = client
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@pyqtSlot(ConnectionConfig) # TODO can be also dict
|
||||
@SafeSlot(ConnectionConfig) # TODO can be also dict
|
||||
def on_config_update(self, config: ConnectionConfig | dict) -> None:
|
||||
"""
|
||||
Update the configuration for the widget.
|
||||
|
||||
@@ -16,9 +16,9 @@ if PYSIDE6:
|
||||
from PySide6.scripts.pyside_tool import (
|
||||
_extend_path_var,
|
||||
init_virtual_env,
|
||||
qt_tool_wrapper,
|
||||
is_pyenv_python,
|
||||
is_virtual_env,
|
||||
qt_tool_wrapper,
|
||||
ui_tool_binary,
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ def list_editable_packages() -> set[str]:
|
||||
return editable_packages
|
||||
|
||||
|
||||
def patch_designer(): # pragma: no cover
|
||||
def patch_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
@@ -119,7 +119,7 @@ def patch_designer(): # pragma: no cover
|
||||
editable_packages = list_editable_packages()
|
||||
for pckg in editable_packages:
|
||||
_extend_path_var("PYTHONPATH", pckg, True)
|
||||
qt_tool_wrapper(ui_tool_binary("designer"), sys.argv[1:])
|
||||
qt_tool_wrapper(ui_tool_binary("designer"), cmd_args)
|
||||
|
||||
|
||||
def find_plugin_paths(base_path: Path):
|
||||
@@ -147,7 +147,7 @@ def set_plugin_environment_variable(plugin_paths):
|
||||
|
||||
|
||||
# Patch the designer function
|
||||
def main(): # pragma: no cover
|
||||
def open_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Exiting...")
|
||||
return
|
||||
@@ -160,7 +160,11 @@ def main(): # pragma: no cover
|
||||
|
||||
set_plugin_environment_variable(plugin_paths)
|
||||
|
||||
patch_designer()
|
||||
patch_designer(cmd_args)
|
||||
|
||||
|
||||
def main():
|
||||
open_designer(sys.argv[1:])
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
@@ -14,19 +14,31 @@ from bec_lib.service_config import ServiceConfig
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
from bec_widgets.utils.serialization import register_serializer_extension
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.endpoints import EndpointInfo
|
||||
|
||||
from bec_widgets.utils.rpc_server import RPCServer
|
||||
|
||||
|
||||
class QtThreadSafeCallback(QObject):
|
||||
"""QtThreadSafeCallback is a wrapper around a callback function to make it thread-safe for Qt."""
|
||||
|
||||
cb_signal = pyqtSignal(dict, dict)
|
||||
|
||||
def __init__(self, cb):
|
||||
def __init__(self, cb: Callable, cb_info: dict | None = None):
|
||||
"""
|
||||
Initialize the QtThreadSafeCallback.
|
||||
|
||||
Args:
|
||||
cb (Callable): The callback function to be wrapped.
|
||||
cb_info (dict, optional): Additional information about the callback. Defaults to None.
|
||||
"""
|
||||
super().__init__()
|
||||
self.cb_info = cb_info
|
||||
|
||||
self.cb = cb
|
||||
self.cb_signal.connect(self.cb)
|
||||
@@ -35,7 +47,7 @@ class QtThreadSafeCallback(QObject):
|
||||
# make 2 differents QtThreadSafeCallback to look
|
||||
# identical when used as dictionary keys, if the
|
||||
# callback is the same
|
||||
return id(self.cb)
|
||||
return f"{id(self.cb)}{self.cb_info}".__hash__()
|
||||
|
||||
def __call__(self, msg_content, metadata):
|
||||
self.cb_signal.emit(msg_content, metadata)
|
||||
@@ -120,6 +132,8 @@ class BECDispatcher:
|
||||
except redis.exceptions.ConnectionError:
|
||||
logger.warning("Could not connect to Redis, skipping start of BECClient.")
|
||||
|
||||
register_serializer_extension()
|
||||
|
||||
logger.success("Initialized BECDispatcher")
|
||||
|
||||
self.start_cli_server(gui_id=gui_id)
|
||||
@@ -137,6 +151,7 @@ class BECDispatcher:
|
||||
self,
|
||||
slot: Callable,
|
||||
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
|
||||
cb_info: dict | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Connect widget's qt slot, so that it is called on new pub/sub topic message.
|
||||
@@ -145,8 +160,9 @@ class BECDispatcher:
|
||||
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
||||
the corresponding pub/sub message
|
||||
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
|
||||
cb_info (dict | None): A dictionary containing information about the callback. Defaults to None.
|
||||
"""
|
||||
slot = QtThreadSafeCallback(slot)
|
||||
slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info)
|
||||
self.client.connector.register(topics, cb=slot, **kwargs)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._slots[slot].update(set(topics_str))
|
||||
|
||||
@@ -21,7 +21,7 @@ def _submodule_specs(module: ModuleType) -> tuple[ModuleSpec | None, ...]:
|
||||
|
||||
|
||||
def _loaded_submodules_from_specs(
|
||||
submodule_specs: tuple[ModuleSpec | None, ...]
|
||||
submodule_specs: tuple[ModuleSpec | None, ...],
|
||||
) -> Generator[ModuleType, None, None]:
|
||||
"""Load all submodules from the given specs."""
|
||||
for submodule in (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
""" This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
||||
"""This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
||||
Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is
|
||||
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
||||
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
||||
analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy
|
||||
will allow you to decide by yourself when to unblock and execute the callback again."""
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ class BECWidget(BECConnector):
|
||||
gui_id: str | None = None,
|
||||
theme_update: bool = False,
|
||||
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
|
||||
parent_id: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -55,12 +54,7 @@ class BECWidget(BECConnector):
|
||||
"""
|
||||
|
||||
super().__init__(
|
||||
client=client,
|
||||
config=config,
|
||||
gui_id=gui_id,
|
||||
parent_dock=parent_dock,
|
||||
parent_id=parent_id,
|
||||
**kwargs,
|
||||
client=client, config=config, gui_id=gui_id, parent_dock=parent_dock, **kwargs
|
||||
)
|
||||
if not isinstance(self, QObject):
|
||||
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
||||
@@ -112,6 +106,8 @@ class BECWidget(BECConnector):
|
||||
def closeEvent(self, event):
|
||||
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
||||
try:
|
||||
self.cleanup()
|
||||
if not self._destroyed:
|
||||
self.cleanup()
|
||||
self._destroyed = True
|
||||
finally:
|
||||
super().closeEvent(event) # pylint: disable=no-member
|
||||
|
||||
@@ -11,7 +11,7 @@ from pydantic_core import PydanticCustomError
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_qthemes._main import AccentColors
|
||||
|
||||
|
||||
|
||||
@@ -266,3 +266,5 @@ class CompactPopupWidget(QWidget):
|
||||
# to ensure proper resources cleanup
|
||||
for child in self.container.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
|
||||
child.close()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import Literal, Type
|
||||
from typing import Any, Type
|
||||
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.cli.client_utils import BECGuiClient
|
||||
|
||||
|
||||
class WidgetContainerUtils:
|
||||
@@ -73,3 +72,36 @@ class WidgetContainerUtils:
|
||||
return None
|
||||
else:
|
||||
raise ValueError(f"No widget of class {widget_class} found.")
|
||||
|
||||
@staticmethod
|
||||
def name_is_protected(name: str, container: Any = None) -> bool:
|
||||
"""
|
||||
Check if the name is not protected.
|
||||
|
||||
Args:
|
||||
name(str): The name to be checked.
|
||||
|
||||
Returns:
|
||||
bool: True if the name is not protected, False otherwise.
|
||||
"""
|
||||
if container is None:
|
||||
container = BECGuiClient
|
||||
gui_client_methods = set(filter(lambda x: not x.startswith("_"), dir(container)))
|
||||
return name in gui_client_methods
|
||||
|
||||
@staticmethod
|
||||
def raise_for_invalid_name(name: str, container: Any = None) -> None:
|
||||
"""
|
||||
Check if the name is valid. If not, raise a ValueError.
|
||||
|
||||
Args:
|
||||
name(str): The name to be checked.
|
||||
Raises:
|
||||
ValueError: If the name is not valid.
|
||||
"""
|
||||
if not WidgetContainerUtils.has_name_valid_chars(name):
|
||||
raise ValueError(
|
||||
f"Name '{name}' contains invalid characters. Only alphanumeric characters, underscores, and dashes are allowed."
|
||||
)
|
||||
if WidgetContainerUtils.name_is_protected(name, container):
|
||||
raise ValueError(f"Name '{name}' is protected. Please choose another name.")
|
||||
|
||||
@@ -96,23 +96,55 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
||||
|
||||
'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
|
||||
otherwise error display is left to the original exception hook
|
||||
'verify_sender' keyword argument can be passed with boolean value if the sender should be verified
|
||||
before executing the slot. If True, the slot will only execute if the sender is a QObject. This is
|
||||
useful to prevent function calls from already deleted objects.
|
||||
'raise_error' keyword argument can be passed with boolean value if the error should be raised
|
||||
after the error is displayed. This is useful to propagate the error to the caller but should be used
|
||||
with great care to avoid segfaults.
|
||||
|
||||
The keywords above are stored in a container which can be overridden by passing
|
||||
'_override_slot_params' keyword argument with a dictionary containing the keywords to override.
|
||||
This is useful to override the default behavior of the decorator for a specific function call.
|
||||
|
||||
"""
|
||||
popup_error = bool(slot_kwargs.pop("popup_error", False))
|
||||
_slot_params = {
|
||||
"popup_error": bool(slot_kwargs.pop("popup_error", False)),
|
||||
"verify_sender": bool(slot_kwargs.pop("verify_sender", False)),
|
||||
"raise_error": bool(slot_kwargs.pop("raise_error", False)),
|
||||
}
|
||||
|
||||
def error_managed(method):
|
||||
@Slot(*slot_args, **slot_kwargs)
|
||||
@functools.wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
_override_slot_params = kwargs.pop("_override_slot_params", {})
|
||||
_slot_params.update(_override_slot_params)
|
||||
try:
|
||||
if not _slot_params["verify_sender"] or len(args) == 0:
|
||||
return method(*args, **kwargs)
|
||||
|
||||
_instance = args[0]
|
||||
if not isinstance(_instance, QObject):
|
||||
return method(*args, **kwargs)
|
||||
sender = _instance.sender()
|
||||
if sender is None:
|
||||
logger.info(
|
||||
f"Sender is None for {method.__module__}.{method.__qualname__}, "
|
||||
"skipping method call."
|
||||
)
|
||||
return
|
||||
return method(*args, **kwargs)
|
||||
|
||||
except Exception:
|
||||
slot_name = f"{method.__module__}.{method.__qualname__}"
|
||||
error_msg = traceback.format_exc()
|
||||
if popup_error:
|
||||
ErrorPopupUtility().custom_exception_hook(
|
||||
*sys.exc_info(), popup_error=popup_error
|
||||
)
|
||||
if _slot_params["popup_error"]:
|
||||
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=True)
|
||||
logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
|
||||
if _slot_params["raise_error"]:
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class ExpandableGroupFrame(QFrame):
|
||||
self._title_layout.addWidget(self._expansion_button)
|
||||
self._title_layout.addWidget(self._title)
|
||||
|
||||
self._contents = QWidget()
|
||||
self._contents = QWidget(self)
|
||||
self._layout.addWidget(self._contents)
|
||||
|
||||
self._expansion_button.clicked.connect(self.switch_expanded_state)
|
||||
|
||||
182
bec_widgets/utils/forms_from_types/forms.py
Normal file
182
bec_widgets/utils/forms_from_types/forms.py
Normal file
@@ -0,0 +1,182 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from types import NoneType
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from qtpy.QtCore import Signal # type: ignore
|
||||
from qtpy.QtWidgets import QGridLayout, QLabel, QLayout, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils.forms_from_types.items import FormItemSpec, widget_from_type
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class TypedForm(BECWidget, QWidget):
|
||||
PLUGIN = True
|
||||
ICON_NAME = "list_alt"
|
||||
|
||||
value_changed = Signal()
|
||||
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
items: list[tuple[str, type]] | None = None,
|
||||
form_item_specs: list[FormItemSpec] | None = None,
|
||||
client=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Widget with a list of form items based on a list of types.
|
||||
|
||||
Args:
|
||||
items (list[tuple[str, type]]): list of tuples of a name for the field and its type.
|
||||
Should be a type supported by the logic in items.py
|
||||
form_item_specs (list[FormItemSpec]): list of form item specs, equivalent to items.
|
||||
only one of items or form_item_specs should be
|
||||
supplied.
|
||||
|
||||
"""
|
||||
if (items is not None and form_item_specs is not None) or (
|
||||
items is None and form_item_specs is None
|
||||
):
|
||||
raise ValueError("Must specify one and only one of items and form_item_specs")
|
||||
super().__init__(parent=parent, client=client, **kwargs)
|
||||
self._items = (
|
||||
form_item_specs
|
||||
if form_item_specs is not None
|
||||
else [
|
||||
FormItemSpec(name=name, item_type=item_type)
|
||||
for name, item_type in items # type: ignore
|
||||
]
|
||||
)
|
||||
self._layout = QVBoxLayout()
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self._layout)
|
||||
|
||||
self._form_grid_container = QWidget(parent=self)
|
||||
self._form_grid = QWidget(parent=self._form_grid_container)
|
||||
self._layout.addWidget(self._form_grid_container)
|
||||
self._form_grid_container.setLayout(QVBoxLayout())
|
||||
self._form_grid.setLayout(self._new_grid_layout())
|
||||
|
||||
self.populate()
|
||||
|
||||
def populate(self):
|
||||
self._clear_grid()
|
||||
for r, item in enumerate(self._items):
|
||||
self._add_griditem(item, r)
|
||||
|
||||
def _add_griditem(self, item: FormItemSpec, row: int):
|
||||
grid = self._form_grid.layout()
|
||||
label = QLabel(item.name)
|
||||
label.setProperty("_model_field_name", item.name)
|
||||
label.setToolTip(item.info.description or item.name)
|
||||
grid.addWidget(label, row, 0)
|
||||
widget = widget_from_type(item.item_type)(parent=self, spec=item)
|
||||
widget.valueChanged.connect(self.value_changed)
|
||||
grid.addWidget(widget, row, 1)
|
||||
|
||||
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
|
||||
grid: QGridLayout = self._form_grid.layout() # type: ignore
|
||||
return {
|
||||
grid.itemAtPosition(i, 0)
|
||||
.widget()
|
||||
.property("_model_field_name"): grid.itemAtPosition(i, 1)
|
||||
.widget()
|
||||
.getValue() # type: ignore # we only add 'DynamicFormItem's here
|
||||
for i in range(grid.rowCount())
|
||||
}
|
||||
|
||||
def _clear_grid(self):
|
||||
if (old_layout := self._form_grid.layout()) is not None:
|
||||
while old_layout.count():
|
||||
item = old_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
widget.deleteLater()
|
||||
old_layout.deleteLater()
|
||||
self._form_grid.deleteLater()
|
||||
self._form_grid = QWidget()
|
||||
|
||||
self._form_grid.setLayout(self._new_grid_layout())
|
||||
self._form_grid_container.layout().addWidget(self._form_grid)
|
||||
|
||||
self._form_grid.adjustSize()
|
||||
self._form_grid_container.adjustSize()
|
||||
self.adjustSize()
|
||||
|
||||
def _new_grid_layout(self):
|
||||
new_grid = QGridLayout()
|
||||
new_grid.setContentsMargins(0, 0, 0, 0)
|
||||
new_grid.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
||||
return new_grid
|
||||
|
||||
|
||||
class PydanticModelForm(TypedForm):
|
||||
metadata_updated = Signal(dict)
|
||||
metadata_cleared = Signal(NoneType)
|
||||
|
||||
def __init__(self, parent=None, metadata_model: type[BaseModel] = None, client=None, **kwargs):
|
||||
"""
|
||||
A form generated from a pydantic model.
|
||||
|
||||
Args:
|
||||
metadata_model (type[BaseModel]): the model class for which to generate a form.
|
||||
"""
|
||||
self._md_schema = metadata_model
|
||||
super().__init__(parent=parent, form_item_specs=self._form_item_specs(), client=client)
|
||||
|
||||
self._validity = CompactPopupWidget()
|
||||
self._validity.compact_view = True # type: ignore
|
||||
self._validity.label = "Metadata validity" # type: ignore
|
||||
self._validity.compact_show_popup.setIcon(
|
||||
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
|
||||
)
|
||||
self._validity_message = QLabel("Not yet validated")
|
||||
self._validity.addWidget(self._validity_message)
|
||||
self._layout.addWidget(self._validity)
|
||||
self.value_changed.connect(self.validate_form)
|
||||
|
||||
def set_schema(self, schema: type[BaseModel]):
|
||||
self._md_schema = schema
|
||||
self.populate()
|
||||
|
||||
def _form_item_specs(self):
|
||||
return [
|
||||
FormItemSpec(name=name, info=info, item_type=info.annotation)
|
||||
for name, info in self._md_schema.model_fields.items()
|
||||
]
|
||||
|
||||
def update_items_from_schema(self):
|
||||
self._items = self._form_item_specs()
|
||||
|
||||
def populate(self):
|
||||
self.update_items_from_schema()
|
||||
super().populate()
|
||||
|
||||
def get_form_data(self):
|
||||
"""Get the entered metadata as a dict."""
|
||||
return self._dict_from_grid()
|
||||
|
||||
def validate_form(self, *_) -> bool:
|
||||
"""validate the currently entered metadata against the pydantic schema.
|
||||
If successful, returns on metadata_emitted and returns true.
|
||||
Otherwise, emits on metadata_cleared and returns false."""
|
||||
try:
|
||||
metadata_dict = self.get_form_data()
|
||||
self._md_schema.model_validate(metadata_dict)
|
||||
self._validity.set_global_state("success")
|
||||
self._validity_message.setText("No errors!")
|
||||
self.metadata_updated.emit(metadata_dict)
|
||||
return True
|
||||
except ValidationError as e:
|
||||
self._validity.set_global_state("emergency")
|
||||
self._validity_message.setText(str(e))
|
||||
self.metadata_cleared.emit(None)
|
||||
return False
|
||||
@@ -2,11 +2,13 @@ from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING, Callable, get_args
|
||||
from types import UnionType
|
||||
from typing import Callable, Protocol
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic.fields import FieldInfo
|
||||
from qtpy.QtCore import Signal # type: ignore
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -33,12 +35,22 @@ from bec_widgets.widgets.editors.scan_metadata._util import (
|
||||
field_precision,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FormItemSpec(BaseModel):
|
||||
"""
|
||||
The specification for an item in a dynamically generated form. Uses a pydantic FieldInfo
|
||||
to store most annotation info, since one of the main purposes is to store data for
|
||||
forms genrated from pydantic models, but can also be composed from other sources or by hand.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
item_type: type | UnionType
|
||||
name: str
|
||||
info: FieldInfo = FieldInfo()
|
||||
|
||||
|
||||
class ClearableBoolEntry(QWidget):
|
||||
stateChanged = Signal()
|
||||
|
||||
@@ -82,21 +94,20 @@ class ClearableBoolEntry(QWidget):
|
||||
self._false.setToolTip(tooltip)
|
||||
|
||||
|
||||
class MetadataWidget(QWidget):
|
||||
|
||||
class DynamicFormItem(QWidget):
|
||||
valueChanged = Signal()
|
||||
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent)
|
||||
self._info = info
|
||||
self._spec = spec
|
||||
self._layout = QHBoxLayout()
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
|
||||
self._default = field_default(self._info)
|
||||
self._desc = self._info.description
|
||||
self._default = field_default(self._spec.info)
|
||||
self._desc = self._spec.info.description
|
||||
self.setLayout(self._layout)
|
||||
self._add_main_widget()
|
||||
if clearable_required(info):
|
||||
if clearable_required(spec.info):
|
||||
self._add_clear_button()
|
||||
|
||||
@abstractmethod
|
||||
@@ -127,15 +138,15 @@ class MetadataWidget(QWidget):
|
||||
self.valueChanged.emit()
|
||||
|
||||
|
||||
class StrMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
class StrMetadataField(DynamicFormItem):
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
self._main_widget.textChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
self._main_widget = QLineEdit()
|
||||
self._layout.addWidget(self._main_widget)
|
||||
min_length, max_length = field_minlen(self._info), field_maxlen(self._info)
|
||||
min_length, max_length = (field_minlen(self._spec.info), field_maxlen(self._spec.info))
|
||||
if max_length:
|
||||
self._main_widget.setMaxLength(max_length)
|
||||
self._main_widget.setToolTip(
|
||||
@@ -156,15 +167,15 @@ class StrMetadataField(MetadataWidget):
|
||||
self._main_widget.setText(value)
|
||||
|
||||
|
||||
class IntMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
class IntMetadataField(DynamicFormItem):
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
self._main_widget.textChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
self._main_widget = QSpinBox()
|
||||
self._layout.addWidget(self._main_widget)
|
||||
min_, max_ = field_limits(self._info, int)
|
||||
min_, max_ = field_limits(self._spec.info, int)
|
||||
self._main_widget.setMinimum(min_)
|
||||
self._main_widget.setMaximum(max_)
|
||||
self._main_widget.setToolTip(f"(range {min_} to {max_}){self._describe()}")
|
||||
@@ -185,18 +196,18 @@ class IntMetadataField(MetadataWidget):
|
||||
self._main_widget.setValue(value)
|
||||
|
||||
|
||||
class FloatDecimalMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
class FloatDecimalMetadataField(DynamicFormItem):
|
||||
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
self._main_widget.textChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
self._main_widget = QDoubleSpinBox()
|
||||
self._layout.addWidget(self._main_widget)
|
||||
min_, max_ = field_limits(self._info, int)
|
||||
min_, max_ = field_limits(self._spec.info, int)
|
||||
self._main_widget.setMinimum(min_)
|
||||
self._main_widget.setMaximum(max_)
|
||||
precision = field_precision(self._info)
|
||||
precision = field_precision(self._spec.info)
|
||||
if precision:
|
||||
self._main_widget.setDecimals(precision)
|
||||
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
|
||||
@@ -219,13 +230,13 @@ class FloatDecimalMetadataField(MetadataWidget):
|
||||
self._main_widget.setValue(value)
|
||||
|
||||
|
||||
class BoolMetadataField(MetadataWidget):
|
||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
||||
super().__init__(info, parent)
|
||||
class BoolMetadataField(DynamicFormItem):
|
||||
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
|
||||
super().__init__(parent=parent, spec=spec)
|
||||
self._main_widget.stateChanged.connect(self._value_changed)
|
||||
|
||||
def _add_main_widget(self) -> None:
|
||||
if clearable_required(self._info):
|
||||
if clearable_required(self._spec.info):
|
||||
self._main_widget = ClearableBoolEntry()
|
||||
else:
|
||||
self._main_widget = QCheckBox()
|
||||
@@ -240,7 +251,7 @@ class BoolMetadataField(MetadataWidget):
|
||||
self._main_widget.setChecked(value)
|
||||
|
||||
|
||||
def widget_from_type(annotation: type | None) -> Callable[[FieldInfo], MetadataWidget]:
|
||||
def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
|
||||
if annotation in [str, str | None]:
|
||||
return StrMetadataField
|
||||
if annotation in [int, int | None]:
|
||||
@@ -5,6 +5,8 @@ from typing import NamedTuple
|
||||
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||
|
||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
|
||||
|
||||
|
||||
@@ -22,7 +24,7 @@ class DesignerPluginInfo:
|
||||
def __init__(self, plugin_class):
|
||||
self.plugin_class = plugin_class
|
||||
self.plugin_name_pascal = plugin_class.__name__
|
||||
self.plugin_name_snake = self.pascal_to_snake(self.plugin_name_pascal)
|
||||
self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal)
|
||||
self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}"
|
||||
plugin_module = (
|
||||
".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin"
|
||||
@@ -38,21 +40,6 @@ class DesignerPluginInfo:
|
||||
|
||||
self.base_path = os.path.dirname(inspect.getfile(plugin_class))
|
||||
|
||||
@staticmethod
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""
|
||||
Convert PascalCase to snake_case.
|
||||
|
||||
Args:
|
||||
name (str): The name to be converted.
|
||||
|
||||
Returns:
|
||||
str: The converted name.
|
||||
"""
|
||||
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
||||
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
|
||||
return s2.lower()
|
||||
|
||||
|
||||
class DesignerPluginGenerator:
|
||||
def __init__(self, widget: type):
|
||||
@@ -122,6 +109,16 @@ class DesignerPluginGenerator:
|
||||
or bool(init_source.find("super().__init__(parent)") > 0)
|
||||
)
|
||||
|
||||
# for the new style classes, we only have one super call. We can therefore check if the
|
||||
# number of __init__ calls is 2 (the class itself and the super class)
|
||||
num_inits = re.findall(r"__init__", init_source)
|
||||
if len(num_inits) == 2 and not super_init_found:
|
||||
super_init_found = bool(
|
||||
init_source.find("super().__init__(parent=parent") > 0
|
||||
or init_source.find("super().__init__(parent,") > 0
|
||||
or init_source.find("super().__init__(parent)") > 0
|
||||
)
|
||||
|
||||
if not cls_init_found and not super_init_found:
|
||||
raise ValueError(
|
||||
f"Widget class {self.widget.__name__} must call the super constructor with parent."
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
""" Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
||||
The class is mainly designed for usage with the BECWaveform and 1D plots. """
|
||||
"""Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
||||
The class is mainly designed for usage with the BECWaveform and 1D plots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
16
bec_widgets/utils/name_utils.py
Normal file
16
bec_widgets/utils/name_utils.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import re
|
||||
|
||||
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""
|
||||
Convert PascalCase to snake_case.
|
||||
|
||||
Args:
|
||||
name (str): The name to be converted.
|
||||
|
||||
Returns:
|
||||
str: The converted name.
|
||||
"""
|
||||
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
||||
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
|
||||
return s2.lower()
|
||||
@@ -18,8 +18,9 @@ from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib import messages
|
||||
from qtpy.QtCore import QObject
|
||||
else:
|
||||
@@ -83,6 +84,7 @@ class RPCServer:
|
||||
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
||||
self._heartbeat_timer.start(200)
|
||||
self._registry_update_callbacks = []
|
||||
self._broadcasted_data = {}
|
||||
|
||||
self.status = messages.BECStatus.RUNNING
|
||||
logger.success(f"Server started with gui_id: {self.gui_id}")
|
||||
@@ -189,6 +191,9 @@ class RPCServer:
|
||||
if not getattr(val, "RPC", True):
|
||||
continue
|
||||
data[key] = self._serialize_bec_connector(val)
|
||||
if self._broadcasted_data == data:
|
||||
return
|
||||
self._broadcasted_data = data
|
||||
|
||||
logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
|
||||
self.client.connector.xadd(
|
||||
@@ -212,6 +217,15 @@ class RPCServer:
|
||||
config_dict = connector.config.model_dump()
|
||||
config_dict["parent_id"] = getattr(connector, "parent_id", None)
|
||||
|
||||
try:
|
||||
parent = connector.parent()
|
||||
if isinstance(parent, BECMainWindow):
|
||||
container_proxy = parent.gui_id
|
||||
else:
|
||||
container_proxy = None
|
||||
except Exception:
|
||||
container_proxy = None
|
||||
|
||||
if wait:
|
||||
while not self.rpc_register.object_is_registered(connector):
|
||||
QApplication.processEvents()
|
||||
@@ -225,6 +239,7 @@ class RPCServer:
|
||||
"object_name": connector.object_name or connector.__class__.__name__,
|
||||
"widget_class": widget_class,
|
||||
"config": config_dict,
|
||||
"container_proxy": container_proxy,
|
||||
"__rpc__": True,
|
||||
}
|
||||
|
||||
|
||||
44
bec_widgets/utils/serialization.py
Normal file
44
bec_widgets/utils/serialization.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from bec_lib.serialization import msgpack
|
||||
from qtpy.QtCore import QPointF
|
||||
|
||||
|
||||
def register_serializer_extension():
|
||||
"""
|
||||
Register the serializer extension for the BECConnector.
|
||||
"""
|
||||
if not module_is_registered("bec_widgets.utils.serialization"):
|
||||
msgpack.register_object_hook(encode_qpointf, decode_qpointf)
|
||||
|
||||
|
||||
def module_is_registered(module_name: str) -> bool:
|
||||
"""
|
||||
Check if the module is registered in the encoder.
|
||||
|
||||
Args:
|
||||
module_name (str): The name of the module to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the module is registered, False otherwise.
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
for enc in msgpack._encoder:
|
||||
if enc[0].__module__ == module_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def encode_qpointf(obj):
|
||||
"""
|
||||
Encode a QPointF object to a list of floats. As this is mostly used for sending
|
||||
data to the client, it is not necessary to convert it back to a QPointF object.
|
||||
"""
|
||||
if isinstance(obj, QPointF):
|
||||
return [obj.x(), obj.y()]
|
||||
return obj
|
||||
|
||||
|
||||
def decode_qpointf(obj):
|
||||
"""
|
||||
no-op function since QPointF is encoded as a list of floats.
|
||||
"""
|
||||
return obj
|
||||
@@ -59,7 +59,7 @@ class SidePanel(QWidget):
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="vertical")
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="vertical")
|
||||
|
||||
self.container = QWidget()
|
||||
self.container.layout = QVBoxLayout(self.container)
|
||||
@@ -89,7 +89,7 @@ class SidePanel(QWidget):
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
||||
|
||||
self.container = QWidget()
|
||||
self.container.layout = QVBoxLayout(self.container)
|
||||
|
||||
@@ -118,7 +118,7 @@ class IconAction(ToolBarAction):
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
icon = QIcon()
|
||||
icon.addFile(self.icon_path, size=QSize(20, 20))
|
||||
self.action = QAction(icon, self.tooltip, target)
|
||||
self.action = QAction(icon=icon, text=self.tooltip, parent=target)
|
||||
self.action.setCheckable(self.checkable)
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
@@ -128,7 +128,7 @@ class QtIconAction(ToolBarAction):
|
||||
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||
self.standard_icon = standard_icon
|
||||
self.icon = QApplication.style().standardIcon(standard_icon)
|
||||
self.action = QAction(self.icon, self.tooltip, parent)
|
||||
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
||||
self.action.setCheckable(self.checkable)
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
@@ -173,7 +173,7 @@ class MaterialIconAction(ToolBarAction):
|
||||
filled=self.filled,
|
||||
color=self.color,
|
||||
)
|
||||
self.action = QAction(self.icon, self.tooltip, parent=parent)
|
||||
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):
|
||||
@@ -212,12 +212,12 @@ class DeviceSelectionAction(ToolBarAction):
|
||||
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
widget = QWidget()
|
||||
widget = QWidget(parent=target)
|
||||
layout = QHBoxLayout(widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
if self.label is not None:
|
||||
label = QLabel(f"{self.label}")
|
||||
label = QLabel(text=f"{self.label}", parent=target)
|
||||
layout.addWidget(label)
|
||||
if self.device_combobox is not None:
|
||||
layout.addWidget(self.device_combobox)
|
||||
@@ -280,7 +280,9 @@ class SwitchableToolBarAction(ToolBarAction):
|
||||
self.main_button.clicked.connect(self._trigger_current_action)
|
||||
menu = QMenu(self.main_button)
|
||||
for key, action_obj in self.actions.items():
|
||||
menu_action = QAction(action_obj.get_icon(), action_obj.tooltip, self.main_button)
|
||||
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)
|
||||
@@ -369,13 +371,13 @@ class WidgetAction(ToolBarAction):
|
||||
toolbar (QToolBar): The toolbar to add the widget to.
|
||||
target (QWidget): The target widget for the action.
|
||||
"""
|
||||
self.container = QWidget()
|
||||
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(f"{self.label}")
|
||||
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)
|
||||
@@ -437,7 +439,7 @@ class ExpandableMenuAction(ToolBarAction):
|
||||
)
|
||||
menu = QMenu(button)
|
||||
for action_id, action in self.actions.items():
|
||||
sub_action = QAction(action.tooltip, target)
|
||||
sub_action = QAction(text=action.tooltip, parent=target)
|
||||
sub_action.setIconVisibleInMenu(True)
|
||||
if action.icon_path:
|
||||
icon = QIcon()
|
||||
@@ -521,7 +523,7 @@ class ModularToolBar(QToolBar):
|
||||
orientation: Literal["horizontal", "vertical"] = "horizontal",
|
||||
background_color: str = "rgba(0, 0, 0, 0)",
|
||||
):
|
||||
super().__init__(parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.widgets = defaultdict(dict)
|
||||
self.background_color = background_color
|
||||
|
||||
@@ -12,10 +12,11 @@ from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
||||
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
@@ -130,7 +131,6 @@ class BECDock(BECWidget, Dock):
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
parent_dock_area: BECDockArea | None = None,
|
||||
parent_id: str | None = None,
|
||||
config: DockConfig | None = None,
|
||||
name: str | None = None,
|
||||
object_name: str | None = None,
|
||||
@@ -279,6 +279,7 @@ class BECDock(BECWidget, Dock):
|
||||
widgets.extend(dock.elements.keys())
|
||||
return widgets
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def new(
|
||||
self,
|
||||
widget: BECWidget | str,
|
||||
@@ -301,6 +302,9 @@ class BECDock(BECWidget, Dock):
|
||||
colspan(int): The number of columns the widget should span.
|
||||
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
||||
"""
|
||||
if name is not None:
|
||||
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
|
||||
|
||||
if row is None:
|
||||
row = self.layout.rowCount()
|
||||
|
||||
@@ -316,11 +320,7 @@ class BECDock(BECWidget, Dock):
|
||||
widget = cast(
|
||||
BECWidget,
|
||||
widget_handler.create_widget(
|
||||
widget_type=widget,
|
||||
object_name=name,
|
||||
parent_dock=self,
|
||||
parent_id=self.gui_id,
|
||||
parent=self,
|
||||
widget_type=widget, object_name=name, parent_dock=self, parent=self
|
||||
),
|
||||
)
|
||||
else:
|
||||
@@ -416,6 +416,7 @@ class BECDock(BECWidget, Dock):
|
||||
self.delete_all()
|
||||
self.widgets.clear()
|
||||
super().cleanup()
|
||||
self.deleteLater()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
|
||||
@@ -14,6 +14,7 @@ from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||
from bec_widgets.utils.toolbar import (
|
||||
ExpandableMenuAction,
|
||||
MaterialIconAction,
|
||||
@@ -101,8 +102,8 @@ class BECDockArea(BECWidget, QWidget):
|
||||
|
||||
self._instructions_visible = True
|
||||
|
||||
self.dark_mode_button = DarkModeButton(parent=self, parent_id=self.gui_id, toolbar=True)
|
||||
self.dock_area = DockArea()
|
||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||
self.dock_area = DockArea(parent=self)
|
||||
self.toolbar = ModularToolBar(
|
||||
parent=self,
|
||||
actions={
|
||||
@@ -181,7 +182,7 @@ class BECDockArea(BECWidget, QWidget):
|
||||
|
||||
self.layout.addWidget(self.toolbar)
|
||||
self.layout.addWidget(self.dock_area)
|
||||
self.spacer = QWidget()
|
||||
self.spacer = QWidget(parent=self)
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar.addWidget(self.spacer)
|
||||
self.toolbar.addWidget(self.dark_mode_button)
|
||||
@@ -242,7 +243,8 @@ class BECDockArea(BECWidget, QWidget):
|
||||
def _create_widget_from_toolbar(self, widget_name: str) -> None:
|
||||
# Run with RPC broadcast to namespace of all widgets
|
||||
with RPCRegister.delayed_broadcast():
|
||||
dock_name = WidgetContainerUtils.generate_unique_name(widget_name, self.panels.keys())
|
||||
name = pascal_to_snake(widget_name)
|
||||
dock_name = WidgetContainerUtils.generate_unique_name(name, self.panels.keys())
|
||||
self.new(name=dock_name, widget=widget_name)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions
|
||||
@@ -306,6 +308,8 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"""
|
||||
if state is None:
|
||||
state = self.config.docks_state
|
||||
if state is None:
|
||||
return
|
||||
self.dock_area.restoreState(state, missing=missing, extra=extra)
|
||||
|
||||
@SafeSlot()
|
||||
@@ -362,6 +366,8 @@ class BECDockArea(BECWidget, QWidget):
|
||||
f"Name {name} must be unique for docks, but already exists in DockArea "
|
||||
f"with name: {self.object_name} and id {self.gui_id}."
|
||||
)
|
||||
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
|
||||
|
||||
else: # Name is not provided
|
||||
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
|
||||
|
||||
@@ -370,7 +376,6 @@ class BECDockArea(BECWidget, QWidget):
|
||||
name=name, # this is dock name pyqtgraph property, this is displayed on label
|
||||
object_name=name, # this is a real qt object name passed to BECConnector
|
||||
parent_dock_area=self,
|
||||
parent_id=self.gui_id,
|
||||
closable=closable,
|
||||
)
|
||||
dock.config.position = position
|
||||
@@ -440,12 +445,8 @@ class BECDockArea(BECWidget, QWidget):
|
||||
Cleanup the dock area.
|
||||
"""
|
||||
self.delete_all()
|
||||
self.toolbar.close()
|
||||
self.toolbar.deleteLater()
|
||||
self.dark_mode_button.close()
|
||||
self.dark_mode_button.deleteLater()
|
||||
self.dock_area.close()
|
||||
self.dock_area.deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
def show(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
from typing import Dict, Literal, Optional, Set, Tuple, Union
|
||||
|
||||
@@ -11,7 +11,7 @@ class AbortButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "cancel"
|
||||
RPC = False
|
||||
RPC = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -11,7 +11,7 @@ class ResetButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "restart_alt"
|
||||
RPC = False
|
||||
RPC = True
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
@@ -11,7 +11,7 @@ class ResumeButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "resume"
|
||||
RPC = False
|
||||
RPC = True
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
@@ -11,7 +11,7 @@ class StopButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "dangerous"
|
||||
RPC = False
|
||||
RPC = True
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
@@ -54,9 +54,20 @@ class StopButton(BECWidget, QWidget):
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = StopButton()
|
||||
w.show()
|
||||
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setLayout(QVBoxLayout())
|
||||
# Create and add the StopButton to the layout
|
||||
self.stop_button = StopButton()
|
||||
self.layout().addWidget(self.stop_button)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
app = QApplication([])
|
||||
my_gui = MyGui()
|
||||
my_gui.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -12,7 +12,16 @@ class PositionIndicator(BECWidget, QWidget):
|
||||
Display a position within a defined range, e.g. motor limits.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["set_value", "set_range", "vertical", "indicator_width", "rounded_corners"]
|
||||
USER_ACCESS = [
|
||||
"set_value",
|
||||
"set_range",
|
||||
"vertical",
|
||||
"vertical.setter",
|
||||
"indicator_width",
|
||||
"indicator_width.setter",
|
||||
"rounded_corners",
|
||||
"rounded_corners.setter",
|
||||
]
|
||||
PLUGIN = True
|
||||
ICON_NAME = "horizontal_distribute"
|
||||
|
||||
@@ -209,6 +218,12 @@ class PositionIndicator(BECWidget, QWidget):
|
||||
@Slot(int)
|
||||
@Slot(float)
|
||||
def set_value(self, position: float):
|
||||
"""
|
||||
Set the position of the indicator
|
||||
|
||||
Args:
|
||||
position: The new position of the indicator
|
||||
"""
|
||||
self.position = position
|
||||
self.update()
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class PositionerBox(PositionerBoxBase):
|
||||
dimensions = (234, 224)
|
||||
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
|
||||
USER_ACCESS = ["set_positioner"]
|
||||
device_changed = Signal(str, str)
|
||||
|
||||
@@ -33,6 +33,7 @@ class PositionerBox2D(PositionerBoxBase):
|
||||
ui_file = "positioner_box_2d.ui"
|
||||
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
|
||||
|
||||
device_changed_hor = Signal(str, str)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Module for a PositionerGroup widget to control a positioner device."""
|
||||
"""Module for a PositionerGroup widget to control a positioner device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class DeviceInputBase(BECWidget):
|
||||
ReadoutPriority.ON_REQUEST: "readout_on_request",
|
||||
}
|
||||
|
||||
def __init__(self, client=None, config=None, gui_id: str | None = None, **kwargs):
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id: str | None = None, **kwargs):
|
||||
|
||||
if config is None:
|
||||
config = DeviceInputConfig(widget_class=self.__class__.__name__)
|
||||
@@ -90,7 +90,9 @@ class DeviceInputBase(BECWidget):
|
||||
if isinstance(config, dict):
|
||||
config = DeviceInputConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs)
|
||||
super().__init__(
|
||||
parent=parent, client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs
|
||||
)
|
||||
self.get_bec_shortcuts()
|
||||
self._device_filter = []
|
||||
self._readout_filter = []
|
||||
|
||||
@@ -64,7 +64,6 @@ class ScanControl(BECWidget, QWidget):
|
||||
default_scan: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
if config is None:
|
||||
config = ScanControlConfig(
|
||||
widget_class=self.__class__.__name__, allowed_scans=allowed_scans
|
||||
@@ -166,7 +165,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.layout.addStretch()
|
||||
|
||||
def _add_metadata_form(self):
|
||||
self._metadata_form = ScanMetadata()
|
||||
self._metadata_form = ScanMetadata(parent=self)
|
||||
self.layout.addWidget(self._metadata_form)
|
||||
self._metadata_form.update_with_new_scan(self.comboBox_scan_selection.currentText())
|
||||
self.scan_selected.connect(self._metadata_form.update_with_new_scan)
|
||||
|
||||
@@ -234,7 +234,7 @@ class ScanGroupBox(QGroupBox):
|
||||
continue
|
||||
if default == "_empty":
|
||||
default = None
|
||||
widget = widget_class(arg_name=arg_name, default=default)
|
||||
widget = widget_class(parent=self.parent(), arg_name=arg_name, default=default)
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
widget.set_device_filter(BECDeviceFilter.DEVICE)
|
||||
self.selected_devices[widget] = ""
|
||||
@@ -274,12 +274,24 @@ class ScanGroupBox(QGroupBox):
|
||||
for widget in self.widgets[-len(self.inputs) :]:
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
self.selected_devices[widget] = ""
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
self.widgets = self.widgets[: -len(self.inputs)]
|
||||
|
||||
selected_devices_str = " ".join(self.selected_devices.values())
|
||||
self.device_selected.emit(selected_devices_str.strip())
|
||||
|
||||
def remove_all_widget_bundles(self):
|
||||
"""Remove every widget bundle from the scan control layout."""
|
||||
for widget in list(self.widgets):
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
self.selected_devices.pop(widget, None)
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
self.layout.removeWidget(widget)
|
||||
self.widgets.clear()
|
||||
self.device_selected.emit("")
|
||||
|
||||
@Property(bool)
|
||||
def hide_add_remove_buttons(self):
|
||||
return self._hide_add_remove_buttons
|
||||
@@ -348,10 +360,21 @@ class ScanGroupBox(QGroupBox):
|
||||
self._set_kwarg_parameters(parameters)
|
||||
|
||||
def _set_arg_parameters(self, parameters: list):
|
||||
while len(parameters) != len(self.widgets):
|
||||
self.add_widget_bundle()
|
||||
for i, parameter in enumerate(parameters):
|
||||
WidgetIO.set_value(self.widgets[i], parameter)
|
||||
self.remove_all_widget_bundles()
|
||||
if not parameters:
|
||||
return
|
||||
|
||||
inputs_per_bundle = len(self.inputs)
|
||||
if inputs_per_bundle == 0:
|
||||
return
|
||||
|
||||
bundles_needed = -(-len(parameters) // inputs_per_bundle)
|
||||
|
||||
for row in range(1, bundles_needed + 1):
|
||||
self.add_input_widgets(self.inputs, row)
|
||||
|
||||
for i, value in enumerate(parameters):
|
||||
WidgetIO.set_value(self.widgets[i], value)
|
||||
|
||||
def _set_kwarg_parameters(self, parameters: dict):
|
||||
for widget in self.widgets:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Module for DapComboBox widget class to select a DAP model from a combobox. """
|
||||
"""Module for DapComboBox widget class to select a DAP model from a combobox."""
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
BECConsole is a Qt widget that runs a Bash shell.
|
||||
BECConsole is a Qt widget that runs a Bash shell.
|
||||
|
||||
BECConsole VT100 emulation is powered by Pyte,
|
||||
(https://github.com/selectel/pyte).
|
||||
@@ -56,12 +56,12 @@ control_keys_mapping = {
|
||||
QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell)
|
||||
QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace)
|
||||
QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab)
|
||||
QtCore.Qt.Key_J: b"\x0A", # Ctrl-J (Line Feed)
|
||||
QtCore.Qt.Key_K: b"\x0B", # Ctrl-K (Vertical Tab)
|
||||
QtCore.Qt.Key_L: b"\x0C", # Ctrl-L (Form Feed)
|
||||
QtCore.Qt.Key_M: b"\x0D", # Ctrl-M (Carriage Return)
|
||||
QtCore.Qt.Key_N: b"\x0E", # Ctrl-N
|
||||
QtCore.Qt.Key_O: b"\x0F", # Ctrl-O
|
||||
QtCore.Qt.Key_J: b"\x0a", # Ctrl-J (Line Feed)
|
||||
QtCore.Qt.Key_K: b"\x0b", # Ctrl-K (Vertical Tab)
|
||||
QtCore.Qt.Key_L: b"\x0c", # Ctrl-L (Form Feed)
|
||||
QtCore.Qt.Key_M: b"\x0d", # Ctrl-M (Carriage Return)
|
||||
QtCore.Qt.Key_N: b"\x0e", # Ctrl-N
|
||||
QtCore.Qt.Key_O: b"\x0f", # Ctrl-O
|
||||
QtCore.Qt.Key_P: b"\x10", # Ctrl-P
|
||||
QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q
|
||||
QtCore.Qt.Key_R: b"\x12", # Ctrl-R
|
||||
@@ -72,10 +72,10 @@ control_keys_mapping = {
|
||||
QtCore.Qt.Key_W: b"\x17", # Ctrl-W
|
||||
QtCore.Qt.Key_X: b"\x18", # Ctrl-X
|
||||
QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y
|
||||
QtCore.Qt.Key_Z: b"\x1A", # Ctrl-Z
|
||||
QtCore.Qt.Key_Escape: b"\x1B", # Ctrl-Escape
|
||||
QtCore.Qt.Key_Backslash: b"\x1C", # Ctrl-\
|
||||
QtCore.Qt.Key_Underscore: b"\x1F", # Ctrl-_
|
||||
QtCore.Qt.Key_Z: b"\x1a", # Ctrl-Z
|
||||
QtCore.Qt.Key_Escape: b"\x1b", # Ctrl-Escape
|
||||
QtCore.Qt.Key_Backslash: b"\x1c", # Ctrl-\
|
||||
QtCore.Qt.Key_Underscore: b"\x1f", # Ctrl-_
|
||||
}
|
||||
|
||||
normal_keys_mapping = {
|
||||
@@ -89,7 +89,7 @@ normal_keys_mapping = {
|
||||
QtCore.Qt.Key_Left: b"\x02",
|
||||
QtCore.Qt.Key_Up: b"\x10",
|
||||
QtCore.Qt.Key_Right: b"\x06",
|
||||
QtCore.Qt.Key_Down: b"\x0E",
|
||||
QtCore.Qt.Key_Down: b"\x0e",
|
||||
QtCore.Qt.Key_PageUp: b"\x49",
|
||||
QtCore.Qt.Key_PageDown: b"\x51",
|
||||
QtCore.Qt.Key_F1: b"\x1b\x31",
|
||||
|
||||
@@ -16,12 +16,20 @@ from qtpy.QtWidgets import (
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
|
||||
class AdditionalMetadataTableModel(QAbstractTableModel):
|
||||
class DictBackedTableModel(QAbstractTableModel):
|
||||
def __init__(self, data):
|
||||
"""A model to go with DictBackedTable, which represents key-value pairs
|
||||
to be displayed in a TreeWidget.
|
||||
|
||||
Args:
|
||||
data (list[list[str]]): list of key-value pairs to initialise with"""
|
||||
super().__init__()
|
||||
self._data: list[list[str]] = data
|
||||
self._disallowed_keys: list[str] = []
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
# see QAbstractTableModel documentation for these methods
|
||||
|
||||
def headerData(
|
||||
self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole()
|
||||
) -> Any:
|
||||
@@ -49,6 +57,10 @@ class AdditionalMetadataTableModel(QAbstractTableModel):
|
||||
return False
|
||||
|
||||
def update_disallowed_keys(self, keys: list[str]):
|
||||
"""Set the list of keys which may not be used.
|
||||
|
||||
Args:
|
||||
keys (list[str]): list of keys which are forbidden."""
|
||||
self._disallowed_keys = keys
|
||||
for i, item in enumerate(self._data):
|
||||
if item[0] in self._disallowed_keys:
|
||||
@@ -95,16 +107,21 @@ class AdditionalMetadataTableModel(QAbstractTableModel):
|
||||
return dict(self._data)
|
||||
|
||||
|
||||
class AdditionalMetadataTable(QWidget):
|
||||
|
||||
class DictBackedTable(QWidget):
|
||||
delete_rows = Signal(list)
|
||||
|
||||
def __init__(self, initial_data: list[list[str]]):
|
||||
"""Widget which uses a DictBackedTableModel to display an editable table
|
||||
which can be extracted as a dict.
|
||||
|
||||
Args:
|
||||
initial_data (list[list[str]]): list of key-value pairs to initialise with
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self._layout = QHBoxLayout()
|
||||
self.setLayout(self._layout)
|
||||
self._table_model = AdditionalMetadataTableModel(initial_data)
|
||||
self._table_model = DictBackedTableModel(initial_data)
|
||||
self._table_view = QTreeView()
|
||||
self._table_view.setModel(self._table_model)
|
||||
self._table_view.setSizePolicy(
|
||||
@@ -126,15 +143,21 @@ class AdditionalMetadataTable(QWidget):
|
||||
self.delete_rows.connect(self._table_model.delete_rows)
|
||||
|
||||
def delete_selected_rows(self):
|
||||
"""Delete rows which are part of the selection model"""
|
||||
cells: list[QModelIndex] = self._table_view.selectionModel().selectedIndexes()
|
||||
row_indices = list({r.row() for r in cells})
|
||||
if row_indices:
|
||||
self.delete_rows.emit(row_indices)
|
||||
|
||||
def dump_dict(self):
|
||||
"""Get the current content of the table as a dict"""
|
||||
return self._table_model.dump_dict()
|
||||
|
||||
def update_disallowed_keys(self, keys: list[str]):
|
||||
"""Set the list of keys which may not be used.
|
||||
|
||||
Args:
|
||||
keys (list[str]): list of keys which are forbidden."""
|
||||
self._table_model.update_disallowed_keys(keys)
|
||||
|
||||
|
||||
@@ -144,6 +167,6 @@ if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
|
||||
window = AdditionalMetadataTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
||||
window = DictBackedTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
||||
window.show()
|
||||
app.exec()
|
||||
@@ -1,7 +0,0 @@
|
||||
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
||||
AdditionalMetadataTable,
|
||||
AdditionalMetadataTableModel,
|
||||
)
|
||||
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
||||
|
||||
__all__ = ["ScanMetadata", "AdditionalMetadataTable", "AdditionalMetadataTableModel"]
|
||||
|
||||
@@ -9,7 +9,7 @@ from annotated_types import Ge, Gt, Le, Lt
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic_core import PydanticUndefined
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -1,52 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from types import NoneType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.metadata_schema import get_metadata_schema_for_scan
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import Field, ValidationError
|
||||
from qtpy.QtCore import Signal # type: ignore
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from pydantic import Field
|
||||
from qtpy.QtWidgets import QApplication, QComboBox, QHBoxLayout, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
||||
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import widget_from_type
|
||||
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
||||
AdditionalMetadataTable,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.fields import FieldInfo
|
||||
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
|
||||
from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ScanMetadata(BECWidget, QWidget):
|
||||
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
|
||||
metadata schema registry supplied in the plugin repo to find pydantic models
|
||||
associated with the scan type. Sets limits for numerical values if specified."""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "list_alt"
|
||||
|
||||
metadata_updated = Signal(dict)
|
||||
metadata_cleared = Signal(NoneType)
|
||||
RPC = False
|
||||
|
||||
class ScanMetadata(PydanticModelForm):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
@@ -55,117 +24,35 @@ class ScanMetadata(BECWidget, QWidget):
|
||||
initial_extras: list[list[str]] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(parent=parent, client=client, **kwargs)
|
||||
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
|
||||
metadata schema registry supplied in the plugin repo to find pydantic models
|
||||
associated with the scan type. Sets limits for numerical values if specified.
|
||||
|
||||
self.set_schema(scan_name)
|
||||
|
||||
self._layout = QVBoxLayout()
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self._layout)
|
||||
|
||||
self._required_md_box = ExpandableGroupFrame("Scan schema metadata")
|
||||
self._layout.addWidget(self._required_md_box)
|
||||
self._required_md_box_layout = QHBoxLayout()
|
||||
self._required_md_box.set_layout(self._required_md_box_layout)
|
||||
|
||||
self._md_grid = QWidget()
|
||||
self._required_md_box_layout.addWidget(self._md_grid)
|
||||
self._grid_container = QVBoxLayout()
|
||||
self._md_grid.setLayout(self._grid_container)
|
||||
self._new_grid_layout()
|
||||
self._grid_container.addLayout(self._md_grid_layout)
|
||||
Args:
|
||||
scan_name (str): The scan for which to generate a metadata form
|
||||
Initial_extras (list[list[str]]): Initial data with which to populate the additional
|
||||
metadata table - inner lists should be key-value pairs
|
||||
"""
|
||||
|
||||
# self.populate() gets called in super().__init__
|
||||
# so make sure self._additional_metadata exists
|
||||
self._additional_md_box = ExpandableGroupFrame("Additional metadata", expanded=False)
|
||||
self._layout.addWidget(self._additional_md_box)
|
||||
self._additional_md_box_layout = QHBoxLayout()
|
||||
self._additional_md_box.set_layout(self._additional_md_box_layout)
|
||||
|
||||
self._additional_metadata = AdditionalMetadataTable(initial_extras or [])
|
||||
self._additional_md_box_layout.addWidget(self._additional_metadata)
|
||||
|
||||
self._validity = CompactPopupWidget()
|
||||
self._validity.compact_view = True # type: ignore
|
||||
self._validity.label = "Metadata validity" # type: ignore
|
||||
self._validity.compact_show_popup.setIcon(
|
||||
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
|
||||
)
|
||||
self._validity_message = QLabel("Not yet validated")
|
||||
self._validity.addWidget(self._validity_message)
|
||||
self._layout.addWidget(self._validity)
|
||||
|
||||
self.populate()
|
||||
|
||||
@SafeSlot(str)
|
||||
def update_with_new_scan(self, scan_name: str):
|
||||
self.set_schema(scan_name)
|
||||
self.populate()
|
||||
self.validate_form()
|
||||
|
||||
def validate_form(self, *_) -> bool:
|
||||
"""validate the currently entered metadata against the pydantic schema.
|
||||
If successful, returns on metadata_emitted and returns true.
|
||||
Otherwise, emits on metadata_cleared and returns false."""
|
||||
try:
|
||||
metadata_dict = self.get_full_model_dict()
|
||||
self._md_schema.model_validate(metadata_dict)
|
||||
self._validity.set_global_state("success")
|
||||
self._validity_message.setText("No errors!")
|
||||
self.metadata_updated.emit(metadata_dict)
|
||||
except ValidationError as e:
|
||||
self._validity.set_global_state("emergency")
|
||||
self._validity_message.setText(str(e))
|
||||
self.metadata_cleared.emit(None)
|
||||
|
||||
def get_full_model_dict(self):
|
||||
"""Get the entered metadata as a dict"""
|
||||
return self._additional_metadata.dump_dict() | self._dict_from_grid()
|
||||
|
||||
def set_schema(self, scan_name: str | None = None):
|
||||
self._additional_metadata = DictBackedTable(initial_extras or [])
|
||||
self._scan_name = scan_name or ""
|
||||
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
|
||||
|
||||
def populate(self):
|
||||
self._clear_grid()
|
||||
self._populate()
|
||||
super().__init__(parent=parent, metadata_model=self._md_schema, client=client, **kwargs)
|
||||
|
||||
def _populate(self):
|
||||
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
|
||||
for i, (field_name, info) in enumerate(self._md_schema.model_fields.items()):
|
||||
self._add_griditem(field_name, info, i)
|
||||
self._layout.addWidget(self._additional_md_box)
|
||||
self._additional_md_box_layout.addWidget(self._additional_metadata)
|
||||
|
||||
def _add_griditem(self, field_name: str, info: FieldInfo, row: int):
|
||||
grid = self._md_grid_layout
|
||||
label = QLabel(info.title or field_name)
|
||||
label.setProperty("_model_field_name", field_name)
|
||||
label.setToolTip(info.description or field_name)
|
||||
grid.addWidget(label, row, 0)
|
||||
widget = widget_from_type(info.annotation)(info)
|
||||
widget.valueChanged.connect(self.validate_form)
|
||||
grid.addWidget(widget, row, 1)
|
||||
|
||||
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
|
||||
grid = self._md_grid_layout
|
||||
return {
|
||||
grid.itemAtPosition(i, 0).widget().property("_model_field_name"): grid.itemAtPosition(i, 1).widget().getValue() # type: ignore # we only add 'MetadataWidget's here
|
||||
for i in range(grid.rowCount())
|
||||
}
|
||||
|
||||
def _clear_grid(self):
|
||||
while self._md_grid_layout.count():
|
||||
item = self._md_grid_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
widget.deleteLater()
|
||||
self._md_grid_layout.deleteLater()
|
||||
self._new_grid_layout()
|
||||
self._grid_container.addLayout(self._md_grid_layout)
|
||||
self._md_grid.adjustSize()
|
||||
self.adjustSize()
|
||||
|
||||
def _new_grid_layout(self):
|
||||
self._md_grid_layout = QGridLayout()
|
||||
self._md_grid_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._md_grid_layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
||||
@SafeSlot(str)
|
||||
def update_with_new_scan(self, scan_name: str):
|
||||
self.set_schema_from_scan(scan_name)
|
||||
self.validate_form()
|
||||
|
||||
@SafeProperty(bool)
|
||||
def hide_optional_metadata(self): # type: ignore
|
||||
@@ -181,8 +68,25 @@ class ScanMetadata(BECWidget, QWidget):
|
||||
"""
|
||||
self._additional_md_box.setVisible(not hide)
|
||||
|
||||
def get_form_data(self):
|
||||
"""Get the entered metadata as a dict"""
|
||||
return self._additional_metadata.dump_dict() | self._dict_from_grid()
|
||||
|
||||
def populate(self):
|
||||
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
|
||||
super().populate()
|
||||
|
||||
def set_schema_from_scan(self, scan_name: str | None):
|
||||
self._scan_name = scan_name or ""
|
||||
self.set_schema(get_metadata_schema_for_scan(self._scan_name))
|
||||
self.populate()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=redefined-outer-name
|
||||
# pylint: disable=protected-access
|
||||
# pylint: disable=disallowed-name
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from bec_lib.metadata_schema import BasicScanMetadata
|
||||
@@ -213,7 +117,6 @@ if __name__ == "__main__": # pragma: no cover
|
||||
"bec_lib.metadata_schema._get_metadata_schema_registry",
|
||||
lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3},
|
||||
):
|
||||
|
||||
app = QApplication([])
|
||||
w = QWidget()
|
||||
selection = QComboBox()
|
||||
|
||||
@@ -117,6 +117,7 @@ class WebsiteWidget(BECWidget, QWidget):
|
||||
Cleanup the widget
|
||||
"""
|
||||
self.website.page().deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -144,7 +144,7 @@ class Minesweeper(BECWidget, QWidget):
|
||||
PLUGIN = True
|
||||
ICON_NAME = "videogame_asset"
|
||||
USER_ACCESS = []
|
||||
RPC = False
|
||||
RPC = True
|
||||
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
@@ -27,7 +27,7 @@ logger = bec_logger.logger
|
||||
# noinspection PyDataclass
|
||||
class ImageConfig(ConnectionConfig):
|
||||
color_map: str = Field(
|
||||
"magma", description="The colormap of the figure widget.", validate_default=True
|
||||
"plasma", description="The colormap of the figure widget.", validate_default=True
|
||||
)
|
||||
color_bar: Literal["full", "simple"] | None = Field(
|
||||
None, description="The type of the color bar."
|
||||
@@ -79,12 +79,6 @@ class Image(PlotBase):
|
||||
"auto_range_x.setter",
|
||||
"auto_range_y",
|
||||
"auto_range_y.setter",
|
||||
"x_log",
|
||||
"x_log.setter",
|
||||
"y_log",
|
||||
"y_log.setter",
|
||||
"legend_label_size",
|
||||
"legend_label_size.setter",
|
||||
# ImageView Specific Settings
|
||||
"color_map",
|
||||
"color_map.setter",
|
||||
@@ -111,8 +105,8 @@ class Image(PlotBase):
|
||||
"fft.setter",
|
||||
"log",
|
||||
"log.setter",
|
||||
"rotation",
|
||||
"rotation.setter",
|
||||
"num_rotation_90",
|
||||
"num_rotation_90.setter",
|
||||
"transpose",
|
||||
"transpose.setter",
|
||||
"image",
|
||||
@@ -137,13 +131,13 @@ class Image(PlotBase):
|
||||
super().__init__(
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
self._main_image = ImageItem(parent_image=self, parent_id=self.gui_id)
|
||||
self._main_image = ImageItem(parent_image=self)
|
||||
|
||||
self.plot_item.addItem(self._main_image)
|
||||
self.scan_id = None
|
||||
|
||||
# Default Color map to magma
|
||||
self.color_map = "magma"
|
||||
# Default Color map to plasma
|
||||
self.color_map = "plasma"
|
||||
|
||||
################################################################################
|
||||
# Widget Specific GUI interactions
|
||||
@@ -656,21 +650,21 @@ class Image(PlotBase):
|
||||
self._main_image.log = enable
|
||||
|
||||
@SafeProperty(int)
|
||||
def rotation(self) -> int:
|
||||
def num_rotation_90(self) -> int:
|
||||
"""
|
||||
The number of 90° rotations to apply.
|
||||
The number of 90° rotations to apply counterclockwise.
|
||||
"""
|
||||
return self._main_image.rotation
|
||||
return self._main_image.num_rotation_90
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, value: int):
|
||||
@num_rotation_90.setter
|
||||
def num_rotation_90(self, value: int):
|
||||
"""
|
||||
Set the number of 90° rotations to apply.
|
||||
Set the number of 90° rotations to apply counterclockwise.
|
||||
|
||||
Args:
|
||||
value(int): The number of 90° rotations to apply.
|
||||
"""
|
||||
self._main_image.rotation = value
|
||||
self._main_image.num_rotation_90 = value
|
||||
|
||||
@SafeProperty(bool)
|
||||
def transpose(self) -> bool:
|
||||
@@ -762,6 +756,19 @@ class Image(PlotBase):
|
||||
self.selection_bundle.dim_combo_box,
|
||||
):
|
||||
combo.blockSignals(False)
|
||||
else:
|
||||
for combo in (
|
||||
self.selection_bundle.device_combo_box,
|
||||
self.selection_bundle.dim_combo_box,
|
||||
):
|
||||
combo.blockSignals(True)
|
||||
self.selection_bundle.device_combo_box.setCurrentText("")
|
||||
self.selection_bundle.dim_combo_box.setCurrentText("auto")
|
||||
for combo in (
|
||||
self.selection_bundle.device_combo_box,
|
||||
self.selection_bundle.dim_combo_box,
|
||||
):
|
||||
combo.blockSignals(False)
|
||||
|
||||
################################################################################
|
||||
# Image Update Methods
|
||||
@@ -812,6 +819,7 @@ class Image(PlotBase):
|
||||
self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
|
||||
)
|
||||
self._main_image.config.monitor = None
|
||||
self._sync_device_selection()
|
||||
|
||||
########################################
|
||||
# 1D updates
|
||||
|
||||
@@ -24,7 +24,7 @@ class ImageItemConfig(ConnectionConfig): # TODO review config
|
||||
monitor: str | None = Field(None, description="The name of the monitor.")
|
||||
monitor_type: Literal["1d", "2d", "auto"] = Field("auto", description="The type of monitor.")
|
||||
source: str | None = Field(None, description="The source of the curve.")
|
||||
color_map: str | None = Field("magma", description="The color map of the image.")
|
||||
color_map: str | None = Field("plasma", description="The color map of the image.")
|
||||
downsample: bool | None = Field(True, description="Whether to downsample the image.")
|
||||
opacity: float | None = Field(1.0, description="The opacity of the image.")
|
||||
v_range: tuple[float | int, float | int] | None = Field(
|
||||
@@ -61,8 +61,8 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
"fft.setter",
|
||||
"log",
|
||||
"log.setter",
|
||||
"rotation",
|
||||
"rotation.setter",
|
||||
"num_rotation_90",
|
||||
"num_rotation_90.setter",
|
||||
"transpose",
|
||||
"transpose.setter",
|
||||
"get_data",
|
||||
@@ -86,7 +86,6 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
self.set_parent(parent_image)
|
||||
else:
|
||||
self.parent_image = None
|
||||
self.parent_id = None
|
||||
super().__init__(config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.raw_data = None
|
||||
@@ -98,7 +97,6 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
|
||||
def set_parent(self, parent: BECConnector):
|
||||
self.parent_image = parent
|
||||
self.parent_id = parent.gui_id
|
||||
|
||||
def parent(self):
|
||||
return self.parent_image
|
||||
@@ -241,13 +239,13 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
self._process_image()
|
||||
|
||||
@property
|
||||
def rotation(self) -> Optional[int]:
|
||||
def num_rotation_90(self) -> Optional[int]:
|
||||
"""Get or set the number of 90° rotations to apply."""
|
||||
return self.config.processing.rotation
|
||||
return self.config.processing.num_rotation_90
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, value: Optional[int]):
|
||||
self.config.processing.rotation = value
|
||||
@num_rotation_90.setter
|
||||
def num_rotation_90(self, value: Optional[int]):
|
||||
self.config.processing.num_rotation_90 = value
|
||||
self._process_image()
|
||||
|
||||
@property
|
||||
@@ -275,3 +273,7 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
self.raw_data = None
|
||||
self.buffer = []
|
||||
self.max_len = 0
|
||||
|
||||
def remove(self):
|
||||
self.parent().disconnect_monitor(self.config.monitor)
|
||||
self.clear()
|
||||
|
||||
@@ -37,7 +37,7 @@ class ProcessingConfig(BaseModel):
|
||||
transpose: bool = Field(
|
||||
False, description="Whether to transpose the monitor data before displaying."
|
||||
)
|
||||
rotation: int = Field(
|
||||
num_rotation_90: int = Field(
|
||||
0, description="The rotation angle of the monitor data before displaying."
|
||||
)
|
||||
stats: ImageStats = Field(
|
||||
@@ -140,8 +140,8 @@ class ImageProcessor(QObject):
|
||||
"""Core processing logic without threading overhead."""
|
||||
if self.config.fft:
|
||||
data = self.FFT(data)
|
||||
if self.config.rotation is not None:
|
||||
data = self.rotation(data, self.config.rotation)
|
||||
if self.config.num_rotation_90 is not None:
|
||||
data = self.rotation(data, self.config.num_rotation_90)
|
||||
if self.config.transpose:
|
||||
data = self.transpose(data)
|
||||
if self.config.log:
|
||||
|
||||
@@ -40,7 +40,7 @@ class MonitorSelectionToolbarBundle(ToolbarBundle):
|
||||
self.add_action("monitor", WidgetAction(widget=self.device_combo_box, adjust_size=True))
|
||||
|
||||
# 2) Dimension combo box
|
||||
self.dim_combo_box = QComboBox()
|
||||
self.dim_combo_box = QComboBox(parent=self.target_widget)
|
||||
self.dim_combo_box.addItems(["auto", "1d", "2d"])
|
||||
self.dim_combo_box.setCurrentText("auto")
|
||||
self.dim_combo_box.setToolTip("Monitor Dimension")
|
||||
|
||||
@@ -55,24 +55,24 @@ class ImageProcessingToolbarBundle(ToolbarBundle):
|
||||
|
||||
@SafeSlot()
|
||||
def rotate_right(self):
|
||||
if self.target_widget.rotation is None:
|
||||
if self.target_widget.num_rotation_90 is None:
|
||||
return
|
||||
rotation = (self.target_widget.rotation - 1) % 4
|
||||
self.target_widget.rotation = rotation
|
||||
rotation = (self.target_widget.num_rotation_90 - 1) % 4
|
||||
self.target_widget.num_rotation_90 = rotation
|
||||
|
||||
@SafeSlot()
|
||||
def rotate_left(self):
|
||||
if self.target_widget.rotation is None:
|
||||
if self.target_widget.num_rotation_90 is None:
|
||||
return
|
||||
rotation = (self.target_widget.rotation + 1) % 4
|
||||
self.target_widget.rotation = rotation
|
||||
rotation = (self.target_widget.num_rotation_90 + 1) % 4
|
||||
self.target_widget.num_rotation_90 = rotation
|
||||
|
||||
@SafeSlot()
|
||||
def reset_settings(self):
|
||||
self.target_widget.fft = False
|
||||
self.target_widget.log = False
|
||||
self.target_widget.transpose = False
|
||||
self.target_widget.rotation = 0
|
||||
self.target_widget.num_rotation_90 = 0
|
||||
|
||||
self.fft.action.setChecked(False)
|
||||
self.log.action.setChecked(False)
|
||||
|
||||
@@ -25,7 +25,7 @@ logger = bec_logger.logger
|
||||
|
||||
class MultiWaveformConfig(ConnectionConfig):
|
||||
color_palette: str | None = Field(
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
"plasma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
curve_limit: int | None = Field(
|
||||
200, description="The maximum number of curves to display on the plot."
|
||||
@@ -308,7 +308,7 @@ class MultiWaveform(PlotBase):
|
||||
################################################################################
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def plot(self, monitor: str, color_palette: str | None = "magma"):
|
||||
def plot(self, monitor: str, color_palette: str | None = "plasma"):
|
||||
"""
|
||||
Create a plot for the given monitor.
|
||||
Args:
|
||||
|
||||
@@ -29,9 +29,7 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||
|
||||
# Monitor Selection
|
||||
self.monitor = DeviceComboBox(
|
||||
device_filter=BECDeviceFilter.DEVICE,
|
||||
readout_priority_filter=ReadoutPriority.ASYNC,
|
||||
parent_id=self.target_widget.gui_id,
|
||||
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=ReadoutPriority.ASYNC
|
||||
)
|
||||
self.monitor.addItem("", None)
|
||||
self.monitor.setCurrentText("")
|
||||
@@ -40,7 +38,7 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||
self.add_action("monitor", WidgetAction(widget=self.monitor, adjust_size=False))
|
||||
|
||||
# Colormap Selection
|
||||
self.colormap_widget = BECColorMapWidget(cmap="magma", parent_id=self.target_widget.gui_id)
|
||||
self.colormap_widget = BECColorMapWidget(cmap="plasma")
|
||||
self.add_action("color_map", WidgetAction(widget=self.colormap_widget, adjust_size=False))
|
||||
|
||||
# Connect slots, a device will be connected upon change of any combobox
|
||||
|
||||
@@ -98,10 +98,11 @@ class PlotBase(BECWidget, QWidget):
|
||||
self._ui_mode = UIMode.POPUP if popups else UIMode.SIDE
|
||||
self.axis_settings_dialog = None
|
||||
self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
|
||||
self.plot_widget.ci.setContentsMargins(0, 0, 0, 0)
|
||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||
self.plot_widget.addItem(self.plot_item)
|
||||
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
||||
self._init_toolbar()
|
||||
|
||||
# PlotItem Addons
|
||||
@@ -795,6 +796,7 @@ class PlotBase(BECWidget, QWidget):
|
||||
"""
|
||||
self.plot_item.showAxis("top", value)
|
||||
self.plot_item.showAxis("right", value)
|
||||
|
||||
self.property_changed.emit("outer_axes", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
|
||||
@@ -814,6 +816,7 @@ class PlotBase(BECWidget, QWidget):
|
||||
"""
|
||||
self.plot_item.showAxis("bottom", value)
|
||||
self.plot_item.showAxis("left", value)
|
||||
|
||||
self._apply_x_label()
|
||||
self._apply_y_label()
|
||||
self.property_changed.emit("inner_axes", value)
|
||||
|
||||
@@ -38,7 +38,7 @@ class ScatterCurveConfig(ConnectionConfig):
|
||||
"solid", description="The style of the pen of the curve."
|
||||
)
|
||||
color_map: str | None = Field(
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
"plasma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
x_device: ScatterDeviceSignal | None = Field(
|
||||
None, description="The x device signal of the scatter waveform."
|
||||
@@ -78,8 +78,8 @@ class ScatterCurve(BECConnector, pg.PlotDataItem):
|
||||
self.config = config
|
||||
name = config.label
|
||||
self.parent_item = parent_item
|
||||
self.parent_id = self.parent_item.gui_id
|
||||
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
|
||||
object_name = name.replace("-", "_").replace(" ", "_") if name else None
|
||||
super().__init__(name=name, object_name=object_name, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.data_z = None # color scaling needs to be cashed for changing colormap
|
||||
self.apply_config()
|
||||
|
||||
@@ -30,7 +30,7 @@ logger = bec_logger.logger
|
||||
# noinspection PyDataclass
|
||||
class ScatterWaveformConfig(ConnectionConfig):
|
||||
color_map: str | None = Field(
|
||||
"magma",
|
||||
"plasma",
|
||||
description="The color map of the z scaling of scatter waveform.",
|
||||
validate_default=True,
|
||||
)
|
||||
@@ -266,7 +266,7 @@ class ScatterWaveform(PlotBase):
|
||||
x_entry: None | str = None,
|
||||
y_entry: None | str = None,
|
||||
z_entry: None | str = None,
|
||||
color_map: str | None = "magma",
|
||||
color_map: str | None = "plasma",
|
||||
label: str | None = None,
|
||||
validate_bec: bool = True,
|
||||
) -> ScatterCurve:
|
||||
@@ -333,12 +333,13 @@ class ScatterWaveform(PlotBase):
|
||||
|
||||
# To have only one main curve
|
||||
if self._main_curve is not None:
|
||||
self.rpc_register.remove_rpc(self._main_curve)
|
||||
self.rpc_register.broadcast()
|
||||
self.plot_item.removeItem(self._main_curve)
|
||||
self._main_curve.deleteLater()
|
||||
self._main_curve = None
|
||||
|
||||
self._main_curve = ScatterCurve(
|
||||
parent_item=self, config=config, gui_id=self.gui_id, name=config.label
|
||||
)
|
||||
self._main_curve = ScatterCurve(parent_item=self, config=config, name=config.label)
|
||||
self.plot_item.addItem(self._main_curve)
|
||||
|
||||
self.sync_signal_update.emit()
|
||||
|
||||
@@ -92,8 +92,7 @@ class Curve(BECConnector, pg.PlotDataItem):
|
||||
else:
|
||||
self.config = config
|
||||
self.parent_item = parent_item
|
||||
self.parent_id = self.parent_item.gui_id
|
||||
object_name = name.replace("-", "_") if name else None
|
||||
object_name = name.replace("-", "_").replace(" ", "_") if name else None
|
||||
super().__init__(name=name, object_name=object_name, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.apply_config()
|
||||
|
||||
@@ -373,7 +373,7 @@ class CurveTree(BECWidget, QWidget):
|
||||
if self.waveform and hasattr(self.waveform, "color_palette"):
|
||||
self.color_palette = self.waveform.color_palette
|
||||
else:
|
||||
self.color_palette = "magma"
|
||||
self.color_palette = "plasma"
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@@ -386,7 +386,7 @@ class CurveTree(BECWidget, QWidget):
|
||||
|
||||
def _init_toolbar(self):
|
||||
"""Initialize the toolbar with actions: add, send, refresh, expand, collapse, renormalize."""
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
||||
add = MaterialIconAction(
|
||||
icon_name="add", tooltip="Add new curve", checkable=False, parent=self
|
||||
)
|
||||
@@ -413,7 +413,7 @@ class CurveTree(BECWidget, QWidget):
|
||||
self.toolbar.add_action("renormalize_colors", renorm_action, self)
|
||||
renorm_action.action.triggered.connect(lambda checked: self.renormalize_colors())
|
||||
|
||||
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "magma")
|
||||
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "plasma")
|
||||
self.toolbar.addWidget(self.colormap_widget)
|
||||
self.colormap_widget.colormap_changed_signal.connect(self.handle_colormap_changed)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from qtpy.QtWidgets import QApplication, QDialog, QHBoxLayout, QMainWindow, QVBo
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
||||
from bec_widgets.utils.colors import Colors, set_theme
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.utils.toolbar import MaterialIconAction
|
||||
@@ -30,7 +31,7 @@ logger = bec_logger.logger
|
||||
# noinspection PyDataclass
|
||||
class WaveformConfig(ConnectionConfig):
|
||||
color_palette: str | None = Field(
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
"plasma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
@@ -89,6 +90,8 @@ class Waveform(PlotBase):
|
||||
"curves",
|
||||
"x_mode",
|
||||
"x_mode.setter",
|
||||
"x_entry",
|
||||
"x_entry.setter",
|
||||
"color_palette",
|
||||
"color_palette.setter",
|
||||
"plot",
|
||||
@@ -134,6 +137,7 @@ class Waveform(PlotBase):
|
||||
# Curve data
|
||||
self._sync_curves = []
|
||||
self._async_curves = []
|
||||
self._async_connected_devices: set[str] = set()
|
||||
self._slice_index = None
|
||||
self._dap_curves = []
|
||||
self._mode: Literal["none", "sync", "async", "mixed"] = "none"
|
||||
@@ -541,6 +545,7 @@ class Waveform(PlotBase):
|
||||
continue
|
||||
config = CurveConfig(**cfg_dict)
|
||||
self._add_curve(config=config)
|
||||
self.update_with_scan_history(-1)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to decode JSON: {e}")
|
||||
|
||||
@@ -644,7 +649,9 @@ class Waveform(PlotBase):
|
||||
# Decide label if not provided
|
||||
if label is None:
|
||||
if source == "custom":
|
||||
label = f"Curve {len(self.curves) + 1}"
|
||||
label = WidgetContainerUtils.generate_unique_name(
|
||||
"Curve", [c.object_name for c in self.curves]
|
||||
)
|
||||
else:
|
||||
label = f"{y_name}-{y_entry}"
|
||||
|
||||
@@ -769,7 +776,9 @@ class Waveform(PlotBase):
|
||||
label = config.label
|
||||
if not label:
|
||||
# Fallback label
|
||||
label = f"Curve {len(self.curves) + 1}"
|
||||
label = WidgetContainerUtils.generate_unique_name(
|
||||
"Curve", [c.object_name for c in self.curves]
|
||||
)
|
||||
config.label = label
|
||||
|
||||
# Check for duplicates
|
||||
@@ -1005,6 +1014,7 @@ class Waveform(PlotBase):
|
||||
return
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self._async_connected_devices.clear()
|
||||
self.reset()
|
||||
self.new_scan.emit()
|
||||
self.new_scan_id.emit(current_scan_id)
|
||||
@@ -1171,17 +1181,22 @@ class Waveform(PlotBase):
|
||||
except KeyError:
|
||||
logger.warning(f"Curve {name} not found in plot item.")
|
||||
pass
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_async_readback,
|
||||
MessageEndpoints.device_async_readback(self.scan_id, name),
|
||||
from_start=True,
|
||||
)
|
||||
logger.info(f"Setup async curve {name}")
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
# Connect only once per device signal
|
||||
if name not in self._async_connected_devices:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_async_readback,
|
||||
MessageEndpoints.device_async_readback(self.scan_id, name),
|
||||
from_start=True,
|
||||
cb_info={"scan_id": self.scan_id},
|
||||
)
|
||||
self._async_connected_devices.add(name)
|
||||
logger.info(f"Async read-back connected for {name}")
|
||||
|
||||
@SafeSlot(dict, dict, verify_sender=True)
|
||||
def on_async_readback(self, msg, metadata):
|
||||
"""
|
||||
Get async data readback. This code needs to be fast, therefor we try
|
||||
Get async data readback. This code needs to be fast; therefore, we try
|
||||
to reduce the number of copies in between cycles. Be careful when refactoring
|
||||
this part as it will affect the performance of the async readback.
|
||||
|
||||
@@ -1197,6 +1212,14 @@ class Waveform(PlotBase):
|
||||
msg(dict): Message with the async data.
|
||||
metadata(dict): Metadata of the message.
|
||||
"""
|
||||
sender = self.sender()
|
||||
if not hasattr(sender, "cb_info"):
|
||||
logger.info(f"Sender {sender} has no cb_info.")
|
||||
return
|
||||
scan_id = sender.cb_info.get("scan_id", None)
|
||||
if scan_id != self.scan_id:
|
||||
logger.info("Scan ID mismatch, ignoring async readback.")
|
||||
|
||||
instruction = metadata.get("async_update", {}).get("type")
|
||||
if instruction not in ["add", "add_slice", "replace"]:
|
||||
logger.warning(f"Invalid async update instruction: {instruction}")
|
||||
@@ -1205,6 +1228,7 @@ class Waveform(PlotBase):
|
||||
plot_mode = self.x_axis_mode["name"]
|
||||
for curve in self._async_curves:
|
||||
x_data = None # Reset x_data
|
||||
y_data = None # Reset y_data
|
||||
# Get the curve data
|
||||
async_data = msg["signals"].get(curve.config.signal.entry, None)
|
||||
if async_data is None:
|
||||
@@ -1748,12 +1772,10 @@ class Waveform(PlotBase):
|
||||
self.proxy_dap_request.cleanup()
|
||||
self.clear_all()
|
||||
if self.curve_settings_dialog is not None:
|
||||
self.curve_settings_dialog.close()
|
||||
self.curve_settings_dialog.deleteLater()
|
||||
self.curve_settings_dialog.reject()
|
||||
self.curve_settings_dialog = None
|
||||
if self.dap_summary_dialog is not None:
|
||||
self.dap_summary_dialog.close()
|
||||
self.dap_summary_dialog.deleteLater()
|
||||
self.dap_summary_dialog.reject()
|
||||
self.dap_summary_dialog = None
|
||||
super().cleanup()
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class BECProgressBar(BECWidget, QWidget):
|
||||
"set_minimum",
|
||||
"label_template",
|
||||
"label_template.setter",
|
||||
"_get_label",
|
||||
]
|
||||
ICON_NAME = "page_control"
|
||||
|
||||
@@ -235,6 +236,10 @@ class BECProgressBar(BECWidget, QWidget):
|
||||
(value - self._user_minimum) / (self._user_maximum - self._user_minimum) * self._maximum
|
||||
)
|
||||
|
||||
def _get_label(self) -> str:
|
||||
"""Return the label text. mostly used for testing rpc."""
|
||||
return self.center_label.text()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -96,7 +96,6 @@ class Ring(BECConnector, QObject):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
parent_progress_widget=None,
|
||||
config: RingConfig | dict | None = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
@@ -111,7 +110,8 @@ class Ring(BECConnector, QObject):
|
||||
self.config = config
|
||||
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.parent_progress_widget = parent_progress_widget
|
||||
self.parent_progress_widget = parent
|
||||
|
||||
self.color = None
|
||||
self.background_color = None
|
||||
self.start_position = None
|
||||
@@ -244,10 +244,10 @@ class Ring(BECConnector, QObject):
|
||||
"""
|
||||
if self.config.connections.endpoint == endpoint and self.config.connections.slot == slot:
|
||||
return
|
||||
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.config.connections.slot, self.config.connections.endpoint
|
||||
)
|
||||
if self.config.connections.slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
getattr(self, self.config.connections.slot), self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections = ProgressbarConnections(slot=slot, endpoint=endpoint)
|
||||
self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ logger = bec_logger.logger
|
||||
|
||||
class RingProgressBarConfig(ConnectionConfig):
|
||||
color_map: Optional[str] = Field(
|
||||
"magma", description="Color scheme for the progress bars.", validate_default=True
|
||||
"plasma", description="Color scheme for the progress bars.", validate_default=True
|
||||
)
|
||||
min_number_of_bars: int | None = Field(
|
||||
1, description="Minimum number of progress bars to display."
|
||||
@@ -123,7 +123,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
|
||||
# For updating bar behaviour
|
||||
self._auto_updates = True
|
||||
self._rings = None
|
||||
self._rings = []
|
||||
|
||||
if num_bars is not None:
|
||||
self.config.num_bars = max(
|
||||
@@ -134,11 +134,12 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
self.enable_auto_updates(self.config.auto_updates)
|
||||
|
||||
@property
|
||||
def rings(self):
|
||||
def rings(self) -> list[Ring]:
|
||||
"""Returns a list of all rings in the progress bar."""
|
||||
return self._rings
|
||||
|
||||
@rings.setter
|
||||
def rings(self, value):
|
||||
def rings(self, value: list[Ring]):
|
||||
self._rings = value
|
||||
|
||||
def update_config(self, config: RingProgressBarConfig | dict):
|
||||
@@ -169,15 +170,15 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
)
|
||||
for i in range(self.config.num_bars)
|
||||
]
|
||||
self._rings = [
|
||||
Ring(parent_progress_widget=self, config=config) for config in self.config.rings
|
||||
]
|
||||
self._rings = [Ring(parent=self, config=config) for config in self.config.rings]
|
||||
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
|
||||
min_size = self._calculate_minimum_size()
|
||||
self.setMinimumSize(min_size)
|
||||
# Set outer ring to listen to scan progress
|
||||
self.rings[0].set_update(mode="scan")
|
||||
self.update()
|
||||
|
||||
def add_ring(self, **kwargs) -> Ring:
|
||||
@@ -199,7 +200,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
directions=-1,
|
||||
**kwargs,
|
||||
)
|
||||
ring = Ring(parent_progress_widget=self, config=ring_config)
|
||||
ring = Ring(parent=self, config=ring_config)
|
||||
self.config.num_bars += 1
|
||||
self._rings.append(ring)
|
||||
self.config.rings.append(ring.config)
|
||||
@@ -218,6 +219,10 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
index(int): Index of the progress bar to remove.
|
||||
"""
|
||||
ring = self._find_ring_by_index(index)
|
||||
self._cleanup_ring(ring)
|
||||
self.update()
|
||||
|
||||
def _cleanup_ring(self, ring: Ring) -> None:
|
||||
ring.reset_connection()
|
||||
self._rings.remove(ring)
|
||||
self.config.rings.remove(ring.config)
|
||||
@@ -225,8 +230,10 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
self._reindex_rings()
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
del ring
|
||||
self.update()
|
||||
# Remove ring from rpc, afterwards call close event.
|
||||
ring.rpc_register.remove_rpc(ring)
|
||||
ring.deleteLater()
|
||||
# del ring
|
||||
|
||||
def _reindex_rings(self):
|
||||
"""
|
||||
@@ -292,7 +299,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
widget_class="Ring", index=i, start_positions=90 * 16, directions=-1
|
||||
)
|
||||
self.config.rings.append(new_ring_config)
|
||||
new_ring = Ring(parent_progress_widget=self, config=new_ring_config)
|
||||
new_ring = Ring(parent=self, config=new_ring_config)
|
||||
self._rings.append(new_ring)
|
||||
|
||||
elif num_bars < current_num_bars:
|
||||
@@ -642,6 +649,6 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
for ring in self._rings:
|
||||
ring.reset_connection()
|
||||
self._cleanup_ring(ring)
|
||||
self._rings.clear()
|
||||
super().cleanup()
|
||||
|
||||
@@ -57,6 +57,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
# self.layout.addWidget(self.table)
|
||||
self.table.setColumnCount(4)
|
||||
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status", "Cancel"])
|
||||
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||
|
||||
@@ -73,15 +74,16 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
"""
|
||||
Set the toolbar.
|
||||
"""
|
||||
widget_label = QLabel("Live Queue")
|
||||
widget_label = QLabel(text="Live Queue", parent=self)
|
||||
widget_label.setStyleSheet("font-weight: bold;")
|
||||
self.toolbar = ModularToolBar(
|
||||
parent=self,
|
||||
actions={
|
||||
"widget_label": WidgetAction(widget=widget_label),
|
||||
"separator_1": SeparatorAction(),
|
||||
"resume": WidgetAction(widget=ResumeButton(toolbar=False)),
|
||||
"stop": WidgetAction(widget=StopButton(toolbar=False)),
|
||||
"reset": WidgetAction(widget=ResetButton(toolbar=False)),
|
||||
"resume": WidgetAction(widget=ResumeButton(parent=self, toolbar=False)),
|
||||
"stop": WidgetAction(widget=StopButton(parent=self, toolbar=False)),
|
||||
"reset": WidgetAction(widget=ResetButton(parent=self, toolbar=False)),
|
||||
},
|
||||
target_widget=self,
|
||||
)
|
||||
@@ -223,7 +225,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
Returns:
|
||||
AbortButton: The abort button.
|
||||
"""
|
||||
abort_button = AbortButton(scan_id=scan_id)
|
||||
abort_button = AbortButton(parent=self, scan_id=scan_id)
|
||||
|
||||
abort_button.button.setText("")
|
||||
abort_button.button.setIcon(
|
||||
@@ -231,7 +233,6 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
)
|
||||
abort_button.button.setStyleSheet("background-color: rgba(0,0,0,0) ")
|
||||
abort_button.button.setFlat(True)
|
||||
|
||||
return abort_button
|
||||
|
||||
def delete_selected_row(self):
|
||||
@@ -239,7 +240,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
button = self.sender()
|
||||
row = self.table.indexAt(button.pos()).row()
|
||||
self.table.removeRow(row)
|
||||
|
||||
button.close()
|
||||
button.deleteLater()
|
||||
|
||||
def reset_content(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.widgets.services.bec_status_box.status_item import StatusItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.client import BECClient
|
||||
|
||||
# TODO : Put normal imports back when Pydantic gets faster
|
||||
@@ -76,6 +76,7 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
|
||||
|
||||
PLUGIN = True
|
||||
CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"]
|
||||
USER_ACCESS = ["get_server_state", "remove"]
|
||||
|
||||
service_update = Signal(BECServiceInfoContainer)
|
||||
bec_core_state = Signal(str)
|
||||
@@ -134,6 +135,10 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
|
||||
"QTreeWidget::item:selected {}"
|
||||
)
|
||||
|
||||
def get_server_state(self) -> str:
|
||||
"""Get the state ("RUNNING", "BUSY", "IDLE", "ERROR") of the BEC server"""
|
||||
return self.status_container[self.box_name]["info"].status
|
||||
|
||||
def _create_status_widget(
|
||||
self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
|
||||
) -> StatusItem:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Module for a StatusItem widget to display status and metrics for a BEC service.
|
||||
"""Module for a StatusItem widget to display status and metrics for a BEC service.
|
||||
The widget is bound to be used with the BECStatusBox widget."""
|
||||
|
||||
import enum
|
||||
|
||||
@@ -7,7 +7,7 @@ from qtpy.QtCore import QMimeData, Qt
|
||||
from qtpy.QtGui import QDrag
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtGui import QMouseEvent
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Utilities for filtering and formatting in the LogPanel"""
|
||||
"""Utilities for filtering and formatting in the LogPanel"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
from collections import deque
|
||||
from functools import partial, reduce
|
||||
from re import Pattern
|
||||
from typing import Literal
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.connector import ConnectorBase
|
||||
@@ -16,7 +16,7 @@ from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import LogLevel, bec_logger
|
||||
from bec_lib.messages import LogMessage, StatusMessage
|
||||
from PySide6.QtCore import QObject
|
||||
from qtpy.QtCore import QDateTime, Qt, Signal, SignalInstance # type: ignore
|
||||
from qtpy.QtCore import QDateTime, Qt, Signal
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -52,6 +52,9 @@ from bec_widgets.widgets.utility.logpanel._util import (
|
||||
simple_color_format,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtCore import SignalInstance
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pyqtgraph.widgets.ColorMapButton import ColorMapButton
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
@@ -6,16 +7,33 @@ from bec_widgets.utils import Colors
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
|
||||
class RoundedColorMapButton(ColorMapButton):
|
||||
"""Thin wrapper around pyqtgraph ColorMapButton to add rounded clipping."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
def paintEvent(self, evt):
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(self.rect(), 8, 8)
|
||||
painter.setClipPath(path)
|
||||
self.paintColorMap(painter, self.contentsRect())
|
||||
painter.end()
|
||||
|
||||
|
||||
class BECColorMapWidget(BECWidget, QWidget):
|
||||
colormap_changed_signal = Signal(str)
|
||||
ICON_NAME = "palette"
|
||||
PLUGIN = True
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, cmap: str = "magma", **kwargs):
|
||||
def __init__(self, parent=None, cmap: str = "plasma", **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
# Create the ColorMapButton
|
||||
self.button = ColorMapButton()
|
||||
self.button = RoundedColorMapButton()
|
||||
|
||||
# Set the size policy and minimum width
|
||||
size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
|
||||
@@ -12,7 +12,7 @@ class DarkModeButton(BECWidget, QWidget):
|
||||
|
||||
ICON_NAME = "dark_mode"
|
||||
PLUGIN = True
|
||||
RPC = False
|
||||
RPC = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -31,9 +31,9 @@ class DarkModeButton(BECWidget, QWidget):
|
||||
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
if toolbar:
|
||||
self.mode_button = QToolButton()
|
||||
self.mode_button = QToolButton(parent=parent)
|
||||
else:
|
||||
self.mode_button = QPushButton()
|
||||
self.mode_button = QPushButton(parent=parent)
|
||||
|
||||
self.dark_mode_enabled = self._get_qapp_dark_mode_state()
|
||||
self.update_mode_button()
|
||||
|
||||
BIN
docs/assets/widget_screenshots/scatter_waveform.png
Normal file
BIN
docs/assets/widget_screenshots/scatter_waveform.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -6,13 +6,13 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
import os
|
||||
import datetime
|
||||
import pathlib
|
||||
|
||||
import tomli
|
||||
|
||||
project = "BEC Widgets"
|
||||
copyright = "2023, Paul Scherrer Institute"
|
||||
copyright = f"{datetime.datetime.today().year}, Paul Scherrer Institute"
|
||||
author = "Paul Scherrer Institute"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -5,7 +5,7 @@ This section provides an overview of the core concepts of BEC Widgets, which are
|
||||
## Moduler Design
|
||||
We develop widgets with the single-responsibility principle in mind, meaning each widget is designed for a specific task. Our goal is to keep widgets simple, using them primarily for visualization or to initiate actions within BEC. Following these ideas, widgets should be designed to be reusable in various applications, making them versatile building blocks for larger GUIs.
|
||||
|
||||
We offer up to three different options for composing larger GUIs from these modular widgets: BECDesigner, DockArea widget, or scripting from the command line interface. More information about these options can be found in the user sections on [applications](user.applications).
|
||||
We offer up to three different options for composing larger GUIs from these modular widgets: BEC Designer, DockArea widget, or scripting from the command line interface. More information about these options can be found in the user sections on [applications](user.applications).
|
||||
|
||||
## Client-Server Architecture
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ and PySide6 are supported, we prefer PySide6 as it is the official Python bindin
|
||||
advantages like bundling all necessary libraries in a single package with pip installation and staying more up-to-date
|
||||
compared to PyQt6.
|
||||
|
||||
Below is a list of useful links to help you start developing with Qt and QtDesigner:
|
||||
Below is a list of useful links to help you start developing with Qt and Qt Designer:
|
||||
|
||||
- [Python GUIs](https://www.pythonguis.com): A great resource with tutorials and examples for creating GUIs in Python
|
||||
using various frameworks.
|
||||
- [PySide6 Quick Start Guide](https://doc.qt.io/qtforpython-6/index.html): The official documentation for PySide6,
|
||||
including quick start guides and tutorials.
|
||||
- [QtDesigner Official Documentation](https://doc.qt.io/qt-6/qtdesigner-manual.html): Comprehensive documentation for
|
||||
QtDesigner.
|
||||
- [Qt Designer Official Documentation](https://doc.qt.io/qt-6/qtdesigner-manual.html): Comprehensive documentation for
|
||||
Qt Designer, the underlying tool for BEC Designer.
|
||||
- [Simple PyQt Tutorial from RealPython](https://realpython.com/python-pyqt-gui-calculator/): A beginner-friendly
|
||||
tutorial on creating your first GUI application with PyQt.
|
||||
- [PyQtGraph Documentation](https://pyqtgraph.readthedocs.io/en/latest/): BEC Widgets relies on PyQtGraph for plotting;
|
||||
|
||||
@@ -28,7 +28,7 @@ from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLa
|
||||
|
||||
|
||||
class MotorControlWidget(QWidget):
|
||||
def __init__(self, motor_name: str, parent=None):
|
||||
def __init__(self, parent=None, motor_name: str = ""):
|
||||
super().__init__(parent)
|
||||
|
||||
self.motor_name = motor_name
|
||||
@@ -70,18 +70,17 @@ from [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api
|
||||
pass the motor name to the widget, and use `get_bec_shortcuts` to access BEC services.
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLayout
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtWidgets import QDoubleSpinBox, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
|
||||
class MotorControlWidget(BECWidget, QWidget):
|
||||
|
||||
def __init__(self, motor_name: str, parent=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
QWidget.__init__(self, parent)
|
||||
def __init__(self, parent=None, motor_name: str = "", **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self.motor_name = motor_name
|
||||
|
||||
@@ -114,13 +113,13 @@ class MotorControlWidget(BECWidget, QWidget):
|
||||
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
|
||||
)
|
||||
|
||||
@Slot()
|
||||
@SafeSlot()
|
||||
def move_motor(self):
|
||||
target_position = self.spin_box.value()
|
||||
self.dev[self.motor_name].move(target_position)
|
||||
print(f"Commanding motor {self.motor_name} to move to {target_position}")
|
||||
|
||||
@Slot(dict, dict)
|
||||
@SafeSlot(dict, dict)
|
||||
def on_motor_update(self, msg_content, metadata):
|
||||
position = msg_content.get("signals", {}).get(self.motor_name, {}).get("value", "N/A")
|
||||
self.label.setText(f"{self.motor_name} : {round(position, 2)}")
|
||||
@@ -132,19 +131,18 @@ Next, we’ll set up an RPC interface to allow remote control of the widget from
|
||||
the `BECIPythonClient`. We’ll expose a method that allows changing the motor name through CLI commands.
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLayout
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtWidgets import QDoubleSpinBox, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
|
||||
class MotorControlWidget(BECWidget, QWidget):
|
||||
USER_ACCESS = ["change_motor"]
|
||||
|
||||
def __init__(self, motor_name: str, parent=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
QWidget.__init__(self, parent)
|
||||
def __init__(self, parent=None, motor_name: str = "", **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self.motor_name = motor_name
|
||||
|
||||
@@ -177,13 +175,13 @@ class MotorControlWidget(BECWidget, QWidget):
|
||||
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
|
||||
)
|
||||
|
||||
@Slot()
|
||||
@SafeSlot()
|
||||
def move_motor(self):
|
||||
target_position = self.spin_box.value()
|
||||
self.dev[self.motor_name].move(target_position)
|
||||
print(f"Commanding motor {self.motor_name} to move to {target_position}")
|
||||
|
||||
@Slot(dict, dict)
|
||||
@SafeSlot(dict, dict)
|
||||
def on_motor_update(self, msg_content, metadata):
|
||||
position = msg_content.get("signals", {}).get(self.motor_name, {}).get("value", "N/A")
|
||||
self.label.setText(f"{self.motor_name} : {round(position, 2)}")
|
||||
@@ -203,11 +201,11 @@ class MotorControlWidget(BECWidget, QWidget):
|
||||
```
|
||||
|
||||
```{warning}
|
||||
After implementing an RPC method, you must run the `cli/generate_cli.py` script to update the CLI commands for `BECIPythonClient`. This script generates the necessary command-line interface bindings, ensuring that your RPC method can be accessed and controlled remotely.
|
||||
After implementing an RPC method, you must run the `bw-generate-cli --target <your plugin repo name>` script to update the CLI commands for `BECIPythonClient`, e.g. `bw-generate-cli --target csaxs_bec`. This script generates the necessary command-line interface bindings, ensuring that your RPC method can be accessed and controlled remotely.
|
||||
```
|
||||
|
||||
```{note}
|
||||
In this tutorial, we used the @Slot decorator from QtCore to mark methods as slots for signals. This decorator ensures that the connected methods are treated as slots by the Qt framework, which can be connected to signals. It’s a best practice to use the @Slot decorator to clearly indicate which methods are intended to handle signal events with correct argument signatures.
|
||||
In this tutorial, we used the @SafeSlot decorator from BEC Widgets to mark methods as slots for signals. This decorator ensures that the connected methods are treated as slots by the Qt framework, which can be connected to signals. It’s a best practice to use the @SafeSlot decorator to clearly indicate which methods are intended to handle signal events with correct argument signatures. @SafeSlot also provides error handling and logging capabilities, making it more robust and easier to debug.
|
||||
```
|
||||
|
||||
## Step 4: Running the Widget
|
||||
|
||||
@@ -7,6 +7,6 @@ sphinx-copybutton
|
||||
sphinx-inline-tabs
|
||||
myst-parser
|
||||
sphinx-design
|
||||
PySide6~=6.7.2
|
||||
PySide6~=6.8.2
|
||||
bec-widgets
|
||||
tomli
|
||||
@@ -110,14 +110,14 @@ window.show()
|
||||
sys.exit(app.exec())
|
||||
```
|
||||
|
||||
## Writing applications using Qt Designer
|
||||
## Writing applications using BEC Designer
|
||||
|
||||
BEC Widgets are designed to be used with QtDesigner to quickly design GUI.
|
||||
BEC Widgets are designed to be used with BEC Designer to quickly design GUI.
|
||||
|
||||
## Example of promoting widgets in Qt Designer
|
||||
## Example of promoting widgets in BEC Designer
|
||||
|
||||
_Work in progress_
|
||||
|
||||
## Implementation of plugins into Qt Designer
|
||||
## Implementation of plugins into BEC Designer
|
||||
|
||||
_Work in progress_
|
||||
|
||||
@@ -2,81 +2,128 @@
|
||||
# Auto updates
|
||||
BEC Widgets provides a simple way to update the entire GUI configuration based on events. These events can be of different types, such as a new scan being started or completed, a button being pressed, a device reporting an error or the existence of a specific metadata key. This allows the users to streamline the experience of the GUI and to focus on the data and the analysis, rather than on the GUI itself.
|
||||
|
||||
The default auto update only takes control over a single `BECFigure` widget, which is automatically added to the GUI instance. The update instance is accessible via the `bec.gui.auto_updates` object. The user can disable / enable the auto updates by setting the `enabled` attribute of the `bec.gui.auto_updates` object, e.g.
|
||||
The auto update widget can be launched through the BEC launcher:
|
||||
|
||||
```python
|
||||
bec.gui.auto_updates.enabled = False
|
||||
```
|
||||

|
||||
|
||||
Without further customization, the auto update will automatically update the `BECFigure` widget based on the currently performed scan. The behaviour is determined by the `handler` method of the `AutoUpdate` class:
|
||||
The auto update's launch tile also provides a combo box to select the specific auto update to be launched. These options are automatically populated with all available auto updates from a plugin repository and the default auto update.
|
||||
|
||||
Once the proper auto update is selected and launched, the CLI will automatically add a new entry to the `gui` object.
|
||||
|
||||
The default auto update only provides a simple handler that switches between `line_scan`, `grid_scan` and `best_effort`. More details can be found in the following snippet.
|
||||
|
||||
````{dropdown} Auto Updates Handler
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
:open:
|
||||
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
|
||||
:pyobject: AutoUpdates.handler
|
||||
```{literalinclude} ../../../bec_widgets/widgets/containers/auto_update/auto_updates.py
|
||||
:pyobject: AutoUpdates.on_scan_open
|
||||
```
|
||||
````
|
||||
|
||||
As shown, the default handler switches between different scan names and updates the `BECFigure` widget accordingly. If the scan is a line scan, the `simple_line_scan` update method is executed.
|
||||
As shown, the default auto updates switches between different visualizations whenever a new scan is started. If the scan is a `line_scan`, the `simple_line_scan` update method is executed.
|
||||
|
||||
````{dropdown} Auto Updates Simple Line Scan
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
:open:
|
||||
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
|
||||
```{literalinclude} ../../../bec_widgets/widgets/containers/auto_update/auto_updates.py
|
||||
:pyobject: AutoUpdates.simple_line_scan
|
||||
```
|
||||
````
|
||||
|
||||
As it can be seen from the above snippet, the update method gets the default figure by calling the `get_default_figure` method. If the figure is not found, maybe because the user has deleted or closed it, no update is performed. If the figure is found, the scan info is used to extract the first reported device for the x axis and the first device of the monitored devices for the y axis. The y axis can also be set by the user using the `selected_device` attribute:
|
||||
As can be seen from the above snippet, the update method changes the dock to a specific widget, in this case to a waveform widget. After selecting the device for the x axis, the y axis is retrieved from the list of monitored devices or from a user-specified `selected_device`.
|
||||
|
||||
The y axis can also be set by the user using the `selected_device` attribute:
|
||||
|
||||
```python
|
||||
bec.gui.auto_updates.selected_device = 'bpm4i'
|
||||
gui.AutoUpdates.selected_device = 'bpm4i'
|
||||
```
|
||||
|
||||
|
||||
````{dropdown} Auto Updates Code
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
|
||||
```{literalinclude} ../../../bec_widgets/widgets/containers/auto_update/auto_updates.py
|
||||
```
|
||||
````
|
||||
|
||||
## Custom Auto Updates
|
||||
The beamline can customize their default behaviour through customized auto update classes. This can be achieved by modifying the class located in the beamline plugin repository: `<beamline_plugin>/bec_widgets/auto_updates.py`. The class should inherit from the `AutoUpdates` class and overwrite the `handler` method.
|
||||
The beamline can customize their default behaviour through customized auto update classes. This can be achieved by adding an auto update class to the plugin repository: `<beamline_plugin>/bec_widgets/auto_updates/auto_updates.py`. The class must inherit from the `AutoUpdates` class.
|
||||
|
||||
An example of a custom auto update class `PXIIIUpdate` is shown below.
|
||||
|
||||
```{note}
|
||||
The code below is simply a copy of the default auto update class's 'GUI Callbacks' section. The user can modify any of the methods to suit their needs but we suggest to have a look at the 'GUI Callbacks' section and the 'Update Functions' section of the default auto update class to understand how to implement the custom auto update class.
|
||||
```
|
||||
|
||||
```python
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates, ScanInfo
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.messages import ScanStatusMessage
|
||||
|
||||
|
||||
class PlotUpdate(AutoUpdates):
|
||||
create_default_dock = True
|
||||
enabled = True
|
||||
class PXIIIUpdate(AutoUpdates):
|
||||
|
||||
#######################################################################
|
||||
################# GUI Callbacks #######################################
|
||||
#######################################################################
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Procedure to run when the auto updates are enabled.
|
||||
"""
|
||||
self.start_default_dock()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Procedure to run when the auto updates are disabled.
|
||||
"""
|
||||
|
||||
def on_scan_open(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan starts.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
if msg.scan_name == "line_scan" and msg.scan_report_devices:
|
||||
return self.simple_line_scan(msg)
|
||||
if msg.scan_name == "grid_scan" and msg.scan_report_devices:
|
||||
return self.simple_grid_scan(msg)
|
||||
if msg.scan_report_devices:
|
||||
return self.best_effort(msg)
|
||||
return None
|
||||
|
||||
def on_scan_closed(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan ends.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
def on_scan_abort(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan is aborted.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
```
|
||||
|
||||
|
||||
````{important}
|
||||
In order for the custom auto update method to be found, the class must be added to the `__init__.py` file of the `auto_updates` folder. This should be done already when the plugin repository is created but it is worth mentioning here. If not, the user can add the following line to the `__init__.py` file:
|
||||
```python
|
||||
from .auto_updates import *
|
||||
```
|
||||
````
|
||||
|
||||
# def simple_line_scan(self, info: ScanInfo) -> None:
|
||||
# """
|
||||
# Simple line scan.
|
||||
# """
|
||||
# fig = self.get_default_figure()
|
||||
# if not fig:
|
||||
# return
|
||||
# dev_x = info.scan_report_devices[0]
|
||||
# dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
# if not dev_y:
|
||||
# return
|
||||
# fig.clear_all()
|
||||
# plt = fig.plot(x_name=dev_x, y_name=dev_y)
|
||||
# plt.set(title=f"Custom Plot {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
def handler(self, info: ScanInfo) -> None:
|
||||
# EXAMPLES:
|
||||
# if info.scan_name == "line_scan" and info.scan_report_devices:
|
||||
# self.simple_line_scan(info)
|
||||
# return
|
||||
# if info.scan_name == "grid_scan" and info.scan_report_devices:
|
||||
# self.run_grid_scan_update(info)
|
||||
# return
|
||||
super().handler(info)
|
||||
```
|
||||
@@ -28,5 +28,5 @@ pip cache purge
|
||||
This can resolve conflicts or issues with package installations.
|
||||
|
||||
```{warning}
|
||||
At the moment PyQt6 is no longer officially supported by BEC Widgets due to incompatibilities with Qt Designer. Please use PySide6 instead.
|
||||
At the moment PyQt6 is no longer officially supported by BEC Widgets due to incompatibilities with BEC Designer. Please use PySide6 instead.
|
||||
```
|
||||
|
||||
BIN
docs/user/getting_started/launcher.png
Normal file
BIN
docs/user/getting_started/launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 399 KiB |
@@ -1,107 +1,141 @@
|
||||
(user.command_line_introduction)=
|
||||
# Quick start
|
||||
In order to use BEC Widgets as a plotting tool for BEC, it needs to be [installed](#user.installation) in the same Python environment as the BEC IPython client (please refer to the [BEC documentation](https://bec.readthedocs.io/en/latest/user/command_line_interface.html#start-up) for more details). Upon startup, the client will automatically launch a GUI and store it as a `bec.gui` object in the client. The GUI backend will also be automatically connect to the BEC server, giving access to all information on the server and allowing the user to visualize the data in real-time.
|
||||
In order to use BEC Widgets as a plotting tool for BEC, it needs to be [installed](#user.installation) in the same Python environment as the BEC IPython client (please refer to the [BEC documentation](https://bec.readthedocs.io/en/latest/user/command_line_interface.html#start-up) for more details). Upon startup, the client will automatically launch a GUI and store it as a `gui` object in the client. The GUI backend will also be automatically connect to the BEC server, giving access to all information on the server and allowing the user to visualize the data in real-time.
|
||||
|
||||
## BECGuiClient
|
||||
The `gui` object is the main entry point for interacting with the BEC Widgets framework. It is an instance of the [`BECGuiClient`](/api_reference/_autosummary/bec_widgets.cli.client.BECGuiClient) class, which provides methods to create and manage GUI components. Upon BEC startup, a default [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance named *bec* is automatically launched.
|
||||
|
||||
A launcher interface is available via the top menu bar under New → Open Launcher. This opens a window where users can launch a new [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance, an [AutoUpdate](#user.auto_updates) instance, individual widgets or a custom *ui file* created with *BEC Designer*. Alternatively, users can launch a new [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) from the command line:
|
||||
|
||||
```python
|
||||
dock_area = gui.new() # launches a new BECDockArea instance
|
||||
gui.new('my_dock_area') # launches a new BECDockArea instance with the name 'my_dock_area'
|
||||
dock_area2 = gui.my_dock_area # Dynamic attribute access to created dock_area
|
||||
```
|
||||
|
||||
``` {note}
|
||||
If a name is provided, the new dock area will use that name. If the name already exists, an error is raised. If no name is specified, a name will be auto-generated following the pattern *dock_area_ii* where *ii* is the next available number. Named dock areas can be accessed dynamically as attributes of the gui object.
|
||||
```
|
||||
|
||||
|
||||
## BECDockArea
|
||||
The `bec.gui` object is your entry point to BEC Widgets. It is a [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance that can be composed of multiple [`BECDock`](/api_reference/_autosummary/bec_widgets.cli.client.BECDock)s that can be attached / detached to the main area. These docks allow users to freely arrange and customize the widgets they add to the gui, providing a flexible and customizable interface to visualize data.
|
||||
The [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) is a versatile container for quickly building customized GUIs. It supports adding new widgets either through the CLI or directly via toolbar actions. Widgets must be added into [`BECDock`](/api_reference/_autosummary/bec_widgets.cli.client.BECDock) instances, which serve as the individual containers. These docks can be arranged freely, detached from the main window, and used as floating panels.
|
||||
|
||||
**Schema of the BECDockArea**
|
||||
From the CLI, you can create new docks like this:
|
||||
|
||||

|
||||
```python
|
||||
dock_area = gui.new()
|
||||
dock = dock_area.new(name='my_dock_area')
|
||||
dock = gui.new().new()
|
||||
```
|
||||
|
||||
<!-- **Schema of the BECDockArea**
|
||||
|
||||
 -->
|
||||
|
||||
## Widgets
|
||||
Widgets are the building blocks of the BEC Widgets framework. They are the visual components that allow users to interact with the data and control the behavior of the application. Each dock can contain multiple widgets, albeit we recommend for most use cases a single widget per dock. BEC Widgets provides a set of core widgets (cf. [widgets](#user.widgets)). More widgets can be added by the users, and we invite you to explore the [developer documentation](developer.widgets) to learn how to create custom widgets.
|
||||
For the introduction given here, we will focus on the `BECFigure` widget, as it is the most commonly used widget for visualizing data from BEC. The same access pattern can be used for all other widgets.
|
||||
|
||||
**BECFigure**
|
||||
|
||||
The [`BECFigure`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure) widget is one of the core widgets developed for BEC and can be used to visualize different plot types, such as [1D waveforms](user.widgets.waveform_1d), [2D scatter plots](user.widgets.scatter_2d), [position maps](user.widgets.motor_map) and [2D images](user.widgets.image_2d).
|
||||
If BEC Widgets is installed, the default behaviour of BEC is to automatically add a BECFigure Widget to the existing GUI instance. This widget is directly accessible via the `fig` object from the client. Moreover, a best-effort attempt is made to automatically determine the best plot type based on the currently performed scan. This behaviour can be changed or disabled by the user. For more details, please refer to the [auto update](user.auto_updates) section.
|
||||
For the introduction given here, we will focus on the plotting widgets of BECWidgets.
|
||||
|
||||
<!-- We also provide two methods [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot), [`image()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.image) and [`motor_map()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.motor_map) as shortcuts to add a plot, image or motor map to the BECFigure. -->
|
||||
|
||||
**Waveform Plot**
|
||||
|
||||
The [`BECWaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform) is a widget that can be used to visualize 1D waveform data, i.e. to plot data of a monitor against a motor position. The method [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot) of BECFigure adds a BECWaveForm widget to the figure, and returns the plot object.
|
||||
The [`WaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm) is a widget that can be used to visualize 1D waveform data, i.e. to plot data of a monitor against a motor position. The method [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm.rst#bec_widgets.cli.client.WaveForm.plot) returns the plot object.
|
||||
|
||||
```python
|
||||
plt = fig.plot(x_name='samx', y_name='bpm4i')
|
||||
plt = gui.new().new().new(gui.available_widgets.Waveform)
|
||||
plt.plot(x_name='samx', y_name='bpm4i')
|
||||
```
|
||||
Here, we create a new plot with a subscription to the devices `samx` and `bpm4i` and assign the plot to the object `plt`. We can now use this object to further customize the plot, e.g. changing the title ([`set_title()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_title)), axis labels ([`set_x_label()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_x_label)) or limits ([`set_x_lim()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_x_lim)). We invite you to explore the API of the BECWaveForm in the [documentation](user.widgets.waveform_1d) or directly in the command line.
|
||||
Here, we create a new plot with a subscription to the devices `samx` and `bpm4i` and assign the plot to the object `plt`. We can now use this object to further customize the plot, e.g. changing the title ([`plt.title = 'my title' `](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.title)), axis labels ([`plt.x_label = 'my x label'`](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.x_label))
|
||||
<!-- or limits ([`set_x_lim()`](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.x_lim)). -->
|
||||
|
||||
We invite you to explore the API of the WaveForm in the [documentation](user.widgets.waveform_1d) or directly in the command line.
|
||||
|
||||
To plot custom data, i.e. data that is not directly available through a scan in BEC, we can use the same method, but provide the data directly to the plot.
|
||||
|
||||
```python
|
||||
plt = fig.plot([1,2,3,4], [1,4,9,16])
|
||||
plt = gui.new().new().new(gui.available_widgets.Waveform)
|
||||
#
|
||||
plt.plot([1,2,3,4], [1,4,9,16])
|
||||
# or
|
||||
plt = fig.plot(x=[1,2,3,4], y=[1,4,9,16])
|
||||
plt.plot(x=np.array([1,2,3,4]), y=np.array([1,4,9,16]))
|
||||
# or
|
||||
plt = fig.plot(x=np.array([1,2,3,4]), y=np.array([1,4,9,16]))
|
||||
# or
|
||||
plt = fig.plot(np.random.rand(10,2))
|
||||
plt.plot(np.random.rand(10,2))
|
||||
# or if you like to receive the custom curve item
|
||||
curve = plt.plot(x=[1,2,3,4], y=[1,4,9,16])
|
||||
```
|
||||
|
||||
**Scatter Plot**
|
||||
|
||||
The [`BECWaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveForm) widget can also be used to visualize 2D scatter plots. More details on setting up the scatter plot are available in the widget documentation of the [scatter plot](user.widgets.scatter_2d).
|
||||
The [`WaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm) widget can also be used to visualize 2D scatter plots. More details on setting up the scatter plot are available in the widget documentation of the [scatter plot](user.widgets.scatter_2d).
|
||||
|
||||
**Motor Map**
|
||||
|
||||
The [`BECMotorMap`](/api_reference/_autosummary/bec_widgets.cli.client.BECMotorMap) widget can be used to visualize the position of motors. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans. More details on setting up the motor map are available in the widget documentation of the [motor map](user.widgets.motor_map).
|
||||
The [`MotorMap`](/api_reference/_autosummary/bec_widgets.cli.client.MotorMap) widget can be used to visualize the position of motors. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans. More details on setting up the motor map are available in the widget documentation of the [motor map](user.widgets.motor_map).
|
||||
|
||||
**Image Plot**
|
||||
|
||||
The [`BECImageItem`](/api_reference/_autosummary/bec_widgets.cli.client.BECImageItem) widget can be used to visualize 2D image data for example a camera. More details on setting up the image plot are available in the widget documentation of the [image plot](user.widgets.image).
|
||||
The [`Image`](/api_reference/_autosummary/bec_widgets.cli.client.Image) widget can be used to visualize 2D image data for example a camera. More details on setting up the image plot are available in the widget documentation of the [image plot](user.widgets.image).
|
||||
|
||||
### Useful Commands
|
||||
We recommend users to explore the API of the widgets by themselves since we assume that the user interface is supposed to be intuitive and self-explanatory. We appreciate feedback from user in order to constantly improve the experience and allow easy access to the gui, widgets and their functionality. We recommend checking the [API documentation](user.api_reference), but also by using BEC Widgets, exploring the available functions and check their dockstrings.
|
||||
```python
|
||||
gui.add_dock? # shows the dockstring of the add_dock method
|
||||
gui.new? # shows the dockstring of the new method
|
||||
```
|
||||
|
||||
In addition, we list below a few useful commands that can be used to interface with the widgets:
|
||||
|
||||
```python
|
||||
gui.panels # returns a dictionary of all docks in the gui
|
||||
gui.add_dock() # adds a new dock to the gui
|
||||
gui.windows # Returns a dictionary of all dock areas in the GUI
|
||||
gui.new() # Adds a new BECDockArea to the GUI
|
||||
|
||||
dock = gui.panels['dock_2']
|
||||
dock.add_widget('BECFigure') # adds a new widget of BECFigure to the dock
|
||||
dock.widget_list # returns a list of all widgets in the dock
|
||||
dock_area = gui.windows['bec']
|
||||
dock = dock_area.new('my_dock') # Adds a new dock named 'my_dock' to the dock area
|
||||
plt = dock.new(gui.available_widgets.Waveform) # Adds a Waveform widget to the dock
|
||||
|
||||
figure = dock.widget_list[0] # assigns the created BECFigure to figure
|
||||
plt = figure.plot(x_name='samx', y_name='bpm4i') # adds a BECWaveForm plot to the figure
|
||||
plt.curves # returns a list of all curves in the plot
|
||||
# Alternative syntax:
|
||||
# dock_area.my_dock.new(gui.available_widgets.Waveform)
|
||||
|
||||
dock.element_list # Returns a list of all widgets in the dock
|
||||
dock.elements # Equivalent to dock.element_list
|
||||
|
||||
plt.curves # Returns a list of all curves in the plot
|
||||
```
|
||||
|
||||
We note that commands can also be chained. For example, `gui.add_dock().add_widget('BECFigure')` will add a new dock to the gui and add a new widget of `BECFigure` to the dock.
|
||||
We note that commands can also be chained. For example, `gui.new().new().new(gui.available_widgets.Waveform)` will add a new dock_area to the gui and add a new dock with a `Waveform` widget to the dock.
|
||||
|
||||
## Composing a larger GUI
|
||||
The example given above introduces BEC Widgets with its different components, and provides an overview of how to interact with the widgets. Nevertheless, another power aspect of BEC Widgets lies in the ability to compose a larger GUI with multiple docks and widgets. This section aims to provide a tutorial like guide on how to compose a more complex GUI that (A) live-plots a 1D waveform, (B) plots data from a camera, and (C) tracks the positions of two motors.
|
||||
Let's assume BEC was just started and the `gui` object is available in the client. A single dock is already attached together with a BEC Figure. Let's add the 1D waveform to this dock, change the color of the line to white and add the title *1D Waveform* to the plot.
|
||||
|
||||
```python
|
||||
plt = fig.plot(x_name='samx', y_name='bpm4i')
|
||||
dock_area = gui.new()
|
||||
plt = dock_area.new().new(gui.available_widgets.Waveform)
|
||||
plt.plot(x_name='samx', y_name='bpm4i')
|
||||
plt.curves[0].set_color(color="white")
|
||||
plt.set_title('1D Waveform')
|
||||
plt.title = '1D Waveform'
|
||||
```
|
||||
|
||||
Next, we add 2 new docks to the gui, one to plot the data of a camera and one to track the positions of two motors.
|
||||
```ipython
|
||||
cam_widget= gui.add_dock(name="cam_dock").add_widget('BECFigure').image("eiger")
|
||||
motor_widget = gui.add_dock(name="mot_dock").add_widget('BECFigure').motor_map("samx", "samy")
|
||||
cam_widget= dock_area.new().new(gui.available_widgets.Image)
|
||||
cam_widget.image("eiger")
|
||||
motor_widget = dock_area.new().new(gui.available_widgets.MotorMap)
|
||||
motor_widget.map("samx", "samy")
|
||||
```
|
||||
Note, we chain commands here which is possible since the `add_dock` and `add_widget` methods return the dock and the widget respectively. We can now further customize the widgets by changing the title, axis labels, etc.
|
||||
|
||||
Note, we chain commands here which is possible since the `new()` and `new()` methods return the dock and the widget respectively. We can now further customize the widgets by changing the title, axis labels, etc.
|
||||
|
||||
```python
|
||||
cam_widget.set_title("Camera Image Eiger")
|
||||
cam_widget.set_vrange(vmin=0, vmax=100)
|
||||
cam_widget.title = "Camera Image Eiger"
|
||||
cam_widget.vrange = [0, 100]
|
||||
```
|
||||
As a final step, we can now add also a RingProgressBar to a new dock, and perform a grid_scan with the motors *samx* and *samy*.
|
||||
As you see in the example below, all docks are arranged below each other. This is the default behavior of the `add_dock` method. However, the docks can be freely arranged by drag and drop as desired by the user. We invite you to explore this by yourself following the example in the video, and build your custom GUI with BEC Widgets.
|
||||
As you see in the example below, all docks are arranged below each other. This is the default behavior of the `new()` method. However, the docks can be freely arranged by drag and drop as desired by the user. We invite you to explore this by yourself following the example in the video, and build your custom GUI with BEC Widgets.
|
||||
|
||||
```python
|
||||
prog_bar = gui.add_dock(name="prog_dock").add_widget('RingProgressBar')
|
||||
prog_bar = dock_area.new().new(gui.available_widgets.RingProgressBar)
|
||||
prog_bar.set_line_widths(15)
|
||||
scans.grid_scan(dev.samy, -2, 2, 10, dev.samx, -5, 5, 10, exp_time=0.1, relative=False)
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
(user.plugin_widgets)=
|
||||
# PLugin repository widgets
|
||||
# Plugin repository widgets
|
||||
|
||||
## Adding widgets to the plugin repository
|
||||
|
||||
@@ -44,7 +44,7 @@ class TestWidget(BECWidget, QWidget):
|
||||
### Generating the plugin files and RPC client template
|
||||
|
||||
To allow the BEC client to communicate with the GUI server and to know which widgets are available,
|
||||
as well as to allow the Qt Designer to find the available widgets, a code generation tool should be
|
||||
as well as to allow the BEC Designer to find the available widgets, a code generation tool should be
|
||||
run to prepare a client file which lists all the available widget classes and functions. Make sure
|
||||
you are in the BEC python environment where your plugin repository is also installed, and run:
|
||||
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
# User
|
||||
Welcome to the User section of the BEC Widgets documentation! BEC Widgets is a versatile GUI framework tailored for beamline scientists, enabling efficient and intuitive interaction with beamline experiments. This section is designed to guide both new and experienced users through the essential aspects of utilizing BEC Widgets.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
hidden: false
|
||||
---
|
||||
|
||||
customisation.md
|
||||
plugin_widgets.md
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
@@ -14,7 +24,6 @@ widgets/widgets.md
|
||||
api_reference/api_reference.md
|
||||
```
|
||||
|
||||
|
||||
***
|
||||
|
||||
````{grid} 2
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -1,105 +0,0 @@
|
||||
(user.widgets.bec_figure)=
|
||||
# BECFigure
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
[`BECFigure`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure) is a robust framework that provides a fast, flexible plotting environment, similar to the Matplotlib figure. With BECFigure, users can dynamically change layouts, add or remove subplots, and customize their plotting environment in real-time. This flexibility makes BECFigure an ideal tool for both rapid prototyping and detailed data visualization.
|
||||
|
||||
- **Dynamic Layout Management**: Easily add, remove, and rearrange subplots within `BECFigure`, enabling tailored visualization setups.
|
||||
- **Widget Integration**: Incorporate various specialized widgets like [`WaveformWidget`](user.widgets.waveform_widget), [`ImageWidget`](user.widgets.image_widget) , and [`MotorMapWidget`](user.widgets.motor_map) into `BECFigure`. Note that these widgets can also be used individually. For more details, please refer to the documentation for each individual widget.
|
||||
- **Interactive Controls**: Provides interactive tools for zooming, panning, and adjusting plots on the fly, streamlining the data exploration process.
|
||||
|
||||
**Schema of the BECFigure components**
|
||||
|
||||

|
||||
|
||||
````
|
||||
|
||||
````{tab} Examples - CLI
|
||||
In the following examples, we will use `BECIPythonClient` with a predefined `BECDockArea` as the `gui` object. These tutorials focus on how to work with the `BECFigure` framework, such as changing layouts, adding new elements, and accessing them. For more detailed examples of each individual component, please refer to the example sections of each individual widget: [`WaveformWidget`](user.widgets.waveform_widget), [`MotorMapWidget`](user.widgets.motor_map), [`ImageWidget`](user.widgets.image_widget).
|
||||
|
||||
## Example 1 - Adding subplots to BECFigure
|
||||
|
||||
In this example, we will demonstrate how to add different subplots to a single `BECFigure` widget.
|
||||
|
||||
```python
|
||||
# Add a new dock with BECFigure widget
|
||||
fig = gui.add_dock().add_widget('BECFigure')
|
||||
|
||||
# Add a WaveformWidget to the BECFigure
|
||||
plt1 = fig.plot(x_name='samx', y_name='bpm4i')
|
||||
|
||||
# Add a second WaveformWidget to the BECFigure, specifying new=True to add it as a new subplot
|
||||
plt2 = fig.plot(x_name='samx', y_name='bpm3i', new=True)
|
||||
|
||||
# Add a MotorMapWidget to the BECFigure
|
||||
mm = fig.motor_map(motor_x='samx', motor_y='samy')
|
||||
|
||||
# Add an ImageWidget to the BECFigure
|
||||
img = fig.image('eiger')
|
||||
```
|
||||
```{note}
|
||||
By default, the [`.plot`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot), [`.image`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.image), and [`.motor_map`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.motor_map) methods always find the first widget of that type in the layout and interact with it. If you want to add a new subplot of the same type, you must either specify the coordinates of the new subplot or use the `new=True` keyword argument, as shown above when adding the second WaveformWidget. Additionally, you can directly add a subplot to a specific, unoccupied position in the layout by specifying the `row` and `col` arguments, such as `fig.plot(x_name='samx', y_name='bpm4i', row=1, col=1)`.
|
||||
```
|
||||
|
||||
## Example 2 - Changing the layout of BECFigure
|
||||
|
||||
The previous example added four subplots into a single `BECFigure` widget. By default, new widgets are always added to the bottom of the BECFigure. However, you can change the layout of the BECFigure by using the [`change_layout`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.change_layout) method, specifying the number of rows and/or columns.
|
||||
|
||||
```python
|
||||
# Change the layout of the BECFigure to have 4 columns -> 4x1 matrix layout
|
||||
fig.change_layout(max_columns=4)
|
||||
|
||||
# Change the layout of the BECFigure to have 2 rows -> 2x2 matrix layout
|
||||
fig.change_layout(max_rows=2)
|
||||
```
|
||||
|
||||
## Example 3 - Accessing Subplots in BECFigure
|
||||
|
||||
The subplots in BECFigure can be accessed in a similar way to Matplotlib figures using the [`axes`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.axes) property. Each subplot can be accessed by its index coordinates within the layout, specified by the row and column index (starting at 0). In following example, we will access the subplots and modify their titles. The layout is a 2x2 matrix, so the subplots are indexed as follows:
|
||||
|
||||
```python
|
||||
# Access the first subplot in the first row and first column (0, 0)
|
||||
subplot1 = fig.axes(0, 0)
|
||||
|
||||
# Access the second subplot in the first row and second column (0, 1)
|
||||
subplot2 = fig.axes(0, 1)
|
||||
|
||||
# Access the first subplot in the second row and first column (1, 0)
|
||||
subplot3 = fig.axes(1, 0)
|
||||
|
||||
# Example: Set title for the first subplot
|
||||
subplot1.set_title("Waveform 1")
|
||||
|
||||
# Example: Set title for the second subplot
|
||||
subplot2.set_title("Waveform 2")
|
||||
|
||||
# Example: Set title for the third subplot
|
||||
subplot3.set_title("Motor Map")
|
||||
```
|
||||
|
||||
In this example, we accessed three different subplots based on their row and column positions and modified their titles.
|
||||
|
||||
## Example 4 - Removing Subplots from BECFigure
|
||||
|
||||
You may want to remove certain subplots from the `BECFigure`. This can be done using the [`remove`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.remove) method, which takes the row and column index of the subplot you want to remove. The [`remove`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.remove) method could be also called on the subplot itself.
|
||||
|
||||
```python
|
||||
|
||||
# Remove the subplot in the second row and second column (1, 1)
|
||||
fig.remove(1, 1)
|
||||
|
||||
# Remove the subplot in the first row and first column (0, 0)
|
||||
fig.remove(0, 0)
|
||||
|
||||
# Remove previously accessed subplot plt2 from Example 1
|
||||
plt2.remove()
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst
|
||||
```
|
||||
````
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
```{tab} Overview
|
||||
|
||||
The BEC Progressbar widget is a general purpose progress bar that follows the BEC theme and style. It can be embedded in any application to display the progress of a task or operation.
|
||||
The [`BECProgressbar`](/api_reference/_autosummary/bec_widgets.cli.client.BECProgressBar) widget is a general purpose progress bar that follows the BEC theme and style. It can be embedded in any application to display the progress of a task or operation.
|
||||
|
||||
## Key Features:
|
||||
- **Modern Design**: The BEC Progressbar widget is designed with a modern and sleek appearance, following the BEC theme.
|
||||
@@ -18,15 +18,16 @@ The BEC Progressbar widget is a general purpose progress bar that follows the BE
|
||||
|
||||
````{tab} Examples
|
||||
|
||||
The `BECProgressBar` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `BECProgressBar` widget.
|
||||
The `BECProgressBar` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`. Below are examples demonstrating how to create and use the `BECProgressBar` widget.
|
||||
|
||||
## Example 1 - Adding BEC Status Box to BECDockArea
|
||||
|
||||
In this example, we demonstrate how to add a `BECProgressBar` widget to a `BECDockArea`, allowing users to manually set and update the progress states.
|
||||
|
||||
```python
|
||||
# Add a new dock with a BECStatusBox widget
|
||||
pb = gui.add_dock().add_widget("BECProgressBar")
|
||||
# Add a new dock with a BEC Progressbar widget
|
||||
dock_area = gui.new()
|
||||
pb = dock_area.new().new(gui.available_widgets.BECProgressBar)
|
||||
pb.set_value(50)
|
||||
```
|
||||
|
||||
@@ -34,6 +35,6 @@ pb.set_value(50)
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECProgressbar.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECProgressBar.rst
|
||||
```
|
||||
````
|
||||
|
||||
@@ -15,7 +15,7 @@ The [`BEC Status Box`](/api_reference/_autosummary/bec_widgets.cli.client.BECSta
|
||||
|
||||
````{tab} Examples
|
||||
|
||||
The `BECStatusBox` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `BECStatusBox` widget.
|
||||
The `BECStatusBox` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`. Below are examples demonstrating how to create and use the `BECStatusBox` widget.
|
||||
|
||||
## Example 1 - Adding BEC Status Box to BECDockArea
|
||||
|
||||
@@ -23,7 +23,7 @@ In this example, we demonstrate how to add a `BECStatusBox` widget to a `BECDock
|
||||
|
||||
```python
|
||||
# Add a new dock with a BECStatusBox widget
|
||||
bec_status_box = gui.add_dock().add_widget("BECStatusBox")
|
||||
sb = gui.bec.new().new(widget=gui.available_widgets.BECStatusBox)
|
||||
```
|
||||
|
||||
```{hint}
|
||||
|
||||
@@ -23,7 +23,7 @@ The `Dark Mode Button` is a toggle control that allows users to switch between l
|
||||
|
||||
**Key Features:**
|
||||
- **Theme Switching**: Enables users to switch between light and dark themes with a single click.
|
||||
- **Configurable from BECDesigner**: The defaults for the dark mode can be set in the BECDesigner, allowing users to customize the startup appearance of the GUI.
|
||||
- **Configurable from BEC Designer**: The defaults for the dark mode can be set in the BEC Designer, allowing users to customize the startup appearance of the GUI.
|
||||
|
||||
## Color Button
|
||||
|
||||
@@ -48,7 +48,7 @@ The `Colormap Button` is a custom widget that displays the current colormap and,
|
||||
- **Current Colormap Display**: Shows the name and a gradient icon of the current colormap directly on the button.
|
||||
- **Nested Menu Selection**: Offers a nested menu with categorized colormaps, making it easy to find and select the desired colormap.
|
||||
- **Signal Emission**: Emits a signal when the colormap changes, providing the new colormap name as a string.
|
||||
- **Qt Designer Integration**: Exposes properties and signals to be used within Qt Designer, allowing for customization within the designer interface.
|
||||
- **BEC Designer Integration**: Exposes properties and signals to be used within BEC Designer, allowing for customization within the designer interface.
|
||||
- **Resizable and Styled**: Features adjustable size policies and styles to match the look and feel of standard `QPushButton` widgets, including rounded edges.
|
||||
`````
|
||||
|
||||
@@ -63,12 +63,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
|
||||
from bec_widgets.widgets.buttons import DarkModeButton
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setLayout(QVBoxLayout(self))
|
||||
|
||||
# Create and add the DarkModeButton to the layout
|
||||
self.dark_mode_button = DarkModeButton()
|
||||
self.dark_mode_button = DarkModeButton(parent=self)
|
||||
self.layout().addWidget(self.dark_mode_button)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
@@ -83,12 +83,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
|
||||
from bec_widgets.widgets.buttons import ColorButton
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setLayout(QVBoxLayout(self))
|
||||
|
||||
# Create and add the ColorButton to the layout
|
||||
self.color_button = ColorButton()
|
||||
self.color_button = ColorButton(self)
|
||||
self.layout().addWidget(self.color_button)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
@@ -103,12 +103,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
|
||||
from bec_widgets.widgets.buttons import ColormapSelector
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setLayout(QVBoxLayout(self))
|
||||
|
||||
# Create and add the ColormapSelector to the layout
|
||||
self.colormap_selector = ColormapSelector()
|
||||
self.colormap_selector = ColormapSelector(self)
|
||||
self.layout().addWidget(self.colormap_selector)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
@@ -123,12 +123,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
|
||||
from bec_widgets.widgets.buttons import ColormapButton
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setLayout(QVBoxLayout(self))
|
||||
|
||||
# Create and add the ColormapButton to the layout
|
||||
self.colormap_button = ColormapButton()
|
||||
self.colormap_button = ColormapButton(self)
|
||||
self.layout().addWidget(self.colormap_button)
|
||||
|
||||
# Connect the signal to handle colormap changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user