1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-11 03:00:54 +02:00

Compare commits

...

40 Commits

Author SHA1 Message Date
a275c5a3b1 wip 2026-03-15 14:31:39 +01:00
b12e6432d6 wip 2026-03-15 13:59:43 +01:00
b802beec69 wip 2026-03-15 13:53:37 +01:00
6c40df4380 wip 2026-03-15 13:36:13 +01:00
dc8ae5a85d wip 2026-03-15 13:31:45 +01:00
2d41a158bc wip 2026-03-15 13:25:42 +01:00
c2a6964875 wip 2026-03-15 13:18:15 +01:00
774ca08cb3 wip 2026-03-15 13:06:18 +01:00
4606bbd570 wip 2026-03-15 13:01:44 +01:00
b2ed9d42f7 wip 2026-03-15 12:57:19 +01:00
d799691e12 wip 2026-03-15 12:47:58 +01:00
6848a9e20b test(e2e): avoid timing issues in rpc_gui_obj test 2026-03-15 12:30:40 +01:00
semantic-release
bd5aafc052 3.2.0
Automatically generated by python-semantic-release
2026-03-11 20:52:57 +00:00
b4f6f5aa8b feat(waveform): composite DAP with multiple models 2026-03-11 21:52:10 +01:00
14d51b8016 feat(curve, waveform): add dap_parameters for lmfit customization in DAP requests 2026-03-11 21:52:10 +01:00
semantic-release
e94554b471 3.1.4
Automatically generated by python-semantic-release
2026-03-11 11:58:34 +00:00
7e0e391888 build: increased minimal version of bec and bec qthemes 2026-03-11 12:57:40 +01:00
53e5ec42b8 fix(profile_utils): renamed to fetch widgets settings 2026-03-11 12:57:40 +01:00
semantic-release
0e49828a23 3.1.3
Automatically generated by python-semantic-release
2026-03-09 08:46:29 +00:00
278d8de058 fix(monaco_dock): optimization, removal of QTimer, eventFilter replaced by signal/slot 2026-03-09 09:45:40 +01:00
semantic-release
cb4c2beed4 3.1.2
Automatically generated by python-semantic-release
2026-03-06 15:34:15 +00:00
4382d5c9b1 fix(dock_area): remove old AdvancedDockArea references 2026-03-06 16:33:23 +01:00
8463b32792 build(deps): update isort requirement 2026-03-06 12:02:48 +01:00
semantic-release
5aff336446 3.1.1
Automatically generated by python-semantic-release
2026-03-06 10:47:17 +00:00
e2daf2e89c build: update min bec dependency to 3.106 2026-03-06 11:46:29 +01:00
ef1233163c test: fix import of bec_lib json extended 2026-03-06 11:46:29 +01:00
419c01bdd4 fix(positioner box): include username in scan queue request 2026-03-06 11:46:29 +01:00
d4e037f338 refactor(black): black 26 applied 2026-03-06 11:25:37 +01:00
e157f0d7c9 build(deps): upgrade to black 26 2026-03-06 11:25:37 +01:00
semantic-release
2136699806 3.1.0
Automatically generated by python-semantic-release
2026-03-06 10:00:11 +00:00
75697f5b1f test: adjust metadata assertions to new schema defaults 2026-03-06 10:59:24 +01:00
2697496515 test: adjust metadata assertions to new schema defaults 2026-03-06 10:59:24 +01:00
56f16b6352 feat(bec_queue): add tooltip support for user metadata in queue display 2026-03-06 10:59:24 +01:00
ab3efdbd0a feat(scan queue): add scan name to queue 2026-03-06 10:59:24 +01:00
0b94ee1485 tests: fix tests to match bec core changes 2026-03-06 10:59:24 +01:00
72e66cf57f fix(scan metadata): set scan_name to current scan if empty in form data 2026-03-06 10:59:24 +01:00
e6b41b4e92 feat(scan control): wrap metadata form in a group box for better organization 2026-03-06 10:59:24 +01:00
ac824f6b83 feat(StrFormItem): set placeholder text from spec description 2026-03-06 10:59:24 +01:00
5e34c8a351 fix(forms): use FieldInfo title for label text in _add_griditem method 2026-03-06 10:59:24 +01:00
semantic-release
29ff45a24b 3.0.0
Automatically generated by python-semantic-release
2026-03-06 08:57:30 +00:00
53 changed files with 1801 additions and 706 deletions

View File

@@ -1,19 +1,19 @@
name: Full CI
on:
on:
push:
pull_request:
workflow_dispatch:
inputs:
BEC_WIDGETS_BRANCH:
description: 'Branch of BEC Widgets to install'
description: "Branch of BEC Widgets to install"
required: false
type: string
BEC_CORE_BRANCH:
description: 'Branch of BEC Core to install'
description: "Branch of BEC Core to install"
required: false
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch of Ophyd Devices to install'
description: "Branch of Ophyd Devices to install"
required: false
type: string
@@ -25,60 +25,58 @@ permissions:
pull-requests: write
jobs:
check_pr_status:
uses: ./.github/workflows/check_pr.yml
# check_pr_status:
# uses: ./.github/workflows/check_pr.yml
formatter:
needs: check_pr_status
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/formatter.yml
# formatter:
# needs: check_pr_status
# if: needs.check_pr_status.outputs.branch-pr == ''
# uses: ./.github/workflows/formatter.yml
unit-test:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/pytest.yml
with:
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref }}
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# unit-test:
# needs: [check_pr_status, formatter]
# if: needs.check_pr_status.outputs.branch-pr == ''
# uses: ./.github/workflows/pytest.yml
# with:
# BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref }}
# BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
# OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}
# secrets:
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
unit-test-matrix:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/pytest-matrix.yml
with:
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha}}
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}
# unit-test-matrix:
# needs: [check_pr_status, formatter]
# if: needs.check_pr_status.outputs.branch-pr == ''
# uses: ./.github/workflows/pytest-matrix.yml
# with:
# BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha}}
# BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
# OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}
generate-cli-test:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/generate-cli-check.yml
# generate-cli-test:
# needs: [check_pr_status, formatter]
# if: needs.check_pr_status.outputs.branch-pr == ''
# uses: ./.github/workflows/generate-cli-check.yml
end2end-test:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/end2end-conda.yml
child-repos:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/child_repos.yml
with:
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main'}}
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
plugin_repos:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: bec-project/bec/.github/workflows/plugin_repos.yml@main
with:
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
# child-repos:
# needs: [check_pr_status, formatter]
# if: needs.check_pr_status.outputs.branch-pr == ''
# uses: ./.github/workflows/child_repos.yml
# with:
# BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
# OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main'}}
# BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
secrets:
GH_READ_TOKEN: ${{ secrets.GH_READ_TOKEN }}
# plugin_repos:
# needs: [check_pr_status, formatter]
# if: needs.check_pr_status.outputs.branch-pr == ''
# uses: bec-project/bec/.github/workflows/plugin_repos.yml@main
# with:
# BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
# BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
# secrets:
# GH_READ_TOKEN: ${{ secrets.GH_READ_TOKEN }}

View File

@@ -48,12 +48,14 @@ jobs:
source ./bin/install_bec_dev.sh -t
cd ../
pip install -e ./ophyd_devices -e .[dev,pyside6] -e ./bec_testing_plugin
pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end
pip install pytest-repeat
pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end/user_interaction/test_user_interaction_e2e.py
- name: Upload logs if job fails
if: failure()
uses: actions/upload-artifact@v4
with:
name: pytest-logs
path: ./logs/*.log
path: ./bec/logs/*.log
retention-days: 7

View File

@@ -1,6 +1,702 @@
# CHANGELOG
## v3.2.0 (2026-03-11)
### Features
- **curve, waveform**: Add dap_parameters for lmfit customization in DAP requests
([`14d51b8`](https://github.com/bec-project/bec_widgets/commit/14d51b80169f5a060dd24287f3a6db9a4b41275a))
- **waveform**: Composite DAP with multiple models
([`b4f6f5a`](https://github.com/bec-project/bec_widgets/commit/b4f6f5aa8bcd0f6091610e8f839ea265c87575e0))
## v3.1.4 (2026-03-11)
### Bug Fixes
- **profile_utils**: Renamed to fetch widgets settings
([`53e5ec4`](https://github.com/bec-project/bec_widgets/commit/53e5ec42b8b33397af777f418fbd8601628226a6))
### Build System
- Increased minimal version of bec and bec qthemes
([`7e0e391`](https://github.com/bec-project/bec_widgets/commit/7e0e391888f2ee4e8528ccb3938e36da4c32f146))
## v3.1.3 (2026-03-09)
### Bug Fixes
- **monaco_dock**: Optimization, removal of QTimer, eventFilter replaced by signal/slot
([`278d8de`](https://github.com/bec-project/bec_widgets/commit/278d8de058c2f5c6c9aa7317e1026651d7a4acd3))
## v3.1.2 (2026-03-06)
### Bug Fixes
- **dock_area**: Remove old AdvancedDockArea references
([`4382d5c`](https://github.com/bec-project/bec_widgets/commit/4382d5c9b1fdac4048692eec53dd43127d67467b))
### Build System
- **deps**: Update isort requirement
([`8463b32`](https://github.com/bec-project/bec_widgets/commit/8463b327923f853cfa1462bc22be1e83d4fd9a75))
## v3.1.1 (2026-03-06)
### Bug Fixes
- **positioner box**: Include username in scan queue request
([`419c01b`](https://github.com/bec-project/bec_widgets/commit/419c01bdd4e80d927761634b03723319b0a58694))
### Build System
- Update min bec dependency to 3.106
([`e2daf2e`](https://github.com/bec-project/bec_widgets/commit/e2daf2e89cd25d4dcedd4895299dbbdc6b7e354f))
- **deps**: Upgrade to black 26
([`e157f0d`](https://github.com/bec-project/bec_widgets/commit/e157f0d7c9bb5b4d93f63ebe6f9a715a314aa1f4))
### Refactoring
- **black**: Black 26 applied
([`d4e037f`](https://github.com/bec-project/bec_widgets/commit/d4e037f3384765e7bb8fb020cecbf3db24fc7494))
### Testing
- Fix import of bec_lib json extended
([`ef12331`](https://github.com/bec-project/bec_widgets/commit/ef1233163cb7c3229630543fe88dbceaccd09297))
## v3.1.0 (2026-03-06)
### Bug Fixes
- **forms**: Use FieldInfo title for label text in _add_griditem method
([`5e34c8a`](https://github.com/bec-project/bec_widgets/commit/5e34c8a3518f24722267b3cde2dd9d3494e350b0))
- **scan metadata**: Set scan_name to current scan if empty in form data
([`72e66cf`](https://github.com/bec-project/bec_widgets/commit/72e66cf57f1d47851728448e2e0f776cd8e278f2))
### Features
- **bec_queue**: Add tooltip support for user metadata in queue display
([`56f16b6`](https://github.com/bec-project/bec_widgets/commit/56f16b63528b5a50f5a2e2d2e9dd93f3993e50e4))
- **scan control**: Wrap metadata form in a group box for better organization
([`e6b41b4`](https://github.com/bec-project/bec_widgets/commit/e6b41b4e92a1ffd0494c2bde6a782347c2364114))
- **scan queue**: Add scan name to queue
([`ab3efdb`](https://github.com/bec-project/bec_widgets/commit/ab3efdbd0a0a80293ba2121e78ea319ddbbd8f82))
- **StrFormItem**: Set placeholder text from spec description
([`ac824f6`](https://github.com/bec-project/bec_widgets/commit/ac824f6b83178e34f015c296008d7a1e21c70878))
### Testing
- Adjust metadata assertions to new schema defaults
([`75697f5`](https://github.com/bec-project/bec_widgets/commit/75697f5b1faefb5cfcbc1b753d3f505d69339559))
- Adjust metadata assertions to new schema defaults
([`2697496`](https://github.com/bec-project/bec_widgets/commit/26974965151748c57334f350e21f3b610f92e011))
## v3.0.0 (2026-03-06)
### Bug Fixes
- 'any' type annotations
([`9c4a544`](https://github.com/bec-project/bec_widgets/commit/9c4a54493adc94afe5d43db5e8cbb8d565670af2))
- Add metadata to scan control export
([`17e678b`](https://github.com/bec-project/bec_widgets/commit/17e678b0ad1739490e901f3dbf7180d99c96950c))
- Address copilot review
([`a1a400f`](https://github.com/bec-project/bec_widgets/commit/a1a400f5409213ee1ab2f7cc9f8da7a2b612972d))
- Adjust ring progress bar to ads
([`7fd7f67`](https://github.com/bec-project/bec_widgets/commit/7fd7f67857e23b04759cf23993a99f4701121f95))
- Don't wait forever
([`c1d0e43`](https://github.com/bec-project/bec_widgets/commit/c1d0e435d5dd9965dbafd5bf469327c7f7620cfd))
- Removal of old BECDock import
([`92ae5fc`](https://github.com/bec-project/bec_widgets/commit/92ae5fc7fbf3a55068e2b42d3f66134baeb71766))
- Remove manual stylesheet deletion/override
([`8bbd519`](https://github.com/bec-project/bec_widgets/commit/8bbd519559c857cdc9f51e9507994e7aa4b07af1))
- Remove singleShots from BECConnector and adjustments of dock area logic
([`e26a90c`](https://github.com/bec-project/bec_widgets/commit/e26a90c62fa6c176bf4425867d1cb895a6fad7cd))
- Sanitize name space util for bec connector and ads
([`beca23e`](https://github.com/bec-project/bec_widgets/commit/beca23e14e18445f6ee440e8c55b57f4180a36c9))
- Tooltip logic and disable button on running scan
([`fa56fc8`](https://github.com/bec-project/bec_widgets/commit/fa56fc88026521f6f13690c4ec621c79e318f434))
- **_OverlayEventFilter**: Fix typo
([`a9f92cf`](https://github.com/bec-project/bec_widgets/commit/a9f92cf15547d207a614a1ed08b5d763a569fe59))
- **advanced_dock_area**: Cli API adjustments docs + names
([`6883982`](https://github.com/bec-project/bec_widgets/commit/6883982bf67c5fff02d72fbe39425af39bc3a65e))
- **advanced_dock_area**: Empty profile is always empty
([`aba67d3`](https://github.com/bec-project/bec_widgets/commit/aba67d3129581c85467ddd83211a03ea51c157a3))
- **advanced_dock_area**: Ensure the general profile exists when launched first time
([`7d2760e`](https://github.com/bec-project/bec_widgets/commit/7d2760eab8e5494992adb1452705f58619842d30))
- **advanced_dock_area**: New profiles are saved with quickselect as default
([`0d6b94a`](https://github.com/bec-project/bec_widgets/commit/0d6b94aaecb56e51bdc1ff930079b6c5535798de))
- **advanced_dock_area**: Profile behaviour adjusted, cleanup of the codebase
([`22df7bb`](https://github.com/bec-project/bec_widgets/commit/22df7bb5320c3b1808ab21e6354350838f5acb63))
- **advanced_dock_area**: Remove all widgets when loading new profiles
([`b841cfb`](https://github.com/bec-project/bec_widgets/commit/b841cfbc5f5021c1f9bea03e7fe88713506f66a7))
- **advanced_dock_area**: Remove widget from dock area by object name
([`8f44213`](https://github.com/bec-project/bec_widgets/commit/8f44213ecccca882f22b8738baef28b68d99c381))
- **advanced_dock_area**: Removed non-functional dock_list and dock_map from RPC
([`88b6e01`](https://github.com/bec-project/bec_widgets/commit/88b6e015bf1ab3b56db843ec13a6473ad67c4acc))
- **advanced_dock_area**: Removed the singleShot for load_initial_profile
([`3236dfb`](https://github.com/bec-project/bec_widgets/commit/3236dfb07f477fb87bcbcd0ee983781d5281beb6))
- **advanced_dock_area**: Replace sanitize_namespace with slugify
([`013b916`](https://github.com/bec-project/bec_widgets/commit/013b916ca3beb7a47db9009b9e07250ae52979b1))
- **basic_dock_area**: Delete_all will also delete floating docks
([`6b2b42f`](https://github.com/bec-project/bec_widgets/commit/6b2b42f21afa98d4ee5cb9d969aaa21cfc633f4e))
- **basic_dock_area**: Removed the singleShot usage
([`6cff8d7`](https://github.com/bec-project/bec_widgets/commit/6cff8d7a41f6f08908c3dd20fd563ab2612976e3))
- **bec_connector**: Use RPC register to fetch all connections
([`56b1e66`](https://github.com/bec-project/bec_widgets/commit/56b1e6687f4ce56e7c836678d397d1ca0fbec459))
- **bec_connector**: Widget_removed and name_established signals added
([`389a93f`](https://github.com/bec-project/bec_widgets/commit/389a93f8d07d44c17772e6183ee129db7692bd89))
- **bec_widget**: Improved qt enums; grab safeguard
([`f38cd3e`](https://github.com/bec-project/bec_widgets/commit/f38cd3e3a043151ce25f91d9a6b325a6c6ac5103))
- **bec_widgets**: Adapt to bec_qthemes 1.0; themes can be only applied on living Qt objects
([`b0cd619`](https://github.com/bec-project/bec_widgets/commit/b0cd619d7dff8f7ce7bc37ea6acea9473b2273d8))
- **becconnector**: Ophyd thread killer on exit + in conftest
([`0b9e5c1`](https://github.com/bec-project/bec_widgets/commit/0b9e5c15afb8b6f271992cb70c235c2be44c24a8))
- **becconnector**: Sanitize the setObjectName from qobject inheritance
([`7507f27`](https://github.com/bec-project/bec_widgets/commit/7507f27d686300a2b42c80dc06f3c78142c7ef84))
- **busy-loader**: Adjust busy loader and tests
([`94faaba`](https://github.com/bec-project/bec_widgets/commit/94faaba24d45a1ff971879486fa044fce49d2d5c))
- **CLI**: Change the default behavior of launching the profiles in CLI
([`b43b6e8`](https://github.com/bec-project/bec_widgets/commit/b43b6e844b4f178f9636b325aee0ce4fa2152199))
- **CLI**: Dock_area can be created from CLI with specific profile or empty
([`9c66dd5`](https://github.com/bec-project/bec_widgets/commit/9c66dd59914e2c8964f811f4e7e522fd3ae75633))
- **cli**: Rpc API from any folder
([`b29648e`](https://github.com/bec-project/bec_widgets/commit/b29648e10b0ea7931ad216221f231b77ab8998d8))
- **client**: Abort, reset, stop button removed from RPC access
([`c923f79`](https://github.com/bec-project/bec_widgets/commit/c923f7929370c3ac721dfa84d7cafcd0aa406c92))
- **client**: Client API regenerated
([`7083f94`](https://github.com/bec-project/bec_widgets/commit/7083f94f467ad4d40bea57dcdc96c75aa3690910))
- **client_utils**: Delete is deleting window and its content
([`be55bf2`](https://github.com/bec-project/bec_widgets/commit/be55bf20c1295c1e710457638c1bc7154b23011e))
- **client_utils**: Safeguard for accessing gui.new and launcher if GUIServer not running
([`4d41be6`](https://github.com/bec-project/bec_widgets/commit/4d41be61b546931c728b584f190aa4de3f418dd3))
- **colors**: Added logger to the apply theme
([`1f363d9`](https://github.com/bec-project/bec_widgets/commit/1f363d9bd4e6f7a01edcbe5d0049560459d184d0))
- **colors**: More benevolent fetching of colormap names, avoid hardcoded wrong colormap mapping
from GradientWidget from pg
([`cd9c7ab`](https://github.com/bec-project/bec_widgets/commit/cd9c7ab079bee1623a93ff63142cac8ebf61facd))
- **dark_mode_button**: Rpc access disabled
([`4fc2522`](https://github.com/bec-project/bec_widgets/commit/4fc252220d3a22f52b1148ba64045f5884d59182))
- **dark_mode_button**: Skip settings added
([`1c18810`](https://github.com/bec-project/bec_widgets/commit/1c18810e5faf0de96bb7381db3d8c4bcd2596596))
- **developer widget**: Save before executing a scripts
([`d085f65`](https://github.com/bec-project/bec_widgets/commit/d085f651532f84e720506745dbd44b80fb05a4be))
- **device-form-dialog**: Adapt device-form-dialog ophyd validation test
([`36be529`](https://github.com/bec-project/bec_widgets/commit/36be5292da1a2c30ef9a8493ad49f361d878c23a))
- **device-form-dialog**: Adapt DeviceFormDialog to run validation of config upon editing/adding a
config, and forward validation results
([`7c28364`](https://github.com/bec-project/bec_widgets/commit/7c283645948999f6a6b2e480418e5c8c7f158fb5))
- **device-init-progress-bar**: Fix ui format for device init progressbar
([`caba3a5`](https://github.com/bec-project/bec_widgets/commit/caba3a55f3a7a62a74f8f36b14a960e9c0fe0981))
- **device-manager**: Fix minor icon synchronization bugs
([`1d654bd`](https://github.com/bec-project/bec_widgets/commit/1d654bd8bdaac581a934cb9bab5a64a9021b4972))
- **device-manager-display-widget**: Fix error message popup on cancelling upload
([`fa49322`](https://github.com/bec-project/bec_widgets/commit/fa49322d1fd94ec4235c435dd6ca5e5234cd6bcc))
- **device-manager-display-widget**: Remove devices from ophyd validation after upload to BEC
([`7805c7a`](https://github.com/bec-project/bec_widgets/commit/7805c7a1916d8d153881eaf6b96825a010ad6a9c))
- **device-progress-bar**: Remove stretch in content layout
([`3fe6a00`](https://github.com/bec-project/bec_widgets/commit/3fe6a00708c459595b2eedb2a902c4ca5cae7171))
- **device_combobox**: Public flag for valid input
([`6c73307`](https://github.com/bec-project/bec_widgets/commit/6c73307bb43dfc2ae6181bd4be3854b7e198eb1d))
- **device_input_widgets**: Removed RPC access
([`940face`](https://github.com/bec-project/bec_widgets/commit/940face1187a0d3480ca3d64c061550271ff54e4))
- **dock_area**: Profile management with empty profile, applied across the whole repo
([`963941a`](https://github.com/bec-project/bec_widgets/commit/963941a788c1ce8a5def15b9a9d930ef9c62f41e))
- **dock_area**: Tabbed dock have correct parent
([`a632f35`](https://github.com/bec-project/bec_widgets/commit/a632f35c40e8323378f2464a6a82a484edf4ff33))
- **dock_area**: The old BECDockArea(pg) removed and replaces by AdvancedDockArea(ADS)
([`a6583ad`](https://github.com/bec-project/bec_widgets/commit/a6583ad53f6a1004af1a87904517d97a52801116))
- **dock_area**: Widget_map and widget_list by default returns only becconnector based widgets
([`3a5317b`](https://github.com/bec-project/bec_widgets/commit/3a5317be53d21130203a534b0dbf6bbef2d1a1c8))
- **editors**: Vscode widget removed
([`48387c0`](https://github.com/bec-project/bec_widgets/commit/48387c0ad9234f5f7600644eb12fa12c6d29efa7))
- **FakeDevice**: Add _info dict
([`2992939`](https://github.com/bec-project/bec_widgets/commit/2992939b0fa504418fe06173c11702e9dd4f3ce2))
- **general_app**: Old general app example removed
([`3ebac55`](https://github.com/bec-project/bec_widgets/commit/3ebac55e2d6aabf971d818fddb53430a690a7392))
- **guided-tour**: Fix skip past invalid step for 'prev' step
([`7bcdc31`](https://github.com/bec-project/bec_widgets/commit/7bcdc31f119b7b0996c7eac75008cef0b6e880ff))
- **heatmap**: Devices are saved as SafeProperties
([`6baf196`](https://github.com/bec-project/bec_widgets/commit/6baf1962faa0628ba872790e6cb34565bc7d0d7c))
- **heatmap**: Interpolation of the image moved to separate thread
([`323c8d5`](https://github.com/bec-project/bec_widgets/commit/323c8d5bc00f12b2d032f3da5daa47ef3e4774bc))
- **heatmap**: Interpolation thread is killed only on exit, logger for dandling thread
([`6fc524c`](https://github.com/bec-project/bec_widgets/commit/6fc524c819903eedd690adcb09f7aa70ee4d2248))
- **launch_window**: Argument to start with the gui class
([`3c16909`](https://github.com/bec-project/bec_widgets/commit/3c16909a875337efdec9e984f952c390ce99cfb4))
- **launch_window**: Launch geometry for widgets launched from launcher to 80% of the primary screen
as default
([`6459281`](https://github.com/bec-project/bec_widgets/commit/6459281387c8f1287347b9569a77aa1e9444013c))
- **launch_window**: Logic for showing launcher
([`d9b7285`](https://github.com/bec-project/bec_widgets/commit/d9b728584fb7e96ebac1c0f29f713290c0092556))
- **launch_window**: Processevents removed
([`c61d00e`](https://github.com/bec-project/bec_widgets/commit/c61d00e761851a67003921c2ad689238e360ad77))
- **main_app**: Center the application window on the screen
([`96a52a0`](https://github.com/bec-project/bec_widgets/commit/96a52a0cb0fb248e83303ee89182fe4ebeb29e75))
- **main_app**: Dock area from main app shares the workspace name with the CLI one to reuse the
profiles created in the cli companion window
([`06745e0`](https://github.com/bec-project/bec_widgets/commit/06745e0511d3ad4e261119118c7767f92bd884a5))
- **main_app**: Refactor main function and update script entry point in pyproject.toml
([`7ccfcc9`](https://github.com/bec-project/bec_widgets/commit/7ccfcc9f52c6ddaf65c350d474bac7260e3dd059))
- **main_app**: Rpc access refined
([`5bcf440`](https://github.com/bec-project/bec_widgets/commit/5bcf440be7172f8c3cadc7cd1d95251c176d33d1))
- **main_app**: Temporarily disable IDE view
([`bfc9f19`](https://github.com/bec-project/bec_widgets/commit/bfc9f1947234b87835d2cde87f961a00b1a0990d))
- **main_app**: The dock area view implemented as a viewBase
([`ab9688d`](https://github.com/bec-project/bec_widgets/commit/ab9688d2b551e4b3525fe9aed76afd772b835b05))
- **main_window**: Cleanup adjusted with shiboken6
([`06cb187`](https://github.com/bec-project/bec_widgets/commit/06cb187d1a030e24d62c5a8e01978ba68f4812df))
- **main_window**: Delete on close
([`522934f`](https://github.com/bec-project/bec_widgets/commit/522934f8cd814c07fde8c62635f2f63ed716e00e))
- **main_window**: Parent fixed for notification broker
([`947bf63`](https://github.com/bec-project/bec_widgets/commit/947bf63e03b3cbdfe2fd8ab803c83175c7bc599b))
- **main_window**: Removed general forced cleanup
([`cab4227`](https://github.com/bec-project/bec_widgets/commit/cab422777c50151b94da71a45a9bda0e1ce2804d))
- **main_window**: Safeguard of fetching the launcher from the main window if GUIServer is not
running
([`f8be437`](https://github.com/bec-project/bec_widgets/commit/f8be43741a5c100a976d2f84c3dc7607938c847e))
- **main_window**: Scan progress bar rpc not exposed
([`04b448e`](https://github.com/bec-project/bec_widgets/commit/04b448e1832796616002a1ea26028e3d42aca9b1))
- **monaco dock**: Update last focused editor when closing
([`3631fc2`](https://github.com/bec-project/bec_widgets/commit/3631fc26499853015ff58283c2b8913aa9a36334))
- **monaco widget**: Reset current_file
([`c53d4c0`](https://github.com/bec-project/bec_widgets/commit/c53d4c0ad7b4c423eaa13828e2b38a04751f148e))
- **monaco_dock**: Update editor metadata handling and improve open_file method
([`3136477`](https://github.com/bec-project/bec_widgets/commit/31364772bd7fcccbc118061d0b601a9f1121bcb0))
- **motor_map**: X/y motor are saved in properties
([`96060fc`](https://github.com/bec-project/bec_widgets/commit/96060fca53f3426dbc43f1ae5d8ebdd7acc39100))
- **ophyd-validation**: Add device_manager_ds argument if available for ophyd validation
([`338ff45`](https://github.com/bec-project/bec_widgets/commit/338ff455cccfc1e8a3b0638fdcc4f1d807f0b6ca))
- **positioner_box**: Layout HV centered and size taken from the ui file
([`6113deb`](https://github.com/bec-project/bec_widgets/commit/6113debc6c1d95a50b7522144fdc820380ae2e28))
- **qt_ads**: Pythons stubs match structure of PySide6QtAds
([`2f9d6d5`](https://github.com/bec-project/bec_widgets/commit/2f9d6d59eee32e373acc0df8a38b426d8142562b))
- **rpc**: Rpc flags adjustment for MainApp and DeveloperWidget
([`5b15c75`](https://github.com/bec-project/bec_widgets/commit/5b15c75b88707f450bfa194d9eed3d726e101981))
- **rpc_register**: Listing only valid connections
([`38eb244`](https://github.com/bec-project/bec_widgets/commit/38eb2441cdf677939354c7066f854c22cf261932))
- **rpc_server**: Add check for rpc_exposed to serialize_object
([`0eabd0f`](https://github.com/bec-project/bec_widgets/commit/0eabd0f72be6247073382d0df02776d30c35a1aa))
- **rpc_server**: Removed unused get _get_becwidget_ancestor
([`047ff2b`](https://github.com/bec-project/bec_widgets/commit/047ff2bef77ca14f060b3b0bc21f78b880535faa))
- **rpc_server**: Use single shot instead of processEvents to avoid dead locks
([`84d6653`](https://github.com/bec-project/bec_widgets/commit/84d6653d1993dd4bebb98fcbf0d1a0dd94119502))
- **scatter waveform**: Fix tab order for settings panel
([`08e1985`](https://github.com/bec-project/bec_widgets/commit/08e19858eadb738358465c9f2a202529d1ccbe45))
- **scatter_waveform**: Devices and entries saved as properties
([`7ab8e0c`](https://github.com/bec-project/bec_widgets/commit/7ab8e0c2ed4f1b49e943f7ec64d3984ede6e134a))
- **scatter_waveform**: Modernization of scatter waveform settings dialog
([`dea73a9`](https://github.com/bec-project/bec_widgets/commit/dea73a97c9f78560e9f11290ba442152cc955057))
- **scatter_waveform**: Remove curve_json from the properties
([`f6712e8`](https://github.com/bec-project/bec_widgets/commit/f6712e8bb855566ca0f308ae3d5bf5109d98d792))
- **screen_utils**: Screen utilities added and fixed sizing for widgets from launch window and main
app
([`fb55e72`](https://github.com/bec-project/bec_widgets/commit/fb55e72713a2209575c555c9dd8c025a0349e795))
- **server**: Gui server can reach shutdown, logic moved to becconnector
([`0d05839`](https://github.com/bec-project/bec_widgets/commit/0d05839e9e3f4c61fc318aa44721436afcebf06f))
- **signal-label**: Fix signal label cleanup, missing parent in constructors
([`72639e7`](https://github.com/bec-project/bec_widgets/commit/72639e7e5fa01ceac6cc864c01cea73f4ddca441))
- **signal_combo_box**: Get_signal_name added; remove duplicates from heatmap and scatter waveform
settings;
([`66a9510`](https://github.com/bec-project/bec_widgets/commit/66a95102dd33dbac5575a3b0d99c4c99c42cce4a))
- **signal_label**: Dispatcher unsubscribed in the cleanup
([`90ba505`](https://github.com/bec-project/bec_widgets/commit/90ba505c10e7ee60d82abb578c7f691cf1125e9a))
- **toggle**: Move toggle to theme colors
([`375d131`](https://github.com/bec-project/bec_widgets/commit/375d131109d37ea7b49aa354b624b0dd8fea89ee))
- **view**: Based on BECWidgets
([`3d049d6`](https://github.com/bec-project/bec_widgets/commit/3d049d67a9303b20862150b3622c4121d4a72b32))
- **web_console**: Added startup kwarg
([`55c8a57`](https://github.com/bec-project/bec_widgets/commit/55c8a57e71653299f3fd66ca7aafca8f32c7aacc))
- **widget_state_manager**: Added shiboken check
([`338b9e1`](https://github.com/bec-project/bec_widgets/commit/338b9e1aa7216d9d38449633fe9d4fffce13ee90))
- **widget_state_manager**: Filtering of not wanted properties
([`7ea4352`](https://github.com/bec-project/bec_widgets/commit/7ea4352a09349e606c97edb72eccf6e683684cf8))
- **widget_state_manager**: Properties_to_skip are not restored even if in ini file
([`84c7360`](https://github.com/bec-project/bec_widgets/commit/84c7360bb8a63426d584a522d6a8969810536d2a))
- **widget_state_manager**: State manager can save all properties recursively to already existing
settings
([`98e2979`](https://github.com/bec-project/bec_widgets/commit/98e29792a2620a9e88c770cd69d7cad88cc94252))
- **widgets**: Processevent removed from widgets using it
([`a56bd57`](https://github.com/bec-project/bec_widgets/commit/a56bd572a000e47dd7d1d2a458dac676e67ec21e))
- **widgets**: Removed isVisible from all SafeProperties
([`b72bf4a`](https://github.com/bec-project/bec_widgets/commit/b72bf4a0f9a67c104cd86c66e9160ab9f0a40c01))
### Build System
- Pyside6-qtads; bec_qtheme V1; dependencies updated and adjusted
([`562001c`](https://github.com/bec-project/bec_widgets/commit/562001c08cdc3ca9fbe28aaed8b6a83921426f97))
- **deps**: Update bec-qthemes requirement
([`4a44ede`](https://github.com/bec-project/bec_widgets/commit/4a44ede8fe02b4c513ec419f85cb447f58dfdf86))
Updates the requirements on [bec-qthemes](https://github.com/bec-project/bec_qthemes) to permit the
latest version. - [Release notes](https://github.com/bec-project/bec_qthemes/releases) -
[Changelog](https://github.com/bec-project/bec_qthemes/blob/main/CHANGELOG.md) -
[Commits](https://github.com/bec-project/bec_qthemes/compare/v0.7.0...v1.3.3)
--- updated-dependencies: - dependency-name: bec-qthemes dependency-version: 1.3.3
dependency-type: direct:production ...
Signed-off-by: dependabot[bot] <support@github.com>
### Code Style
- Wrap progress bar in widget to fix background
([`793779d`](https://github.com/bec-project/bec_widgets/commit/793779db68c9725fae767d6cd0096c89a4caa700))
### Continuous Integration
- Add artifact upload
([`d301fdf`](https://github.com/bec-project/bec_widgets/commit/d301fdfeb237acd61fd579a0e8147f2037df62d5))
- Cancel previous CI run for PR or branch
([`37298c2`](https://github.com/bec-project/bec_widgets/commit/37298c21c3b76667459f2a62453692e99ff8191e))
- Install ttyd
([`b6d70c3`](https://github.com/bec-project/bec_widgets/commit/b6d70c34df29d2f44e7f5da88cb0daaef39ceed1))
- Use shared issue sync action instead of local version
([`c9a8e64`](https://github.com/bec-project/bec_widgets/commit/c9a8e64217d3c2047a4a8f5e2348c0a725a0066a))
### Features
- Add export and load settings methods to BECConnector; add SafeProperty safe getter flag
([`5435fec`](https://github.com/bec-project/bec_widgets/commit/5435fec68a11caa83e8566cde21ad382729e6792))
- Add guided tour docs to device-manager-view
([`fcb4306`](https://github.com/bec-project/bec_widgets/commit/fcb43066e4abe469e0f06163b4abcce6e0d9250b))
- Add SafeConnect
([`4b5a45c`](https://github.com/bec-project/bec_widgets/commit/4b5a45c320d701e6878d6af7259c530596118053))
- Attach config cancellation to closeEvent
([`c1443fa`](https://github.com/bec-project/bec_widgets/commit/c1443fa27afc63c69c4b56cf8be7eb2792704784))
- Guided tour for main app
([`3ffdf11`](https://github.com/bec-project/bec_widgets/commit/3ffdf11c3e419d71e22c484c618eec51e9168f9d))
- **actions**: Actions can be created with label text with beside or under alignment
([`9c3a6e1`](https://github.com/bec-project/bec_widgets/commit/9c3a6e1691fd02230651a4d871911f365d4a3129))
- **ads**: Add pyi stub file to provide type hints for ads
([`4c4fc25`](https://github.com/bec-project/bec_widgets/commit/4c4fc25a42be9bc8ecce6f550c4f357372233289))
- **advanced_dock_area**: Added ads based dock area with profiles
([`d25314e`](https://github.com/bec-project/bec_widgets/commit/d25314e6eeb6323a6ffcde3c119f7b1bc0ebed16))
- **advanced_dock_area**: Created DockAreaWidget base class; profile management through namespaces;
dock area variants
([`58b88ef`](https://github.com/bec-project/bec_widgets/commit/58b88efcb66627f9e9c3c9de65366d55465e1e44))
- **advanced_dock_area**: Floating docks restore with relative geometry
([`440cecd`](https://github.com/bec-project/bec_widgets/commit/440cecddf740a5f320f53771b93a148fb3be544b))
- **advanced_dock_area**: Instance lock for multiple ads in same session
([`bcaf013`](https://github.com/bec-project/bec_widgets/commit/bcaf013d2b5b45830cc37079b7d0f388ead98bc1))
- **advanced_dock_area**: Ui/ux for profile management improved, saving directories logic adjusted
([`7305498`](https://github.com/bec-project/bec_widgets/commit/730549847563b552887a5529b2b0fed308ed8b98))
- **bec-login**: Add login widget in material design style
([`b798ea2`](https://github.com/bec-project/bec_widgets/commit/b798ea2340a6aa8c0325a1cd1995eba028279816))
- **bec_widget**: Attach/detach method for all widgets + client regenerated
([`82dbf31`](https://github.com/bec-project/bec_widgets/commit/82dbf31da54288b7228bc5c7bdc271a8178f8d02))
- **bec_widget**: Save screenshot to bytes
([`ed2651a`](https://github.com/bec-project/bec_widgets/commit/ed2651a914a283dc7cc45a9bf185d2a4e053d307))
- **becconnector**: Added rpc_passthrough_children flag in addition to rpc_exposed
([`010373f`](https://github.com/bec-project/bec_widgets/commit/010373fd5b334c6616efce467608356b36c2130b))
- **becconnector**: Exposed rpc flag added to the BECConnector
([`de6c628`](https://github.com/bec-project/bec_widgets/commit/de6c6284ad6d73b40137e9bba56e748c59a4ade9))
- **busy_loader**: Busy loader added to bec widget base class
([`92c15a7`](https://github.com/bec-project/bec_widgets/commit/92c15a7f829fa3f0b69cf5584ac45a21dce0b01d))
- **client_utils**: Theme can be changed from the CLI
([`c1d4758`](https://github.com/bec-project/bec_widgets/commit/c1d4758e4ca33d094fabdfbd4e024a2836f2fa9a))
- **color**: Add relative luminance calculation
([`a84b924`](https://github.com/bec-project/bec_widgets/commit/a84b924162280fc6b6ca31af511b78c4f5baafc9))
- **developer_view**: Add developer view
([`bdef594`](https://github.com/bec-project/bec_widgets/commit/bdef594b5885b5fab60ef94addbce1ab771c4244))
- **developer_widget**: Add signal connection for focused editor changes to disable run button for
macro files
([`fa79179`](https://github.com/bec-project/bec_widgets/commit/fa79179f89f048aeee0a3947350f3a7bc2169d9f))
- **device-initialization-progress-bar**: Add progress bar for device initialization
([`5deafb9`](https://github.com/bec-project/bec_widgets/commit/5deafb97979eb1a2e8bcba3321dfd1a15553a5da))
- **device-manager**: Add DeviceManager Widget for BEC Widget main applications
([`a6357af`](https://github.com/bec-project/bec_widgets/commit/a6357af8ffda640eaee1c1c75c3a4bdf0c5de068))
- **device_combobox**: Device filter added based on its signal classes
([`fbddf4a`](https://github.com/bec-project/bec_widgets/commit/fbddf4a28442dab6e9e4585aa0c3a0131d6bdf7b))
- **dm-view**: Initial device manager view added
([`9e4be38`](https://github.com/bec-project/bec_widgets/commit/9e4be38c0b8b6e654313bf232a597d09978d2436))
- **generate_cli**: Rpc API from content widget can be merged with the RPC API of the container
widget statically
([`758956b`](https://github.com/bec-project/bec_widgets/commit/758956be098d6629a0cd641b1525965ebfe19345))
- **guided_tour**: Add guided tour
([`9b753c1`](https://github.com/bec-project/bec_widgets/commit/9b753c1f24419292790ca60e4bd55bb1aa5e1a70))
- **help-inspector**: Add help inspector widget
([`5ac629d`](https://github.com/bec-project/bec_widgets/commit/5ac629de8c7bbdf0e2c07c9a7cf25e430cd031c1))
- **image**: Modernization of image widget
([`80c0dfa`](https://github.com/bec-project/bec_widgets/commit/80c0dfa4f28e3eb2c6f944a517c92f822f51266d))
- **jupyter_console_window**: Adjustment for general usage
([`66f3e51`](https://github.com/bec-project/bec_widgets/commit/66f3e517f0fb8fa1ea678ec09ef852d5b8a63d51))
- **main_app**: Main app with interactive app switcher
([`b30e1e4`](https://github.com/bec-project/bec_widgets/commit/b30e1e4c5e182903721fe7c16a8069f2c95704d3))
- **motor_map**: Motor selection adopted to splitter action
([`168bb3c`](https://github.com/bec-project/bec_widgets/commit/168bb3cb77ca3a270a958f4f941445383c8bec99))
- **plot_base**: Plot_base, image and heatmap widget adopted to property-toolbar sync
([`dd69578`](https://github.com/bec-project/bec_widgets/commit/dd69578b912b44130d33427fa8d5d948889e8c07))
- **SafeProperty**: Safeproperty emits property_changed signal
([`7cce3bd`](https://github.com/bec-project/bec_widgets/commit/7cce3bd54210f82a5cf68e6219ea073e972234d6))
- **signal_combobox**: Extended that can filter by signal class and dimension of the signal
([`cfd6bde`](https://github.com/bec-project/bec_widgets/commit/cfd6bde268cea5bd119354db8b6ab1661b575293))
- **toolbar**: Splitter action added
([`0752f3d`](https://github.com/bec-project/bec_widgets/commit/0752f3d6a9cd9b080bf87464eac9eb05f99f108f))
- **toolbar**: Toolbar can be synced with the property_changed for toggle actions
([`4357d98`](https://github.com/bec-project/bec_widgets/commit/4357d984c8f89fa51bc0c8d9a217b2a2028e3ca9))
- **web console**: Add support for shared web console sessions
([`5e111cf`](https://github.com/bec-project/bec_widgets/commit/5e111cfc54f2771a0ff5080a77bb4ac5b491bc8f))
- **widget_hierarchy_tree**: Widget displaying parent child hierarchy from the app widgets
([`5f46fa0`](https://github.com/bec-project/bec_widgets/commit/5f46fa09943017fdadbe12522b38a2733d5b6001))
- **widget_highlighter**: Reusable separate widget highlighter
([`8b782ac`](https://github.com/bec-project/bec_widgets/commit/8b782ac302b4ccbfe768c066c3c9fbe31fdace75))
- **widget_io**: Widget hierarchy can grap all bec connectors from the widget recursively
([`db83576`](https://github.com/bec-project/bec_widgets/commit/db83576346980eef59b5366bc07258edcbf6333b))
### Performance Improvements
- **heatmap**: Thread worker optimization
([`f98a5de`](https://github.com/bec-project/bec_widgets/commit/f98a5de7e9f154e6e9fc65a257776c9dec74eb84))
### Refactoring
- Add extra tour steps, add enter button
([`2826919`](https://github.com/bec-project/bec_widgets/commit/2826919c5a330e2ba9666cfec1f9561b4cfd4bcf))
- Global refactoring to use device-signal pair names
([`b93fbc5`](https://github.com/bec-project/bec_widgets/commit/b93fbc5cd31dbaa1bf4b18b9d30e3463ea539f72))
- Improvements to enum access
([`19b7310`](https://github.com/bec-project/bec_widgets/commit/19b73104337a100cef39936dd7ec5c32c346f99b))
- **advanced_dock_area**: Change remove_widget to delete
([`eda30e3`](https://github.com/bec-project/bec_widgets/commit/eda30e31396ec1e34c13be047564de334d9a5c6f))
- **bec_main_window**: Main app theme renamed to View
([`37bfad7`](https://github.com/bec-project/bec_widgets/commit/37bfad7174982f7c3489e38cf715615719b34862))
- **busy-loader**: Refactor busy loader to use custom widget
([`332ca20`](https://github.com/bec-project/bec_widgets/commit/332ca205c12c445513472a25366699e870e5a879))
- **busy-loager**: Improve eventFilter to avoid crashs if target or overlay is None.
([`229da62`](https://github.com/bec-project/bec_widgets/commit/229da6244ae2bb2521ff0257db1772e5cceeee59))
- **developer_view**: Changed to use DockAreaWidget
([`4d40918`](https://github.com/bec-project/bec_widgets/commit/4d40918b7c84c833d46287fec365d1810683adec))
- **developer_widget**: Enhance documentation and add missing imports
([`5e0c376`](https://github.com/bec-project/bec_widgets/commit/5e0c3767742bcac8b39972d0972db0580c1863cd))
- **device-form-dialog**: Use native QDialogButtonBox instead of GroupBox layout
([`12b4d3a`](https://github.com/bec-project/bec_widgets/commit/12b4d3a9e0ffe0539d5884bbedf4f14349a5e117))
- **dock_area**: Change name to BECDockArea
([`71ed2d3`](https://github.com/bec-project/bec_widgets/commit/71ed2d353acc0e68eaef1fa55474db0b8e1f1eb9))
- **guided-tour**: Add support for QTableWidgetItem
([`83489b7`](https://github.com/bec-project/bec_widgets/commit/83489b7519f41b75f2d3f2cdcf31b0075e41d52d))
- **main_app**: Adapted for DockAreaWidget changes
([`ac850ec`](https://github.com/bec-project/bec_widgets/commit/ac850ec650695c12a77e0e8e598094d740312a89))
- **main_app**: Simpler id and object name management
([`654aeb7`](https://github.com/bec-project/bec_widgets/commit/654aeb711626f0f85d288cd3c0a85d69ad2826d8))
- **monaco_dock**: Changed to use DockAreaWidget
([`ed0d34a`](https://github.com/bec-project/bec_widgets/commit/ed0d34a60f8348a970da71d77801154ea70c24c6))
- **ophyd-validation**: Allow option to keep device visible after successful validation
([`89d5c5a`](https://github.com/bec-project/bec_widgets/commit/89d5c5abdb0081e29d2c31ae6ded75a3f9abe0ff))
- **widget_io**: Hierarchy logic generalized
([`00bf01c`](https://github.com/bec-project/bec_widgets/commit/00bf01c1290c4ead6d8270942fbfda2cbd7e9873))
### Testing
- Fix test
([`de835e8`](https://github.com/bec-project/bec_widgets/commit/de835e81d8cf0ec6d3bca9d07ac21d4737666e31))
- **config-communicator**: Add test for cancel action
([`24701c2`](https://github.com/bec-project/bec_widgets/commit/24701c2a270520de739e4615d0f52a6386bbadc0))
- **device-form-dialog**: Adapt tests
([`f827e77`](https://github.com/bec-project/bec_widgets/commit/f827e77e870109b21e10b4cc28d6c09b8f77b2a6))
- **device-manager**: Use mocked client for tests
([`836fedd`](https://github.com/bec-project/bec_widgets/commit/836fedd50e4fdb66bd7614a55c8e0f95a14c3fac))
- **device-manager-view**: Improve test coverage for device-manager-view
([`4edc571`](https://github.com/bec-project/bec_widgets/commit/4edc57158be30d2500ad04d1b015bc8627cfb873))
- **e2e**: Raise with widget name
([`3f76ade`](https://github.com/bec-project/bec_widgets/commit/3f76ade6289a75b76d7a5f67e9d72175378bedbe))
- **script_tree**: Improve hover event handling with waitUntil
([`6296055`](https://github.com/bec-project/bec_widgets/commit/6296055c664070b8caeffda3c7047774bd692691))
- **widget_io**: Add dedicated unit tests for iter_widget_tree and helper methods
([`041afc6`](https://github.com/bec-project/bec_widgets/commit/041afc68b1c7202a4609149e6f0e212fca629c87))
Co-authored-by: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com>
## v2.45.14 (2026-01-23)
### Bug Fixes

View File

@@ -147,8 +147,7 @@ class LaunchTile(RoundedFrame):
# Action button
self.action_button = QPushButton("Open")
self.action_button.setStyleSheet(
"""
self.action_button.setStyleSheet("""
QPushButton {
background-color: #007AFF;
border: none;
@@ -160,8 +159,7 @@ class LaunchTile(RoundedFrame):
QPushButton:hover {
background-color: #005BB5;
}
"""
)
""")
self.layout.addWidget(self.action_button, alignment=Qt.AlignmentFlag.AlignCenter)
def _fit_label_to_width(self, label: QLabel, max_width: int, min_pt: int = 10):

View File

@@ -127,12 +127,10 @@ class NavigationItem(QWidget):
self._icon_size_expanded = QtCore.QSize(26, 26)
self.icon_btn.setIconSize(self._icon_size_collapsed)
# Remove QToolButton hover/pressed background/outline
self.icon_btn.setStyleSheet(
"""
self.icon_btn.setStyleSheet("""
QToolButton:hover { background: transparent; border: none; }
QToolButton:pressed { background: transparent; border: none; }
"""
)
""")
# Mini label below icon
self.mini_lbl = QLabel(self._mini_text, self)

View File

@@ -31,7 +31,7 @@ logger = bec_logger.logger
class DeviceManagerOphydValidationDialog(QtWidgets.QDialog):
"""Popup dialog to test Ophyd device configurations interactively."""
def __init__(self, parent=None, config: dict | None = None): # type:ignore
def __init__(self, parent=None, config: dict | None = None): # type: ignore
super().__init__(parent)
self.setWindowTitle("Device Manager Ophyd Test")
self._config_status = ConfigStatus.UNKNOWN.value
@@ -133,7 +133,7 @@ class DeviceFormDialog(QtWidgets.QDialog):
# validated: config_status, connection_status
accepted_data = QtCore.Signal(dict, int, int, str, str)
def __init__(self, parent=None, add_btn_text: str = "Add Device"): # type:ignore
def __init__(self, parent=None, add_btn_text: str = "Add Device"): # type: ignore
super().__init__(parent)
# Track old device name if config is edited
self._old_device_name: str = ""

View File

@@ -103,16 +103,14 @@ class CustomBusyWidget(QWidget):
button_width = int(button_height * aspect_ratio)
self.cancel_button.setFixedSize(button_width, button_height)
color = get_accent_colors()
self.cancel_button.setStyleSheet(
f"""
self.cancel_button.setStyleSheet(f"""
QPushButton {{
background-color: {color.emergency.name()};
color: white;
font-weight: 600;
border-radius: 6px;
}}
"""
)
""")
# Layout
content_layout = QVBoxLayout(self)
@@ -128,12 +126,10 @@ class CustomBusyWidget(QWidget):
bg_color = color._colors.get("BG", None)
if bg_color is None: # Fallback if missing
bg_color = QColor(50, 50, 50, 255)
self.setStyleSheet(
f"""
self.setStyleSheet(f"""
background-color: {bg_color.name()};
border-radius: 12px;
"""
)
""")
def _ui_scale(self) -> int:
parent = self.parent()

View File

@@ -187,7 +187,7 @@ class BECDockArea(RPCBase):
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
such as `BECDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
object_name(str | None): Optional object name to assign to the created widget.
@@ -1141,7 +1141,7 @@ class DockAreaView(RPCBase):
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
such as `BECDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
object_name(str | None): Optional object name to assign to the created widget.
@@ -1386,7 +1386,7 @@ class DockAreaWidget(RPCBase):
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
such as `BECDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
dock_icon(QIcon | None): Optional icon applied to the dock via ``CDockWidget.setIcon``.
@@ -3192,7 +3192,7 @@ class MonacoDock(RPCBase):
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
such as `BECDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
dock_icon(QIcon | None): Optional icon applied to the dock via ``CDockWidget.setIcon``.
@@ -6249,7 +6249,8 @@ class Waveform(RPCBase):
signal_y: "str | None" = None,
color: "str | None" = None,
label: "str | None" = None,
dap: "str | None" = None,
dap: "str | list[str] | None" = None,
dap_parameters: "dict | list | lmfit.Parameters | None | object" = None,
scan_id: "str | None" = None,
scan_number: "int | None" = None,
**kwargs,
@@ -6271,9 +6272,14 @@ class Waveform(RPCBase):
signal_y(str): The name of the entry for the y-axis.
color(str): The color of the curve.
label(str): The label of the curve.
dap(str): The dap model to use for the curve. When provided, a DAP curve is
dap(str | list[str]): The dap model to use for the curve. When provided, a DAP curve is
attached automatically for device, history, or custom data sources. Use
the same string as the LMFit model name.
the same string as the LMFit model name, or a list of model names to build a composite.
dap_parameters(dict | list | lmfit.Parameters | None): Optional lmfit parameter overrides sent to
the DAP server. For a single model: values can be numeric (interpreted as fixed parameters)
or dicts like `{"value": 1.0, "vary": False}`. For composite models (dap is list), use either
a list aligned to the model list (each item is a param dict), or a dict of
`{ "ModelName": { "param": {...} } }` when model names are unique.
scan_id(str): Optional scan ID. When provided, the curve is treated as a **history** curve and
the ydata (and optional xdata) are fetched from that historical scan. Such curves are
never cleared by livescan resets.
@@ -6287,9 +6293,10 @@ class Waveform(RPCBase):
def add_dap_curve(
self,
device_label: "str",
dap_name: "str",
dap_name: "str | list[str]",
color: "str | None" = None,
dap_oversample: "int" = 1,
dap_parameters: "dict | list | lmfit.Parameters | None" = None,
**kwargs,
) -> "Curve":
"""
@@ -6299,9 +6306,11 @@ class Waveform(RPCBase):
Args:
device_label(str): The label of the source curve to add DAP to.
dap_name(str): The name of the DAP model to use.
dap_name(str | list[str]): The name of the DAP model to use, or a list of model
names to build a composite model.
color(str): The color of the curve.
dap_oversample(int): The oversampling factor for the DAP curve.
dap_parameters(dict | list | lmfit.Parameters | None): Optional lmfit parameter overrides sent to the DAP server.
**kwargs
Returns:

View File

@@ -20,15 +20,13 @@ class BECLogin(QWidget):
title = QLabel("Sign in", parent=self)
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet(
"""
title.setStyleSheet("""
#QLabel
{
font-size: 18px;
font-weight: 600;
}
"""
)
""")
self.username = QLineEdit(parent=self)
self.username.setPlaceholderText("Username")
@@ -57,13 +55,11 @@ class BECLogin(QWidget):
self.username.setFocus()
self.setStyleSheet(
"""
self.setStyleSheet("""
QLineEdit {
padding: 8px;
}
"""
)
""")
def _clear_password(self):
"""Clear the password field."""

View File

@@ -106,7 +106,9 @@ class TypedForm(BECWidget, QWidget):
def _add_griditem(self, item: FormItemSpec, row: int):
grid = self._form_grid.layout()
label = QLabel(parent=self._form_grid, text=item.name)
# Use title from FieldInfo if available, otherwise use the property name
label_text = item.info.title if item.info.title else item.name
label = QLabel(parent=self._form_grid, text=label_text)
label.setProperty("_model_field_name", item.name)
label.setToolTip(item.info.description or item.name)
grid.addWidget(label, row, 0)

View File

@@ -231,6 +231,8 @@ class StrFormItem(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)
if spec.info.description:
self._main_widget.setPlaceholderText(spec.info.description)
def _add_main_widget(self) -> None:
self._main_widget = QLineEdit()

View File

@@ -67,15 +67,13 @@ class TutorialOverlay(QWidget):
box = QFrame(self)
app = QApplication.instance()
bg_color = app.palette().window().color()
box.setStyleSheet(
f"""
box.setStyleSheet(f"""
QFrame {{
background-color: {bg_color.name()};
border-radius: 8px;
padding: 8px;
}}
"""
)
""")
layout = QVBoxLayout(box)
# Top layout with close button (left) and step indicator (right)

View File

@@ -69,13 +69,11 @@ class RoundedFrame(QFrame):
"""
Update the style of the frame based on the background color.
"""
self.setStyleSheet(
f"""
self.setStyleSheet(f"""
QFrame#roundedFrame {{
border-radius: {self._radius}px;
}}
"""
)
""")
self.apply_plot_widget_style()
def apply_plot_widget_style(self, border: str = "none"):

View File

@@ -118,6 +118,9 @@ class RPCServer:
logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
try:
logger.info(
f"Processing RPC instruction: {msg['action']} with request_id: {request_id}. Parameters: {msg.get('parameter')}"
)
method = msg["action"]
args = msg["parameter"].get("args", [])
kwargs = msg["parameter"].get("kwargs", {})
@@ -257,6 +260,10 @@ class RPCServer:
else:
name = WidgetContainerUtils.generate_unique_name("dock_area", existing_dock_areas)
logger.info(
f"Launching new dock area with name: {name} and startup_profile: {startup_profile}"
)
result_widget = bw_launch.dock_area(object_name=name, startup_profile=startup_profile)
result_widget.window().setWindowTitle(f"BEC - {name}")
@@ -296,6 +303,9 @@ class RPCServer:
else:
res = self.serialize_object(res)
except RegistryNotReadyError:
logger.info(
f"Object not registered yet for RPC request {request_id}, retrying serialization after {retry_delay} ms"
)
try:
self._rpc_singleshot_repeats[request_id] += retry_delay
QTimer.singleShot(

View File

@@ -599,16 +599,14 @@ class ExpandableMenuAction(ToolBarAction):
button.setIcon(QIcon(self.icon_path))
button.setText(self.tooltip)
button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
button.setStyleSheet(
"""
button.setStyleSheet("""
QToolButton {
font-size: 14px;
}
QMenu {
font-size: 14px;
}
"""
)
""")
menu = QMenu(button)
for action_container in self.actions.values():
action: QAction = action_container.action

View File

@@ -106,8 +106,7 @@ class ResizableSpacer(QWidget):
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.setStyleSheet(
"""
self.setStyleSheet("""
ResizableSpacer {
background-color: transparent;
margin: 0px;
@@ -117,8 +116,7 @@ class ResizableSpacer(QWidget):
ResizableSpacer:hover {
background-color: rgba(100, 100, 200, 80);
}
"""
)
""")
self.setContentsMargins(0, 0, 0, 0)

View File

@@ -291,8 +291,7 @@ class ModularToolBar(QToolBar):
menu = QMenu(self)
theme = get_theme_name()
if theme == "dark":
menu.setStyleSheet(
"""
menu.setStyleSheet("""
QMenu {
background-color: rgba(50, 50, 50, 0.9);
border: 1px solid rgba(255, 255, 255, 0.2);
@@ -300,12 +299,10 @@ class ModularToolBar(QToolBar):
QMenu::item:selected {
background-color: rgba(0, 0, 255, 0.2);
}
"""
)
""")
else:
# Light theme styling
menu.setStyleSheet(
"""
menu.setStyleSheet("""
QMenu {
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.2);
@@ -313,8 +310,7 @@ class ModularToolBar(QToolBar):
QMenu::item:selected {
background-color: rgba(0, 0, 255, 0.2);
}
"""
)
""")
for ii, bundle in enumerate(self.shown_bundles):
self.handle_bundle_context_menu(menu, bundle)
if ii < len(self.shown_bundles) - 1:

View File

@@ -46,7 +46,7 @@ class AutoUpdates(BECMainWindow):
self.setCentralWidget(self.dock_area)
self._auto_update_selected_device: str | None = None
self._default_dock = None # type:ignore
self._default_dock = None # type: ignore
self.current_widget: BECWidget | None = None
self.dock_name = None
self._enabled = True
@@ -63,7 +63,7 @@ class AutoUpdates(BECMainWindow):
Disconnect all connections for the auto updates.
"""
self.bec_dispatcher.disconnect_slot(
self._on_scan_status, MessageEndpoints.scan_status() # type:ignore
self._on_scan_status, MessageEndpoints.scan_status() # type: ignore
)
@property
@@ -244,10 +244,10 @@ class AutoUpdates(BECMainWindow):
wf = self.set_dock_to_widget("Waveform")
# Get the scan report devices reported by the scan
dev_x = info.scan_report_devices[0] # type:ignore
dev_x = info.scan_report_devices[0] # type: ignore
# For the y axis, get the selected device
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type: ignore
if not dev_y:
return
@@ -279,8 +279,8 @@ class AutoUpdates(BECMainWindow):
scatter = self.set_dock_to_widget("ScatterWaveform")
# Get the scan report devices reported by the scan
dev_x, dev_y = info.scan_report_devices[0], info.scan_report_devices[1] # type:ignore
dev_z = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
dev_x, dev_y = info.scan_report_devices[0], info.scan_report_devices[1] # type: ignore
dev_z = self.get_selected_device(info.readout_priority["monitored"]) # type: ignore
if None in (dev_x, dev_y, dev_z):
return
@@ -309,8 +309,8 @@ class AutoUpdates(BECMainWindow):
# If the scan report devices are empty, there is nothing we can do
if not info.scan_report_devices:
return
dev_x = info.scan_report_devices[0] # type:ignore
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
dev_x = info.scan_report_devices[0] # type: ignore
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type: ignore
if not dev_y:
return

View File

@@ -1315,7 +1315,7 @@ class DockAreaWidget(BECWidget, QWidget):
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
such as `BECDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
dock_icon(QIcon | None): Optional icon applied to the dock via ``CDockWidget.setIcon``.

View File

@@ -136,10 +136,7 @@ class BECDockArea(DockAreaWidget):
self._profile_management_enabled = enable_profile_management
self._startup_profile = self._normalize_startup_profile(startup_profile)
super().__init__(
parent,
default_add_direction=default_add_direction,
title="Advanced Dock Area",
**kwargs,
parent, default_add_direction=default_add_direction, title="BEC Dock Area", **kwargs
)
# Initialize mode property first (before toolbar setup)
@@ -168,7 +165,7 @@ class BECDockArea(DockAreaWidget):
# State manager
self.state_manager = WidgetStateManager(
self, serialize_from_root=True, root_id="AdvancedDockArea"
self, serialize_from_root=True, root_id="BECDockArea"
)
# Developer mode state
@@ -304,7 +301,7 @@ class BECDockArea(DockAreaWidget):
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
such as `BECDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
object_name(str | None): Optional object name to assign to the created widget.

View File

@@ -1,5 +1,5 @@
"""
Utilities for managing AdvancedDockArea profiles stored in INI files.
Utilities for managing BECDockArea profiles stored in INI files.
Policy:
- All created/modified profiles are stored under the BEC settings root: <base_path>/profiles/{default,user}
@@ -36,12 +36,12 @@ ProfileOrigin = Literal["module", "plugin", "settings", "unknown"]
def module_profiles_dir() -> str:
"""
Return the built-in AdvancedDockArea profiles directory bundled with the module.
Return the built-in BECDockArea profiles directory bundled with the module.
Returns:
str: Absolute path of the read-only module profiles directory.
"""
return os.path.join(MODULE_PATH, "containers", "advanced_dock_area", "profiles")
return os.path.join(MODULE_PATH, "containers", "dock_area", "profiles")
@lru_cache(maxsize=1)
@@ -115,12 +115,12 @@ def _settings_profiles_root() -> str:
str: Absolute path to the profiles root. The directory is created if missing.
"""
client = BECClient()
bec_widgets_settings = client._service_config.config.get("bec_widgets_settings")
bec_widgets_settings = client._service_config.config.get("widgets_settings")
bec_widgets_setting_path = (
bec_widgets_settings.get("base_path") if bec_widgets_settings else None
)
default_path = os.path.join(bec_widgets_setting_path, "profiles")
root = os.environ.get("BECWIDGETS_PROFILE_DIR", default_path)
root = os.path.expanduser(os.environ.get("BECWIDGETS_PROFILE_DIR", default_path))
os.makedirs(root, exist_ok=True)
return root
@@ -138,7 +138,7 @@ def _profiles_dir(segment: str, namespace: str | None) -> str:
"""
base = os.path.join(_settings_profiles_root(), segment)
ns = slugify.slugify(namespace, separator="_") if namespace else None
path = os.path.join(base, ns) if ns else base
path = os.path.expanduser(os.path.join(base, ns) if ns else base)
os.makedirs(path, exist_ok=True)
return path

View File

@@ -330,7 +330,7 @@ class WorkSpaceManager(BECWidget, QWidget):
return
self.target_widget.save_profile_dialog()
# AdvancedDockArea will emit profile_changed which will trigger table refresh,
# BECDockArea will emit profile_changed which will trigger table refresh,
# but ensure the UI stays in sync even if the signal is delayed.
self.render_table()
current = getattr(self.target_widget, "_current_profile_name", None)

View File

@@ -118,7 +118,7 @@ class ProfileComboBox(QComboBox):
def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -> ToolbarBundle:
"""
Creates a workspace toolbar bundle for AdvancedDockArea.
Creates a workspace toolbar bundle for BECDockArea.
Args:
components (ToolbarComponents): The components to be added to the bundle.
@@ -171,7 +171,7 @@ def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -
class WorkspaceConnection(BundleConnection):
"""
Connection class for workspace actions in AdvancedDockArea.
Connection class for workspace actions in BECDockArea.
"""
def __init__(self, components: ToolbarComponents, target_widget=None):

View File

@@ -101,14 +101,12 @@ class Explorer(BECWidget, QWidget):
palette = get_theme_palette()
separator_color = palette.mid().color()
self.splitter.setStyleSheet(
f"""
self.splitter.setStyleSheet(f"""
QSplitter::handle {{
height: 0.1px;
background-color: rgba({separator_color.red()}, {separator_color.green()}, {separator_color.blue()}, 60);
}}
"""
)
""")
def _update_spacer(self) -> None:
"""Update the spacer size based on section states"""

View File

@@ -134,15 +134,13 @@ class NotificationToast(QFrame):
bg.setAlphaF(0.30)
icon_bg = bg.name(QtGui.QColor.HexArgb)
icon_btn.setFixedSize(40, 40)
icon_btn.setStyleSheet(
f"""
icon_btn.setStyleSheet(f"""
QToolButton {{
background: {icon_bg};
border: none;
border-radius: 20px; /* perfect circle */
}}
"""
)
""")
title_lbl = QtWidgets.QLabel(self._title)
@@ -327,15 +325,13 @@ class NotificationToast(QFrame):
bg = QtGui.QColor(SEVERITY[value.value]["color"])
bg.setAlphaF(0.30)
icon_bg = bg.name(QtGui.QColor.HexArgb)
self._icon_btn.setStyleSheet(
f"""
self._icon_btn.setStyleSheet(f"""
QToolButton {{
background: {icon_bg};
border: none;
border-radius: 20px;
}}
"""
)
""")
self.apply_theme(self._theme)
# keep injected gradient in sync
if getattr(self, "_hg_enabled", False):
@@ -391,8 +387,7 @@ class NotificationToast(QFrame):
card_bg.setAlphaF(0.88)
btn_hover = self._accent_color.name()
self.setStyleSheet(
f"""
self.setStyleSheet(f"""
#NotificationToast {{
background: {card_bg.name(QtGui.QColor.HexArgb)};
border-radius: 12px;
@@ -406,18 +401,15 @@ class NotificationToast(QFrame):
font-size: 14px;
}}
#NotificationToast QPushButton:hover {{ color: {btn_hover}; }}
"""
)
""")
# traceback panel colours
trace_bg = "#1e1e1e" if theme == "dark" else "#f0f0f0"
self.trace_view.setStyleSheet(
f"""
self.trace_view.setStyleSheet(f"""
background:{trace_bg};
color:{palette['body']};
border:none;
border-radius:8px;
"""
)
""")
# icon glyph vs badge background: darker badge, lighter icon in light mode
icon_fg = "#ffffff" if theme == "light" else self._accent_color.name()
@@ -438,15 +430,13 @@ class NotificationToast(QFrame):
else:
badge_bg.setAlphaF(0.30)
icon_bg = badge_bg.name(QtGui.QColor.HexArgb)
self._icon_btn.setStyleSheet(
f"""
self._icon_btn.setStyleSheet(f"""
QToolButton {{
background: {icon_bg};
border: none;
border-radius: 20px;
}}
"""
)
""")
# stronger accent wash in light mode, slightly stronger in dark too
self._accent_alpha = 110 if theme == "light" else 60
@@ -593,8 +583,7 @@ class NotificationCentre(QScrollArea):
self.setWidgetResizable(True)
# transparent background so only the toast cards are visible
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.setStyleSheet(
"""
self.setStyleSheet("""
#NotificationCentre { background: transparent; }
#NotificationCentre QScrollBar:vertical {
background: transparent;
@@ -610,8 +599,7 @@ class NotificationCentre(QScrollArea):
#NotificationCentre QScrollBar::sub-line:vertical { height: 0; }
#NotificationCentre QScrollBar::add-page:vertical,
#NotificationCentre QScrollBar::sub-page:vertical { background: transparent; }
"""
)
""")
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setFixedWidth(fixed_width)
@@ -958,8 +946,7 @@ class NotificationIndicator(QWidget):
self._group.buttonToggled.connect(self._button_toggled)
# minimalistic look: no frames or backgrounds on the buttons
self.setStyleSheet(
"""
self.setStyleSheet("""
QToolButton {
border: none;
background: transparent;
@@ -970,8 +957,7 @@ class NotificationIndicator(QWidget):
background: rgba(255, 255, 255, 40);
font-weight: 600;
}
"""
)
""")
# initial state: none checked (autodismiss behaviour)
for k in kinds:

View File

@@ -123,7 +123,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
queue="emergency",
metadata={"RID": request_id, "response": False},
)
self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
self.client.connector.send(MessageEndpoints.scan_queue_request(self.client.username), msg)
# pylint: disable=unused-argument
def _on_device_readback(

View File

@@ -1,7 +1,7 @@
import json
from typing import Any, Callable, Generator, Iterable, TypeVar
from bec_lib.utils.json import ExtendedEncoder
from bec_lib.utils.json_extended import ExtendedEncoder
from qtpy.QtCore import QByteArray, QMimeData, QObject, Signal # type: ignore
from qtpy.QtWidgets import QListWidgetItem

View File

@@ -51,8 +51,7 @@ class _DeviceEntryWidget(QFrame):
self.setToolTip(self._rich_text())
def _rich_text(self):
return dedent(
f"""
return dedent(f"""
<b><u><h2> {self._device_spec.name}: </h2></u></b>
<table>
<tr><td> description: </td><td><i> {self._device_spec.description} </i></td></tr>
@@ -60,8 +59,7 @@ class _DeviceEntryWidget(QFrame):
<tr><td> enabled: </td><td><i> {self._device_spec.enabled} </i></td></tr>
<tr><td> read only: </td><td><i> {self._device_spec.readOnly} </i></td></tr>
</table>
"""
)
""")
def setup_title_layout(self, device_spec: HashableDevice):
self._title_layout = QHBoxLayout()

View File

@@ -9,6 +9,7 @@ from qtpy.QtGui import QColor
from qtpy.QtWidgets import (
QApplication,
QComboBox,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
@@ -171,7 +172,13 @@ class ScanControl(BECWidget, QWidget):
self.layout.addStretch()
def _add_metadata_form(self):
self.layout.addWidget(self._metadata_form)
# Wrap metadata form in a group box
self._metadata_group = QGroupBox("Scan Metadata", self)
self._metadata_group.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
metadata_layout = QVBoxLayout(self._metadata_group)
metadata_layout.addWidget(self._metadata_form)
self.layout.addWidget(self._metadata_group)
self._metadata_form.update_with_new_scan(self.comboBox_scan_selection.currentText())
self.scan_selected.connect(self._metadata_form.update_with_new_scan)
self._metadata_form.form_data_updated.connect(self.update_scan_metadata)

View File

@@ -275,12 +275,10 @@ class LMFitDialog(BECWidget, QWidget):
button.setEnabled(True)
else:
button.setEnabled(False)
button.setStyleSheet(
f"""
button.setStyleSheet(f"""
QPushButton:enabled {{ background-color: {self._accent_colors.success.name()};color: white; }}
QPushButton:disabled {{ background-color: grey;color: white; }}
"""
)
""")
self.action_buttons[param_name] = button
layout = QVBoxLayout()
layout.addWidget(self.action_buttons[param_name])

View File

@@ -47,15 +47,13 @@ class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
)
def _init_bec_kernel(self):
self.execute(
"""
self.execute("""
from bec_ipython_client.main import BECIPythonClient
bec = BECIPythonClient()
bec.start()
dev = bec.device_manager.devices if bec else None
scans = bec.scans if bec else None
"""
)
""")
def _cleanup_bec(self):
if getattr(self, "ipyclient", None) is not None and self.inprocess is True:

View File

@@ -6,7 +6,7 @@ from typing import Any, cast
from bec_lib.logger import bec_logger
from bec_lib.macro_update_handler import has_executable_code
from qtpy.QtCore import QEvent, QTimer, Signal
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QFileDialog, QMessageBox, QToolButton, QWidget
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
@@ -36,12 +36,12 @@ class MonacoDock(DockAreaWidget):
**kwargs,
)
self.dock_manager.focusedDockWidgetChanged.connect(self._on_focus_event)
self.dock_manager.installEventFilter(self)
self._last_focused_editor: CDockWidget | None = None
self.focused_editor.connect(self._on_last_focused_editor_changed)
initial_editor = self.add_editor()
if isinstance(initial_editor, CDockWidget):
self.last_focused_editor = initial_editor
self._install_manager_scan_and_fix_guards()
def _create_editor_widget(self) -> MonacoWidget:
"""Create a configured Monaco editor widget."""
@@ -73,7 +73,8 @@ class MonacoDock(DockAreaWidget):
logger.info(f"Editor '{widget.current_file}' has unsaved changes: {widget.get_text()}")
self.save_enabled.emit(widget.modified)
def _update_tab_title_for_modification(self, dock: CDockWidget, modified: bool):
@staticmethod
def _update_tab_title_for_modification(dock: CDockWidget, modified: bool):
"""Update the tab title to show modification status with a dot indicator."""
current_title = dock.windowTitle()
@@ -98,14 +99,12 @@ class MonacoDock(DockAreaWidget):
return
active_sig = signatures[signature.get("activeSignature", 0)]
active_param = signature.get("activeParameter", 0) # TODO: Add highlight for active_param
# Get signature label and documentation
label = active_sig.get("label", "")
doc_obj = active_sig.get("documentation", {})
documentation = doc_obj.get("value", "") if isinstance(doc_obj, dict) else str(doc_obj)
# Format the markdown output
# Format the Markdown output
markdown = f"```python\n{label}\n```\n\n{documentation}"
self.signature_help.emit(markdown)
@@ -156,9 +155,10 @@ class MonacoDock(DockAreaWidget):
if self.last_focused_editor is dock:
self.last_focused_editor = None
# After topology changes, make sure single-tab areas get a plus button
QTimer.singleShot(0, self._scan_and_fix_areas)
self._scan_and_fix_areas()
def reset_widget(self, widget: MonacoWidget):
@staticmethod
def reset_widget(widget: MonacoWidget):
"""
Reset the given Monaco editor widget to its initial state.
@@ -193,23 +193,23 @@ class MonacoDock(DockAreaWidget):
# pylint: disable=protected-access
area._monaco_plus_btn = plus_btn
def _scan_and_fix_areas(self):
def _install_manager_scan_and_fix_guards(self) -> None:
"""
Track ADS structural changes to trigger scan and fix of dock areas for plus button injection.
"""
self.dock_manager.dockAreaCreated.connect(self._scan_and_fix_areas)
self.dock_manager.dockWidgetAdded.connect(self._scan_and_fix_areas)
self.dock_manager.stateRestored.connect(self._scan_and_fix_areas)
self.dock_manager.restoringState.connect(self._scan_and_fix_areas)
self.dock_manager.focusedDockWidgetChanged.connect(self._scan_and_fix_areas)
self._scan_and_fix_areas()
def _scan_and_fix_areas(self, *_arg):
# Find all dock areas under this manager and ensure each single-tab area has a plus button
areas = self.dock_manager.findChildren(CDockAreaWidget)
for a in areas:
self._ensure_area_plus(a)
def eventFilter(self, obj, event):
# Track dock manager events
if obj is self.dock_manager and event.type() in (
QEvent.Type.ChildAdded,
QEvent.Type.ChildRemoved,
QEvent.Type.LayoutRequest,
):
QTimer.singleShot(0, self._scan_and_fix_areas)
return super().eventFilter(obj, event)
def add_editor(
self, area: Any | None = None, title: str | None = None, tooltip: str | None = None
) -> CDockWidget:
@@ -258,7 +258,7 @@ class MonacoDock(DockAreaWidget):
if area_widget is not None:
self._ensure_area_plus(area_widget)
QTimer.singleShot(0, self._scan_and_fix_areas)
self._scan_and_fix_areas()
self.last_focused_editor = dock
return dock

View File

@@ -362,8 +362,7 @@ if __name__ == "__main__": # pragma: no cover
widget.set_language("python")
widget.set_theme("vs-dark")
widget.editor.set_minimap_enabled(False)
widget.set_text(
"""
widget.set_text("""
import numpy as np
from typing import TYPE_CHECKING
@@ -380,8 +379,7 @@ if TYPE_CHECKING:
# This is a comment
def hello_world():
print("Hello, world!")
"""
)
""")
widget.set_highlighted_lines(1, 3)
widget.show()
qapp.exec_()

View File

@@ -53,6 +53,8 @@ class ScanMetadata(PydanticModelForm):
super().__init__(parent=parent, data_model=self._md_schema, client=client, **kwargs)
self._layout.setContentsMargins(0, 0, 0, 0)
self._form_grid_container.layout().setContentsMargins(0, 0, 0, 0)
self._layout.addWidget(self._additional_md_box)
self._additional_md_box_layout.addWidget(self._additional_metadata)
@@ -78,12 +80,27 @@ class ScanMetadata(PydanticModelForm):
def get_form_data(self):
"""Get the entered metadata as a dict"""
return self._additional_metadata.dump_dict() | self._dict_from_grid()
form_data = self._additional_metadata.dump_dict() | self._dict_from_grid()
# If scan_name is empty, set it to the current scan
if "scan_name" in form_data and not form_data["scan_name"]:
form_data["scan_name"] = self._scan_name
return form_data
def populate(self):
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
super().populate()
# Set scan_name field to current scan if it exists and is empty
if "scan_name" not in self.widget_dict:
return
scan_name_widget = self.widget_dict["scan_name"]
if not hasattr(scan_name_widget, "getValue") or scan_name_widget.getValue():
return
if hasattr(scan_name_widget, "setValue"):
scan_name_widget.setValue(self._scan_name)
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))

View File

@@ -22,8 +22,9 @@ class DeviceSignal(BaseModel):
device: str
signal: str
dap: str | None = None
dap: str | list[str] | None = None
dap_oversample: int = 1
dap_parameters: dict | list | None = None
model_config: dict = {"validate_assignment": True}

View File

@@ -1,13 +1,13 @@
from __future__ import annotations
import json
from typing import Literal
from typing import TYPE_CHECKING, Literal
import lmfit
import numpy as np
import pyqtgraph as pg
from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.lmfit_serializer import serialize_lmfit_params, serialize_param_object
from bec_lib.scan_data_container import ScanDataContainer
from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import Qt, QTimer, Signal
@@ -41,6 +41,18 @@ from bec_widgets.widgets.services.scan_history_browser.scan_history_browser impo
)
logger = bec_logger.logger
_DAP_PARAM = object()
if TYPE_CHECKING: # pragma: no cover
import lmfit # type: ignore
else:
try:
import lmfit # type: ignore
except Exception as e: # pragma: no cover
logger.warning(
f"lmfit could not be imported: {e}. Custom DAP functionality will be unavailable."
)
lmfit = None
# noinspection PyDataclass
@@ -696,7 +708,8 @@ class Waveform(PlotBase):
signal_y: str | None = None,
color: str | None = None,
label: str | None = None,
dap: str | None = None,
dap: str | list[str] | None = None,
dap_parameters: dict | list | lmfit.Parameters | None | object = None,
scan_id: str | None = None,
scan_number: int | None = None,
**kwargs,
@@ -718,9 +731,14 @@ class Waveform(PlotBase):
signal_y(str): The name of the entry for the y-axis.
color(str): The color of the curve.
label(str): The label of the curve.
dap(str): The dap model to use for the curve. When provided, a DAP curve is
dap(str | list[str]): The dap model to use for the curve. When provided, a DAP curve is
attached automatically for device, history, or custom data sources. Use
the same string as the LMFit model name.
the same string as the LMFit model name, or a list of model names to build a composite.
dap_parameters(dict | list | lmfit.Parameters | None): Optional lmfit parameter overrides sent to
the DAP server. For a single model: values can be numeric (interpreted as fixed parameters)
or dicts like `{"value": 1.0, "vary": False}`. For composite models (dap is list), use either
a list aligned to the model list (each item is a param dict), or a dict of
`{ "ModelName": { "param": {...} } }` when model names are unique.
scan_id(str): Optional scan ID. When provided, the curve is treated as a **history** curve and
the ydata (and optional xdata) are fetched from that historical scan. Such curves are
never cleared by livescan resets.
@@ -733,6 +751,8 @@ class Waveform(PlotBase):
source = "custom"
x_data = None
y_data = None
if dap_parameters is _DAP_PARAM:
dap_parameters = kwargs.pop("dap_parameters", None) or kwargs.pop("parameters", None)
# 1. Custom curve logic
if x is not None and y is not None:
@@ -810,7 +830,9 @@ class Waveform(PlotBase):
curve = self._add_curve(config=config, x_data=x_data, y_data=y_data)
if dap is not None and curve.config.source in ("device", "history", "custom"):
self.add_dap_curve(device_label=curve.name(), dap_name=dap, **kwargs)
self.add_dap_curve(
device_label=curve.name(), dap_name=dap, dap_parameters=dap_parameters, **kwargs
)
return curve
@@ -820,9 +842,10 @@ class Waveform(PlotBase):
def add_dap_curve(
self,
device_label: str,
dap_name: str,
dap_name: str | list[str],
color: str | None = None,
dap_oversample: int = 1,
dap_parameters: dict | list | lmfit.Parameters | None = None,
**kwargs,
) -> Curve:
"""
@@ -832,9 +855,11 @@ class Waveform(PlotBase):
Args:
device_label(str): The label of the source curve to add DAP to.
dap_name(str): The name of the DAP model to use.
dap_name(str | list[str]): The name of the DAP model to use, or a list of model
names to build a composite model.
color(str): The color of the curve.
dap_oversample(int): The oversampling factor for the DAP curve.
dap_parameters(dict | list | lmfit.Parameters | None): Optional lmfit parameter overrides sent to the DAP server.
**kwargs
Returns:
@@ -859,7 +884,7 @@ class Waveform(PlotBase):
dev_entry = "custom"
# 2) Build a label for the new DAP curve
dap_label = f"{device_label}-{dap_name}"
dap_label = f"{device_label}-{self._format_dap_label(dap_name)}"
# 3) Possibly raise if the DAP curve already exists
if self._check_curve_id(dap_label):
@@ -882,7 +907,11 @@ class Waveform(PlotBase):
# Attach device signal with DAP
config.signal = DeviceSignal(
device=dev_name, signal=dev_entry, dap=dap_name, dap_oversample=dap_oversample
device=dev_name,
signal=dev_entry,
dap=dap_name,
dap_oversample=dap_oversample,
dap_parameters=self._normalize_dap_parameters(dap_parameters, dap_name=dap_name),
)
# 4) Create the DAP curve config using `_add_curve(...)`
@@ -1754,7 +1783,9 @@ class Waveform(PlotBase):
x_data, y_data = parent_curve.get_data()
model_name = dap_curve.config.signal.dap
model = getattr(self.dap, model_name)
model = None
if not isinstance(model_name, (list, tuple)):
model = getattr(self.dap, model_name)
try:
x_min, x_max = self.roi_region
x_data, y_data = self._crop_data(x_data, y_data, x_min, x_max)
@@ -1762,20 +1793,132 @@ class Waveform(PlotBase):
x_min = None
x_max = None
dap_parameters = getattr(dap_curve.config.signal, "dap_parameters", None)
dap_kwargs = {
"data_x": x_data,
"data_y": y_data,
"oversample": dap_curve.dap_oversample,
}
if dap_parameters:
dap_kwargs["parameters"] = dap_parameters
if model is not None:
class_args = model._plugin_info["class_args"]
class_kwargs = model._plugin_info["class_kwargs"]
else:
class_args = []
class_kwargs = {"model": model_name}
msg = messages.DAPRequestMessage(
dap_cls="LmfitService1D",
dap_type="on_demand",
config={
"args": [],
"kwargs": {"data_x": x_data, "data_y": y_data},
"class_args": model._plugin_info["class_args"],
"class_kwargs": model._plugin_info["class_kwargs"],
"kwargs": dap_kwargs,
"class_args": class_args,
"class_kwargs": class_kwargs,
"curve_label": dap_curve.name(),
},
metadata={"RID": f"{self.scan_id}-{self.gui_id}"},
)
self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg)
@staticmethod
def _normalize_dap_parameters(
parameters: dict | list | lmfit.Parameters | None, dap_name: str | list[str] | None = None
) -> dict | list | None:
"""
Normalize user-provided lmfit parameters into a JSON-serializable dict suitable for the DAP server.
Supports:
- `lmfit.Parameters` (single-model only)
- `dict[name -> number]` (treated as fixed parameter with `vary=False`)
- `dict[name -> dict]` (lmfit.Parameter fields; defaults to `vary=False` if unspecified)
- `dict[name -> lmfit.Parameter]`
- composite: `list[dict[param_name -> spec]]` aligned to model list
- composite: `dict[model_name -> dict[param_name -> spec]]` (unique model names only)
"""
if parameters is None:
return None
if isinstance(dap_name, (list, tuple)):
if lmfit is not None and isinstance(parameters, lmfit.Parameters):
raise TypeError("dap_parameters must be a dict when using composite dap models.")
if isinstance(parameters, (list, tuple)):
normalized_list: list[dict | None] = []
for idx, item in enumerate(parameters):
if item is None:
normalized_list.append(None)
continue
if not isinstance(item, dict):
raise TypeError(
f"dap_parameters list item {idx} must be a dict of parameter overrides."
)
normalized_list.append(Waveform._normalize_param_overrides(item))
return normalized_list or None
if not isinstance(parameters, dict):
raise TypeError(
"dap_parameters must be a dict of model->params when using composite dap models."
)
model_names = set(dap_name)
invalid_models = set(parameters.keys()) - model_names
if invalid_models:
raise TypeError(
f"Invalid dap_parameters keys for composite model: {sorted(invalid_models)}"
)
normalized_composite: dict[str, dict] = {}
for model_name in dap_name:
model_params = parameters.get(model_name)
if model_params is None:
continue
if not isinstance(model_params, dict):
raise TypeError(
f"dap_parameters for '{model_name}' must be a dict of parameter overrides."
)
normalized = Waveform._normalize_param_overrides(model_params)
if normalized:
normalized_composite[model_name] = normalized
return normalized_composite or None
if lmfit is not None and isinstance(parameters, lmfit.Parameters):
return serialize_lmfit_params(parameters)
if not isinstance(parameters, dict):
if lmfit is None:
raise TypeError(
"dap_parameters must be a dict when lmfit is not installed on the client."
)
raise TypeError("dap_parameters must be a dict or lmfit.Parameters (or omitted).")
return Waveform._normalize_param_overrides(parameters)
@staticmethod
def _normalize_param_overrides(parameters: dict) -> dict | None:
normalized: dict[str, dict] = {}
for name, spec in parameters.items():
if spec is None:
continue
if isinstance(spec, (int, float, np.number)):
normalized[name] = {"name": name, "value": float(spec), "vary": False}
continue
if lmfit is not None and isinstance(spec, lmfit.Parameter):
normalized[name] = serialize_param_object(spec)
continue
if isinstance(spec, dict):
normalized[name] = {"name": name, **spec}
if "vary" not in normalized[name]:
normalized[name]["vary"] = False
continue
raise TypeError(
f"Invalid dap_parameters entry for '{name}': expected number, dict, or lmfit.Parameter."
)
return normalized or None
@staticmethod
def _format_dap_label(dap_name: str | list[str]) -> str:
if isinstance(dap_name, (list, tuple)):
return "+".join(dap_name)
return dap_name
@SafeSlot(dict, dict)
def update_dap_curves(self, msg, metadata):
"""
@@ -1793,14 +1936,6 @@ class Waveform(PlotBase):
if not curve:
return
# Get data from the parent (device) curve
parent_curve = self._find_curve_by_label(curve.config.parent_label)
if parent_curve is None:
return
x_parent, _ = parent_curve.get_data()
if x_parent is None or len(x_parent) == 0:
return
# Retrieve and store the fit parameters and summary from the DAP server response
try:
curve.dap_params = msg["data"][1]["fit_parameters"]
@@ -1809,19 +1944,13 @@ class Waveform(PlotBase):
logger.warning(f"Failed to retrieve DAP data for curve '{curve.name()}'")
return
# Render model according to the DAP model name and parameters
model_name = curve.config.signal.dap
model_function = getattr(lmfit.models, model_name)()
x_min, x_max = x_parent.min(), x_parent.max()
oversample = curve.dap_oversample
new_x = np.linspace(x_min, x_max, int(len(x_parent) * oversample))
# Evaluate the model with the provided parameters to generate the y values
new_y = model_function.eval(**curve.dap_params, x=new_x)
# Update the curve with the new data
curve.setData(new_x, new_y)
# Plot the fitted curve using the server-provided output to avoid requiring lmfit on the client.
try:
fit_data = msg["data"][0]
curve.setData(np.asarray(fit_data["x"]), np.asarray(fit_data["y"]))
except Exception as e:
logger.exception(f"Failed to plot DAP result for curve '{curve.name()}', error: {e}")
return
metadata.update({"curve_id": curve_id})
self.dap_params_update.emit(curve.dap_params, metadata)
@@ -2341,24 +2470,20 @@ class DemoApp(QMainWindow): # pragma: no cover
def __init__(self):
super().__init__()
self.setWindowTitle("Waveform Demo")
self.resize(1200, 600)
self.resize(1600, 600)
self.main_widget = QWidget(self)
self.layout = QHBoxLayout(self.main_widget)
self.setCentralWidget(self.main_widget)
self.waveform_popup = Waveform(popups=True)
self.waveform_popup.plot(device_y="waveform")
self.waveform_side = Waveform(popups=False)
self.waveform_side.plot(device_y="bpm4i", signal_y="bpm4i", dap="GaussianModel")
self.waveform_side.plot(device_y="bpm3a", signal_y="bpm3a")
self.custom_waveform = Waveform(popups=True)
self._populate_custom_curve_demo()
self.layout.addWidget(self.waveform_side)
self.layout.addWidget(self.waveform_popup)
self.sine_waveform = Waveform(popups=True)
self.sine_waveform.dap_params_update.connect(self._log_sine_dap_params)
self._populate_sine_curve_demo()
self.layout.addWidget(self.custom_waveform)
self.layout.addWidget(self.sine_waveform)
def _populate_custom_curve_demo(self):
"""
@@ -2377,8 +2502,141 @@ class DemoApp(QMainWindow): # pragma: no cover
sigma = 0.8
y = amplitude * np.exp(-((x - center) ** 2) / (2 * sigma**2)) + noise
# 1) No explicit parameters: server will use lmfit defaults/guesses.
self.custom_waveform.plot(x=x, y=y, label="custom-gaussian", dap="GaussianModel")
# 2) Easy dict: numbers mean "fix this parameter to value" (vary=False).
self.custom_waveform.plot(
x=x,
y=y,
label="custom-gaussian-fixed-easy",
dap="GaussianModel",
dap_parameters={"amplitude": 1.0},
dap_oversample=5,
)
# 3) Partial parameter override: this should still trigger guessing on the server
# because not all Gaussian parameters are explicitly specified.
self.custom_waveform.plot(
x=x,
y=y,
label="custom-gaussian-partial-guess",
dap="GaussianModel",
dap_parameters={
"center": {"value": 1.2, "vary": True},
"sigma": {"value": sigma, "vary": False, "min": 0.0},
},
)
# 4) Complete parameter override: this should skip guessing on the server.
if lmfit is not None:
params_gauss = lmfit.models.GaussianModel().make_params()
params_gauss["amplitude"].set(value=amplitude, vary=False)
params_gauss["center"].set(value=center, vary=False)
params_gauss["sigma"].set(value=sigma, vary=False, min=0.0)
self.custom_waveform.plot(
x=x,
y=y,
label="custom-gaussian-complete-no-guess",
dap="GaussianModel",
dap_parameters=params_gauss,
)
else:
logger.info("Skipping lmfit.Parameters demo (lmfit not installed on client).")
# Composite example: spectrum with three Gaussians (DAP-only)
x_spec = np.linspace(-5, 5, 800)
rng_spec = np.random.default_rng(123)
centers = [-2.0, 0.6, 2.4]
amplitudes = [2.5, 3.2, 1.8]
sigmas = [0.35, 0.5, 0.3]
y_spec = (
amplitudes[0] * np.exp(-((x_spec - centers[0]) ** 2) / (2 * sigmas[0] ** 2))
+ amplitudes[1] * np.exp(-((x_spec - centers[1]) ** 2) / (2 * sigmas[1] ** 2))
+ amplitudes[2] * np.exp(-((x_spec - centers[2]) ** 2) / (2 * sigmas[2] ** 2))
+ rng_spec.normal(loc=0, scale=0.06, size=x_spec.size)
)
# 5) Composite model with partial overrides only: this should still trigger guessing.
self.custom_waveform.plot(
x=x_spec,
y=y_spec,
label="custom-gaussian-spectrum-partial-guess",
dap=["GaussianModel", "GaussianModel", "GaussianModel"],
dap_parameters=[
{"center": {"value": centers[0], "vary": False}},
{"center": {"value": centers[1], "vary": False}},
{"center": {"value": centers[2], "vary": False}},
],
)
# 6) Composite model with all component parameters specified: this should skip guessing.
self.custom_waveform.plot(
x=x_spec,
y=y_spec,
label="custom-gaussian-spectrum-complete-no-guess",
dap=["GaussianModel", "GaussianModel", "GaussianModel"],
dap_parameters=[
{
"amplitude": {"value": amplitudes[0], "vary": False},
"center": {"value": centers[0], "vary": False},
"sigma": {"value": sigmas[0], "vary": False, "min": 0.0},
},
{
"amplitude": {"value": amplitudes[1], "vary": False},
"center": {"value": centers[1], "vary": False},
"sigma": {"value": sigmas[1], "vary": False, "min": 0.0},
},
{
"amplitude": {"value": amplitudes[2], "vary": False},
"center": {"value": centers[2], "vary": False},
"sigma": {"value": sigmas[2], "vary": False, "min": 0.0},
},
],
)
def _populate_sine_curve_demo(self):
"""
Showcase how lmfit's base SineModel can struggle with a drifting baseline.
"""
x = np.linspace(0, 6 * np.pi, 600)
rng = np.random.default_rng(7)
amplitude = 1.6
frequency = 0.75
phase = 0.4
offset = 0.8
slope = 0.08
noise = rng.normal(loc=0, scale=0.12, size=x.size)
y = offset + slope * x + amplitude * np.sin(2 * np.pi * frequency * x + phase) + noise
# Base SineModel (no offset support) to show the mismatch
self.sine_waveform.plot(x=x, y=y, label="custom-sine-data", dap="SineModel")
# Composite model: Sine + Linear baseline (offset + slope)
self.sine_waveform.plot(
x=x,
y=y,
label="custom-sine-composite",
dap=["SineModel", "LinearModel"],
dap_oversample=4,
)
if lmfit is None:
logger.info("Skipping sine lmfit demo (lmfit not installed on client).")
return
return
@staticmethod
def _log_sine_dap_params(params: dict, metadata: dict):
curve_id = metadata.get("curve_id")
if curve_id not in {
"custom-sine-data-SineModel",
"custom-sine-composite-SineModel+LinearModel",
}:
return
logger.info(f"SineModel DAP fit params ({curve_id}): {params}")
if __name__ == "__main__": # pragma: no cover
import sys

View File

@@ -44,14 +44,12 @@ class RingCardWidget(QFrame):
self.setObjectName("RingCardWidget")
bg = self._get_theme_color("BORDER")
self.setStyleSheet(
f"""
self.setStyleSheet(f"""
#RingCardWidget {{
border: 1px solid {bg.name() if bg else '#CCCCCC'};
border-radius: 4px;
}}
"""
)
""")
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import json
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_qthemes import material_icon
@@ -59,8 +61,10 @@ class BECQueue(BECWidget, CompactPopupWidget):
# Set up the table
self.table = QTableWidget(self)
# self.layout.addWidget(self.table)
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status", "Cancel"])
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(
["Scan Number", "Scan Name", "Type", "Status", "Cancel"]
)
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
@@ -169,15 +173,23 @@ class BECQueue(BECWidget, CompactPopupWidget):
blocks = item.request_blocks
scan_types = []
scan_numbers = []
scan_names = []
scan_ids = []
user_metadatas = []
status = item.status
for request_block in blocks:
scan_type = request_block.msg.scan_type
user_metadata = request_block.msg.metadata.get("user_metadata", {})
scan_name = user_metadata.get("scan_name", scan_type)
if scan_type:
scan_types.append(scan_type)
scan_number = request_block.scan_number
if scan_number:
scan_numbers.append(str(scan_number))
if scan_name:
scan_names.append(scan_name)
if user_metadata:
user_metadatas.append(user_metadata)
scan_id = request_block.scan_id
if scan_id:
scan_ids.append(scan_id)
@@ -185,9 +197,18 @@ class BECQueue(BECWidget, CompactPopupWidget):
scan_types = ", ".join(scan_types)
if scan_numbers:
scan_numbers = ", ".join(scan_numbers)
if scan_names:
scan_names = ", ".join(scan_names)
# Pretty print user metadata as tooltip
tooltip = ""
if user_metadatas:
if len(user_metadatas) == 1:
tooltip = json.dumps(user_metadatas[0], indent=2)
else:
tooltip = json.dumps(user_metadatas, indent=2)
if scan_ids:
scan_ids = ", ".join(scan_ids)
self.set_row(index, scan_numbers, scan_types, status, scan_ids)
self.set_row(index, scan_numbers, scan_names, scan_types, status, scan_ids, tooltip)
busy = (
False
if all(item.status in ("STOPPED", "COMPLETED", "IDLE") for item in queue_info)
@@ -196,13 +217,13 @@ class BECQueue(BECWidget, CompactPopupWidget):
self.set_global_state("warning" if busy else "default")
self.queue_busy.emit(busy)
def format_item(self, content: str, status=False) -> QTableWidgetItem:
def format_item(self, content: str, status=False, tooltip: str = "") -> QTableWidgetItem:
"""
Format the content of the table item.
Args:
content (str): The content to be formatted.
tooltip (str): Optional tooltip to display.
Returns:
QTableWidgetItem: The formatted item.
"""
@@ -210,7 +231,8 @@ class BECQueue(BECWidget, CompactPopupWidget):
content = ""
item = QTableWidgetItem(content)
item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
# item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
if tooltip:
item.setToolTip(tooltip)
if status:
try:
@@ -220,23 +242,36 @@ class BECQueue(BECWidget, CompactPopupWidget):
return item
return item
def set_row(self, index: int, scan_number: str, scan_type: str, status: str, scan_id: str):
def set_row(
self,
index: int,
scan_number: str,
scan_name: str,
scan_type: str,
status: str,
scan_id: str,
tooltip: str = "",
):
"""
Set the row of the table.
Args:
index (int): The index of the row.
scan_number (str): The scan number.
scan_name (str): The scan name.
scan_type (str): The scan type.
status (str): The status.
scan_id (str): The scan id.
tooltip (str): Optional tooltip to display (pretty-printed user metadata).
"""
abort_button = self._create_abort_button(scan_id)
abort_button.button.clicked.connect(self.delete_selected_row)
self.table.setItem(index, 0, self.format_item(scan_number))
self.table.setItem(index, 1, self.format_item(scan_type))
self.table.setItem(index, 2, self.format_item(status, status=True))
self.table.setCellWidget(index, 3, abort_button)
self.table.setItem(index, 0, self.format_item(scan_number, tooltip=tooltip))
self.table.setItem(index, 1, self.format_item(scan_name, tooltip=tooltip))
self.table.setItem(index, 2, self.format_item(scan_type, tooltip=tooltip))
self.table.setItem(index, 3, self.format_item(status, status=True, tooltip=tooltip))
self.table.setCellWidget(index, 4, abort_button)
def _create_abort_button(self, scan_id: str) -> AbortButton:
"""
@@ -254,14 +289,12 @@ class BECQueue(BECWidget, CompactPopupWidget):
abort_button.button.setIcon(
material_icon("cancel", color="#cc181e", filled=True, convert_to_pixmap=False)
)
abort_button.setStyleSheet(
"""
abort_button.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
}
"""
)
""")
return abort_button
@@ -279,7 +312,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
"""
self.table.setRowCount(1)
self.set_row(0, "", "", "", "")
self.set_row(0, "", "", "", "", "")
if __name__ == "__main__": # pragma: no cover

View File

@@ -227,13 +227,11 @@ class IDEExplorer(BECWidget, QWidget):
try:
# Create the file with a basic template
with open(file_path, "w", encoding="utf-8") as f:
f.write(
f"""
f.write(f"""
\"\"\"
{filename} - Created at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
\"\"\"
"""
)
""")
except Exception as e:
# Show error if file creation failed
@@ -281,8 +279,7 @@ class IDEExplorer(BECWidget, QWidget):
try:
# Create the file with a macro function template
with open(file_path, "w", encoding="utf-8") as f:
f.write(
f'''"""
f.write(f'''"""
{function_name} macro - Created at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
"""
@@ -296,8 +293,7 @@ def {function_name}():
print("Executing macro: {function_name}")
# TODO: Add your macro code here
pass
'''
)
''')
# Refresh the macro tree to show the new function
macro_dir_section.content_widget.refresh()

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "2.45.14"
version = "3.2.0"
description = "BEC Widgets"
requires-python = ">=3.11"
classifiers = [
@@ -13,11 +13,11 @@ classifiers = [
"Topic :: Scientific/Engineering",
]
dependencies = [
"bec_ipython_client~=3.70", # needed for jupyter console
"bec_lib~=3.70",
"bec_qthemes~=1.0, >=1.3.3",
"black~=25.0", # needed for bw-generate-cli
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
"bec_ipython_client~=3.107,>=3.107.2", # needed for jupyter console
"bec_lib~=3.107,>=3.107.2",
"bec_qthemes~=1.0, >=1.3.4",
"black>=26,<27", # needed for bw-generate-cli
"isort>=5.13, <9.0", # needed for bw-generate-cli
"ophyd_devices~=1.29, >=1.29.1",
"pydantic~=2.0",
"pyqtgraph==0.13.7",

View File

@@ -150,7 +150,9 @@ def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
# communication should work, main dock area should have same id and be visible
yw = gui.new("Y")
qtbot.waitUntil(lambda: len(gui.windows) == 2, timeout=3000)
yw.delete_all()
assert len(gui.windows) == 2
yw.remove()
assert len(gui.windows) == 1 # only bec is left
qtbot.waitUntil(lambda: len(gui.windows) == 1, timeout=3000)
assert len(gui.windows) == 1

View File

@@ -72,6 +72,7 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
"dap": None,
"device": "bpm4i",
"signal": "bpm4i",
"dap_parameters": None,
"dap_oversample": 1,
}
assert c1._config_dict["source"] == "device"

View File

@@ -135,144 +135,160 @@ def maybe_remove_dock_area(qtbot, gui: BECGuiClient, random_int_gen: random.Rand
wait_for_namespace_change(
qtbot, gui=gui, parent_widget=gui, object_name=name, widget_gui_id=gui_id, exists=False
)
qtbot.waitUntil(lambda: hasattr(gui, "dock_area") is False, timeout=5000)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_bec_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the BECProgressBar widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.BECProgressBar)
widget: client.BECProgressBar
# Check rpc calls
assert widget.label_template == "$value / $maximum - $percentage %"
widget.set_maximum(100)
widget.set_minimum(50)
widget.set_value(75)
assert widget._get_label() == "75 / 100 - 50 %"
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_bec_queue(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the BECQueue widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.BECQueue)
widget: client.BECQueue
# No rpc calls to test so far
# maybe we can add an rpc call to check the queue length
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_bec_status_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the BECStatusBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.BECStatusBox)
# Check rpc calls
assert widget.get_server_state() in ["RUNNING", "IDLE", "BUSY", "ERROR"]
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_dap_combo_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the DAPComboBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.DapComboBox)
widget: client.DAPComboBox
# Check rpc calls
widget.select_fit_model("PseudoVoigtModel")
widget.select_x_axis("samx")
widget.select_y_axis("bpm4i")
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_device_browser(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the DeviceBrowser widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.DeviceBrowser)
widget: client.DeviceBrowser
# No rpc calls yet to check
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the Image widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.Image)
widget: client.Image
scans = bec.scans
dev = bec.device_manager.devices
# Test rpc calls
img = widget.image(device=dev.eiger.name, signal="preview")
assert img.get_data() is None
# Run a scan and plot the image
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
s.wait()
def _wait_for_scan_in_history():
# Get scan item from history
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
return scan_item is not None
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
# Check that last image is equivalent to data in Redis
last_img = bec.connector.get_last(MessageEndpoints.device_preview("eiger", "preview"))[
"data"
].data
assert np.allclose(img.get_data(), last_img)
# Now add a device with a preview signal
img = widget.image(device="eiger", signal="preview")
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
s.wait()
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# TODO re-enable when issue is resolved #560
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_log_panel(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the LogPanel widget."""
# def test_widgets_e2e_bec_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the BECProgressBar widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.LogPanel)
# widget: client.LogPanel
# widget = create_widget(qtbot, gui, gui.available_widgets.BECProgressBar)
# widget: client.BECProgressBar
# # Check rpc calls
# assert widget.label_template == "$value / $maximum - $percentage %"
# widget.set_maximum(100)
# widget.set_minimum(50)
# widget.set_value(75)
# assert widget._get_label() == "75 / 100 - 50 %"
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_bec_queue(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the BECQueue widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.BECQueue)
# widget: client.BECQueue
# # No rpc calls to test so far
# # maybe we can add an rpc call to check the queue length
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_bec_status_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the BECStatusBox widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.BECStatusBox)
# # Check rpc calls
# assert widget.get_server_state() in ["RUNNING", "IDLE", "BUSY", "ERROR"]
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_dap_combo_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the DAPComboBox widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.DapComboBox)
# widget: client.DAPComboBox
# # Check rpc calls
# widget.select_fit_model("PseudoVoigtModel")
# widget.select_x_axis("samx")
# widget.select_y_axis("bpm4i")
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_device_browser(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the DeviceBrowser widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.DeviceBrowser)
# widget: client.DeviceBrowser
# # No rpc calls yet to check
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the Image widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.Image)
# widget: client.Image
# scans = bec.scans
# dev = bec.device_manager.devices
# # Test rpc calls
# img = widget.image(device=dev.eiger.name, signal="preview")
# assert img.get_data() is None
# # Run a scan and plot the image
# s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
# s.wait()
# def _wait_for_scan_in_history():
# # Get scan item from history
# scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
# return scan_item is not None
# qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
# # Check that last image is equivalent to data in Redis
# last_img = bec.connector.get_last(MessageEndpoints.device_preview("eiger", "preview"))[
# "data"
# ].data
# assert np.allclose(img.get_data(), last_img)
# # Now add a device with a preview signal
# img = widget.image(device="eiger", signal="preview")
# s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
# s.wait()
# qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # TODO re-enable when issue is resolved #560
# # @pytest.mark.timeout(PYTEST_TIMEOUT)
# # def test_widgets_e2e_log_panel(qtbot, connected_client_gui_obj, random_generator_from_seed):
# # """Test the LogPanel widget."""
# # gui = connected_client_gui_obj
# # bec = gui._client
# # # Create dock_area and widget
# # widget = create_widget(qtbot, gui, gui.available_widgets.LogPanel)
# # widget: client.LogPanel
# # # No rpc calls to check so far
# # # Test removing the widget, or leaving it open for the next test
# # maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_minesweeper(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the MineSweeper widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.Minesweeper)
# widget: client.MineSweeper
# # No rpc calls to check so far
@@ -280,175 +296,160 @@ def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_fro
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_minesweeper(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the MineSweeper widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.Minesweeper)
widget: client.MineSweeper
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_motor_map(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the MotorMap widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.MotorMap)
# widget: client.MotorMap
# No rpc calls to check so far
# # Test RPC calls
# dev = bec.device_manager.devices
# scans = bec.scans
# # Set motor map to names
# widget.map(dev.samx, dev.samy)
# # Move motor samx to pos
# pos = dev.samx.limits[1] - 1 # -1 from higher limit
# scans.mv(dev.samx, pos, relative=False).wait()
# # Check that data is up to date
# assert np.isclose(widget.get_data()["x"][-1], pos, dev.samx.precision)
# # Move motor samy to pos
# pos = dev.samy.limits[0] + 1 # +1 from lower limit
# scans.mv(dev.samy, pos, relative=False).wait()
# # Check that data is up to date
# assert np.isclose(widget.get_data()["y"][-1], pos, dev.samy.precision)
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_motor_map(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the MotorMap widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.MotorMap)
widget: client.MotorMap
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_multi_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test MultiWaveform widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.MultiWaveform)
# widget: client.MultiWaveform
# Test RPC calls
dev = bec.device_manager.devices
scans = bec.scans
# Set motor map to names
widget.map(dev.samx, dev.samy)
# Move motor samx to pos
pos = dev.samx.limits[1] - 1 # -1 from higher limit
scans.mv(dev.samx, pos, relative=False).wait()
# Check that data is up to date
assert np.isclose(widget.get_data()["x"][-1], pos, dev.samx.precision)
# Move motor samy to pos
pos = dev.samy.limits[0] + 1 # +1 from lower limit
scans.mv(dev.samy, pos, relative=False).wait()
# Check that data is up to date
assert np.isclose(widget.get_data()["y"][-1], pos, dev.samy.precision)
# # Test RPC calls
# dev = bec.device_manager.devices
# scans = bec.scans
# # test plotting
# cm = "cividis"
# widget.plot(dev.waveform, color_palette=cm)
# assert widget.monitor == dev.waveform.name
# assert widget.color_palette == cm
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Scan with BEC
# s = scans.line_scan(dev.samx, -3, 3, steps=5, exp_time=0.01, relative=False)
# s.wait()
# def _wait_for_scan_in_history():
# # Get scan item from history
# scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
# return scan_item is not None
# qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
# # Wait for data in history (should be plotted?)
# # TODO how can we check that the data was plotted, implement get_data()
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_multi_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test MultiWaveform widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.MultiWaveform)
widget: client.MultiWaveform
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_positioner_indicator(
# qtbot, connected_client_gui_obj, random_generator_from_seed
# ):
# """Test the PositionIndicator widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.PositionIndicator)
# widget: client.PositionIndicator
# Test RPC calls
dev = bec.device_manager.devices
scans = bec.scans
# test plotting
cm = "cividis"
widget.plot(dev.waveform, color_palette=cm)
assert widget.monitor == dev.waveform.name
assert widget.color_palette == cm
# # TODO check what these rpc calls are supposed to do! Issue created #461
# widget.set_value(5)
# Scan with BEC
s = scans.line_scan(dev.samx, -3, 3, steps=5, exp_time=0.01, relative=False)
s.wait()
def _wait_for_scan_in_history():
# Get scan item from history
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
return scan_item is not None
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
# Wait for data in history (should be plotted?)
# TODO how can we check that the data was plotted, implement get_data()
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_positioner_indicator(
qtbot, connected_client_gui_obj, random_generator_from_seed
):
"""Test the PositionIndicator widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionIndicator)
widget: client.PositionIndicator
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_positioner_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the PositionerBox widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox)
# widget: client.PositionerBox
# TODO check what these rpc calls are supposed to do! Issue created #461
widget.set_value(5)
# # Test rpc calls
# dev = bec.device_manager.devices
# scans = bec.scans
# # No rpc calls to check so far
# widget.set_positioner(dev.samx)
# widget.set_positioner(dev.samy.name)
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# scans.mv(dev.samy, -3, relative=False).wait()
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_positioner_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the PositionerBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox)
widget: client.PositionerBox
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_positioner_box_2d(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the PositionerBox2D widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox2D)
# widget: client.PositionerBox2D
# Test rpc calls
dev = bec.device_manager.devices
scans = bec.scans
# No rpc calls to check so far
widget.set_positioner(dev.samx)
widget.set_positioner(dev.samy.name)
# # Test rpc calls
# dev = bec.device_manager.devices
# scans = bec.scans
# # No rpc calls to check so far
# widget.set_positioner_hor(dev.samx)
# widget.set_positioner_ver(dev.samy)
scans.mv(dev.samy, -3, relative=False).wait()
# # Try moving the motors
# scans.mv(dev.samx, 3, relative=False).wait()
# scans.mv(dev.samy, -3, relative=False).wait()
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_positioner_box_2d(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the PositionerBox2D widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox2D)
widget: client.PositionerBox2D
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_positioner_control_line(
# qtbot, connected_client_gui_obj, random_generator_from_seed
# ):
# """Test the positioner control line widget"""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.PositionerControlLine)
# widget: client.PositionerControlLine
# Test rpc calls
dev = bec.device_manager.devices
scans = bec.scans
# No rpc calls to check so far
widget.set_positioner_hor(dev.samx)
widget.set_positioner_ver(dev.samy)
# # Test rpc calls
# dev = bec.device_manager.devices
# scans = bec.scans
# # Set positioner
# widget.set_positioner(dev.samx)
# scans.mv(dev.samx, 3, relative=False).wait()
# widget.set_positioner(dev.samy.name)
# scans.mv(dev.samy, -3, relative=False).wait()
# Try moving the motors
scans.mv(dev.samx, 3, relative=False).wait()
scans.mv(dev.samy, -3, relative=False).wait()
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_positioner_control_line(
qtbot, connected_client_gui_obj, random_generator_from_seed
):
"""Test the positioner control line widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionerControlLine)
widget: client.PositionerControlLine
# Test rpc calls
dev = bec.device_manager.devices
scans = bec.scans
# Set positioner
widget.set_positioner(dev.samx)
scans.mv(dev.samx, 3, relative=False).wait()
widget.set_positioner(dev.samy.name)
scans.mv(dev.samy, -3, relative=False).wait()
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# TODO passes locally, fails on CI for some reason... -> issue #1003
@pytest.mark.repeat(20)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the RingProgressBar widget"""
@@ -477,86 +478,86 @@ def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_g
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_scan_control(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the ScanControl widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.ScanControl)
widget: client.ScanControl
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_scan_control(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the ScanControl widget"""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.ScanControl)
# widget: client.ScanControl
# No rpc calls to check so far
# # No rpc calls to check so far
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_scatter_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the ScatterWaveform widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.ScatterWaveform)
widget: client.ScatterWaveform
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_scatter_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the ScatterWaveform widget"""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.ScatterWaveform)
# widget: client.ScatterWaveform
# Test rpc calls
dev = bec.device_manager.devices
scans = bec.scans
widget.plot(dev.samx, dev.samy, dev.bpm4i)
scans.grid_scan(dev.samx, -5, 5, 5, dev.samy, -5, 5, 5, exp_time=0.01, relative=False).wait()
# # Test rpc calls
# dev = bec.device_manager.devices
# scans = bec.scans
# widget.plot(dev.samx, dev.samy, dev.bpm4i)
# scans.grid_scan(dev.samx, -5, 5, 5, dev.samy, -5, 5, 5, exp_time=0.01, relative=False).wait()
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_text_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the TextBox widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.TextBox)
widget: client.TextBox
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_text_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the TextBox widget"""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.TextBox)
# widget: client.TextBox
# RPC calls
widget.set_plain_text("Hello World")
widget.set_html_text("<b> Hello World HTML </b>")
# # RPC calls
# widget.set_plain_text("Hello World")
# widget.set_html_text("<b> Hello World HTML </b>")
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the Waveform widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.Waveform)
widget: client.Waveform
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the Waveform widget"""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.Waveform)
# widget: client.Waveform
# Test rpc calls
dev = bec.device_manager.devices
scans = bec.scans
widget.plot(dev.bpm4i)
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
s.wait()
# # Test rpc calls
# dev = bec.device_manager.devices
# scans = bec.scans
# widget.plot(dev.bpm4i)
# s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
# s.wait()
def _wait_for_scan_in_history():
# Get scan item from history
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
return scan_item is not None
# def _wait_for_scan_in_history():
# # Get scan item from history
# scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
# return scan_item is not None
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
# qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
samx_data = scan_item.devices.samx.samx.read()["value"]
bpm4i_data = scan_item.devices.bpm4i.bpm4i.read()["value"]
curve = widget.curves[0]
assert np.allclose(curve.get_data()[0], samx_data)
assert np.allclose(curve.get_data()[1], bpm4i_data)
# scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
# samx_data = scan_item.devices.samx.samx.read()["value"]
# bpm4i_data = scan_item.devices.bpm4i.bpm4i.read()["value"]
# curve = widget.curves[0]
# assert np.allclose(curve.get_data()[0], samx_data)
# assert np.allclose(curve.get_data()[1], bpm4i_data)
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)

View File

@@ -100,7 +100,8 @@ def test_bec_queue(bec_queue, bec_queue_msg_full):
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
assert bec_queue.table.item(0, 2).text() == "line_scan"
assert bec_queue.table.item(0, 3).text() == "COMPLETED"
def test_bec_queue_empty(bec_queue):
@@ -109,6 +110,7 @@ def test_bec_queue_empty(bec_queue):
assert bec_queue.table.item(0, 0).text() == ""
assert bec_queue.table.item(0, 1).text() == ""
assert bec_queue.table.item(0, 2).text() == ""
assert bec_queue.table.item(0, 3).text() == ""
def test_queue_abort(bec_queue, bec_queue_msg_full):
@@ -118,9 +120,10 @@ def test_queue_abort(bec_queue, bec_queue_msg_full):
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
assert bec_queue.table.item(0, 2).text() == "line_scan"
assert bec_queue.table.item(0, 3).text() == "COMPLETED"
abort_button = bec_queue.table.cellWidget(0, 3)
abort_button = bec_queue.table.cellWidget(0, 4)
abort_button.button.click()
bec_queue.update_queue(bec_queue_msg_full.content, {})
@@ -128,9 +131,10 @@ def test_queue_abort(bec_queue, bec_queue_msg_full):
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
assert bec_queue.table.item(0, 2).text() == "line_scan"
assert bec_queue.table.item(0, 3).text() == "COMPLETED"
abort_button = bec_queue.table.cellWidget(0, 3)
abort_button = bec_queue.table.cellWidget(0, 4)
abort_button.button.click()
bec_queue.update_queue(bec_queue_msg_full.content, {})
@@ -138,7 +142,8 @@ def test_queue_abort(bec_queue, bec_queue_msg_full):
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
assert bec_queue.table.item(0, 2).text() == "line_scan"
assert bec_queue.table.item(0, 3).text() == "COMPLETED"
abort_button = bec_queue.table.cellWidget(0, 3)
abort_button = bec_queue.table.cellWidget(0, 4)
abort_button.button.click()

View File

@@ -38,8 +38,7 @@ def developer_view(qtbot, mocked_client):
def temp_python_file():
"""Create a temporary Python file for testing."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(
"""# Test Python file
f.write("""# Test Python file
import os
import sys
@@ -48,8 +47,7 @@ def test_function():
if __name__ == "__main__":
print(test_function())
"""
)
""")
temp_file_path = f.name
yield temp_file_path

View File

@@ -88,8 +88,7 @@ def test_client_generator_with_black_formatting():
generator.generate_client(container)
# Format the expected output with black to ensure it matches the generator output
expected_output = dedent(
'''\
expected_output = dedent('''\
# This file was automatically generated by generate_cli.py
# type: ignore
@@ -174,8 +173,7 @@ def test_client_generator_with_black_formatting():
"""
Set the amplitude of the waveform.
"""
'''
)
''')
expected_output_formatted = black.format_str(
expected_output, mode=black.FileMode(line_length=100)

View File

@@ -68,21 +68,17 @@ def test_shared_macros_section_with_files(ide_explorer, tmpdir):
"""Test that shared macros section is created when plugin directory has files"""
# Create dummy shared macro files
shared_macros_dir = tmpdir.mkdir("shared_macros")
shared_macros_dir.join("shared_macro1.py").write(
"""
shared_macros_dir.join("shared_macro1.py").write("""
def shared_function1():
return "shared1"
def shared_function2():
return "shared2"
"""
)
shared_macros_dir.join("utilities.py").write(
"""
""")
shared_macros_dir.join("utilities.py").write("""
def utility_function():
return "utility"
"""
)
""")
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = str(shared_macros_dir)

View File

@@ -20,8 +20,7 @@ def temp_macro_files(tmpdir):
# Create a simple macro file with functions
macro_file1 = macro_dir / "test_macros.py"
macro_file1.write_text(
'''
macro_file1.write_text('''
def test_macro_function():
"""A test macro function."""
return "test"
@@ -34,13 +33,11 @@ class TestClass:
"""This class should be ignored."""
def method(self):
pass
'''
)
''')
# Create another macro file
macro_file2 = macro_dir / "utils_macros.py"
macro_file2.write_text(
'''
macro_file2.write_text('''
def utility_function():
"""A utility function."""
pass
@@ -48,37 +45,30 @@ def utility_function():
def deprecated_function():
"""Old function."""
return None
'''
)
''')
# Create a file with no functions (should be ignored)
empty_file = macro_dir / "empty.py"
empty_file.write_text(
"""
empty_file.write_text("""
# Just a comment
x = 1
y = 2
"""
)
""")
# Create a file starting with underscore (should be ignored)
private_file = macro_dir / "_private.py"
private_file.write_text(
"""
private_file.write_text("""
def private_function():
return "private"
"""
)
""")
# Create a file with syntax errors
error_file = macro_dir / "error_file.py"
error_file.write_text(
"""
error_file.write_text("""
def broken_function(
# Missing closing parenthesis and colon
pass
"""
)
""")
return macro_dir
@@ -406,13 +396,11 @@ class TestMacroTreeRefresh:
# Add a new macro file
new_file = temp_macro_files / "new_macros.py"
new_file.write_text(
'''
new_file.write_text('''
def new_function():
"""A new function."""
return "new"
'''
)
''')
# Refresh the tree
macro_tree.refresh()
@@ -439,14 +427,12 @@ def new_function():
# Modify the file to add a new function
with open(test_file_path, "a") as f:
f.write(
'''
f.write('''
def newly_added_function():
"""A newly added function."""
return "added"
'''
)
''')
# Refresh just this file
macro_tree.refresh_file_item(test_file_path)

View File

@@ -116,5 +116,5 @@ def test_monaco_widget_get_scan_control_code(monaco_widget: MonacoWidget, qtbot,
scan_control_dialog.accept()
out = scan_control_dialog.get_scan_code()
expected_code = "scans.grid_scan(dev.samx, -10.0, 10.0, 20, dev.samy, -5.0, 5.0, 10, exp_time=0.2, settling_time=0.1, burst_at_each_point=2, relative=False, optim_trajectory=None, metadata={'sample_name': ''})"
expected_code = "scans.grid_scan(dev.samx, -10.0, 10.0, 20, dev.samy, -5.0, 5.0, 10, exp_time=0.2, settling_time=0.1, burst_at_each_point=2, relative=False, optim_trajectory=None, metadata={'comment': '', 'sample_name': '', 'scan_name': 'grid_scan'})"
assert out == expected_code

View File

@@ -98,7 +98,9 @@ def test_positioner_box_on_stop(positioner_box):
queue="emergency",
metadata={"RID": "fake_uuid", "response": False},
)
mock_send.assert_called_once_with(MessageEndpoints.scan_queue_request(), msg)
mock_send.assert_called_once_with(
MessageEndpoints.scan_queue_request(positioner_box.client.username), msg
)
def test_positioner_box_setpoint_change(positioner_box):

View File

@@ -377,7 +377,9 @@ def test_run_line_scan_with_parameters(scan_control, mocked_client):
expected_device = mocked_client.device_manager.devices.samx
expected_args_list = [expected_device, args["start"], args["stop"]]
assert called_args == tuple(expected_args_list)
assert called_kwargs == kwargs | {"metadata": {"sample_name": ""}}
assert called_kwargs == kwargs | {
"metadata": {"sample_name": "", "comment": "", "scan_name": "line_scan"}
}
# Check the emitted signal
mock_slot.assert_called_once()
@@ -453,7 +455,10 @@ def test_run_grid_scan_with_parameters(scan_control, mocked_client):
args_row2["steps"],
]
assert called_args == tuple(expected_args_list)
assert called_kwargs == kwargs | {"metadata": {"sample_name": ""}, "optim_trajectory": None}
assert called_kwargs == kwargs | {
"metadata": {"sample_name": "", "comment": "", "scan_name": "grid_scan"},
"optim_trajectory": None,
}
# Check the emitted signal
mock_slot.assert_called_once()
@@ -510,11 +515,17 @@ def test_get_scan_parameters_from_redis(scan_control, mocked_client):
"relative": False,
"exp_time": 2.0,
"burst_at_each_point": 1,
"metadata": {"sample_name": ""},
"metadata": {"comment": "", "sample_name": "", "scan_name": "line_scan"},
}
TEST_MD = {"sample_name": "Test Sample", "test key 1": "test value 1", "test key 2": "test value 2"}
TEST_MD = {
"comment": "",
"sample_name": "Test Sample",
"scan_name": "grid_scan",
"test key 1": "test value 1",
"test key 2": "test value 2",
}
TEST_TABLE_ENTRY = [["test key 1", "test value 1"], ["test key 2", "test value 2"]]
@@ -534,14 +545,19 @@ def test_scan_metadata_is_updated_even_without_default_form_changes(
assert scan_control._metadata_form._additional_metadata.dump_dict() == {
"test key 1": "test value 1"
}
assert scan_control._scan_metadata == {"sample_name": "", "test key 1": "test value 1"}
assert scan_control._scan_metadata == {
"comment": "",
"sample_name": "",
"scan_name": "grid_scan",
"test key 1": "test value 1",
}
def test_scan_metadata_is_connected(scan_control):
assert scan_control._metadata_form._scan_name == "line_scan"
scan_control.comboBox_scan_selection.setCurrentText("grid_scan")
assert scan_control._metadata_form._scan_name == "grid_scan"
sample_name = scan_control._metadata_form._form_grid.layout().itemAtPosition(0, 1).widget()
sample_name = scan_control._metadata_form._form_grid.layout().itemAtPosition(2, 1).widget()
assert isinstance(sample_name, StrFormItem)
sample_name._main_widget.setText("Test Sample")
@@ -553,7 +569,7 @@ def test_scan_metadata_is_connected(scan_control):
def test_scan_metadata_is_passed_to_scan_function(scan_control: ScanControl):
scan_control.comboBox_scan_selection.setCurrentText("grid_scan")
sample_name = scan_control._metadata_form._form_grid.layout().itemAtPosition(0, 1).widget()
sample_name = scan_control._metadata_form._form_grid.layout().itemAtPosition(2, 1).widget()
sample_name._main_widget.setText("Test Sample")
scan_control._metadata_form._additional_metadata._table_model._data = TEST_TABLE_ENTRY
scan_control._metadata_form.validate_form()

View File

@@ -41,6 +41,8 @@ class ExampleSchema(BasicScanMetadata):
TEST_DICT = {
"scan_name": "",
"comment": "",
"sample_name": "test name",
"str_optional": None,
"str_required": "something",
@@ -75,22 +77,26 @@ def metadata_widget(empty_metadata_widget: ScanMetadata):
widget._md_schema = ExampleSchema
widget.populate()
sample_name = widget._form_grid.layout().itemAtPosition(0, 1).widget()
str_optional = widget._form_grid.layout().itemAtPosition(1, 1).widget()
str_required = widget._form_grid.layout().itemAtPosition(2, 1).widget()
bool_optional = widget._form_grid.layout().itemAtPosition(3, 1).widget()
bool_required_default = widget._form_grid.layout().itemAtPosition(4, 1).widget()
bool_required_nodefault = widget._form_grid.layout().itemAtPosition(5, 1).widget()
int_default = widget._form_grid.layout().itemAtPosition(6, 1).widget()
int_nodefault_optional = widget._form_grid.layout().itemAtPosition(7, 1).widget()
float_nodefault = widget._form_grid.layout().itemAtPosition(8, 1).widget()
decimal_dp_limits_nodefault = widget._form_grid.layout().itemAtPosition(9, 1).widget()
dict_default = widget._form_grid.layout().itemAtPosition(10, 1).widget()
unsupported_class = widget._form_grid.layout().itemAtPosition(11, 1).widget()
scan_name = widget._form_grid.layout().itemAtPosition(0, 1).widget()
comment = widget._form_grid.layout().itemAtPosition(1, 1).widget()
sample_name = widget._form_grid.layout().itemAtPosition(2, 1).widget()
str_optional = widget._form_grid.layout().itemAtPosition(3, 1).widget()
str_required = widget._form_grid.layout().itemAtPosition(4, 1).widget()
bool_optional = widget._form_grid.layout().itemAtPosition(5, 1).widget()
bool_required_default = widget._form_grid.layout().itemAtPosition(6, 1).widget()
bool_required_nodefault = widget._form_grid.layout().itemAtPosition(7, 1).widget()
int_default = widget._form_grid.layout().itemAtPosition(8, 1).widget()
int_nodefault_optional = widget._form_grid.layout().itemAtPosition(9, 1).widget()
float_nodefault = widget._form_grid.layout().itemAtPosition(10, 1).widget()
decimal_dp_limits_nodefault = widget._form_grid.layout().itemAtPosition(11, 1).widget()
dict_default = widget._form_grid.layout().itemAtPosition(12, 1).widget()
unsupported_class = widget._form_grid.layout().itemAtPosition(13, 1).widget()
yield (
widget,
{
"scan_name": scan_name,
"comment": comment,
"sample_name": sample_name,
"str_optional": str_optional,
"str_required": str_required,

View File

@@ -516,6 +516,112 @@ def test_plot_custom_curve_with_inline_dap(qtbot, mocked_client_with_dap):
assert dap_curve.config.signal.dap == "GaussianModel"
def test_normalize_dap_parameters_number_dict():
normalized = Waveform._normalize_dap_parameters({"amplitude": 1.0, "center": 2})
assert normalized == {
"amplitude": {"name": "amplitude", "value": 1.0, "vary": False},
"center": {"name": "center", "value": 2.0, "vary": False},
}
def test_normalize_dap_parameters_dict_spec_defaults_vary_false():
normalized = Waveform._normalize_dap_parameters({"sigma": {"value": 0.8, "min": 0.0}})
assert normalized["sigma"]["name"] == "sigma"
assert normalized["sigma"]["value"] == 0.8
assert normalized["sigma"]["min"] == 0.0
assert normalized["sigma"]["vary"] is False
def test_normalize_dap_parameters_invalid_type_raises():
with pytest.raises(TypeError):
Waveform._normalize_dap_parameters(["amplitude", 1.0]) # type: ignore[arg-type]
def test_normalize_dap_parameters_composite_list():
normalized = Waveform._normalize_dap_parameters(
[{"center": 1.0}, {"sigma": {"value": 0.5, "min": 0.0}}],
dap_name=["GaussianModel", "GaussianModel"],
)
assert normalized == [
{"center": {"name": "center", "value": 1.0, "vary": False}},
{"sigma": {"name": "sigma", "value": 0.5, "min": 0.0, "vary": False}},
]
def test_normalize_dap_parameters_composite_dict():
normalized = Waveform._normalize_dap_parameters(
{
"GaussianModel": {"center": {"value": 1.0, "vary": True}},
"LorentzModel": {"amplitude": 2.0},
},
dap_name=["GaussianModel", "LorentzModel"],
)
assert normalized["GaussianModel"]["center"]["value"] == 1.0
assert normalized["GaussianModel"]["center"]["vary"] is True
assert normalized["LorentzModel"]["amplitude"]["value"] == 2.0
assert normalized["LorentzModel"]["amplitude"]["vary"] is False
def test_request_dap_includes_normalized_parameters(qtbot, mocked_client_with_dap, monkeypatch):
wf = create_widget(qtbot, Waveform, client=mocked_client_with_dap)
curve = wf.plot(
x=[0, 1, 2],
y=[1, 2, 3],
label="custom-inline-params",
dap="GaussianModel",
dap_parameters={"amplitude": 1.0},
)
dap_curve = wf.get_curve(f"{curve.name()}-GaussianModel")
assert dap_curve is not None
dap_curve.dap_oversample = 3
captured = {}
def capture(topic, msg, *args, **kwargs): # noqa: ARG001
captured["topic"] = topic
captured["msg"] = msg
monkeypatch.setattr(wf.client.connector, "set_and_publish", capture)
wf.request_dap()
msg = captured["msg"]
dap_kwargs = msg.content["config"]["kwargs"]
assert dap_kwargs["oversample"] == 3
assert dap_kwargs["parameters"] == {
"amplitude": {"name": "amplitude", "value": 1.0, "vary": False}
}
def test_request_dap_includes_composite_parameters_list(qtbot, mocked_client_with_dap, monkeypatch):
wf = create_widget(qtbot, Waveform, client=mocked_client_with_dap)
curve = wf.plot(
x=[0, 1, 2],
y=[1, 2, 3],
label="custom-composite",
dap=["GaussianModel", "GaussianModel"],
dap_parameters=[{"center": 0.0}, {"center": 1.0}],
)
dap_curve = wf.get_curve(f"{curve.name()}-GaussianModel+GaussianModel")
assert dap_curve is not None
captured = {}
def capture(topic, msg, *args, **kwargs): # noqa: ARG001
captured["topic"] = topic
captured["msg"] = msg
monkeypatch.setattr(wf.client.connector, "set_and_publish", capture)
wf.request_dap()
msg = captured["msg"]
dap_kwargs = msg.content["config"]["kwargs"]
assert dap_kwargs["parameters"] == [
{"center": {"name": "center", "value": 0.0, "vary": False}},
{"center": {"name": "center", "value": 1.0, "vary": False}},
]
assert msg.content["config"]["class_kwargs"]["model"] == ["GaussianModel", "GaussianModel"]
def test_fetch_scan_data_and_access(qtbot, mocked_client, monkeypatch):
"""
Test the _fetch_scan_data_and_access method returns live_data/val if in a live scan,