1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-09 18:20:55 +02:00

Compare commits

...

69 Commits

Author SHA1 Message Date
semantic-release
fe21b39b7f 2.0.2
Automatically generated by python-semantic-release
2025-05-01 11:00:33 +00:00
1b78840fd8 fix(plot_base): no content margin for plot_widget window 2025-05-01 12:02:47 +02:00
semantic-release
46519342b6 2.0.1
Automatically generated by python-semantic-release
2025-04-30 11:52:51 +00:00
9079ddd727 fix(dock_area): restore state safeguard to not pass none to pyqtgraph restoreState 2025-04-30 13:14:16 +02:00
semantic-release
205745cc72 2.0.0
Automatically generated by python-semantic-release
2025-04-29 17:22:28 +00:00
717017e69e doc(image): rotation update 2025-04-29 19:06:47 +02:00
a3de1f0a31 refactor(plots): waveform and image rpc api review 2025-04-29 18:37:53 +02:00
8eef4253b0 feat(slot): add 'verify_sender' argument to SafeSlot for sender verification 2025-04-29 17:49:01 +02:00
1f2db927f5 fix(scan_control): restore scan parameters always regenerate the arg box, preventing infinite loop 2025-04-29 17:38:56 +02:00
98f159b25f fix(image): ImageItem remove adjusted to disconnect and remove current displayed image 2025-04-29 16:31:11 +02:00
061f3481da fix(becconnector): widgets can be flagged as root widget, skipping the BECMainWindow in CLI usage 2025-04-29 16:16:35 +02:00
f35f4c4b29 fix(becconnector,widgets): parent_id is always fetched from the real bec widget parent; all widgets adjusted; hardcoded parent_ids removed 2025-04-29 13:23:09 +02:00
c36852b2ef fix(rpc_server): broadcasted data check 2025-04-29 11:48:35 +02:00
4eaadd1545 fix(scan_matadata): parent passing 2025-04-29 11:35:10 +02:00
David Perl
d04770fe91 refactor: rearrange base of metadata forms for generic use 2025-04-29 11:35:10 +02:00
23fee22ef8 test: fix tests for launcher close / hide behavior 2025-04-29 10:09:47 +02:00
6e7920c119 fix(launcher): hide launcher when launcher is closed even though it is not the last widget 2025-04-29 09:43:19 +02:00
e3d0d5566c test: add IPython client GUI object test module with tab completion 2025-04-28 15:38:50 +02:00
e5b532274e refactor(assets): new icon for ui loader 2025-04-28 14:20:42 +02:00
eb0323b989 build(dependencies): update min bec_lib version to 3.29 2025-04-28 08:39:05 +02:00
60852e228f docs: replaces instances of QtDesigner with BEC Designer for improved clarity 2025-04-27 16:58:40 +02:00
b3dbe922de fix(launch_window): return None when cancelling the ui file launcher 2025-04-27 13:50:43 +02:00
fde912005d fix(cleanup): prevent double cleanup by tracking object destruction state 2025-04-27 13:45:58 +02:00
5e4965fe1f docs(lmfit): fix links 2025-04-25 20:29:26 +02:00
aff5a51f4c fix(type hints): add future import to prevent sphinx from crashing 2025-04-25 20:29:26 +02:00
b4af2cc77a docs: updated docs for v2 (#531) 2025-04-25 20:29:26 +02:00
25bd905cef docs: update docs for v2 2025-04-25 20:08:21 +02:00
2f0d213e32 docs(position-indicator): update docs for positioner indicator 2025-04-25 19:41:20 +02:00
b6695b45d0 docs: update docs for various widgets 2025-04-25 19:41:20 +02:00
77f9d42576 fix: unique name for widgets, fix new method for docks; closes #534 2025-04-25 19:41:20 +02:00
8cca510fa1 fix(client): import reduce 2025-04-25 16:59:53 +02:00
06a4954d3d fix(BECGuiClient): add launch_script parameter to dock area creation 2025-04-24 17:39:55 +02:00
4acf5befb1 docs: review quick_start 2025-04-24 14:38:07 +02:00
99d76236ca test: add tests for name creation of custom curves, and object name handling 2025-04-24 08:49:33 +02:00
afc818bf7d docs: update quick_start 2025-04-24 08:49:33 +02:00
8e846d4499 fix(curve): fix unique names for custom curves 2025-04-24 08:49:33 +02:00
a1c859c743 docs: remove BECFigure from docs, fix wrong api for docs of plotting widgets 2025-04-24 08:49:33 +02:00
75cc45d767 docs: remove BECFigure 2025-04-24 08:49:33 +02:00
1d091071e1 fix: bugfix in cleanup of ScatterWaveform ScatterCurve; closes #520 2025-04-24 08:49:33 +02:00
8e64b65c2d feat: delete bec_app 2025-04-24 08:49:33 +02:00
27ea92d120 feat: deprecated and delete alignment_1d gui 2025-04-24 08:49:33 +02:00
3ddfeaa49f fix(serialization): add serialization for qpointf 2025-04-23 20:42:54 +02:00
074bbbc166 fix: change default colormap to plasma 2025-04-23 19:05:54 +02:00
3709cdc866 fix(bec_connector): improve cleanup handling on deleted parent to prevent errors 2025-04-23 17:45:58 +02:00
9d6d0b406a refactor(bec_connector): replace pyqtSlot with SafeSlot for consistency 2025-04-23 17:45:58 +02:00
6318b2d822 fix(designer-plugin-generator): enhance super constructor validation for new style classes 2025-04-23 17:45:58 +02:00
f89e74b199 refactor: add template for debugging the cli generator 2025-04-23 17:45:58 +02:00
0ac14a74b8 fix: ensure provided dock and dock_area names are valid and defaults are snake_case 2025-04-23 16:22:13 +02:00
1910993b2b fix(positioner-indicator): fix property setters for position indicator 2025-04-23 14:00:06 +02:00
7c303d0129 fix(ring-progress-bar): fix bug in disconnect slot of rings, enable 'scan' mode as default for init with first ring 2025-04-23 07:30:07 +02:00
113938e71a test: fix rpc widgets e2e test 2025-04-22 21:19:37 +02:00
e0f146beeb fix(compact_popup): forward close event 2025-04-22 21:19:37 +02:00
fc1cdc814f fix(bec_connector): call cleanup on widgets if the parent was deleted 2025-04-22 21:19:37 +02:00
a13de45131 fix(rpc): call close on container widget if needed 2025-04-22 21:19:37 +02:00
8ff2063bc8 fix: proper cleanup of progressbar 2025-04-22 21:19:37 +02:00
cdc613b6e7 fix(bec_queue): set parent for toolbar buttons 2025-04-22 21:19:37 +02:00
1fc6125369 fix: forward parent to children 2025-04-22 21:19:37 +02:00
fef07ac8e1 fix: import from qtpy instead of PySide6 2025-04-22 21:19:37 +02:00
86647b9b7e fix(rpc-base): deprecate widget_name in favor of object_name; closes #499 2025-04-22 21:19:37 +02:00
36dc174bfe test: add function scoped rpc_widgets e2e test; closes #510 2025-04-22 21:19:37 +02:00
a06f0600c1 fix(dark-mode-button): fix parent passed to QObjects in various classes 2025-04-22 21:19:37 +02:00
f88dfc8f1b refactor: add pragma no cover to various TYPE_CHECKING 2025-04-22 21:19:37 +02:00
c70cd9d6e8 fix(moduar-toolbar): fix cleanup of modular toolbar and dock_area 2025-04-22 21:19:37 +02:00
8fbd54c3aa fix(website-widget): add super().cleanup() in website widget 2025-04-22 21:19:37 +02:00
ef4a52cc17 fix: RPC access enabled for certain widgets. 2025-04-22 21:19:37 +02:00
b460ea9955 fix(progress-ring-bar): fix parent inheritance and cleanup of ring objects; closes #496 2025-04-22 21:19:37 +02:00
1fe052e9da docs: grammar improvement 2025-04-22 15:22:18 +02:00
f2d5b57e86 fix(docs): update copyright year to be dynamic 2025-04-22 15:22:18 +02:00
6630ba1c42 docs(auto_updates): update documentation for auto updates functionality and add launcher image 2025-04-22 15:22:18 +02:00
137 changed files with 2674 additions and 2180 deletions

View File

@@ -1,6 +1,653 @@
# CHANGELOG
## v2.0.2 (2025-05-01)
### Bug Fixes
- **plot_base**: No content margin for plot_widget window
([`1b78840`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b78840fd87ea0f156c73beeb57c6c06f685f7b1))
## v2.0.1 (2025-04-30)
### Bug Fixes
- **dock_area**: Restore state safeguard to not pass none to pyqtgraph restoreState
([`9079ddd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9079ddd7278ede7a9a12d7b39797154e83659c20))
## v2.0.0 (2025-04-29)
### Bug Fixes
- Add designer plugin for ScanMetadata
([`43e1aa9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43e1aa9505cfa6e87b4fce1d065efb48b4111190))
- Add support for 'add_slice', add downsampling for performance improvements. add tests
([`7f7891d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7f7891dfa54588f5d902448b760f141b183a7fa1))
- Broadcast context manager to emit registry changes just once
([`a5f06c8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a5f06c8f8380156a763445a69df29ee0e62e434c))
- Bugfix in cleanup of ScatterWaveform ScatterCurve; closes #520
([`1d09107`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1d091071e1179821bb1dcd47fb97f3d0959b972f))
- Change default colormap to plasma
([`074bbbc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/074bbbc16648849bdfcfc28b2c520b0e38dd07c2))
- Create widget enum programatically
([`7726d83`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7726d83b6834b8145e48e709e2f839fb0ec1b971))
- Ensure provided dock and dock_area names are valid and defaults are snake_case
([`0ac14a7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0ac14a74b851578fff668fb8c6722f990130831d))
- Expose common classes from bec_widgets package
([`28ae0d2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/28ae0d2b577d7c926ee54690898fe8e327e1229f))
- Forward parent to children
([`1fc6125`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fc61253699425a2bf64a0f8b560f8474549b841))
- Import from qtpy instead of PySide6
([`fef07ac`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fef07ac8e12399e7e49bcd673a5fc7cbf713bc50))
- Proper cleanup of progressbar
([`8ff2063`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ff2063bc8978c5b2a97f720d5da055e8ec08f0c))
- Rpc access enabled for certain widgets.
([`ef4a52c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef4a52cc17748f35ed627170b1025e6e028d70b8))
- Server shutdown widgets
([`75b2446`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/75b24467def65284ea6b6114b25098437e31ec95))
- Support auto_range_x/y for viewAll during measurement
([`af28e2e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af28e2e433c9b0233436da850be97cd63df90a74))
- Unique name for widgets, fix new method for docks; closes #534
([`77f9d42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77f9d425765061f137c997062a3bf769a939bc64))
- Warning in logpanel
([`1d7b423`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1d7b423bb307b6aae3879987776310c14380895d))
- chain a signal to the child BecLogsQueue rather than passing the signal instance in
- Wrap fetching plugin widgets in case of errors
([`ef14831`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef148317dea9c7ff985b2a3ff06ccdb37258153f))
- **auto_updates**: Fix condition to skip auto update
([`18e4ba6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/18e4ba6cfe9f67512efbd3989156de5670aab3fe))
- **bec_connector**: Add assertion to ensure BECConnector is used with a QObject; closes #475
([`1921444`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1921444e152e06c4decc790452f3c496cf8ee961))
- **bec_connector**: Add setObjectName method to update object name and broadcast if registered;
closes #472
([`064343a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/064343acf2631e4ae62b2a5e08bc08087246570c))
- **bec_connector**: Call cleanup on widgets if the parent was deleted
([`fc1cdc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc1cdc814fc3c44a571c20986bc627935f90ff91))
- **bec_connector**: Improve cleanup handling on deleted parent to prevent errors
([`3709cdc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3709cdc86671e5219afca7a8e11bdd01f03dd30e))
- **bec_connector**: Move RPC registration into single shot method to ensure the rpc name is in sync
([`3b16c9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3b16c9f5a2f7f16b23f25560b1e8fb4e42359ef0))
- **bec_queue**: Set parent for toolbar buttons
([`cdc613b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cdc613b6e7d7eac806d458515321590e9344244a))
- **becconnector**: Widgets can be flagged as root widget, skipping the BECMainWindow in CLI usage
([`061f348`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/061f3481daae6844a83c44e9caca7ed56a1bb100))
- **becconnector,widgets**: Parent_id is always fetched from the real bec widget parent; all widgets
adjusted; hardcoded parent_ids removed
([`f35f4c4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f35f4c4b295139b99a2dad9e8241f900d2565aeb))
- **BECGuiClient**: Add launch_script parameter to dock area creation
([`06a4954`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06a4954d3da44c6805232d34e47e242b28ba7fd1))
- **cleanup**: Prevent double cleanup by tracking object destruction state
([`fde9120`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fde912005db61a60707e7181c3425a4557bdc011))
- **cli**: Add type ignore comment to generated files
([`d171255`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d1712552ffd1118845dc7121218df86ce10e8750))
- **client**: Import reduce
([`8cca510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8cca510fa1cbda00a07edbef9d36fdd74e63d201))
- **client**: Regenerated client
([`c97db6a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c97db6aaae81d08019a13c344414c16c42691654))
- **client**: Rpc API adjusted for DockArea, ImageItem and Waveform
([`6ca4aa0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ca4aa0f9b9d5ace9fb1e174219f4da5617ebbac))
- **client_utils**: Simplify RPC client instantiation in BECGuiClient
([`96b31a4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96b31a450998aca2b7ac94138b07223418d2bacd))
- **colormap_widget**: Size policy fixed
([`1cc2a98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1cc2a9848906a7013e86687976d42d4b9676b25f))
- **compact_popup**: Forward close event
([`e0f146b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e0f146beeb34367a4d3454a7012af4728d594b9b))
- **crosshair**: Adapted for 2D image
([`a85402d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a85402dde1af1d9c4a154892c46422ac3e1f22f9))
- **curve**: Fix unique names for custom curves
([`8e846d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e846d449955ded3cb8090e44ea36d26efccb80e))
- **dark-mode-button**: Fix parent passed to QObjects in various classes
([`a06f060`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a06f0600c1c9a80436f01533a82905a6f3633895))
- **designer**: Avoid touching deleted widgets during init as QtDesigner will segfault
([`4381fcc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4381fcc4c212cd03ce91f1638dc361c3315f8c45))
- **designer-plugin-generator**: Enhance super constructor validation for new style classes
([`6318b2d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6318b2d822be0ded561a1afd0d485158614e2406))
- **device_input_base**: Removed enums from Pydantic models to make them serialisable
([`43b747e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43b747ec8a761530d78b26650b0ec2ee4581ffaf))
- **dock_area**: Close BECMainWindow if dock area is central widget
([`e725de3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e725de3c4504d43fbcad25d69c5cb8cbe7a70867))
- **docs**: Update copyright year to be dynamic
([`f2d5b57`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f2d5b57e86d0c9d690b8d9f988035427608f0b4c))
- **entry_validator**: Validator reports list of signal if user chooses the wrong one
([`da05877`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/da05877dd04fa618cdb45268cb62df602a5e808f))
- **image**: Imageitem remove adjusted to disconnect and remove current displayed image
([`98f159b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98f159b25f6bf7e1f2dd76726d7ab66a0baf88de))
- **launch_window**: Redesign
([`7e65d4f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7e65d4f2d6d840d3895e023f5cd090a56ea6e5f3))
- **launch_window**: Return None when cancelling the ui file launcher
([`b3dbe92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b3dbe922dea2cea9190d1583bd6b69f1a45d6b90))
- **launch_window**: Update LaunchTile icon to use new UI loader tile image
([`3cd6e05`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3cd6e05b2478654210049ca8e1756ad592f1da81))
- **launcher**: Hide launcher when launcher is closed even though it is not the last widget
([`6e7920c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6e7920c119824650006e7357ca2f4ff95d413e13))
- **lmfit_dialog_vertical**: Vertical sizePolicy fixed
([`584b945`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/584b94500565a33e5daed86b7552ec54f1135cf6))
- **main_window**: Connected to theme change
([`11feeff`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/11feeff37ce0b02fcbc8e506c67c14e1fc5e0cb6))
- **main_window**: Show app id only when connected to redis
([`be72268`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be722683a7cc7b215c572f9c2e996839b010b64e))
- **moduar-toolbar**: Fix cleanup of modular toolbar and dock_area
([`c70cd9d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c70cd9d6e8f7ea9d5f81b10ac437cdcc9ee900e9))
- **motor_map**: Limit map creating optimized
([`9f2a083`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9f2a083abbcfb465ebea9acee8263dcc9a6da5d9))
- **plot_base**: Ability to set y label suffix
([`890b501`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/890b50115fef845c2a77242fdb05863d2eec4a00))
- **plot_base**: Aspect ratio removed from the PlotBase
([`19d8aeb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/19d8aeb16249a1093cfec124d0ebdf6af11d94a8))
- **plot_base**: Axis setting filter for relevant properties
([`0204d9c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0204d9c86f9665dcefdcbe7f49ac23918d74dd66))
- **plot_base**: Do not enable inner axes when label is changed
([`98eda03`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98eda03f4d6d449605d5559a1db44c900d93cb79))
- **plot_base**: Enable popup property fixed
([`30db183`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30db18367e9c6d6375fda970a1bb255d966cba5a))
- **plot_base**: Fix cleanup of popups if popups are still open when PlotBase is closed
([`39cf4dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39cf4ddd5a033ee7f589d508f765669186e776bc))
- **plot_base**: Improved handling of matplotlib exporter errors
([`4f9514f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f9514fbd1ff0059248d3b7b5b4fcd85c3eb9c72))
- **plot_base**: Inner and outer axis setting in popup mode
([`055b968`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/055b96818aa69d66119caee9a3e8c24575ce60b4))
- **plot_base**: Update mouse mode state on mode change
([`fc24c8b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc24c8b3a5f9cd55fb3d49f753b53a65a2a0fa26))
- **plot_framework**: All widgets, popups and side menus cleanups adjusted
([`337a332`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/337a332ed123f99729b8cf6869f7fe4b056c2b16))
- **plot_indicators**: Cleanup adjusted
([`4865341`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/48653410101a2a38d5067fbfca7712d255d89625))
- **plot_indicators**: Plot indicators added to the PlotBase
([`42e3b9c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/42e3b9c13786e67220874a1275a3d9ee9515541a))
- **positioner-indicator**: Fix property setters for position indicator
([`1910993`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1910993b2b3d30ecb8e4977b4a362f46adae3c75))
- **progress-ring-bar**: Fix parent inheritance and cleanup of ring objects; closes #496
([`b460ea9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b460ea9955318879ddfe4f9ae963249ba342bbb5))
- **ring-progress-bar**: Fix bug in disconnect slot of rings, enable 'scan' mode as default for init
with first ring
([`7c303d0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7c303d01294493a55fd26db8c1475e8c58b3e492))
- **ring_progress_bar**: Replaced hard-coded endpoints by MessageEndpoints
([`e4e9feb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e4e9febc98268a4b6b9774b253419e88ea044811))
- **round_frame**: Orientation can be vertical
([`c1bbb16`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c1bbb16dad481c628e7680180d7250ba8a560c46))
- **round_frame**: Roundframe removed from BECWidget inheritance
([`b58a098`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b58a098ed4afbe62721fd2bf8497f363deecbfa6))
- **rpc**: Call close on container widget if needed
([`a13de45`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a13de45131309771c1438407f3733a8c0897d495))
- **rpc-base**: Deprecate widget_name in favor of object_name; closes #499
([`86647b9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/86647b9b7e2fa111105ae483808883a624fa4cd6))
- **rpc_base**: Ensure message wait event is set after processing RPC response
([`4dc59aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4dc59aa5e9b15e5ec40401e80e7965acd88e2fce))
- **rpc_base**: Timeout run_rpc 3s
([`8558b46`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8558b46114760a9434eaa827f81d5fd9d047112f))
- **rpc_register**: _lock and _skip_broad_cast moved to instance attributes
([`8d17f7e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d17f7e32f81894294d7da472268e8d9eb3bb74b))
- **rpc_register**: Change add_rpc parameter type to BECConnector and add object_is_registered
method
([`82b8265`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/82b82659b7919b15d629375866302624b5b6e457))
- **rpc_register**: Lock changed to RLock
([`6c90ca3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6c90ca31078d97124a3ad535ffe83da138558d67))
- **rpc_server**: Broadcasted data check
([`c36852b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c36852b2ef762cdae3fde569bbd0d5f2f6f2725b))
- **rpc_server**: Enhance serialization logic for BECConnector objects and fix return types
([`125afc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/125afc89073b4fc69a3f42650b3d4f6fa6ccaa47))
- **rpc_server**: Update _serialize_bec_connector to include wait parameter for registration check
([`d6fccd1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d6fccd10f5d600ea67cf7b2a5ebb42295d15cdfe))
- **RPCReference**: Setattr added
([`a2128ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a2128ad8d688995551c5e26974396fd0588b6804))
- **scan_control**: Restore scan parameters always regenerate the arg box, preventing infinite loop
([`1f2db92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f2db927f50f4f30d43ebe52e39118c7d79994d4))
- **scan_matadata**: Parent passing
([`4eaadd1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4eaadd1545885b111fce3f8cab527a77b8633ff3))
- **scatter_waveform,waveform**: Added QTimer to fetch the last data points after 500ms
([`e6795dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6795dd87ccd93cfd53e22cd94d71bffe1ef54dd))
- **serialization**: Add serialization for qpointf
([`3ddfeaa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3ddfeaa49fd4a7fdbff7cae47b90c25720f6dca0))
- **server**: Becdockarea type added
([`4a74891`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4a74891184f112751258866b6bc9d800dbc5ed05))
- **server**: Remove window.hide() since widgets will be teared down on kill_server before siginit
signals is sent
([`58b0c7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58b0c7ddc1d0b85b35e7e18434c0b83aac01a735))
- **server**: Turn_off_the_lights cleanup fixed for parent_id widgets
([`20a86ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/20a86ad325d36aa5aec73aeda7ff43ea9cc6c1f7))
- **setting_widget**: Added parent kwarg into all settings widgets in plotting framework
([`94c2e2d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/94c2e2db6518402207b2a1077bb16403a8e61cee))
- **side_panel**: Side panel menu can be initialized without a title
([`112eed6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/112eed694c0ef6eb80ec7a7cfdfbaacf732d5b9f))
- **toolbar**: Update action check handling logic for SwitchableToolBarAction
([`ac08bdf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac08bdfab2162ac8fd103e60779a76d36e9a3765))
- **type hints**: Add future import to prevent sphinx from crashing
([`aff5a51`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aff5a51f4c059ce21ec72cefc263f37df2491480))
- **waveform**: Dap curve flickering
([`b03d2ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b03d2eaeed4263846c470bc45eba9208ced2370b))
- **waveform**: Error where scan history is empty
([`288ea4d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/288ea4dbbde6d5f770c37f4daf377da9ec8fe729))
- **waveform**: Fix dap curve categorization logic
([`b91f1fe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b91f1fe4879e43e71a1be49ce5a206efbae19315))
- **waveform**: Legend is correctly updated when changed from curve dialog
([`c2d2c48`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c2d2c484cd1f133f45fe7147616c22c0b5fd5611))
- **waveform**: Signals for x device can be defined from gui
([`39164fe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39164feb18f9e996d97814f58157892e8db816ae))
- **waveform, rpc_reference**: __getitem__ removed form waveform and rpc_reference
([`3a82c95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3a82c95f60cb2b7f0b29a1ea5cdcbfa5bf602af8))
- **website-widget**: Add super().cleanup() in website widget
([`8fbd54c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8fbd54c3aa864623e42b39a1ebf92ba098ba437d))
- **widgets**: Becconnector resolves hierarchy including objectName, parent, parent_id upon init;
all widgets adjusted
([`a1bec75`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a1bec7511549277da231928d989b16ecad0eed1b))
### Build System
- Pyside6 capped to 6.9
([`9dabf2c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9dabf2c66c8023194964b9ad308e06197471f89f))
- **bec_lib**: Raised required version to 3.28.1
([`a5f1f47`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a5f1f4781ed9787148053d71e0d12fefe42e142a))
- **dependencies**: Update min bec_lib version to 3.29
([`eb0323b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb0323b989e96e89d2eb1ff7b648edb43f5fe198))
### Continuous Integration
- **e2e**: E2e tests are saving logs
([`d4106c5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d4106c548e2373463a48268fd991ded7f554e3a6))
### Documentation
- Add docs on widget plugins
([`52a9f29`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/52a9f29bdcb20a9339a8970508bc0a93ba8bef5f))
- Add missing class doc strings for rpc-enabled widgets
([`cfc8272`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cfc8272ac288541d1e20c0840bd2ce6fa930897c))
- Better document logpanel code
([`d2c9075`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2c90757c21e040940a378325cad75c4d94470f9))
- Grammar improvement
([`1fe052e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fe052e9da9e44bf9872db0d42218843a8e6d275))
- Remove BECFigure
([`75cc45d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/75cc45d767970e985c566fd4aeccd4394f48dfa3))
- Remove BECFigure from docs, fix wrong api for docs of plotting widgets
([`a1c859c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a1c859c7434357e6fb82f0912314c203fb73e890))
- Replaces instances of QtDesigner with BEC Designer for improved clarity
([`60852e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/60852e228f80f0d2e74813f82bd30f1ba83ff154))
- Review quick_start
([`4acf5be`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4acf5befb1dbcc69e8cc7da70ebf5663b9ec15f2))
- Update docs for v2
([`25bd905`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/25bd905cef987a713e24aca178c04aef1ab59656))
- Update docs for various widgets
([`b6695b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b6695b45d076dbf3896e94eedcf73d542022d764))
- Update quick_start
([`afc818b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/afc818bf7d17f42703c3cebafd2b292b8444647a))
- Updated docs for v2 ([#531](https://gitlab.psi.ch/bec/bec_widgets/-/merge_requests/531),
[`b4af2cc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4af2cc77aa0013f4547cd98345b0c77abb7101b))
- **auto_updates**: Update documentation for auto updates functionality and add launcher image
([`6630ba1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6630ba1c421e566bf86ac38701a86eff624395d2))
- **lmfit**: Fix links
([`5e4965f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5e4965fe1f88d18fcc7e6875777ff3eb01ab08ec))
- **plot_base**: Update docstrings for properties and setters
([`b085ef6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b085ef6e730d529149bcb696b1ad4cd9c5220a83))
- **position-indicator**: Update docs for positioner indicator
([`2f0d213`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2f0d213e32fd662bfffd4df73b9281fa30cef6e3))
### Features
- Add loader/helper for widget plugins
([`ca2bb4f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca2bb4f9b42ebaac2fc544d3da36267d93e9903d))
- Add rpc broadcast
([`2ba9b4c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2ba9b4cb236a2182261dfb88398d5ece733ba393))
- Add support for auto updates
([`2511056`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2511056557daf0b5dd78d3e85ac4befb8bf8c316))
- Delete bec_app
([`8e64b65`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e64b65c2d3b8a8f3c6e5376e694369b41733da4))
- Deprecated and delete alignment_1d gui
([`27ea92d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/27ea92d120cc8ef01fff10341ce0954b4f7fed5d))
- Namespace update for gui, dock_area and docks.
([`ac3c5a3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac3c5a38e449c2c3e4a1c61d5f9a59acfbf0cab5))
- **auto_update**: Add GUI highlight management for auto updates status
([`5f272a6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5f272a66a4d4f65273d7b2a6709336cd3582d695))
- **auto_updates**: Enforce rpc widget class for subclasses of auto updates
([`778230b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/778230b5edf5d24df8a10c78c90ea065510e8344))
- **image**: New Image widget based on new PlotBase
([`cb39ff3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cb39ff3fbde99f4e4bed49dee8a5e5987d257b23))
- **launch_window**: Add custom UI file launching functionality and UI tile
([`3089ca1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3089ca15ec4a8c110d11c57aff2da42f4af5bd08))
- **launch_window**: Add user access permissions
([`8efa93d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8efa93d2d2c5e6c28008e1bbde89e5cc8a01d139))
- **launch_window**: Enhance auto update functionality with selector and dynamic loading
([`2965323`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/29653239c5cf43313224cc5123d066fcba4b831b))
- **launcher**: Add option for launching with auto updates
([`20a1c5d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/20a1c5ddb3cd0763ce69bba5a893f54c56678706))
- **main_window**: Add launcher menu and functionality to show launcher
([`55baa84`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/55baa84eb6723b30b407092bc36f826b826cc934))
- **motor_map**: New MotorMap widget based on PlotBase
([`fec26d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fec26d793e14965a719a4d038838418b9a7603bb))
- **multi_waveform**: Multi-waveform widget based on new PlotBase
([`77f9616`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77f96160ab348c1a65ceb55986ea4ea75f8be04a))
- **plugin_utils**: Add functionality to retrieve auto update classes from plugins
([`c434af9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c434af9b92d68d08da87112ef424738e5e42ae6e))
- **positioner_box**: Add units QLabel to device UI components and update visibility logic
([`f653fc5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f653fc5f7ebf8ad5297facd739a8a49ea0a06c95))
- **scatter_waveform**: Scatter waveform widget based on new Plotbase
([`95fcf01`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/95fcf016c32c52330acfd5900a3996c99c4ee01f))
- **server,launcher**: Rpc server separated with the launcher window introduced
([`5f27a90`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5f27a9098903ffd8ec27c1b45565f1c113892cca))
- **slot**: Add 'verify_sender' argument to SafeSlot for sender verification
([`8eef425`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8eef4253b0507f60f50c06ed48b59a1b19b29644))
- **ui_launch_window**: Add UILaunchWindow class
([`45cd82e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/45cd82e6354c72e1e35cd6366aa7aad93f8b12ca))
- **waveform**: New Waveform widget based on NextGen PlotBase
([`4bec181`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4bec181f3aff34d9de7d3f9ec012b641c125a661))
- **widget_io**: Added handler for Sliders
([`1a0097e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1a0097e02728b6470217d3a574260f376776d81f))
### Refactoring
- Add fallback to 'index' plotting in case of missmatch in length
([`515d7ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/515d7ad05584086a8e8ac626b476d629e27aacf3))
- Add pragma no cover to various TYPE_CHECKING
([`f88dfc8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f88dfc8f1bbc0819736a4f32bf21682366fd3437))
- Add support to plot against x_data
([`0e276d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0e276d4c09cddb459688aecf28684a963d8f6613))
- Add template for debugging the cli generator
([`f89e74b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f89e74b199d007cf47f355a1c5e1f582daeea90a))
- Autoupdate disabled
([`4e29291`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4e29291b3a0891657a8d2011bcaf1d6e65de125a))
- Cleanup MR
([`0b00cd2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0b00cd24fd43dbc87c81f7dbfae816343f7da4c4))
- Cleanup rpc reference tracking, fix appquit, fix namespace updates edge cases
([`7ba93ce`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7ba93ce934cc644ad6340f141a6a0888bd1d3d98))
- Cleanup, fix tests and _top_level dict/windows
([`5872253`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58722531232b2290f9fd974bae24877c9d5451f4))
- Fix cleanup bug for BECConnector items, renamed _registry_state to _server_registry
([`be83c7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be83c7d5f4bc04d110734b491727dc60d8dd61ef))
- Fix cleanup for various widgets, including RoundedFrame
([`d05179a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d05179a519d6419c9631ffdf4fa6aa262966c2ed))
- Improve plotting behaviour from history
([`ed2d958`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ed2d958de62223cd796c869b6c8b9b75170e66f5))
- Rearrange base of metadata forms for generic use
([`d04770f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d04770fe913474ec9d4e06b056c85e720d1470c4))
- Set downsampling to auto=True, method 'peak', activate clipToView for (Async)-Curves and fix
ViewAll hook from pg.view_box menu
([`25820a1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/25820a1cdec2cff99ab0d6085aece0e3e7dd9092))
- Tidy client generation and add options
([`b492591`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4925918f7acf31e40971814639be8a6c55d46df))
- **assets**: New icon for ui loader
([`e5b5322`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e5b532274ede281456a14b02a99855302603490a))
- **auto_update**: Auto_update changed to be BECMainWindow; removed auto update logic from
BECDockArea
([`56c2827`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56c282714037f733bcfd8a659f34baadcd1aa223))
- **auto_updates**: Move cleanup method from user section to internal section
([`ac9224e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ac9224e5f2d3edcb5b1cc1cbc1a8583f81d0b912))
- **bec_connector**: Replace pyqtSlot with SafeSlot for consistency
([`9d6d0b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d6d0b406a812e08ca8417415b5def98b40bdf92))
- **bec_figure**: Becfigure removed
([`f76d931`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f76d9319bd13bb52b1ae2524c1c5e44a167cc330))
- **client_utils**: Remove unused auto update attributes from BECGuiClient
([`b7795b4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b7795b4d0ae21641bead0f1f1541f920ae95702a))
- **image_widget**: Old BECImageWidget removed
([`de10609`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/de10609b3c714b80a14bf6940e86763d0779402b))
- **launch_window**: Remove cleanup method
([`9a940bb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9a940bb8d58f37d1fc24ce4fdb38282d02349efb))
- **launcher,main_window**: Launcher window moved to inherit from BECMainWindow
([`99383b7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99383b77150ca7c74c19c899a0e6a7879b770376))
- **motor_map_widget**: Becmotormapwidget removed
([`f878e87`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f878e87ad545e0fe68292030d9f06dee693e0da2))
- **multi_waveform_widget**: Becmultiwaveformwidget removed
([`7c31bbd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7c31bbd9c2e0230e54f4dca0f1e5c4d2cd6e7674))
- **plots**: Plot_next_gen module renamed to plots
([`9fb9a1c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9fb9a1cfd2a94efd5e2a9fcbaa05d65c7b7105ee))
- **plots**: Waveform and image rpc api review
([`a3de1f0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3de1f0a31dfb9048493fb61983167960577fb97))
- **rpc_reference**: Refactor rpc reference tracking
([`bd5e251`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bd5e251ee9396633f419732e43411821726250aa))
- **rpc_server**: Add type hint for _get_becwidget_ancestor method parameter; minor cleanup of
imports
([`cb91ebc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cb91ebc0c34ae29b6b293996199f4624d36a3cc0))
- **rpc_server**: Add type hints and docstrings for heartbeat and registry update methods
([`08168f2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/08168f28d3c5375b9ace9df0d7aa31e33adb97e9))
- **rpc_server**: Cli_server renamed to rpc_server
([`6082e7a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6082e7a6907c2fe15e4e5ebca857fbf8f222d192))
- **tests**: Create dummy scan item moved to client_mocks.py
([`0dd9617`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0dd9617e6e5ea756edc344c324451480a62bdae2))
- **ui_loader**: Remove unnecessary parent_id handling
([`d60cf6c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d60cf6c843ecc135a6065d1e913f9f6abb1a483d))
- **ui_loader**: Remove unused import
([`a6ce312`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a6ce312f7c60c2babcc37127f7c69d54c1b32573))
- **utils**: Qt_utils moved to utils
([`be552d3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be552d3ece97e7f472c4534b4af8438b95c518aa))
- **waveform_widget**: Removed and replaced by Waveform
([`96cff49`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96cff49cd4453fa70d8802653d5afe62d71c6b2a))
### Testing
- Add function scoped rpc_widgets e2e test; closes #510
([`36dc174`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/36dc174bfedf212532658b84f8ab64971863d292))
- Add IPython client GUI object test module with tab completion
([`e3d0d55`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e3d0d5566c50f6a80ba861d4e3e0789f17785a46))
- Add tests for name creation of custom curves, and object name handling
([`99d7623`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99d76236cac63042f0d7d1db580dde8aa7cfd214))
- Disable test_bec_dock_rpc_e2e module, issue to fix this created #450
([`17f2dda`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17f2dda977025bc422e26289293d3fcbd224a6f6))
- Fix rpc widgets e2e test
([`113938e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/113938e71a6dacba37164069e2c795cc9db168d4))
- Fix tests for launcher close / hide behavior
([`23fee22`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/23fee22ef8f2c22f191dfc1da57b921484ede6cd))
- Fix tests for namespace updates
([`f3d3c94`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f3d3c9425d3ed619b978427cea782137beedfb59))
- Qapp must shutdown cli server before checking for leaked QTimer
([`d066051`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d06605122e5c2e225650b44ebfc047daa5aa6f55))
- **bec_connector**: Becconnector requires a QObject
([`23bdd95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/23bdd95d8c6311d989cb3b807921e3fb2a3d62a0))
- **device_signal_input**: Fix init of device input widget
([`31c3b64`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/31c3b64d7b157e5e26d44e5288afabef343c5e13))
- **e2e**: E2e tests adjusted for new plotting framework
([`378398a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/378398a29b34e43f0cca0a49b08adfcb144e4777))
- **generate_cli**: Fix reference output
([`a8adb06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8adb064f5011e1708ee2dc0090326f533407260))
- **launch_window**: Add test for launching UI file that raises ValueError for QMainWindow
([`33a8a76`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33a8a767f31a57bcfda624d503ed39e0e4578dcb))
- **launch_window**: Add unit tests for LaunchWindow initialization and custom UI file launching
([`d5e422c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d5e422c7fc0e169c35d7f206937e8c7902fbf123))
- **launch_window**: Tests for default and plugin auto updates
([`e10f5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e10f5ec088c6937beb26ec468f510a209c7cc782))
- **plot_base**: Test for plot base re-enabled
([`b51d637`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b51d637c5ff3418420801cd9b457fc073fa98adc))
- **plot_indicators**: Tests adapted to not be dependent on BECWaveformWidget
([`360fe4c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/360fe4c9c3b5c3c1f26e97cb795aef8f4aba3b46))
- **setting_dialog**: Test that settings reject calls cleanup
([`8914f1d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8914f1d50600cab588a6cbecb08d85bfd1a715a1))
- **unit_tests**: Unit tests adjusted to use a modern plotting framework instead of BECFigure
([`6ade934`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ade93435632fa66fb012d92f9b8b548d96e718f))
## v1.25.1 (2025-03-24)
### Bug Fixes

View File

@@ -1,199 +0,0 @@
""" This module contains the GUI for the 1D alignment application.
It is a preliminary version of the GUI, which will be added to the main branch and steadily updated to be improved.
"""
import os
from typing import Optional
from bec_lib.device import Signal as BECSignal
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from qtpy.QtCore import QSize
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication
import bec_widgets
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot as Slot
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
from bec_widgets.widgets.control.device_control.positioner_group.positioner_group import (
PositionerGroup,
)
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import BECProgressBar
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
logger = bec_logger.logger
# FIXME BECWaveFormWidget is gone, this app will not work until adapted to new Waveform
class Alignment1D:
"""Alignment GUI to perform 1D scans"""
def __init__(self, client=None, gui_id: Optional[str] = None) -> None:
"""Initialization
Args:
config: Configuration of the application.
client: BEC client object.
gui_id: GUI ID.
"""
self.bec_dispatcher = BECDispatcher(client=client)
self.client = self.bec_dispatcher.client if client is None else client
QApplication.instance().aboutToQuit.connect(self.close)
self.dev = self.client.device_manager.devices
self._accent_colors = get_accent_colors()
self.ui_file = "alignment_1d.ui"
self.ui = None
self.progress_bar = None
self.waveform = None
self.init_ui()
def init_ui(self):
"""Initialise the UI from QT Designer file"""
current_path = os.path.dirname(__file__)
self.ui = UILoader(None).loader(os.path.join(current_path, self.ui_file))
# Customize the plotting widget
self.waveform = self.ui.findChild(BECWaveformWidget, "bec_waveform_widget")
self._customise_bec_waveform_widget()
# Setup comboboxes for motor and signal selection
# FIXME after changing the filtering in the combobox
self._setup_signal_combobox()
# Setup motor indicator
self._setup_motor_indicator()
# Setup progress bar
self._setup_progress_bar()
# Add actions buttons
self._customise_buttons()
# Hook scaninfo updates
self.bec_dispatcher.connect_slot(self.scan_status_callback, MessageEndpoints.scan_status())
def show(self):
return self.ui.show()
##############################
############ SLOTS ###########
##############################
@Slot(dict, dict)
def scan_status_callback(self, content: dict, _) -> None:
"""This slot allows to enable/disable the UI critical components when a scan is running"""
if content["status"] in ["open"]:
self.enable_ui(False)
elif content["status"] in ["aborted", "halted", "closed"]:
self.enable_ui(True)
@Slot(tuple)
def move_to_center(self, move_request: tuple) -> None:
"""Move the selected motor to the center"""
motor = self.ui.device_combobox.currentText()
if move_request[0] in ["center", "center1", "center2"]:
pos = move_request[1]
self.dev.get(motor).move(float(pos), relative=False)
@Slot()
def reset_progress_bar(self) -> None:
"""Reset the progress bar"""
self.progress_bar.set_value(0)
self.progress_bar.set_minimum(0)
@Slot(dict, dict)
def update_progress_bar(self, content: dict, _) -> None:
"""Hook to update the progress bar
Args:
content: Content of the scan progress message.
metadata: Metadata of the message.
"""
if content["max_value"] == 0:
self.progress_bar.set_value(0)
return
self.progress_bar.set_maximum(content["max_value"])
self.progress_bar.set_value(content["value"])
@Slot()
def clear_queue(self) -> None:
"""Clear the scan queue"""
self.queue.request_queue_reset()
##############################
######## END OF SLOTS ########
##############################
def enable_ui(self, enable: bool) -> None:
"""Enable or disable the UI components"""
# Enable/disable motor and signal selection
self.ui.device_combobox_2.setEnabled(enable)
# Enable/disable DAP selection
self.ui.dap_combo_box.setEnabled(enable)
# Enable/disable Scan Button
# self.ui.scan_button.setEnabled(enable)
# Disable move to buttons in LMFitDialog
self.ui.findChild(LMFitDialog).set_actions_enabled(enable)
def _customise_buttons(self) -> None:
"""Add action buttons for the Action Control.
In addition, we are adding a callback to also clear the queue to the stop button
to ensure that upon clicking the button, no scans from another client may be queued
which would be confusing without the queue widget.
"""
fit_dialog = self.ui.findChild(LMFitDialog)
fit_dialog.active_action_list = ["center", "center1", "center2"]
fit_dialog.move_action.connect(self.move_to_center)
stop_button = self.ui.findChild(StopButton)
stop_button.button.setText("Stop and Clear Queue")
stop_button.button.clicked.connect(self.clear_queue)
def _customise_bec_waveform_widget(self) -> None:
"""Customise the BEC Waveform Widget, i.e. clear the toolbar"""
self.waveform.toolbar.clear()
def _setup_motor_indicator(self) -> None:
"""Setup the arrow item"""
self.waveform.waveform.tick_item.add_to_plot()
positioner_box = self.ui.findChild(PositionerGroup)
positioner_box.position_update.connect(self.waveform.waveform.tick_item.set_position)
self.waveform.waveform.tick_item.set_position(0)
def _setup_signal_combobox(self) -> None:
"""Setup signal selection"""
# FIXME after changing the filtering in the combobox
signals = [name for name in self.dev if isinstance(self.dev.get(name), BECSignal)]
self.ui.device_combobox_2.setCurrentText(signals[0])
self.ui.device_combobox_2.set_device_filter("Signal")
def _setup_progress_bar(self) -> None:
"""Setup progress bar"""
# FIXME once the BECScanProgressBar is implemented
self.progress_bar = self.ui.findChild(BECProgressBar, "bec_progress_bar")
self.progress_bar.set_value(0)
self.ui.bec_waveform_widget.new_scan.connect(self.reset_progress_bar)
self.bec_dispatcher.connect_slot(self.update_progress_bar, MessageEndpoints.scan_progress())
def close(self):
logger.info("Disconnecting", repr(self.bec_dispatcher))
self.bec_dispatcher.disconnect_all()
logger.info("Shutting down BEC Client", repr(self.client))
self.client.shutdown()
def main():
import sys
app = QApplication(sys.argv)
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "app_icons", "alignment_1d.png"), size=QSize(48, 48)
)
app.setWindowIcon(icon)
window = Alignment1D()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -1,615 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>mainWindow</class>
<widget class="QMainWindow" name="mainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1611</width>
<height>1019</height>
</rect>
</property>
<property name="windowTitle">
<string>Alignment tool</string>
</property>
<widget class="QWidget" name="widget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="DarkModeButton" name="dark_mode_button"/>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="BECStatusBox" name="bec_status_box">
<property name="compact_view" stdset="0">
<bool>true</bool>
</property>
<property name="label" stdset="0">
<string>BEC Servers</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="BECQueue" name="bec_queue">
<property name="compact_view" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QRadioButton" name="radioButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>SLS Light On</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoExclusive">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QRadioButton" name="radioButton_3">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>BEAMLINE Checks</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoExclusive">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="StopButton" name="stop_button">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>40</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="BECProgressBar" name="bec_progress_bar">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="ControlTab">
<attribute name="title">
<string>Alignment Control</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QWidget" name="widget_4" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="ScanControl" name="scan_control">
<property name="current_scan" stdset="0">
<string>line_scan</string>
</property>
<property name="hide_arg_box" stdset="0">
<bool>false</bool>
</property>
<property name="hide_scan_selection_combobox" stdset="0">
<bool>true</bool>
</property>
<property name="hide_add_remove_buttons" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="PositionerGroup" name="positioner_group"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_3" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>4</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget_2" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font/>
</property>
<property name="text">
<string>Monitor</string>
</property>
</widget>
</item>
<item>
<widget class="DeviceComboBox" name="device_combobox_2"/>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="font">
<font/>
</property>
<property name="text">
<string>LMFit Model</string>
</property>
</widget>
</item>
<item>
<widget class="DapComboBox" name="dap_combo_box"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enable ROI</string>
</property>
</widget>
</item>
<item>
<widget class="ToggleSwitch" name="toggle_switch">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>3</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Activate linear region select for LMFit</string>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="checked" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="BECWaveformWidget" name="bec_waveform_widget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>600</width>
<height>450</height>
</size>
</property>
<property name="clear_curves_on_plot_update" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LMFitDialog" name="lm_fit_dialog">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>190</height>
</size>
</property>
<property name="always_show_latest" stdset="0">
<bool>true</bool>
</property>
<property name="hide_curve_selection" stdset="0">
<bool>true</bool>
</property>
<property name="hide_summary" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Logbook</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="WebsiteWidget" name="website_widget">
<property name="url" stdset="0">
<string>https://scilog.psi.ch/login</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>DapComboBox</class>
<extends>QWidget</extends>
<header>dap_combo_box</header>
</customwidget>
<customwidget>
<class>StopButton</class>
<extends>QWidget</extends>
<header>stop_button</header>
</customwidget>
<customwidget>
<class>WebsiteWidget</class>
<extends>QWidget</extends>
<header>website_widget</header>
</customwidget>
<customwidget>
<class>BECQueue</class>
<extends>QWidget</extends>
<header>bec_queue</header>
</customwidget>
<customwidget>
<class>ScanControl</class>
<extends>QWidget</extends>
<header>scan_control</header>
</customwidget>
<customwidget>
<class>ToggleSwitch</class>
<extends>QWidget</extends>
<header>toggle_switch</header>
</customwidget>
<customwidget>
<class>BECProgressBar</class>
<extends>QWidget</extends>
<header>bec_progress_bar</header>
</customwidget>
<customwidget>
<class>DarkModeButton</class>
<extends>QWidget</extends>
<header>dark_mode_button</header>
</customwidget>
<customwidget>
<class>PositionerGroup</class>
<extends>QWidget</extends>
<header>positioner_group</header>
</customwidget>
<customwidget>
<class>BECWaveformWidget</class>
<extends>QWidget</extends>
<header>bec_waveform_widget</header>
</customwidget>
<customwidget>
<class>DeviceComboBox</class>
<extends>QComboBox</extends>
<header>device_combobox</header>
</customwidget>
<customwidget>
<class>LMFitDialog</class>
<extends>QWidget</extends>
<header>lm_fit_dialog</header>
</customwidget>
<customwidget>
<class>BECStatusBox</class>
<extends>QWidget</extends>
<header>bec_status_box</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>toggle_switch</sender>
<signal>enabled(bool)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>toogle_roi_select(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>1042</x>
<y>212</y>
</hint>
<hint type="destinationlabel">
<x>1416</x>
<y>322</y>
</hint>
</hints>
</connection>
<connection>
<sender>bec_waveform_widget</sender>
<signal>dap_summary_update(QVariantMap,QVariantMap)</signal>
<receiver>lm_fit_dialog</receiver>
<slot>update_summary_tree(QVariantMap,QVariantMap)</slot>
<hints>
<hint type="sourcelabel">
<x>1099</x>
<y>258</y>
</hint>
<hint type="destinationlabel">
<x>1157</x>
<y>929</y>
</hint>
</hints>
</connection>
<connection>
<sender>device_combobox_2</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>plot(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>577</x>
<y>215</y>
</hint>
<hint type="destinationlabel">
<x>1416</x>
<y>427</y>
</hint>
</hints>
</connection>
<connection>
<sender>device_combobox_2</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>dap_combo_box</receiver>
<slot>select_y_axis(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>577</x>
<y>215</y>
</hint>
<hint type="destinationlabel">
<x>909</x>
<y>215</y>
</hint>
</hints>
</connection>
<connection>
<sender>dap_combo_box</sender>
<signal>new_dap_config(QString,QString,QString)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>add_dap(QString,QString,QString)</slot>
<hints>
<hint type="sourcelabel">
<x>909</x>
<y>215</y>
</hint>
<hint type="destinationlabel">
<x>1416</x>
<y>447</y>
</hint>
</hints>
</connection>
<connection>
<sender>scan_control</sender>
<signal>device_selected(QString)</signal>
<receiver>positioner_group</receiver>
<slot>set_positioners(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>230</x>
<y>306</y>
</hint>
<hint type="destinationlabel">
<x>187</x>
<y>926</y>
</hint>
</hints>
</connection>
<connection>
<sender>scan_control</sender>
<signal>device_selected(QString)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>set_x(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>187</x>
<y>356</y>
</hint>
<hint type="destinationlabel">
<x>972</x>
<y>509</y>
</hint>
</hints>
</connection>
<connection>
<sender>scan_control</sender>
<signal>device_selected(QString)</signal>
<receiver>dap_combo_box</receiver>
<slot>select_x_axis(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>187</x>
<y>356</y>
</hint>
<hint type="destinationlabel">
<x>794</x>
<y>202</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,84 +0,0 @@
"""
Launcher for BEC GUI Applications
Application must be located in bec_widgets/applications ;
in order for the launcher to find the application, it has to be put in
a subdirectory with the same name as the main Python module:
/bec_widgets/applications
├── alignment
│ └── alignment_1d
│ └── alignment_1d.py
├── other_app
└── other_app.py
The tree above would contain 2 applications, alignment_1d and other_app.
The Python module for the application must have `if __name__ == "__main__":`
in order for the launcher to execute it (it is run with `python -m`).
"""
import argparse
import os
import sys
MODULE_PATH = os.path.dirname(__file__)
def find_apps(base_dir: str) -> list[str]:
matching_modules = []
for root, dirs, files in os.walk(base_dir):
parent_dir = os.path.basename(root)
for file in files:
if file.endswith(".py") and file != "__init__.py":
file_name_without_ext = os.path.splitext(file)[0]
if file_name_without_ext == parent_dir:
rel_path = os.path.relpath(root, base_dir)
module_path = rel_path.replace(os.sep, ".")
module_name = f"{module_path}.{file_name_without_ext}"
matching_modules.append((file_name_without_ext, module_name))
return matching_modules
def main():
parser = argparse.ArgumentParser(description="BEC application launcher")
parser.add_argument("-m", "--module", type=str, help="The module to run (string argument).")
# Add a positional argument for the module, which acts as a fallback if -m is not provided
parser.add_argument(
"positional_module",
nargs="?", # This makes the positional argument optional
help="Positional argument that is treated as module if -m is not specified.",
)
args = parser.parse_args()
# If the -m/--module is not provided, fallback to the positional argument
module = args.module if args.module else args.positional_module
if module:
for app_name, app_module in find_apps(MODULE_PATH):
if module in (app_name, app_module):
print("Starting:", app_name)
python_executable = sys.executable
# Replace the current process with the new Python module
os.execvp(
python_executable,
[python_executable, "-m", f"bec_widgets.applications.{app_module}"],
)
print(f"Error: cannot find application {module}")
# display list of apps
print("Available applications:")
for app, _ in find_apps(MODULE_PATH):
print(f" - {app}")
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -1,9 +1,11 @@
from __future__ import annotations
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
def dock_area(object_name: str | None = None) -> BECDockArea:
_dock_area = BECDockArea(object_name=object_name)
_dock_area = BECDockArea(object_name=object_name, root_widget=True)
return _dock_area

View File

@@ -146,7 +146,7 @@ class LaunchWindow(BECMainWindow):
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
self.toolbar = ModularToolBar(parent=self)
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
self.spacer = QWidget()
self.spacer = QWidget(self)
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar.addWidget(self.spacer)
self.toolbar.addWidget(self.dark_mode_button)
@@ -204,13 +204,17 @@ class LaunchWindow(BECMainWindow):
list(self.available_auto_updates.keys()) + ["Default"]
)
self.register = RPCRegister()
self.register.callbacks.append(self._turn_off_the_lights)
self.register.broadcast()
def launch(
self,
launch_script: str,
name: str | None = None,
geometry: tuple[int, int, int, int] | None = None,
**kwargs,
) -> QWidget:
) -> QWidget | None:
"""Launch the specified script. If the launch script creates a QWidget, it will be
embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown
as a separate window.
@@ -231,6 +235,10 @@ class LaunchWindow(BECMainWindow):
raise ValueError(
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
)
if not WidgetContainerUtils.has_name_valid_chars(name):
raise ValueError(
f"Name {name} contains invalid characters. Only alphanumeric characters, underscores, and dashes are allowed."
)
else:
name = "dock_area"
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
@@ -242,6 +250,8 @@ class LaunchWindow(BECMainWindow):
if launch_script == "custom_ui_file":
ui_file = kwargs.pop("ui_file", None)
if not ui_file:
return None
return self._launch_custom_ui_file(ui_file)
if launch_script == "auto_update":
@@ -371,6 +381,45 @@ class LaunchWindow(BECMainWindow):
super().showEvent(event)
self.setFixedSize(self.size())
def _launcher_is_last_widget(self, connections: dict) -> bool:
"""
Check if the launcher is the last widget in the application.
"""
remaining_connections = [
connection for connection in connections.values() if connection.parent_id != self.gui_id
]
return len(remaining_connections) <= 1
def _turn_off_the_lights(self, connections: dict):
"""
If there is only one connection remaining, it is the launcher, so we show it.
Once the launcher is closed as the last window, we quit the application.
"""
if self._launcher_is_last_widget(connections):
self.show()
self.activateWindow()
self.raise_()
if self.app:
self.app.setQuitOnLastWindowClosed(True) # type: ignore
return
self.hide()
if self.app:
self.app.setQuitOnLastWindowClosed(False) # type: ignore
def closeEvent(self, event):
"""
Close the launcher window.
"""
connections = self.register.list_all_connections()
if self._launcher_is_last_widget(connections):
event.accept()
return
event.ignore()
self.hide()
if __name__ == "__main__":
import sys

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -6,6 +6,7 @@ from __future__ import annotations
import enum
import inspect
import traceback
from functools import reduce
from typing import Literal, Optional
from bec_lib.logger import bec_logger
@@ -25,22 +26,31 @@ class _WidgetsEnumType(str, enum.Enum):
_Widgets = {
"AbortButton": "AbortButton",
"BECDockArea": "BECDockArea",
"BECProgressBar": "BECProgressBar",
"BECQueue": "BECQueue",
"BECStatusBox": "BECStatusBox",
"DapComboBox": "DapComboBox",
"DarkModeButton": "DarkModeButton",
"DeviceBrowser": "DeviceBrowser",
"DeviceComboBox": "DeviceComboBox",
"DeviceLineEdit": "DeviceLineEdit",
"Image": "Image",
"LogPanel": "LogPanel",
"Minesweeper": "Minesweeper",
"MotorMap": "MotorMap",
"MultiWaveform": "MultiWaveform",
"PositionIndicator": "PositionIndicator",
"PositionerBox": "PositionerBox",
"PositionerBox2D": "PositionerBox2D",
"PositionerControlLine": "PositionerControlLine",
"ResetButton": "ResetButton",
"ResumeButton": "ResumeButton",
"RingProgressBar": "RingProgressBar",
"ScanControl": "ScanControl",
"ScatterWaveform": "ScatterWaveform",
"StopButton": "StopButton",
"TextBox": "TextBox",
"VSCodeEditor": "VSCodeEditor",
"Waveform": "Waveform",
@@ -76,6 +86,16 @@ except ImportError as e:
logger.error(f"Failed loading plugins: \n{reduce(add, traceback.format_exception(e))}")
class AbortButton(RPCBase):
"""A button that abort the scan."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class AutoUpdates(RPCBase):
@property
@rpc_call
@@ -665,6 +685,14 @@ class DapComboBox(RPCBase):
"""
class DarkModeButton(RPCBase):
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class DeviceBrowser(RPCBase):
"""DeviceBrowser is a widget that displays all available devices in the current BEC session."""
@@ -923,48 +951,6 @@ class Image(RPCBase):
Set auto range for the y-axis.
"""
@property
@rpc_call
def x_log(self) -> "bool":
"""
Set X-axis to log scale if True, linear if False.
"""
@x_log.setter
@rpc_call
def x_log(self) -> "bool":
"""
Set X-axis to log scale if True, linear if False.
"""
@property
@rpc_call
def y_log(self) -> "bool":
"""
Set Y-axis to log scale if True, linear if False.
"""
@y_log.setter
@rpc_call
def y_log(self) -> "bool":
"""
Set Y-axis to log scale if True, linear if False.
"""
@property
@rpc_call
def legend_label_size(self) -> "int":
"""
The font size of the legend font.
"""
@legend_label_size.setter
@rpc_call
def legend_label_size(self) -> "int":
"""
The font size of the legend font.
"""
@property
@rpc_call
def color_map(self) -> "str":
@@ -1159,16 +1145,16 @@ class Image(RPCBase):
@property
@rpc_call
def rotation(self) -> "int":
def num_rotation_90(self) -> "int":
"""
The number of 90° rotations to apply.
The number of 90° rotations to apply counterclockwise.
"""
@rotation.setter
@num_rotation_90.setter
@rpc_call
def rotation(self) -> "int":
def num_rotation_90(self) -> "int":
"""
The number of 90° rotations to apply.
The number of 90° rotations to apply counterclockwise.
"""
@property
@@ -1388,6 +1374,9 @@ class LogPanel(RPCBase):
"""
class Minesweeper(RPCBase): ...
class MotorMap(RPCBase):
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
@@ -2166,7 +2155,7 @@ class MultiWaveform(RPCBase):
"""
@rpc_call
def plot(self, monitor: "str", color_palette: "str | None" = "magma"):
def plot(self, monitor: "str", color_palette: "str | None" = "plasma"):
"""
Create a plot for the given monitor.
Args:
@@ -2196,7 +2185,10 @@ class PositionIndicator(RPCBase):
@rpc_call
def set_value(self, position: float):
"""
None
Set the position of the indicator
Args:
position: The new position of the indicator
"""
@rpc_call
@@ -2216,6 +2208,13 @@ class PositionIndicator(RPCBase):
Property to determine the orientation of the position indicator
"""
@vertical.setter
@rpc_call
def vertical(self):
"""
Property to determine the orientation of the position indicator
"""
@property
@rpc_call
def indicator_width(self):
@@ -2223,6 +2222,13 @@ class PositionIndicator(RPCBase):
Property to get the width of the indicator
"""
@indicator_width.setter
@rpc_call
def indicator_width(self):
"""
Property to get the width of the indicator
"""
@property
@rpc_call
def rounded_corners(self):
@@ -2230,6 +2236,61 @@ class PositionIndicator(RPCBase):
Property to get the rounded corners of the position indicator
"""
@rounded_corners.setter
@rpc_call
def rounded_corners(self):
"""
Property to get the rounded corners of the position indicator
"""
class PositionerBox(RPCBase):
"""Simple Widget to control a positioner in box form"""
@rpc_call
def set_positioner(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class PositionerBox2D(RPCBase):
"""Simple Widget to control two positioners in box form"""
@rpc_call
def set_positioner_hor(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
@rpc_call
def set_positioner_ver(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class PositionerControlLine(RPCBase):
"""A widget that controls a single device."""
@rpc_call
def set_positioner(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class PositionerGroup(RPCBase):
"""Simple Widget to control a positioner in box form"""
@@ -2243,6 +2304,26 @@ class PositionerGroup(RPCBase):
"""
class ResetButton(RPCBase):
"""A button that resets the scan queue."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class ResumeButton(RPCBase):
"""A button that continue scan queue."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class Ring(RPCBase):
@rpc_call
def _get_all_rpc(self) -> "dict":
@@ -2371,9 +2452,9 @@ class RingProgressBar(RPCBase):
@property
@rpc_call
def rings(self):
def rings(self) -> "list[Ring]":
"""
None
Returns a list of all rings in the progress bar.
"""
@rpc_call
@@ -2847,7 +2928,7 @@ class ScatterWaveform(RPCBase):
x_entry: "None | str" = None,
y_entry: "None | str" = None,
z_entry: "None | str" = None,
color_map: "str | None" = "magma",
color_map: "str | None" = "plasma",
label: "str | None" = None,
validate_bec: "bool" = True,
) -> "ScatterCurve":
@@ -2887,6 +2968,16 @@ class ScatterWaveform(RPCBase):
"""
class StopButton(RPCBase):
"""A button that stops the current scan."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class TextBox(RPCBase):
"""A widget that displays text in plain and HTML format"""
@@ -3231,6 +3322,20 @@ class Waveform(RPCBase):
None
"""
@property
@rpc_call
def x_entry(self) -> "str | None":
"""
The x signal name.
"""
@x_entry.setter
@rpc_call
def x_entry(self) -> "str | None":
"""
The x signal name.
"""
@property
@rpc_call
def color_palette(self) -> "str":

View File

@@ -20,6 +20,7 @@ from rich.table import Table
import bec_widgets.cli.client as client
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
from bec_widgets.utils.serialization import register_serializer_extension
if TYPE_CHECKING: # pragma: no cover
from bec_lib.messages import GUIRegistryStateMessage
@@ -31,7 +32,8 @@ logger = bec_logger.logger
IGNORE_WIDGETS = ["LaunchWindow"]
RegistryState: TypeAlias = dict[
Literal["gui_id", "name", "widget_class", "config", "__rpc__"], str | bool | dict
Literal["gui_id", "name", "widget_class", "config", "__rpc__", "container_proxy"],
str | bool | dict,
]
# pylint: disable=redefined-outer-scope
@@ -214,6 +216,7 @@ class BECGuiClient(RPCBase):
self._server_registry: dict[str, RegistryState] = {}
self._ipython_registry: dict[str, RPCReference] = {}
self.available_widgets = AvailableWidgetsNamespace()
register_serializer_extension()
####################
#### Client API ####
@@ -275,6 +278,8 @@ class BECGuiClient(RPCBase):
name: str | None = None,
wait: bool = True,
geometry: tuple[int, int, int, int] | None = None,
launch_script: str = "dock_area",
**kwargs,
) -> client.BECDockArea:
"""Create a new top-level dock area.
@@ -290,11 +295,11 @@ class BECGuiClient(RPCBase):
if wait:
with wait_for_server(self):
widget = self.launcher._run_rpc(
"launch", "dock_area", name, geometry
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
) # pylint: disable=protected-access
return widget
widget = self.launcher._run_rpc(
"new_dock_area", name, geometry
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
) # pylint: disable=protected-access
return widget

View File

@@ -40,6 +40,7 @@ class ClientGenerator:
"""import enum
import inspect
import traceback
from functools import reduce
from typing import Literal, Optional
"""
if self._base
@@ -315,4 +316,7 @@ def main():
if __name__ == "__main__": # pragma: no cover
import sys
sys.argv = ["bw-generate-cli", "--target", "csaxs_bec"]
main()

View File

@@ -130,6 +130,7 @@ class RPCBase:
config: dict | None = None,
object_name: str | None = None,
parent=None,
**kwargs,
) -> None:
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
self._config = config if config is not None else {}
@@ -144,21 +145,21 @@ class RPCBase:
def __repr__(self):
type_ = type(self)
qualname = type_.__qualname__
return f"<{qualname} with name: {self.widget_name}>"
return f"<{qualname} with name: {self.object_name}>"
def remove(self):
"""
Remove the widget.
"""
obj = self._root._server_registry.get(self._gui_id)
if obj is None:
raise ValueError(f"Widget {self._gui_id} not found.")
if proxy := obj.get("container_proxy"):
assert isinstance(proxy, str)
self._run_rpc("remove", gui_id=proxy)
return
self._run_rpc("remove")
@property
def widget_name(self):
"""
Get the widget name.
"""
return self.object_name
@property
def _root(self) -> BECGuiClient:
"""
@@ -171,7 +172,15 @@ class RPCBase:
parent = parent._parent
return parent # type: ignore
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=5, **kwargs) -> Any:
def _run_rpc(
self,
method,
*args,
wait_for_rpc_response=True,
timeout=5,
gui_id: str | None = None,
**kwargs,
) -> Any:
"""
Run the RPC call.
@@ -179,6 +188,8 @@ class RPCBase:
method: The method to call.
args: The arguments to pass to the method.
wait_for_rpc_response: Whether to wait for the RPC response.
timeout: The timeout for the RPC response.
gui_id: The GUI ID to use for the RPC call. If None, the default GUI ID is used.
kwargs: The keyword arguments to pass to the method.
Returns:
@@ -187,7 +198,7 @@ class RPCBase:
request_id = str(uuid.uuid4())
rpc_msg = messages.GUIInstructionMessage(
action=method,
parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
parameter={"args": args, "kwargs": kwargs, "gui_id": gui_id or self._gui_id},
metadata={"request_id": request_id},
)
# pylint: disable=protected-access

View File

@@ -83,29 +83,6 @@ class GUIServer:
service_config = ServiceConfig()
return service_config
def _turn_off_the_lights(self, connections: dict):
"""
If there is only one connection remaining, it is the launcher, so we show it.
Once the launcher is closed as the last window, we quit the application.
"""
self.launcher_window = cast(LaunchWindow, self.launcher_window)
remaining_connections = [
connection
for connection in connections.values()
if connection.parent_id != self.launcher_window.gui_id
]
if len(remaining_connections) <= 1:
self.launcher_window.show()
self.launcher_window.activateWindow()
self.launcher_window.raise_()
if self.app:
self.app.setQuitOnLastWindowClosed(True)
else:
self.launcher_window.hide()
if self.app:
self.app.setQuitOnLastWindowClosed(False)
def _run(self):
"""
Run the GUI server.
@@ -125,10 +102,6 @@ class GUIServer:
self.app.aboutToQuit.connect(self.shutdown)
self.app.setQuitOnLastWindowClosed(False)
register = RPCRegister()
register.callbacks.append(self._turn_off_the_lights)
register.broadcast()
if self.gui_class:
# If the server is started with a specific gui class, we launch it.
# This will automatically hide the launcher.

View File

@@ -43,22 +43,22 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
"pg": pg,
"wh": wh,
"dock": self.dock,
"im": self.im,
"mi": self.mi,
"mm": self.mm,
"lm": self.lm,
"btn1": self.btn1,
"btn2": self.btn2,
"btn3": self.btn3,
"btn4": self.btn4,
"btn5": self.btn5,
"btn6": self.btn6,
"pb": self.pb,
"pi": self.pi,
"wf": self.wf,
"scatter": self.scatter,
"scatter_mi": self.scatter,
"mwf": self.mwf,
# "im": self.im,
# "mi": self.mi,
# "mm": self.mm,
# "lm": self.lm,
# "btn1": self.btn1,
# "btn2": self.btn2,
# "btn3": self.btn3,
# "btn4": self.btn4,
# "btn5": self.btn5,
# "btn6": self.btn6,
# "pb": self.pb,
# "pi": self.pi,
# "wf": self.wf,
# "scatter": self.scatter,
# "scatter_mi": self.scatter,
# "mwf": self.mwf,
}
)
@@ -77,76 +77,76 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
first_tab_layout.addWidget(self.dock)
tab_widget.addTab(first_tab, "Dock Area")
third_tab = QWidget()
third_tab_layout = QVBoxLayout(third_tab)
self.lm = LayoutManagerWidget()
third_tab_layout.addWidget(self.lm)
tab_widget.addTab(third_tab, "Layout Manager Widget")
fourth_tab = QWidget()
fourth_tab_layout = QVBoxLayout(fourth_tab)
self.pb = PlotBase()
self.pi = self.pb.plot_item
fourth_tab_layout.addWidget(self.pb)
tab_widget.addTab(fourth_tab, "PlotBase")
tab_widget.setCurrentIndex(3)
# third_tab = QWidget()
# third_tab_layout = QVBoxLayout(third_tab)
# self.lm = LayoutManagerWidget()
# third_tab_layout.addWidget(self.lm)
# tab_widget.addTab(third_tab, "Layout Manager Widget")
#
# fourth_tab = QWidget()
# fourth_tab_layout = QVBoxLayout(fourth_tab)
# self.pb = PlotBase()
# self.pi = self.pb.plot_item
# fourth_tab_layout.addWidget(self.pb)
# tab_widget.addTab(fourth_tab, "PlotBase")
#
# tab_widget.setCurrentIndex(3)
#
group_box = QGroupBox("Jupyter Console", splitter)
group_box_layout = QVBoxLayout(group_box)
self.console = BECJupyterConsole(inprocess=True)
group_box_layout.addWidget(self.console)
# Some buttons for layout testing
self.btn1 = QPushButton("Button 1")
self.btn2 = QPushButton("Button 2")
self.btn3 = QPushButton("Button 3")
self.btn4 = QPushButton("Button 4")
self.btn5 = QPushButton("Button 5")
self.btn6 = QPushButton("Button 6")
fifth_tab = QWidget()
fifth_tab_layout = QVBoxLayout(fifth_tab)
self.wf = Waveform()
fifth_tab_layout.addWidget(self.wf)
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
tab_widget.setCurrentIndex(4)
sixth_tab = QWidget()
sixth_tab_layout = QVBoxLayout(sixth_tab)
self.im = Image()
self.mi = self.im.main_image
sixth_tab_layout.addWidget(self.im)
tab_widget.addTab(sixth_tab, "Image Next Gen")
tab_widget.setCurrentIndex(5)
seventh_tab = QWidget()
seventh_tab_layout = QVBoxLayout(seventh_tab)
self.scatter = ScatterWaveform()
self.scatter_mi = self.scatter.main_curve
self.scatter.plot("samx", "samy", "bpm4i")
seventh_tab_layout.addWidget(self.scatter)
tab_widget.addTab(seventh_tab, "Scatter Waveform")
tab_widget.setCurrentIndex(6)
eighth_tab = QWidget()
eighth_tab_layout = QVBoxLayout(eighth_tab)
self.mm = MotorMap()
eighth_tab_layout.addWidget(self.mm)
tab_widget.addTab(eighth_tab, "Motor Map")
tab_widget.setCurrentIndex(7)
ninth_tab = QWidget()
ninth_tab_layout = QVBoxLayout(ninth_tab)
self.mwf = MultiWaveform()
ninth_tab_layout.addWidget(self.mwf)
tab_widget.addTab(ninth_tab, "MultiWaveform")
tab_widget.setCurrentIndex(8)
# add stuff to the new Waveform widget
self._init_waveform()
self.setWindowTitle("Jupyter Console Window")
#
# # Some buttons for layout testing
# self.btn1 = QPushButton("Button 1")
# self.btn2 = QPushButton("Button 2")
# self.btn3 = QPushButton("Button 3")
# self.btn4 = QPushButton("Button 4")
# self.btn5 = QPushButton("Button 5")
# self.btn6 = QPushButton("Button 6")
#
# fifth_tab = QWidget()
# fifth_tab_layout = QVBoxLayout(fifth_tab)
# self.wf = Waveform()
# fifth_tab_layout.addWidget(self.wf)
# tab_widget.addTab(fifth_tab, "Waveform Next Gen")
# tab_widget.setCurrentIndex(4)
#
# sixth_tab = QWidget()
# sixth_tab_layout = QVBoxLayout(sixth_tab)
# self.im = Image()
# self.mi = self.im.main_image
# sixth_tab_layout.addWidget(self.im)
# tab_widget.addTab(sixth_tab, "Image Next Gen")
# tab_widget.setCurrentIndex(5)
#
# seventh_tab = QWidget()
# seventh_tab_layout = QVBoxLayout(seventh_tab)
# self.scatter = ScatterWaveform()
# self.scatter_mi = self.scatter.main_curve
# self.scatter.plot("samx", "samy", "bpm4i")
# seventh_tab_layout.addWidget(self.scatter)
# tab_widget.addTab(seventh_tab, "Scatter Waveform")
# tab_widget.setCurrentIndex(6)
#
# eighth_tab = QWidget()
# eighth_tab_layout = QVBoxLayout(eighth_tab)
# self.mm = MotorMap()
# eighth_tab_layout.addWidget(self.mm)
# tab_widget.addTab(eighth_tab, "Motor Map")
# tab_widget.setCurrentIndex(7)
#
# ninth_tab = QWidget()
# ninth_tab_layout = QVBoxLayout(ninth_tab)
# self.mwf = MultiWaveform()
# ninth_tab_layout.addWidget(self.mwf)
# tab_widget.addTab(ninth_tab, "MultiWaveform")
# tab_widget.setCurrentIndex(8)
#
# # add stuff to the new Waveform widget
# self._init_waveform()
#
# self.setWindowTitle("Jupyter Console Window")
def _init_waveform(self):
self.wf.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
@@ -174,7 +174,7 @@ if __name__ == "__main__": # pragma: no cover
icon = material_icon("terminal", color=(255, 255, 255, 255), filled=True)
app.setWindowIcon(icon)
bec_dispatcher = BECDispatcher()
bec_dispatcher = BECDispatcher(gui_id="jupyter_console")
client = bec_dispatcher.client
client.start()

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import os
import time
import traceback
import uuid
from datetime import datetime
from typing import TYPE_CHECKING, Optional
@@ -14,8 +15,7 @@ from qtpy.QtCore import QObject, QRunnable, QThreadPool, QTimer, Signal
from qtpy.QtWidgets import QApplication
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.error_popups import ErrorPopupUtility
from bec_widgets.utils.error_popups import SafeSlot as pyqtSlot
from bec_widgets.utils.error_popups import ErrorPopupUtility, SafeSlot
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
@@ -85,9 +85,21 @@ class BECConnector:
gui_id: str | None = None,
object_name: str | None = None,
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
parent_id: str | None = None,
root_widget: bool = False,
**kwargs,
):
"""
BECConnector mixin class to handle BEC client and device manager.
Args:
client(BECClient, optional): The BEC client.
config(ConnectionConfig, optional): The connection configuration with specific gui id.
gui_id(str, optional): The GUI ID.
object_name(str, optional): The object name.
parent_dock(BECDock, optional): The parent dock.# TODO should go away -> issue created #473
root_widget(bool, optional): If set to True, the parent_id will be always set to None, thus enforcing that the widget is accessible as a root widget of the BECGuiClient object.
**kwargs:
"""
# Extract object_name from kwargs to not pass it to Qt class
object_name = object_name or kwargs.pop("objectName", None)
# Ensure the parent is always the first argument for QObject
@@ -99,6 +111,9 @@ class BECConnector:
self, QObject
), "BECConnector must be used with a QObject or any qt related class."
# flag to check if the object was destroyed and its cleanup was called
self._destroyed = False
# BEC related connections
self.bec_dispatcher = BECDispatcher(client=client)
self.client = self.bec_dispatcher.client if client is None else client
@@ -108,7 +123,7 @@ class BECConnector:
if not self.client in BECConnector.EXIT_HANDLERS:
# register function to clean connections at exit;
# the function depends on BECClient, and BECDispatcher
@pyqtSlot()
@SafeSlot()
def terminate(client=self.client, dispatcher=self.bec_dispatcher):
logger.info("Disconnecting", repr(dispatcher))
dispatcher.disconnect_all()
@@ -128,7 +143,6 @@ class BECConnector:
)
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
self.parent_id = parent_id
# If the gui_id is passed, it should be respected. However, this should be revisted since
# the gui_id has to be unique, and may no longer be.
if gui_id:
@@ -147,10 +161,8 @@ class BECConnector:
# 2) Enforce unique objectName among siblings with the same BECConnector parent
self.setParent(parent)
if parent_id is None:
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
if connector_parent is not None:
self.parent_id = connector_parent.gui_id
if isinstance(self.parent(), QObject) and hasattr(self, "cleanup"):
self.parent().destroyed.connect(self._run_cleanup_on_deleted_parent)
# Error popups
self.error_utility = ErrorPopupUtility()
@@ -159,8 +171,40 @@ class BECConnector:
# Store references to running workers so they're not garbage collected prematurely.
self._workers = []
# If set to True, the parent_id will be always set to None, thus enforcing that the widget is accessible as a root widget of the BECGuiClient object.
self.root_widget = root_widget
QTimer.singleShot(0, self._update_object_name)
@property
def parent_id(self) -> str | None:
try:
if self.root_widget:
return None
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
return connector_parent.gui_id if connector_parent else None
except:
logger.error(f"Error getting parent_id for {self.__class__.__name__}")
@SafeSlot()
def _run_cleanup_on_deleted_parent(self) -> None:
"""
Run cleanup on the deleted parent.
This method is called when the parent is deleted.
"""
if not hasattr(self, "cleanup"):
return
try:
if not self._destroyed:
self.cleanup()
self._destroyed = True
except Exception:
content = traceback.format_exc()
logger.info(
"Failed to run cleanup on deleted parent. "
f"This is not necessarily an error as the parent may be deleted before the child and includes already a cleanup. The following exception was raised:\n{content}"
)
def _update_object_name(self) -> None:
"""
Enforce a unique object name among siblings and register the object for RPC.
@@ -228,7 +272,7 @@ class BECConnector:
if self.rpc_register.object_is_registered(self):
self.rpc_register.broadcast()
def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker:
def submit_task(self, fn, *args, on_complete: SafeSlot = None, **kwargs) -> Worker:
"""
Submit a task to run in a separate thread. The task will run the specified
function with the provided arguments and emit the completed signal when done.
@@ -356,7 +400,7 @@ class BECConnector:
file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
save_yaml(file_path, self._config_dict)
# @pyqtSlot(str)
# @SafeSlot(str)
def _set_gui_id(self, gui_id: str) -> None:
"""
Set the GUI ID for the widget.
@@ -388,7 +432,7 @@ class BECConnector:
self.client = client
self.get_bec_shortcuts()
@pyqtSlot(ConnectionConfig) # TODO can be also dict
@SafeSlot(ConnectionConfig) # TODO can be also dict
def on_config_update(self, config: ConnectionConfig | dict) -> None:
"""
Update the configuration for the widget.

View File

@@ -14,9 +14,11 @@ from bec_lib.service_config import ServiceConfig
from qtpy.QtCore import QObject
from qtpy.QtCore import Signal as pyqtSignal
from bec_widgets.utils.serialization import register_serializer_extension
logger = bec_logger.logger
if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from bec_lib.endpoints import EndpointInfo
from bec_widgets.utils.rpc_server import RPCServer
@@ -120,6 +122,8 @@ class BECDispatcher:
except redis.exceptions.ConnectionError:
logger.warning("Could not connect to Redis, skipping start of BECClient.")
register_serializer_extension()
logger.success("Initialized BECDispatcher")
self.start_cli_server(gui_id=gui_id)

View File

@@ -33,7 +33,6 @@ class BECWidget(BECConnector):
gui_id: str | None = None,
theme_update: bool = False,
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
parent_id: str | None = None,
**kwargs,
):
"""
@@ -55,12 +54,7 @@ class BECWidget(BECConnector):
"""
super().__init__(
client=client,
config=config,
gui_id=gui_id,
parent_dock=parent_dock,
parent_id=parent_id,
**kwargs,
client=client, config=config, gui_id=gui_id, parent_dock=parent_dock, **kwargs
)
if not isinstance(self, QObject):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
@@ -112,6 +106,8 @@ class BECWidget(BECConnector):
def closeEvent(self, event):
"""Wrap the close even to ensure the rpc_register is cleaned up."""
try:
self.cleanup()
if not self._destroyed:
self.cleanup()
self._destroyed = True
finally:
super().closeEvent(event) # pylint: disable=no-member

View File

@@ -11,7 +11,7 @@ from pydantic_core import PydanticCustomError
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QApplication
if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from bec_qthemes._main import AccentColors

View File

@@ -266,3 +266,5 @@ class CompactPopupWidget(QWidget):
# to ensure proper resources cleanup
for child in self.container.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
child.close()
super().closeEvent(event)

View File

@@ -96,15 +96,33 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
otherwise error display is left to the original exception hook
'verify_sender' keyword argument can be passed with boolean value if the sender should be verified
before executing the slot. If True, the slot will only execute if the sender is a QObject. This is
useful to prevent function calls from already deleted objects.
"""
popup_error = bool(slot_kwargs.pop("popup_error", False))
verify_sender = bool(slot_kwargs.pop("verify_sender", False))
def error_managed(method):
@Slot(*slot_args, **slot_kwargs)
@functools.wraps(method)
def wrapper(*args, **kwargs):
try:
if not verify_sender or len(args) == 0:
return method(*args, **kwargs)
_instance = args[0]
if not isinstance(_instance, QObject):
return method(*args, **kwargs)
sender = _instance.sender()
if sender is None:
logger.info(
f"Sender is None for {method.__module__}.{method.__qualname__}, "
"skipping method call."
)
return
return method(*args, **kwargs)
except Exception:
slot_name = f"{method.__module__}.{method.__qualname__}"
error_msg = traceback.format_exc()

View File

@@ -37,7 +37,7 @@ class ExpandableGroupFrame(QFrame):
self._title_layout.addWidget(self._expansion_button)
self._title_layout.addWidget(self._title)
self._contents = QWidget()
self._contents = QWidget(self)
self._layout.addWidget(self._contents)
self._expansion_button.clicked.connect(self.switch_expanded_state)

View File

@@ -0,0 +1,182 @@
from __future__ import annotations
from decimal import Decimal
from types import NoneType
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from pydantic import BaseModel, ValidationError
from qtpy.QtCore import Signal # type: ignore
from qtpy.QtWidgets import QGridLayout, QLabel, QLayout, QVBoxLayout, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.compact_popup import CompactPopupWidget
from bec_widgets.utils.forms_from_types.items import FormItemSpec, widget_from_type
logger = bec_logger.logger
class TypedForm(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "list_alt"
value_changed = Signal()
RPC = False
def __init__(
self,
parent=None,
items: list[tuple[str, type]] | None = None,
form_item_specs: list[FormItemSpec] | None = None,
client=None,
**kwargs,
):
"""Widget with a list of form items based on a list of types.
Args:
items (list[tuple[str, type]]): list of tuples of a name for the field and its type.
Should be a type supported by the logic in items.py
form_item_specs (list[FormItemSpec]): list of form item specs, equivalent to items.
only one of items or form_item_specs should be
supplied.
"""
if (items is not None and form_item_specs is not None) or (
items is None and form_item_specs is None
):
raise ValueError("Must specify one and only one of items and form_item_specs")
super().__init__(parent=parent, client=client, **kwargs)
self._items = (
form_item_specs
if form_item_specs is not None
else [
FormItemSpec(name=name, item_type=item_type)
for name, item_type in items # type: ignore
]
)
self._layout = QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout)
self._form_grid_container = QWidget(parent=self)
self._form_grid = QWidget(parent=self._form_grid_container)
self._layout.addWidget(self._form_grid_container)
self._form_grid_container.setLayout(QVBoxLayout())
self._form_grid.setLayout(self._new_grid_layout())
self.populate()
def populate(self):
self._clear_grid()
for r, item in enumerate(self._items):
self._add_griditem(item, r)
def _add_griditem(self, item: FormItemSpec, row: int):
grid = self._form_grid.layout()
label = QLabel(item.name)
label.setProperty("_model_field_name", item.name)
label.setToolTip(item.info.description or item.name)
grid.addWidget(label, row, 0)
widget = widget_from_type(item.item_type)(parent=self, spec=item)
widget.valueChanged.connect(self.value_changed)
grid.addWidget(widget, row, 1)
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
grid: QGridLayout = self._form_grid.layout() # type: ignore
return {
grid.itemAtPosition(i, 0)
.widget()
.property("_model_field_name"): grid.itemAtPosition(i, 1)
.widget()
.getValue() # type: ignore # we only add 'DynamicFormItem's here
for i in range(grid.rowCount())
}
def _clear_grid(self):
if (old_layout := self._form_grid.layout()) is not None:
while old_layout.count():
item = old_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
old_layout.deleteLater()
self._form_grid.deleteLater()
self._form_grid = QWidget()
self._form_grid.setLayout(self._new_grid_layout())
self._form_grid_container.layout().addWidget(self._form_grid)
self._form_grid.adjustSize()
self._form_grid_container.adjustSize()
self.adjustSize()
def _new_grid_layout(self):
new_grid = QGridLayout()
new_grid.setContentsMargins(0, 0, 0, 0)
new_grid.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
return new_grid
class PydanticModelForm(TypedForm):
metadata_updated = Signal(dict)
metadata_cleared = Signal(NoneType)
def __init__(self, parent=None, metadata_model: type[BaseModel] = None, client=None, **kwargs):
"""
A form generated from a pydantic model.
Args:
metadata_model (type[BaseModel]): the model class for which to generate a form.
"""
self._md_schema = metadata_model
super().__init__(parent=parent, form_item_specs=self._form_item_specs(), client=client)
self._validity = CompactPopupWidget()
self._validity.compact_view = True # type: ignore
self._validity.label = "Metadata validity" # type: ignore
self._validity.compact_show_popup.setIcon(
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
)
self._validity_message = QLabel("Not yet validated")
self._validity.addWidget(self._validity_message)
self._layout.addWidget(self._validity)
self.value_changed.connect(self.validate_form)
def set_schema(self, schema: type[BaseModel]):
self._md_schema = schema
self.populate()
def _form_item_specs(self):
return [
FormItemSpec(name=name, info=info, item_type=info.annotation)
for name, info in self._md_schema.model_fields.items()
]
def update_items_from_schema(self):
self._items = self._form_item_specs()
def populate(self):
self.update_items_from_schema()
super().populate()
def get_form_data(self):
"""Get the entered metadata as a dict."""
return self._dict_from_grid()
def validate_form(self, *_) -> bool:
"""validate the currently entered metadata against the pydantic schema.
If successful, returns on metadata_emitted and returns true.
Otherwise, emits on metadata_cleared and returns false."""
try:
metadata_dict = self.get_form_data()
self._md_schema.model_validate(metadata_dict)
self._validity.set_global_state("success")
self._validity_message.setText("No errors!")
self.metadata_updated.emit(metadata_dict)
return True
except ValidationError as e:
self._validity.set_global_state("emergency")
self._validity_message.setText(str(e))
self.metadata_cleared.emit(None)
return False

View File

@@ -2,11 +2,13 @@ from __future__ import annotations
from abc import abstractmethod
from decimal import Decimal
from typing import TYPE_CHECKING, Callable, get_args
from types import UnionType
from typing import Callable, Protocol
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
from pydantic.fields import FieldInfo
from qtpy.QtCore import Signal # type: ignore
from qtpy.QtWidgets import (
QApplication,
@@ -33,12 +35,22 @@ from bec_widgets.widgets.editors.scan_metadata._util import (
field_precision,
)
if TYPE_CHECKING:
from pydantic.fields import FieldInfo
logger = bec_logger.logger
class FormItemSpec(BaseModel):
"""
The specification for an item in a dynamically generated form. Uses a pydantic FieldInfo
to store most annotation info, since one of the main purposes is to store data for
forms genrated from pydantic models, but can also be composed from other sources or by hand.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
item_type: type | UnionType
name: str
info: FieldInfo = FieldInfo()
class ClearableBoolEntry(QWidget):
stateChanged = Signal()
@@ -82,21 +94,20 @@ class ClearableBoolEntry(QWidget):
self._false.setToolTip(tooltip)
class MetadataWidget(QWidget):
class DynamicFormItem(QWidget):
valueChanged = Signal()
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
super().__init__(parent)
self._info = info
self._spec = spec
self._layout = QHBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
self._default = field_default(self._info)
self._desc = self._info.description
self._default = field_default(self._spec.info)
self._desc = self._spec.info.description
self.setLayout(self._layout)
self._add_main_widget()
if clearable_required(info):
if clearable_required(spec.info):
self._add_clear_button()
@abstractmethod
@@ -127,15 +138,15 @@ class MetadataWidget(QWidget):
self.valueChanged.emit()
class StrMetadataField(MetadataWidget):
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
super().__init__(info, parent)
class StrMetadataField(DynamicFormItem):
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
super().__init__(parent=parent, spec=spec)
self._main_widget.textChanged.connect(self._value_changed)
def _add_main_widget(self) -> None:
self._main_widget = QLineEdit()
self._layout.addWidget(self._main_widget)
min_length, max_length = field_minlen(self._info), field_maxlen(self._info)
min_length, max_length = (field_minlen(self._spec.info), field_maxlen(self._spec.info))
if max_length:
self._main_widget.setMaxLength(max_length)
self._main_widget.setToolTip(
@@ -156,15 +167,15 @@ class StrMetadataField(MetadataWidget):
self._main_widget.setText(value)
class IntMetadataField(MetadataWidget):
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
super().__init__(info, parent)
class IntMetadataField(DynamicFormItem):
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
super().__init__(parent=parent, spec=spec)
self._main_widget.textChanged.connect(self._value_changed)
def _add_main_widget(self) -> None:
self._main_widget = QSpinBox()
self._layout.addWidget(self._main_widget)
min_, max_ = field_limits(self._info, int)
min_, max_ = field_limits(self._spec.info, int)
self._main_widget.setMinimum(min_)
self._main_widget.setMaximum(max_)
self._main_widget.setToolTip(f"(range {min_} to {max_}){self._describe()}")
@@ -185,18 +196,18 @@ class IntMetadataField(MetadataWidget):
self._main_widget.setValue(value)
class FloatDecimalMetadataField(MetadataWidget):
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
super().__init__(info, parent)
class FloatDecimalMetadataField(DynamicFormItem):
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
super().__init__(parent=parent, spec=spec)
self._main_widget.textChanged.connect(self._value_changed)
def _add_main_widget(self) -> None:
self._main_widget = QDoubleSpinBox()
self._layout.addWidget(self._main_widget)
min_, max_ = field_limits(self._info, int)
min_, max_ = field_limits(self._spec.info, int)
self._main_widget.setMinimum(min_)
self._main_widget.setMaximum(max_)
precision = field_precision(self._info)
precision = field_precision(self._spec.info)
if precision:
self._main_widget.setDecimals(precision)
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
@@ -219,13 +230,13 @@ class FloatDecimalMetadataField(MetadataWidget):
self._main_widget.setValue(value)
class BoolMetadataField(MetadataWidget):
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
super().__init__(info, parent)
class BoolMetadataField(DynamicFormItem):
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
super().__init__(parent=parent, spec=spec)
self._main_widget.stateChanged.connect(self._value_changed)
def _add_main_widget(self) -> None:
if clearable_required(self._info):
if clearable_required(self._spec.info):
self._main_widget = ClearableBoolEntry()
else:
self._main_widget = QCheckBox()
@@ -240,7 +251,7 @@ class BoolMetadataField(MetadataWidget):
self._main_widget.setChecked(value)
def widget_from_type(annotation: type | None) -> Callable[[FieldInfo], MetadataWidget]:
def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
if annotation in [str, str | None]:
return StrMetadataField
if annotation in [int, int | None]:

View File

@@ -5,6 +5,8 @@ from typing import NamedTuple
from qtpy.QtCore import QObject
from bec_widgets.utils.name_utils import pascal_to_snake
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
@@ -22,7 +24,7 @@ class DesignerPluginInfo:
def __init__(self, plugin_class):
self.plugin_class = plugin_class
self.plugin_name_pascal = plugin_class.__name__
self.plugin_name_snake = self.pascal_to_snake(self.plugin_name_pascal)
self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal)
self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}"
plugin_module = (
".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin"
@@ -38,21 +40,6 @@ class DesignerPluginInfo:
self.base_path = os.path.dirname(inspect.getfile(plugin_class))
@staticmethod
def pascal_to_snake(name: str) -> str:
"""
Convert PascalCase to snake_case.
Args:
name (str): The name to be converted.
Returns:
str: The converted name.
"""
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
return s2.lower()
class DesignerPluginGenerator:
def __init__(self, widget: type):
@@ -122,6 +109,16 @@ class DesignerPluginGenerator:
or bool(init_source.find("super().__init__(parent)") > 0)
)
# for the new style classes, we only have one super call. We can therefore check if the
# number of __init__ calls is 2 (the class itself and the super class)
num_inits = re.findall(r"__init__", init_source)
if len(num_inits) == 2 and not super_init_found:
super_init_found = bool(
init_source.find("super().__init__(parent=parent") > 0
or init_source.find("super().__init__(parent,") > 0
or init_source.find("super().__init__(parent)") > 0
)
if not cls_init_found and not super_init_found:
raise ValueError(
f"Widget class {self.widget.__name__} must call the super constructor with parent."

View File

@@ -1,6 +1,8 @@
""" Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
The class is mainly designed for usage with the BECWaveform and 1D plots. """
from __future__ import annotations
import pyqtgraph as pg
from qtpy.QtCore import QObject, Signal, Slot
from qtpy.QtGui import QColor

View File

@@ -0,0 +1,16 @@
import re
def pascal_to_snake(name: str) -> str:
"""
Convert PascalCase to snake_case.
Args:
name (str): The name to be converted.
Returns:
str: The converted name.
"""
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
return s2.lower()

View File

@@ -18,8 +18,9 @@ from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.error_popups import ErrorPopupUtility
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from bec_lib import messages
from qtpy.QtCore import QObject
else:
@@ -83,6 +84,7 @@ class RPCServer:
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
self._heartbeat_timer.start(200)
self._registry_update_callbacks = []
self._broadcasted_data = {}
self.status = messages.BECStatus.RUNNING
logger.success(f"Server started with gui_id: {self.gui_id}")
@@ -189,6 +191,9 @@ class RPCServer:
if not getattr(val, "RPC", True):
continue
data[key] = self._serialize_bec_connector(val)
if self._broadcasted_data == data:
return
self._broadcasted_data = data
logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
self.client.connector.xadd(
@@ -212,6 +217,15 @@ class RPCServer:
config_dict = connector.config.model_dump()
config_dict["parent_id"] = getattr(connector, "parent_id", None)
try:
parent = connector.parent()
if isinstance(parent, BECMainWindow):
container_proxy = parent.gui_id
else:
container_proxy = None
except Exception:
container_proxy = None
if wait:
while not self.rpc_register.object_is_registered(connector):
QApplication.processEvents()
@@ -225,6 +239,7 @@ class RPCServer:
"object_name": connector.object_name or connector.__class__.__name__,
"widget_class": widget_class,
"config": config_dict,
"container_proxy": container_proxy,
"__rpc__": True,
}

View File

@@ -0,0 +1,44 @@
from bec_lib.serialization import msgpack
from qtpy.QtCore import QPointF
def register_serializer_extension():
"""
Register the serializer extension for the BECConnector.
"""
if not module_is_registered("bec_widgets.utils.serialization"):
msgpack.register_object_hook(encode_qpointf, decode_qpointf)
def module_is_registered(module_name: str) -> bool:
"""
Check if the module is registered in the encoder.
Args:
module_name (str): The name of the module to check.
Returns:
bool: True if the module is registered, False otherwise.
"""
# pylint: disable=protected-access
for enc in msgpack._encoder:
if enc[0].__module__ == module_name:
return True
return False
def encode_qpointf(obj):
"""
Encode a QPointF object to a list of floats. As this is mostly used for sending
data to the client, it is not necessary to convert it back to a QPointF object.
"""
if isinstance(obj, QPointF):
return [obj.x(), obj.y()]
return obj
def decode_qpointf(obj):
"""
no-op function since QPointF is encoded as a list of floats.
"""
return obj

View File

@@ -59,7 +59,7 @@ class SidePanel(QWidget):
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
self.toolbar = ModularToolBar(target_widget=self, orientation="vertical")
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="vertical")
self.container = QWidget()
self.container.layout = QVBoxLayout(self.container)
@@ -89,7 +89,7 @@ class SidePanel(QWidget):
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
self.container = QWidget()
self.container.layout = QVBoxLayout(self.container)

View File

@@ -118,7 +118,7 @@ class IconAction(ToolBarAction):
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
icon = QIcon()
icon.addFile(self.icon_path, size=QSize(20, 20))
self.action = QAction(icon, self.tooltip, target)
self.action = QAction(icon=icon, text=self.tooltip, parent=target)
self.action.setCheckable(self.checkable)
toolbar.addAction(self.action)
@@ -128,7 +128,7 @@ class QtIconAction(ToolBarAction):
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
self.standard_icon = standard_icon
self.icon = QApplication.style().standardIcon(standard_icon)
self.action = QAction(self.icon, self.tooltip, parent)
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
self.action.setCheckable(self.checkable)
def add_to_toolbar(self, toolbar, target):
@@ -173,7 +173,7 @@ class MaterialIconAction(ToolBarAction):
filled=self.filled,
color=self.color,
)
self.action = QAction(self.icon, self.tooltip, parent=parent)
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
self.action.setCheckable(self.checkable)
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
@@ -212,12 +212,12 @@ class DeviceSelectionAction(ToolBarAction):
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
def add_to_toolbar(self, toolbar, target):
widget = QWidget()
widget = QWidget(parent=target)
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
if self.label is not None:
label = QLabel(f"{self.label}")
label = QLabel(text=f"{self.label}", parent=target)
layout.addWidget(label)
if self.device_combobox is not None:
layout.addWidget(self.device_combobox)
@@ -280,7 +280,9 @@ class SwitchableToolBarAction(ToolBarAction):
self.main_button.clicked.connect(self._trigger_current_action)
menu = QMenu(self.main_button)
for key, action_obj in self.actions.items():
menu_action = QAction(action_obj.get_icon(), action_obj.tooltip, self.main_button)
menu_action = QAction(
icon=action_obj.get_icon(), text=action_obj.tooltip, parent=self.main_button
)
menu_action.setIconVisibleInMenu(True)
menu_action.setCheckable(self.checkable)
menu_action.setChecked(key == self.current_key)
@@ -369,13 +371,13 @@ class WidgetAction(ToolBarAction):
toolbar (QToolBar): The toolbar to add the widget to.
target (QWidget): The target widget for the action.
"""
self.container = QWidget()
self.container = QWidget(parent=target)
layout = QHBoxLayout(self.container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
if self.label is not None:
label_widget = QLabel(f"{self.label}")
label_widget = QLabel(text=f"{self.label}", parent=target)
label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
layout.addWidget(label_widget)
@@ -437,7 +439,7 @@ class ExpandableMenuAction(ToolBarAction):
)
menu = QMenu(button)
for action_id, action in self.actions.items():
sub_action = QAction(action.tooltip, target)
sub_action = QAction(text=action.tooltip, parent=target)
sub_action.setIconVisibleInMenu(True)
if action.icon_path:
icon = QIcon()
@@ -521,7 +523,7 @@ class ModularToolBar(QToolBar):
orientation: Literal["horizontal", "vertical"] = "horizontal",
background_color: str = "rgba(0, 0, 0, 0)",
):
super().__init__(parent)
super().__init__(parent=parent)
self.widgets = defaultdict(dict)
self.background_color = background_color

View File

@@ -12,10 +12,11 @@ from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeSlot
logger = bec_logger.logger
if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from qtpy.QtWidgets import QWidget
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
@@ -130,7 +131,6 @@ class BECDock(BECWidget, Dock):
self,
parent: QWidget | None = None,
parent_dock_area: BECDockArea | None = None,
parent_id: str | None = None,
config: DockConfig | None = None,
name: str | None = None,
object_name: str | None = None,
@@ -279,6 +279,7 @@ class BECDock(BECWidget, Dock):
widgets.extend(dock.elements.keys())
return widgets
@SafeSlot(popup_error=True)
def new(
self,
widget: BECWidget | str,
@@ -301,6 +302,13 @@ class BECDock(BECWidget, Dock):
colspan(int): The number of columns the widget should span.
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
"""
if name is not None:
if not WidgetContainerUtils.has_name_valid_chars(name):
raise ValueError(
f"Name {name} contains invalid characters. "
f"Only alphanumeric characters and underscores are allowed."
)
if row is None:
row = self.layout.rowCount()
@@ -316,11 +324,7 @@ class BECDock(BECWidget, Dock):
widget = cast(
BECWidget,
widget_handler.create_widget(
widget_type=widget,
object_name=name,
parent_dock=self,
parent_id=self.gui_id,
parent=self,
widget_type=widget, object_name=name, parent_dock=self, parent=self
),
)
else:
@@ -416,6 +420,7 @@ class BECDock(BECWidget, Dock):
self.delete_all()
self.widgets.clear()
super().cleanup()
self.deleteLater()
def close(self):
"""

View File

@@ -14,6 +14,7 @@ from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.name_utils import pascal_to_snake
from bec_widgets.utils.toolbar import (
ExpandableMenuAction,
MaterialIconAction,
@@ -101,8 +102,8 @@ class BECDockArea(BECWidget, QWidget):
self._instructions_visible = True
self.dark_mode_button = DarkModeButton(parent=self, parent_id=self.gui_id, toolbar=True)
self.dock_area = DockArea()
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
self.dock_area = DockArea(parent=self)
self.toolbar = ModularToolBar(
parent=self,
actions={
@@ -181,7 +182,7 @@ class BECDockArea(BECWidget, QWidget):
self.layout.addWidget(self.toolbar)
self.layout.addWidget(self.dock_area)
self.spacer = QWidget()
self.spacer = QWidget(parent=self)
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar.addWidget(self.spacer)
self.toolbar.addWidget(self.dark_mode_button)
@@ -242,7 +243,8 @@ class BECDockArea(BECWidget, QWidget):
def _create_widget_from_toolbar(self, widget_name: str) -> None:
# Run with RPC broadcast to namespace of all widgets
with RPCRegister.delayed_broadcast():
dock_name = WidgetContainerUtils.generate_unique_name(widget_name, self.panels.keys())
name = pascal_to_snake(widget_name)
dock_name = WidgetContainerUtils.generate_unique_name(name, self.panels.keys())
self.new(name=dock_name, widget=widget_name)
def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions
@@ -306,6 +308,8 @@ class BECDockArea(BECWidget, QWidget):
"""
if state is None:
state = self.config.docks_state
if state is None:
return
self.dock_area.restoreState(state, missing=missing, extra=extra)
@SafeSlot()
@@ -362,6 +366,11 @@ class BECDockArea(BECWidget, QWidget):
f"Name {name} must be unique for docks, but already exists in DockArea "
f"with name: {self.object_name} and id {self.gui_id}."
)
if not WidgetContainerUtils.has_name_valid_chars(name):
raise ValueError(
f"Name {name} contains invalid characters. "
f"Only alphanumeric characters and underscores are allowed."
)
else: # Name is not provided
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
@@ -370,7 +379,6 @@ class BECDockArea(BECWidget, QWidget):
name=name, # this is dock name pyqtgraph property, this is displayed on label
object_name=name, # this is a real qt object name passed to BECConnector
parent_dock_area=self,
parent_id=self.gui_id,
closable=closable,
)
dock.config.position = position
@@ -440,12 +448,8 @@ class BECDockArea(BECWidget, QWidget):
Cleanup the dock area.
"""
self.delete_all()
self.toolbar.close()
self.toolbar.deleteLater()
self.dark_mode_button.close()
self.dark_mode_button.deleteLater()
self.dock_area.close()
self.dock_area.deleteLater()
super().cleanup()
def show(self):

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import math
import sys
from typing import Dict, Literal, Optional, Set, Tuple, Union

View File

@@ -11,7 +11,7 @@ class AbortButton(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "cancel"
RPC = False
RPC = True
def __init__(
self,

View File

@@ -11,7 +11,7 @@ class ResetButton(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "restart_alt"
RPC = False
RPC = True
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)

View File

@@ -11,7 +11,7 @@ class ResumeButton(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "resume"
RPC = False
RPC = True
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)

View File

@@ -11,7 +11,7 @@ class StopButton(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "dangerous"
RPC = False
RPC = True
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
@@ -54,9 +54,20 @@ class StopButton(BECWidget, QWidget):
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
app = QApplication(sys.argv)
w = StopButton()
w.show()
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout())
# Create and add the StopButton to the layout
self.stop_button = StopButton()
self.layout().addWidget(self.stop_button)
# Example of how this custom GUI might be used:
app = QApplication([])
my_gui = MyGui()
my_gui.show()
sys.exit(app.exec_())

View File

@@ -12,7 +12,16 @@ class PositionIndicator(BECWidget, QWidget):
Display a position within a defined range, e.g. motor limits.
"""
USER_ACCESS = ["set_value", "set_range", "vertical", "indicator_width", "rounded_corners"]
USER_ACCESS = [
"set_value",
"set_range",
"vertical",
"vertical.setter",
"indicator_width",
"indicator_width.setter",
"rounded_corners",
"rounded_corners.setter",
]
PLUGIN = True
ICON_NAME = "horizontal_distribute"
@@ -209,6 +218,12 @@ class PositionIndicator(BECWidget, QWidget):
@Slot(int)
@Slot(float)
def set_value(self, position: float):
"""
Set the position of the indicator
Args:
position: The new position of the indicator
"""
self.position = position
self.update()

View File

@@ -31,6 +31,7 @@ class PositionerBox(PositionerBoxBase):
dimensions = (234, 224)
PLUGIN = True
RPC = True
USER_ACCESS = ["set_positioner"]
device_changed = Signal(str, str)

View File

@@ -33,6 +33,7 @@ class PositionerBox2D(PositionerBoxBase):
ui_file = "positioner_box_2d.ui"
PLUGIN = True
RPC = True
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
device_changed_hor = Signal(str, str)

View File

@@ -82,7 +82,7 @@ class DeviceInputBase(BECWidget):
ReadoutPriority.ON_REQUEST: "readout_on_request",
}
def __init__(self, client=None, config=None, gui_id: str | None = None, **kwargs):
def __init__(self, parent=None, client=None, config=None, gui_id: str | None = None, **kwargs):
if config is None:
config = DeviceInputConfig(widget_class=self.__class__.__name__)
@@ -90,7 +90,9 @@ class DeviceInputBase(BECWidget):
if isinstance(config, dict):
config = DeviceInputConfig(**config)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs)
super().__init__(
parent=parent, client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs
)
self.get_bec_shortcuts()
self._device_filter = []
self._readout_filter = []

View File

@@ -64,7 +64,6 @@ class ScanControl(BECWidget, QWidget):
default_scan: str | None = None,
**kwargs,
):
if config is None:
config = ScanControlConfig(
widget_class=self.__class__.__name__, allowed_scans=allowed_scans
@@ -166,7 +165,7 @@ class ScanControl(BECWidget, QWidget):
self.layout.addStretch()
def _add_metadata_form(self):
self._metadata_form = ScanMetadata()
self._metadata_form = ScanMetadata(parent=self)
self.layout.addWidget(self._metadata_form)
self._metadata_form.update_with_new_scan(self.comboBox_scan_selection.currentText())
self.scan_selected.connect(self._metadata_form.update_with_new_scan)

View File

@@ -234,7 +234,7 @@ class ScanGroupBox(QGroupBox):
continue
if default == "_empty":
default = None
widget = widget_class(arg_name=arg_name, default=default)
widget = widget_class(parent=self.parent(), arg_name=arg_name, default=default)
if isinstance(widget, DeviceLineEdit):
widget.set_device_filter(BECDeviceFilter.DEVICE)
self.selected_devices[widget] = ""
@@ -274,12 +274,24 @@ class ScanGroupBox(QGroupBox):
for widget in self.widgets[-len(self.inputs) :]:
if isinstance(widget, DeviceLineEdit):
self.selected_devices[widget] = ""
widget.close()
widget.deleteLater()
self.widgets = self.widgets[: -len(self.inputs)]
selected_devices_str = " ".join(self.selected_devices.values())
self.device_selected.emit(selected_devices_str.strip())
def remove_all_widget_bundles(self):
"""Remove every widget bundle from the scan control layout."""
for widget in list(self.widgets):
if isinstance(widget, DeviceLineEdit):
self.selected_devices.pop(widget, None)
widget.close()
widget.deleteLater()
self.layout.removeWidget(widget)
self.widgets.clear()
self.device_selected.emit("")
@Property(bool)
def hide_add_remove_buttons(self):
return self._hide_add_remove_buttons
@@ -348,10 +360,21 @@ class ScanGroupBox(QGroupBox):
self._set_kwarg_parameters(parameters)
def _set_arg_parameters(self, parameters: list):
while len(parameters) != len(self.widgets):
self.add_widget_bundle()
for i, parameter in enumerate(parameters):
WidgetIO.set_value(self.widgets[i], parameter)
self.remove_all_widget_bundles()
if not parameters:
return
inputs_per_bundle = len(self.inputs)
if inputs_per_bundle == 0:
return
bundles_needed = -(-len(parameters) // inputs_per_bundle)
for row in range(1, bundles_needed + 1):
self.add_input_widgets(self.inputs, row)
for i, value in enumerate(parameters):
WidgetIO.set_value(self.widgets[i], value)
def _set_kwarg_parameters(self, parameters: dict):
for widget in self.widgets:

View File

@@ -16,12 +16,20 @@ from qtpy.QtWidgets import (
from bec_widgets.utils.error_popups import SafeSlot
class AdditionalMetadataTableModel(QAbstractTableModel):
class DictBackedTableModel(QAbstractTableModel):
def __init__(self, data):
"""A model to go with DictBackedTable, which represents key-value pairs
to be displayed in a TreeWidget.
Args:
data (list[list[str]]): list of key-value pairs to initialise with"""
super().__init__()
self._data: list[list[str]] = data
self._disallowed_keys: list[str] = []
# pylint: disable=missing-function-docstring
# see QAbstractTableModel documentation for these methods
def headerData(
self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole()
) -> Any:
@@ -49,6 +57,10 @@ class AdditionalMetadataTableModel(QAbstractTableModel):
return False
def update_disallowed_keys(self, keys: list[str]):
"""Set the list of keys which may not be used.
Args:
keys (list[str]): list of keys which are forbidden."""
self._disallowed_keys = keys
for i, item in enumerate(self._data):
if item[0] in self._disallowed_keys:
@@ -95,16 +107,21 @@ class AdditionalMetadataTableModel(QAbstractTableModel):
return dict(self._data)
class AdditionalMetadataTable(QWidget):
class DictBackedTable(QWidget):
delete_rows = Signal(list)
def __init__(self, initial_data: list[list[str]]):
"""Widget which uses a DictBackedTableModel to display an editable table
which can be extracted as a dict.
Args:
initial_data (list[list[str]]): list of key-value pairs to initialise with
"""
super().__init__()
self._layout = QHBoxLayout()
self.setLayout(self._layout)
self._table_model = AdditionalMetadataTableModel(initial_data)
self._table_model = DictBackedTableModel(initial_data)
self._table_view = QTreeView()
self._table_view.setModel(self._table_model)
self._table_view.setSizePolicy(
@@ -126,15 +143,21 @@ class AdditionalMetadataTable(QWidget):
self.delete_rows.connect(self._table_model.delete_rows)
def delete_selected_rows(self):
"""Delete rows which are part of the selection model"""
cells: list[QModelIndex] = self._table_view.selectionModel().selectedIndexes()
row_indices = list({r.row() for r in cells})
if row_indices:
self.delete_rows.emit(row_indices)
def dump_dict(self):
"""Get the current content of the table as a dict"""
return self._table_model.dump_dict()
def update_disallowed_keys(self, keys: list[str]):
"""Set the list of keys which may not be used.
Args:
keys (list[str]): list of keys which are forbidden."""
self._table_model.update_disallowed_keys(keys)
@@ -144,6 +167,6 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication([])
set_theme("dark")
window = AdditionalMetadataTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
window = DictBackedTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
window.show()
app.exec()

View File

@@ -1,7 +0,0 @@
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
AdditionalMetadataTable,
AdditionalMetadataTableModel,
)
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
__all__ = ["ScanMetadata", "AdditionalMetadataTable", "AdditionalMetadataTableModel"]

View File

@@ -9,7 +9,7 @@ from annotated_types import Ge, Gt, Le, Lt
from bec_lib.logger import bec_logger
from pydantic_core import PydanticUndefined
if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from pydantic.fields import FieldInfo
logger = bec_logger.logger

View File

@@ -1,52 +1,21 @@
from __future__ import annotations
from decimal import Decimal
from types import NoneType
from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from bec_lib.metadata_schema import get_metadata_schema_for_scan
from bec_qthemes import material_icon
from pydantic import Field, ValidationError
from qtpy.QtCore import Signal # type: ignore
from qtpy.QtWidgets import (
QApplication,
QComboBox,
QGridLayout,
QHBoxLayout,
QLabel,
QLayout,
QVBoxLayout,
QWidget,
)
from pydantic import Field
from qtpy.QtWidgets import QApplication, QComboBox, QHBoxLayout, QVBoxLayout, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.compact_popup import CompactPopupWidget
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import widget_from_type
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
AdditionalMetadataTable,
)
if TYPE_CHECKING:
from pydantic.fields import FieldInfo
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
logger = bec_logger.logger
class ScanMetadata(BECWidget, QWidget):
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
metadata schema registry supplied in the plugin repo to find pydantic models
associated with the scan type. Sets limits for numerical values if specified."""
PLUGIN = True
ICON_NAME = "list_alt"
metadata_updated = Signal(dict)
metadata_cleared = Signal(NoneType)
RPC = False
class ScanMetadata(PydanticModelForm):
def __init__(
self,
parent=None,
@@ -55,117 +24,35 @@ class ScanMetadata(BECWidget, QWidget):
initial_extras: list[list[str]] | None = None,
**kwargs,
):
super().__init__(parent=parent, client=client, **kwargs)
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
metadata schema registry supplied in the plugin repo to find pydantic models
associated with the scan type. Sets limits for numerical values if specified.
self.set_schema(scan_name)
self._layout = QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout)
self._required_md_box = ExpandableGroupFrame("Scan schema metadata")
self._layout.addWidget(self._required_md_box)
self._required_md_box_layout = QHBoxLayout()
self._required_md_box.set_layout(self._required_md_box_layout)
self._md_grid = QWidget()
self._required_md_box_layout.addWidget(self._md_grid)
self._grid_container = QVBoxLayout()
self._md_grid.setLayout(self._grid_container)
self._new_grid_layout()
self._grid_container.addLayout(self._md_grid_layout)
Args:
scan_name (str): The scan for which to generate a metadata form
Initial_extras (list[list[str]]): Initial data with which to populate the additional
metadata table - inner lists should be key-value pairs
"""
# self.populate() gets called in super().__init__
# so make sure self._additional_metadata exists
self._additional_md_box = ExpandableGroupFrame("Additional metadata", expanded=False)
self._layout.addWidget(self._additional_md_box)
self._additional_md_box_layout = QHBoxLayout()
self._additional_md_box.set_layout(self._additional_md_box_layout)
self._additional_metadata = AdditionalMetadataTable(initial_extras or [])
self._additional_md_box_layout.addWidget(self._additional_metadata)
self._validity = CompactPopupWidget()
self._validity.compact_view = True # type: ignore
self._validity.label = "Metadata validity" # type: ignore
self._validity.compact_show_popup.setIcon(
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
)
self._validity_message = QLabel("Not yet validated")
self._validity.addWidget(self._validity_message)
self._layout.addWidget(self._validity)
self.populate()
@SafeSlot(str)
def update_with_new_scan(self, scan_name: str):
self.set_schema(scan_name)
self.populate()
self.validate_form()
def validate_form(self, *_) -> bool:
"""validate the currently entered metadata against the pydantic schema.
If successful, returns on metadata_emitted and returns true.
Otherwise, emits on metadata_cleared and returns false."""
try:
metadata_dict = self.get_full_model_dict()
self._md_schema.model_validate(metadata_dict)
self._validity.set_global_state("success")
self._validity_message.setText("No errors!")
self.metadata_updated.emit(metadata_dict)
except ValidationError as e:
self._validity.set_global_state("emergency")
self._validity_message.setText(str(e))
self.metadata_cleared.emit(None)
def get_full_model_dict(self):
"""Get the entered metadata as a dict"""
return self._additional_metadata.dump_dict() | self._dict_from_grid()
def set_schema(self, scan_name: str | None = None):
self._additional_metadata = DictBackedTable(initial_extras or [])
self._scan_name = scan_name or ""
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
def populate(self):
self._clear_grid()
self._populate()
super().__init__(parent=parent, metadata_model=self._md_schema, client=client, **kwargs)
def _populate(self):
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
for i, (field_name, info) in enumerate(self._md_schema.model_fields.items()):
self._add_griditem(field_name, info, i)
self._layout.addWidget(self._additional_md_box)
self._additional_md_box_layout.addWidget(self._additional_metadata)
def _add_griditem(self, field_name: str, info: FieldInfo, row: int):
grid = self._md_grid_layout
label = QLabel(info.title or field_name)
label.setProperty("_model_field_name", field_name)
label.setToolTip(info.description or field_name)
grid.addWidget(label, row, 0)
widget = widget_from_type(info.annotation)(info)
widget.valueChanged.connect(self.validate_form)
grid.addWidget(widget, row, 1)
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
grid = self._md_grid_layout
return {
grid.itemAtPosition(i, 0).widget().property("_model_field_name"): grid.itemAtPosition(i, 1).widget().getValue() # type: ignore # we only add 'MetadataWidget's here
for i in range(grid.rowCount())
}
def _clear_grid(self):
while self._md_grid_layout.count():
item = self._md_grid_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
self._md_grid_layout.deleteLater()
self._new_grid_layout()
self._grid_container.addLayout(self._md_grid_layout)
self._md_grid.adjustSize()
self.adjustSize()
def _new_grid_layout(self):
self._md_grid_layout = QGridLayout()
self._md_grid_layout.setContentsMargins(0, 0, 0, 0)
self._md_grid_layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
@SafeSlot(str)
def update_with_new_scan(self, scan_name: str):
self.set_schema_from_scan(scan_name)
self.validate_form()
@SafeProperty(bool)
def hide_optional_metadata(self): # type: ignore
@@ -181,8 +68,25 @@ class ScanMetadata(BECWidget, QWidget):
"""
self._additional_md_box.setVisible(not hide)
def get_form_data(self):
"""Get the entered metadata as a dict"""
return self._additional_metadata.dump_dict() | self._dict_from_grid()
def populate(self):
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
super().populate()
def set_schema_from_scan(self, scan_name: str | None):
self._scan_name = scan_name or ""
self.set_schema(get_metadata_schema_for_scan(self._scan_name))
self.populate()
if __name__ == "__main__": # pragma: no cover
# pylint: disable=redefined-outer-name
# pylint: disable=protected-access
# pylint: disable=disallowed-name
from unittest.mock import patch
from bec_lib.metadata_schema import BasicScanMetadata
@@ -213,7 +117,6 @@ if __name__ == "__main__": # pragma: no cover
"bec_lib.metadata_schema._get_metadata_schema_registry",
lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3},
):
app = QApplication([])
w = QWidget()
selection = QComboBox()

View File

@@ -117,6 +117,7 @@ class WebsiteWidget(BECWidget, QWidget):
Cleanup the widget
"""
self.website.page().deleteLater()
super().cleanup()
if __name__ == "__main__":

View File

@@ -144,7 +144,7 @@ class Minesweeper(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "videogame_asset"
USER_ACCESS = []
RPC = False
RPC = True
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent=parent, *args, **kwargs)

View File

@@ -27,7 +27,7 @@ logger = bec_logger.logger
# noinspection PyDataclass
class ImageConfig(ConnectionConfig):
color_map: str = Field(
"magma", description="The colormap of the figure widget.", validate_default=True
"plasma", description="The colormap of the figure widget.", validate_default=True
)
color_bar: Literal["full", "simple"] | None = Field(
None, description="The type of the color bar."
@@ -79,12 +79,6 @@ class Image(PlotBase):
"auto_range_x.setter",
"auto_range_y",
"auto_range_y.setter",
"x_log",
"x_log.setter",
"y_log",
"y_log.setter",
"legend_label_size",
"legend_label_size.setter",
# ImageView Specific Settings
"color_map",
"color_map.setter",
@@ -111,8 +105,8 @@ class Image(PlotBase):
"fft.setter",
"log",
"log.setter",
"rotation",
"rotation.setter",
"num_rotation_90",
"num_rotation_90.setter",
"transpose",
"transpose.setter",
"image",
@@ -137,13 +131,13 @@ class Image(PlotBase):
super().__init__(
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
)
self._main_image = ImageItem(parent_image=self, parent_id=self.gui_id)
self._main_image = ImageItem(parent_image=self)
self.plot_item.addItem(self._main_image)
self.scan_id = None
# Default Color map to magma
self.color_map = "magma"
# Default Color map to plasma
self.color_map = "plasma"
################################################################################
# Widget Specific GUI interactions
@@ -656,21 +650,21 @@ class Image(PlotBase):
self._main_image.log = enable
@SafeProperty(int)
def rotation(self) -> int:
def num_rotation_90(self) -> int:
"""
The number of 90° rotations to apply.
The number of 90° rotations to apply counterclockwise.
"""
return self._main_image.rotation
return self._main_image.num_rotation_90
@rotation.setter
def rotation(self, value: int):
@num_rotation_90.setter
def num_rotation_90(self, value: int):
"""
Set the number of 90° rotations to apply.
Set the number of 90° rotations to apply counterclockwise.
Args:
value(int): The number of 90° rotations to apply.
"""
self._main_image.rotation = value
self._main_image.num_rotation_90 = value
@SafeProperty(bool)
def transpose(self) -> bool:
@@ -762,6 +756,19 @@ class Image(PlotBase):
self.selection_bundle.dim_combo_box,
):
combo.blockSignals(False)
else:
for combo in (
self.selection_bundle.device_combo_box,
self.selection_bundle.dim_combo_box,
):
combo.blockSignals(True)
self.selection_bundle.device_combo_box.setCurrentText("")
self.selection_bundle.dim_combo_box.setCurrentText("auto")
for combo in (
self.selection_bundle.device_combo_box,
self.selection_bundle.dim_combo_box,
):
combo.blockSignals(False)
################################################################################
# Image Update Methods
@@ -812,6 +819,7 @@ class Image(PlotBase):
self.on_image_update_2d, MessageEndpoints.device_monitor_2d(monitor)
)
self._main_image.config.monitor = None
self._sync_device_selection()
########################################
# 1D updates

View File

@@ -24,7 +24,7 @@ class ImageItemConfig(ConnectionConfig): # TODO review config
monitor: str | None = Field(None, description="The name of the monitor.")
monitor_type: Literal["1d", "2d", "auto"] = Field("auto", description="The type of monitor.")
source: str | None = Field(None, description="The source of the curve.")
color_map: str | None = Field("magma", description="The color map of the image.")
color_map: str | None = Field("plasma", description="The color map of the image.")
downsample: bool | None = Field(True, description="Whether to downsample the image.")
opacity: float | None = Field(1.0, description="The opacity of the image.")
v_range: tuple[float | int, float | int] | None = Field(
@@ -86,7 +86,6 @@ class ImageItem(BECConnector, pg.ImageItem):
self.set_parent(parent_image)
else:
self.parent_image = None
self.parent_id = None
super().__init__(config=config, gui_id=gui_id, **kwargs)
self.raw_data = None
@@ -98,7 +97,6 @@ class ImageItem(BECConnector, pg.ImageItem):
def set_parent(self, parent: BECConnector):
self.parent_image = parent
self.parent_id = parent.gui_id
def parent(self):
return self.parent_image
@@ -241,13 +239,13 @@ class ImageItem(BECConnector, pg.ImageItem):
self._process_image()
@property
def rotation(self) -> Optional[int]:
def num_rotation_90(self) -> Optional[int]:
"""Get or set the number of 90° rotations to apply."""
return self.config.processing.rotation
return self.config.processing.num_rotation_90
@rotation.setter
def rotation(self, value: Optional[int]):
self.config.processing.rotation = value
@num_rotation_90.setter
def num_rotation_90(self, value: Optional[int]):
self.config.processing.num_rotation_90 = value
self._process_image()
@property
@@ -275,3 +273,7 @@ class ImageItem(BECConnector, pg.ImageItem):
self.raw_data = None
self.buffer = []
self.max_len = 0
def remove(self):
self.parent().disconnect_monitor(self.config.monitor)
self.clear()

View File

@@ -37,7 +37,7 @@ class ProcessingConfig(BaseModel):
transpose: bool = Field(
False, description="Whether to transpose the monitor data before displaying."
)
rotation: int = Field(
num_rotation_90: int = Field(
0, description="The rotation angle of the monitor data before displaying."
)
stats: ImageStats = Field(
@@ -140,8 +140,8 @@ class ImageProcessor(QObject):
"""Core processing logic without threading overhead."""
if self.config.fft:
data = self.FFT(data)
if self.config.rotation is not None:
data = self.rotation(data, self.config.rotation)
if self.config.num_rotation_90 is not None:
data = self.rotation(data, self.config.num_rotation_90)
if self.config.transpose:
data = self.transpose(data)
if self.config.log:

View File

@@ -40,7 +40,7 @@ class MonitorSelectionToolbarBundle(ToolbarBundle):
self.add_action("monitor", WidgetAction(widget=self.device_combo_box, adjust_size=True))
# 2) Dimension combo box
self.dim_combo_box = QComboBox()
self.dim_combo_box = QComboBox(parent=self.target_widget)
self.dim_combo_box.addItems(["auto", "1d", "2d"])
self.dim_combo_box.setCurrentText("auto")
self.dim_combo_box.setToolTip("Monitor Dimension")

View File

@@ -55,24 +55,24 @@ class ImageProcessingToolbarBundle(ToolbarBundle):
@SafeSlot()
def rotate_right(self):
if self.target_widget.rotation is None:
if self.target_widget.num_rotation_90 is None:
return
rotation = (self.target_widget.rotation - 1) % 4
self.target_widget.rotation = rotation
rotation = (self.target_widget.num_rotation_90 - 1) % 4
self.target_widget.num_rotation_90 = rotation
@SafeSlot()
def rotate_left(self):
if self.target_widget.rotation is None:
if self.target_widget.num_rotation_90 is None:
return
rotation = (self.target_widget.rotation + 1) % 4
self.target_widget.rotation = rotation
rotation = (self.target_widget.num_rotation_90 + 1) % 4
self.target_widget.num_rotation_90 = rotation
@SafeSlot()
def reset_settings(self):
self.target_widget.fft = False
self.target_widget.log = False
self.target_widget.transpose = False
self.target_widget.rotation = 0
self.target_widget.num_rotation_90 = 0
self.fft.action.setChecked(False)
self.log.action.setChecked(False)

View File

@@ -25,7 +25,7 @@ logger = bec_logger.logger
class MultiWaveformConfig(ConnectionConfig):
color_palette: str | None = Field(
"magma", description="The color palette of the figure widget.", validate_default=True
"plasma", description="The color palette of the figure widget.", validate_default=True
)
curve_limit: int | None = Field(
200, description="The maximum number of curves to display on the plot."
@@ -308,7 +308,7 @@ class MultiWaveform(PlotBase):
################################################################################
@SafeSlot(popup_error=True)
def plot(self, monitor: str, color_palette: str | None = "magma"):
def plot(self, monitor: str, color_palette: str | None = "plasma"):
"""
Create a plot for the given monitor.
Args:

View File

@@ -29,9 +29,7 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
# Monitor Selection
self.monitor = DeviceComboBox(
device_filter=BECDeviceFilter.DEVICE,
readout_priority_filter=ReadoutPriority.ASYNC,
parent_id=self.target_widget.gui_id,
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=ReadoutPriority.ASYNC
)
self.monitor.addItem("", None)
self.monitor.setCurrentText("")
@@ -40,7 +38,7 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
self.add_action("monitor", WidgetAction(widget=self.monitor, adjust_size=False))
# Colormap Selection
self.colormap_widget = BECColorMapWidget(cmap="magma", parent_id=self.target_widget.gui_id)
self.colormap_widget = BECColorMapWidget(cmap="plasma")
self.add_action("color_map", WidgetAction(widget=self.colormap_widget, adjust_size=False))
# Connect slots, a device will be connected upon change of any combobox

View File

@@ -98,10 +98,11 @@ class PlotBase(BECWidget, QWidget):
self._ui_mode = UIMode.POPUP if popups else UIMode.SIDE
self.axis_settings_dialog = None
self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
self.plot_widget.ci.setContentsMargins(0, 0, 0, 0)
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
self.plot_widget.addItem(self.plot_item)
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
self._init_toolbar()
# PlotItem Addons
@@ -795,6 +796,7 @@ class PlotBase(BECWidget, QWidget):
"""
self.plot_item.showAxis("top", value)
self.plot_item.showAxis("right", value)
self.property_changed.emit("outer_axes", value)
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
@@ -814,6 +816,7 @@ class PlotBase(BECWidget, QWidget):
"""
self.plot_item.showAxis("bottom", value)
self.plot_item.showAxis("left", value)
self._apply_x_label()
self._apply_y_label()
self.property_changed.emit("inner_axes", value)

View File

@@ -38,7 +38,7 @@ class ScatterCurveConfig(ConnectionConfig):
"solid", description="The style of the pen of the curve."
)
color_map: str | None = Field(
"magma", description="The color palette of the figure widget.", validate_default=True
"plasma", description="The color palette of the figure widget.", validate_default=True
)
x_device: ScatterDeviceSignal | None = Field(
None, description="The x device signal of the scatter waveform."
@@ -78,8 +78,8 @@ class ScatterCurve(BECConnector, pg.PlotDataItem):
self.config = config
name = config.label
self.parent_item = parent_item
self.parent_id = self.parent_item.gui_id
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
object_name = name.replace("-", "_").replace(" ", "_") if name else None
super().__init__(name=name, object_name=object_name, config=config, gui_id=gui_id, **kwargs)
self.data_z = None # color scaling needs to be cashed for changing colormap
self.apply_config()

View File

@@ -30,7 +30,7 @@ logger = bec_logger.logger
# noinspection PyDataclass
class ScatterWaveformConfig(ConnectionConfig):
color_map: str | None = Field(
"magma",
"plasma",
description="The color map of the z scaling of scatter waveform.",
validate_default=True,
)
@@ -266,7 +266,7 @@ class ScatterWaveform(PlotBase):
x_entry: None | str = None,
y_entry: None | str = None,
z_entry: None | str = None,
color_map: str | None = "magma",
color_map: str | None = "plasma",
label: str | None = None,
validate_bec: bool = True,
) -> ScatterCurve:
@@ -333,12 +333,13 @@ class ScatterWaveform(PlotBase):
# To have only one main curve
if self._main_curve is not None:
self.rpc_register.remove_rpc(self._main_curve)
self.rpc_register.broadcast()
self.plot_item.removeItem(self._main_curve)
self._main_curve.deleteLater()
self._main_curve = None
self._main_curve = ScatterCurve(
parent_item=self, config=config, gui_id=self.gui_id, name=config.label
)
self._main_curve = ScatterCurve(parent_item=self, config=config, name=config.label)
self.plot_item.addItem(self._main_curve)
self.sync_signal_update.emit()

View File

@@ -92,8 +92,7 @@ class Curve(BECConnector, pg.PlotDataItem):
else:
self.config = config
self.parent_item = parent_item
self.parent_id = self.parent_item.gui_id
object_name = name.replace("-", "_") if name else None
object_name = name.replace("-", "_").replace(" ", "_") if name else None
super().__init__(name=name, object_name=object_name, config=config, gui_id=gui_id, **kwargs)
self.apply_config()

View File

@@ -373,7 +373,7 @@ class CurveTree(BECWidget, QWidget):
if self.waveform and hasattr(self.waveform, "color_palette"):
self.color_palette = self.waveform.color_palette
else:
self.color_palette = "magma"
self.color_palette = "plasma"
self.get_bec_shortcuts()
@@ -386,7 +386,7 @@ class CurveTree(BECWidget, QWidget):
def _init_toolbar(self):
"""Initialize the toolbar with actions: add, send, refresh, expand, collapse, renormalize."""
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
add = MaterialIconAction(
icon_name="add", tooltip="Add new curve", checkable=False, parent=self
)
@@ -413,7 +413,7 @@ class CurveTree(BECWidget, QWidget):
self.toolbar.add_action("renormalize_colors", renorm_action, self)
renorm_action.action.triggered.connect(lambda checked: self.renormalize_colors())
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "magma")
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "plasma")
self.toolbar.addWidget(self.colormap_widget)
self.colormap_widget.colormap_changed_signal.connect(self.handle_colormap_changed)

View File

@@ -15,6 +15,7 @@ from qtpy.QtWidgets import QApplication, QDialog, QHBoxLayout, QMainWindow, QVBo
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
from bec_widgets.utils.colors import Colors, set_theme
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.settings_dialog import SettingsDialog
from bec_widgets.utils.toolbar import MaterialIconAction
@@ -30,7 +31,7 @@ logger = bec_logger.logger
# noinspection PyDataclass
class WaveformConfig(ConnectionConfig):
color_palette: str | None = Field(
"magma", description="The color palette of the figure widget.", validate_default=True
"plasma", description="The color palette of the figure widget.", validate_default=True
)
model_config: dict = {"validate_assignment": True}
@@ -89,6 +90,8 @@ class Waveform(PlotBase):
"curves",
"x_mode",
"x_mode.setter",
"x_entry",
"x_entry.setter",
"color_palette",
"color_palette.setter",
"plot",
@@ -644,7 +647,9 @@ class Waveform(PlotBase):
# Decide label if not provided
if label is None:
if source == "custom":
label = f"Curve {len(self.curves) + 1}"
label = WidgetContainerUtils.generate_unique_name(
"Curve", [c.object_name for c in self.curves]
)
else:
label = f"{y_name}-{y_entry}"
@@ -769,7 +774,9 @@ class Waveform(PlotBase):
label = config.label
if not label:
# Fallback label
label = f"Curve {len(self.curves) + 1}"
label = WidgetContainerUtils.generate_unique_name(
"Curve", [c.object_name for c in self.curves]
)
config.label = label
# Check for duplicates
@@ -1748,12 +1755,10 @@ class Waveform(PlotBase):
self.proxy_dap_request.cleanup()
self.clear_all()
if self.curve_settings_dialog is not None:
self.curve_settings_dialog.close()
self.curve_settings_dialog.deleteLater()
self.curve_settings_dialog.reject()
self.curve_settings_dialog = None
if self.dap_summary_dialog is not None:
self.dap_summary_dialog.close()
self.dap_summary_dialog.deleteLater()
self.dap_summary_dialog.reject()
self.dap_summary_dialog = None
super().cleanup()

View File

@@ -96,7 +96,6 @@ class Ring(BECConnector, QObject):
def __init__(
self,
parent=None,
parent_progress_widget=None,
config: RingConfig | dict | None = None,
client=None,
gui_id: Optional[str] = None,
@@ -111,7 +110,8 @@ class Ring(BECConnector, QObject):
self.config = config
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
self.parent_progress_widget = parent_progress_widget
self.parent_progress_widget = parent
self.color = None
self.background_color = None
self.start_position = None
@@ -244,10 +244,10 @@ class Ring(BECConnector, QObject):
"""
if self.config.connections.endpoint == endpoint and self.config.connections.slot == slot:
return
self.bec_dispatcher.disconnect_slot(
self.config.connections.slot, self.config.connections.endpoint
)
if self.config.connections.slot is not None:
self.bec_dispatcher.disconnect_slot(
getattr(self, self.config.connections.slot), self.config.connections.endpoint
)
self.config.connections = ProgressbarConnections(slot=slot, endpoint=endpoint)
self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint)

View File

@@ -20,7 +20,7 @@ logger = bec_logger.logger
class RingProgressBarConfig(ConnectionConfig):
color_map: Optional[str] = Field(
"magma", description="Color scheme for the progress bars.", validate_default=True
"plasma", description="Color scheme for the progress bars.", validate_default=True
)
min_number_of_bars: int | None = Field(
1, description="Minimum number of progress bars to display."
@@ -123,7 +123,7 @@ class RingProgressBar(BECWidget, QWidget):
# For updating bar behaviour
self._auto_updates = True
self._rings = None
self._rings = []
if num_bars is not None:
self.config.num_bars = max(
@@ -134,11 +134,12 @@ class RingProgressBar(BECWidget, QWidget):
self.enable_auto_updates(self.config.auto_updates)
@property
def rings(self):
def rings(self) -> list[Ring]:
"""Returns a list of all rings in the progress bar."""
return self._rings
@rings.setter
def rings(self, value):
def rings(self, value: list[Ring]):
self._rings = value
def update_config(self, config: RingProgressBarConfig | dict):
@@ -169,15 +170,15 @@ class RingProgressBar(BECWidget, QWidget):
)
for i in range(self.config.num_bars)
]
self._rings = [
Ring(parent_progress_widget=self, config=config) for config in self.config.rings
]
self._rings = [Ring(parent=self, config=config) for config in self.config.rings]
if self.config.color_map:
self.set_colors_from_map(self.config.color_map)
min_size = self._calculate_minimum_size()
self.setMinimumSize(min_size)
# Set outer ring to listen to scan progress
self.rings[0].set_update(mode="scan")
self.update()
def add_ring(self, **kwargs) -> Ring:
@@ -199,7 +200,7 @@ class RingProgressBar(BECWidget, QWidget):
directions=-1,
**kwargs,
)
ring = Ring(parent_progress_widget=self, config=ring_config)
ring = Ring(parent=self, config=ring_config)
self.config.num_bars += 1
self._rings.append(ring)
self.config.rings.append(ring.config)
@@ -218,6 +219,10 @@ class RingProgressBar(BECWidget, QWidget):
index(int): Index of the progress bar to remove.
"""
ring = self._find_ring_by_index(index)
self._cleanup_ring(ring)
self.update()
def _cleanup_ring(self, ring: Ring) -> None:
ring.reset_connection()
self._rings.remove(ring)
self.config.rings.remove(ring.config)
@@ -225,8 +230,10 @@ class RingProgressBar(BECWidget, QWidget):
self._reindex_rings()
if self.config.color_map:
self.set_colors_from_map(self.config.color_map)
del ring
self.update()
# Remove ring from rpc, afterwards call close event.
ring.rpc_register.remove_rpc(ring)
ring.deleteLater()
# del ring
def _reindex_rings(self):
"""
@@ -292,7 +299,7 @@ class RingProgressBar(BECWidget, QWidget):
widget_class="Ring", index=i, start_positions=90 * 16, directions=-1
)
self.config.rings.append(new_ring_config)
new_ring = Ring(parent_progress_widget=self, config=new_ring_config)
new_ring = Ring(parent=self, config=new_ring_config)
self._rings.append(new_ring)
elif num_bars < current_num_bars:
@@ -642,6 +649,6 @@ class RingProgressBar(BECWidget, QWidget):
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
)
for ring in self._rings:
ring.reset_connection()
self._cleanup_ring(ring)
self._rings.clear()
super().cleanup()

View File

@@ -57,6 +57,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
# self.layout.addWidget(self.table)
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status", "Cancel"])
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
@@ -73,15 +74,16 @@ class BECQueue(BECWidget, CompactPopupWidget):
"""
Set the toolbar.
"""
widget_label = QLabel("Live Queue")
widget_label = QLabel(text="Live Queue", parent=self)
widget_label.setStyleSheet("font-weight: bold;")
self.toolbar = ModularToolBar(
parent=self,
actions={
"widget_label": WidgetAction(widget=widget_label),
"separator_1": SeparatorAction(),
"resume": WidgetAction(widget=ResumeButton(toolbar=False)),
"stop": WidgetAction(widget=StopButton(toolbar=False)),
"reset": WidgetAction(widget=ResetButton(toolbar=False)),
"resume": WidgetAction(widget=ResumeButton(parent=self, toolbar=False)),
"stop": WidgetAction(widget=StopButton(parent=self, toolbar=False)),
"reset": WidgetAction(widget=ResetButton(parent=self, toolbar=False)),
},
target_widget=self,
)
@@ -223,7 +225,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
Returns:
AbortButton: The abort button.
"""
abort_button = AbortButton(scan_id=scan_id)
abort_button = AbortButton(parent=self, scan_id=scan_id)
abort_button.button.setText("")
abort_button.button.setIcon(
@@ -231,7 +233,6 @@ class BECQueue(BECWidget, CompactPopupWidget):
)
abort_button.button.setStyleSheet("background-color: rgba(0,0,0,0) ")
abort_button.button.setFlat(True)
return abort_button
def delete_selected_row(self):
@@ -239,7 +240,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
button = self.sender()
row = self.table.indexAt(button.pos()).row()
self.table.removeRow(row)
button.close()
button.deleteLater()
def reset_content(self):

View File

@@ -16,7 +16,7 @@ from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.compact_popup import CompactPopupWidget
from bec_widgets.widgets.services.bec_status_box.status_item import StatusItem
if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from bec_lib.client import BECClient
# TODO : Put normal imports back when Pydantic gets faster

View File

@@ -7,7 +7,7 @@ from qtpy.QtCore import QMimeData, Qt
from qtpy.QtGui import QDrag
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from qtpy.QtGui import QMouseEvent
logger = bec_logger.logger

View File

@@ -8,7 +8,7 @@ import re
from collections import deque
from functools import partial, reduce
from re import Pattern
from typing import Literal
from typing import TYPE_CHECKING, Literal
from bec_lib.client import BECClient
from bec_lib.connector import ConnectorBase
@@ -16,7 +16,7 @@ from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import LogLevel, bec_logger
from bec_lib.messages import LogMessage, StatusMessage
from PySide6.QtCore import QObject
from qtpy.QtCore import QDateTime, Qt, Signal, SignalInstance # type: ignore
from qtpy.QtCore import QDateTime, Qt, Signal
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (
QApplication,
@@ -52,6 +52,9 @@ from bec_widgets.widgets.utility.logpanel._util import (
simple_color_format,
)
if TYPE_CHECKING: # pragma: no cover
from qtpy.QtCore import SignalInstance
logger = bec_logger.logger
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import sys
from bec_qthemes import material_icon

View File

@@ -12,7 +12,7 @@ class BECColorMapWidget(BECWidget, QWidget):
PLUGIN = True
RPC = False
def __init__(self, parent=None, cmap: str = "magma", **kwargs):
def __init__(self, parent=None, cmap: str = "plasma", **kwargs):
super().__init__(parent=parent, **kwargs)
# Create the ColorMapButton
self.button = ColorMapButton()

View File

@@ -12,7 +12,7 @@ class DarkModeButton(BECWidget, QWidget):
ICON_NAME = "dark_mode"
PLUGIN = True
RPC = False
RPC = True
def __init__(
self,
@@ -31,9 +31,9 @@ class DarkModeButton(BECWidget, QWidget):
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
if toolbar:
self.mode_button = QToolButton()
self.mode_button = QToolButton(parent=parent)
else:
self.mode_button = QPushButton()
self.mode_button = QPushButton(parent=parent)
self.dark_mode_enabled = self._get_qapp_dark_mode_state()
self.update_mode_button()

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -6,13 +6,13 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import os
import datetime
import pathlib
import tomli
project = "BEC Widgets"
copyright = "2023, Paul Scherrer Institute"
copyright = f"{datetime.datetime.today().year}, Paul Scherrer Institute"
author = "Paul Scherrer Institute"
# -- General configuration ---------------------------------------------------

View File

@@ -5,7 +5,7 @@ This section provides an overview of the core concepts of BEC Widgets, which are
## Moduler Design
We develop widgets with the single-responsibility principle in mind, meaning each widget is designed for a specific task. Our goal is to keep widgets simple, using them primarily for visualization or to initiate actions within BEC. Following these ideas, widgets should be designed to be reusable in various applications, making them versatile building blocks for larger GUIs.
We offer up to three different options for composing larger GUIs from these modular widgets: BECDesigner, DockArea widget, or scripting from the command line interface. More information about these options can be found in the user sections on [applications](user.applications).
We offer up to three different options for composing larger GUIs from these modular widgets: BEC Designer, DockArea widget, or scripting from the command line interface. More information about these options can be found in the user sections on [applications](user.applications).
## Client-Server Architecture

View File

@@ -9,14 +9,14 @@ and PySide6 are supported, we prefer PySide6 as it is the official Python bindin
advantages like bundling all necessary libraries in a single package with pip installation and staying more up-to-date
compared to PyQt6.
Below is a list of useful links to help you start developing with Qt and QtDesigner:
Below is a list of useful links to help you start developing with Qt and Qt Designer:
- [Python GUIs](https://www.pythonguis.com): A great resource with tutorials and examples for creating GUIs in Python
using various frameworks.
- [PySide6 Quick Start Guide](https://doc.qt.io/qtforpython-6/index.html): The official documentation for PySide6,
including quick start guides and tutorials.
- [QtDesigner Official Documentation](https://doc.qt.io/qt-6/qtdesigner-manual.html): Comprehensive documentation for
QtDesigner.
- [Qt Designer Official Documentation](https://doc.qt.io/qt-6/qtdesigner-manual.html): Comprehensive documentation for
Qt Designer, the underlying tool for BEC Designer.
- [Simple PyQt Tutorial from RealPython](https://realpython.com/python-pyqt-gui-calculator/): A beginner-friendly
tutorial on creating your first GUI application with PyQt.
- [PyQtGraph Documentation](https://pyqtgraph.readthedocs.io/en/latest/): BEC Widgets relies on PyQtGraph for plotting;

View File

@@ -28,7 +28,7 @@ from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLa
class MotorControlWidget(QWidget):
def __init__(self, motor_name: str, parent=None):
def __init__(self, parent=None, motor_name: str = ""):
super().__init__(parent)
self.motor_name = motor_name
@@ -70,18 +70,17 @@ from [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api
pass the motor name to the widget, and use `get_bec_shortcuts` to access BEC services.
```python
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLayout
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtWidgets import QDoubleSpinBox, QLabel, QPushButton, QVBoxLayout, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
class MotorControlWidget(BECWidget, QWidget):
def __init__(self, motor_name: str, parent=None, *args, **kwargs):
super().__init__(*args, **kwargs)
QWidget.__init__(self, parent)
def __init__(self, parent=None, motor_name: str = "", **kwargs):
super().__init__(parent=parent, **kwargs)
self.motor_name = motor_name
@@ -114,13 +113,13 @@ class MotorControlWidget(BECWidget, QWidget):
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
)
@Slot()
@SafeSlot()
def move_motor(self):
target_position = self.spin_box.value()
self.dev[self.motor_name].move(target_position)
print(f"Commanding motor {self.motor_name} to move to {target_position}")
@Slot(dict, dict)
@SafeSlot(dict, dict)
def on_motor_update(self, msg_content, metadata):
position = msg_content.get("signals", {}).get(self.motor_name, {}).get("value", "N/A")
self.label.setText(f"{self.motor_name} : {round(position, 2)}")
@@ -132,19 +131,18 @@ Next, well set up an RPC interface to allow remote control of the widget from
the `BECIPythonClient`. Well expose a method that allows changing the motor name through CLI commands.
```python
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLayout
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtWidgets import QDoubleSpinBox, QLabel, QPushButton, QVBoxLayout, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
class MotorControlWidget(BECWidget, QWidget):
USER_ACCESS = ["change_motor"]
def __init__(self, motor_name: str, parent=None, *args, **kwargs):
super().__init__(*args, **kwargs)
QWidget.__init__(self, parent)
def __init__(self, parent=None, motor_name: str = "", **kwargs):
super().__init__(parent=parent, **kwargs)
self.motor_name = motor_name
@@ -177,13 +175,13 @@ class MotorControlWidget(BECWidget, QWidget):
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
)
@Slot()
@SafeSlot()
def move_motor(self):
target_position = self.spin_box.value()
self.dev[self.motor_name].move(target_position)
print(f"Commanding motor {self.motor_name} to move to {target_position}")
@Slot(dict, dict)
@SafeSlot(dict, dict)
def on_motor_update(self, msg_content, metadata):
position = msg_content.get("signals", {}).get(self.motor_name, {}).get("value", "N/A")
self.label.setText(f"{self.motor_name} : {round(position, 2)}")
@@ -203,11 +201,11 @@ class MotorControlWidget(BECWidget, QWidget):
```
```{warning}
After implementing an RPC method, you must run the `cli/generate_cli.py` script to update the CLI commands for `BECIPythonClient`. This script generates the necessary command-line interface bindings, ensuring that your RPC method can be accessed and controlled remotely.
After implementing an RPC method, you must run the `bw-generate-cli --target <your plugin repo name>` script to update the CLI commands for `BECIPythonClient`, e.g. `bw-generate-cli --target csaxs_bec`. This script generates the necessary command-line interface bindings, ensuring that your RPC method can be accessed and controlled remotely.
```
```{note}
In this tutorial, we used the @Slot decorator from QtCore to mark methods as slots for signals. This decorator ensures that the connected methods are treated as slots by the Qt framework, which can be connected to signals. Its a best practice to use the @Slot decorator to clearly indicate which methods are intended to handle signal events with correct argument signatures.
In this tutorial, we used the @SafeSlot decorator from BEC Widgets to mark methods as slots for signals. This decorator ensures that the connected methods are treated as slots by the Qt framework, which can be connected to signals. Its a best practice to use the @SafeSlot decorator to clearly indicate which methods are intended to handle signal events with correct argument signatures. @SafeSlot also provides error handling and logging capabilities, making it more robust and easier to debug.
```
## Step 4: Running the Widget

View File

@@ -7,6 +7,6 @@ sphinx-copybutton
sphinx-inline-tabs
myst-parser
sphinx-design
PySide6~=6.7.2
PySide6~=6.8.2
bec-widgets
tomli

View File

@@ -110,14 +110,14 @@ window.show()
sys.exit(app.exec())
```
## Writing applications using Qt Designer
## Writing applications using BEC Designer
BEC Widgets are designed to be used with QtDesigner to quickly design GUI.
BEC Widgets are designed to be used with BEC Designer to quickly design GUI.
## Example of promoting widgets in Qt Designer
## Example of promoting widgets in BEC Designer
_Work in progress_
## Implementation of plugins into Qt Designer
## Implementation of plugins into BEC Designer
_Work in progress_

View File

@@ -2,81 +2,128 @@
# Auto updates
BEC Widgets provides a simple way to update the entire GUI configuration based on events. These events can be of different types, such as a new scan being started or completed, a button being pressed, a device reporting an error or the existence of a specific metadata key. This allows the users to streamline the experience of the GUI and to focus on the data and the analysis, rather than on the GUI itself.
The default auto update only takes control over a single `BECFigure` widget, which is automatically added to the GUI instance. The update instance is accessible via the `bec.gui.auto_updates` object. The user can disable / enable the auto updates by setting the `enabled` attribute of the `bec.gui.auto_updates` object, e.g.
The auto update widget can be launched through the BEC launcher:
```python
bec.gui.auto_updates.enabled = False
```
![BEC launcher](launcher.png)
Without further customization, the auto update will automatically update the `BECFigure` widget based on the currently performed scan. The behaviour is determined by the `handler` method of the `AutoUpdate` class:
The auto update's launch tile also provides a combo box to select the specific auto update to be launched. These options are automatically populated with all available auto updates from a plugin repository and the default auto update.
Once the proper auto update is selected and launched, the CLI will automatically add a new entry to the `gui` object.
The default auto update only provides a simple handler that switches between `line_scan`, `grid_scan` and `best_effort`. More details can be found in the following snippet.
````{dropdown} Auto Updates Handler
:icon: code-square
:animate: fade-in-slide-down
:open:
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
:pyobject: AutoUpdates.handler
```{literalinclude} ../../../bec_widgets/widgets/containers/auto_update/auto_updates.py
:pyobject: AutoUpdates.on_scan_open
```
````
As shown, the default handler switches between different scan names and updates the `BECFigure` widget accordingly. If the scan is a line scan, the `simple_line_scan` update method is executed.
As shown, the default auto updates switches between different visualizations whenever a new scan is started. If the scan is a `line_scan`, the `simple_line_scan` update method is executed.
````{dropdown} Auto Updates Simple Line Scan
:icon: code-square
:animate: fade-in-slide-down
:open:
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
```{literalinclude} ../../../bec_widgets/widgets/containers/auto_update/auto_updates.py
:pyobject: AutoUpdates.simple_line_scan
```
````
As it can be seen from the above snippet, the update method gets the default figure by calling the `get_default_figure` method. If the figure is not found, maybe because the user has deleted or closed it, no update is performed. If the figure is found, the scan info is used to extract the first reported device for the x axis and the first device of the monitored devices for the y axis. The y axis can also be set by the user using the `selected_device` attribute:
As can be seen from the above snippet, the update method changes the dock to a specific widget, in this case to a waveform widget. After selecting the device for the x axis, the y axis is retrieved from the list of monitored devices or from a user-specified `selected_device`.
The y axis can also be set by the user using the `selected_device` attribute:
```python
bec.gui.auto_updates.selected_device = 'bpm4i'
gui.AutoUpdates.selected_device = 'bpm4i'
```
````{dropdown} Auto Updates Code
:icon: code-square
:animate: fade-in-slide-down
```{literalinclude} ../../../bec_widgets/cli/auto_updates.py
```{literalinclude} ../../../bec_widgets/widgets/containers/auto_update/auto_updates.py
```
````
## Custom Auto Updates
The beamline can customize their default behaviour through customized auto update classes. This can be achieved by modifying the class located in the beamline plugin repository: `<beamline_plugin>/bec_widgets/auto_updates.py`. The class should inherit from the `AutoUpdates` class and overwrite the `handler` method.
The beamline can customize their default behaviour through customized auto update classes. This can be achieved by adding an auto update class to the plugin repository: `<beamline_plugin>/bec_widgets/auto_updates/auto_updates.py`. The class must inherit from the `AutoUpdates` class.
An example of a custom auto update class `PXIIIUpdate` is shown below.
```{note}
The code below is simply a copy of the default auto update class's 'GUI Callbacks' section. The user can modify any of the methods to suit their needs but we suggest to have a look at the 'GUI Callbacks' section and the 'Update Functions' section of the default auto update class to understand how to implement the custom auto update class.
```
```python
from bec_widgets.cli.auto_updates import AutoUpdates, ScanInfo
from __future__ import annotations
from typing import TYPE_CHECKING
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
if TYPE_CHECKING: # pragma: no cover
from bec_lib.messages import ScanStatusMessage
class PlotUpdate(AutoUpdates):
create_default_dock = True
enabled = True
class PXIIIUpdate(AutoUpdates):
#######################################################################
################# GUI Callbacks #######################################
#######################################################################
def on_start(self) -> None:
"""
Procedure to run when the auto updates are enabled.
"""
self.start_default_dock()
def on_stop(self) -> None:
"""
Procedure to run when the auto updates are disabled.
"""
def on_scan_open(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan starts.
Args:
msg (ScanStatusMessage): The scan status message.
"""
if msg.scan_name == "line_scan" and msg.scan_report_devices:
return self.simple_line_scan(msg)
if msg.scan_name == "grid_scan" and msg.scan_report_devices:
return self.simple_grid_scan(msg)
if msg.scan_report_devices:
return self.best_effort(msg)
return None
def on_scan_closed(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan ends.
Args:
msg (ScanStatusMessage): The scan status message.
"""
def on_scan_abort(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan is aborted.
Args:
msg (ScanStatusMessage): The scan status message.
"""
```
````{important}
In order for the custom auto update method to be found, the class must be added to the `__init__.py` file of the `auto_updates` folder. This should be done already when the plugin repository is created but it is worth mentioning here. If not, the user can add the following line to the `__init__.py` file:
```python
from .auto_updates import *
```
````
# def simple_line_scan(self, info: ScanInfo) -> None:
# """
# Simple line scan.
# """
# fig = self.get_default_figure()
# if not fig:
# return
# dev_x = info.scan_report_devices[0]
# dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
# if not dev_y:
# return
# fig.clear_all()
# plt = fig.plot(x_name=dev_x, y_name=dev_y)
# plt.set(title=f"Custom Plot {info.scan_number}", x_label=dev_x, y_label=dev_y)
def handler(self, info: ScanInfo) -> None:
# EXAMPLES:
# if info.scan_name == "line_scan" and info.scan_report_devices:
# self.simple_line_scan(info)
# return
# if info.scan_name == "grid_scan" and info.scan_report_devices:
# self.run_grid_scan_update(info)
# return
super().handler(info)
```

View File

@@ -28,5 +28,5 @@ pip cache purge
This can resolve conflicts or issues with package installations.
```{warning}
At the moment PyQt6 is no longer officially supported by BEC Widgets due to incompatibilities with Qt Designer. Please use PySide6 instead.
At the moment PyQt6 is no longer officially supported by BEC Widgets due to incompatibilities with BEC Designer. Please use PySide6 instead.
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

View File

@@ -1,107 +1,141 @@
(user.command_line_introduction)=
# Quick start
In order to use BEC Widgets as a plotting tool for BEC, it needs to be [installed](#user.installation) in the same Python environment as the BEC IPython client (please refer to the [BEC documentation](https://bec.readthedocs.io/en/latest/user/command_line_interface.html#start-up) for more details). Upon startup, the client will automatically launch a GUI and store it as a `bec.gui` object in the client. The GUI backend will also be automatically connect to the BEC server, giving access to all information on the server and allowing the user to visualize the data in real-time.
In order to use BEC Widgets as a plotting tool for BEC, it needs to be [installed](#user.installation) in the same Python environment as the BEC IPython client (please refer to the [BEC documentation](https://bec.readthedocs.io/en/latest/user/command_line_interface.html#start-up) for more details). Upon startup, the client will automatically launch a GUI and store it as a `gui` object in the client. The GUI backend will also be automatically connect to the BEC server, giving access to all information on the server and allowing the user to visualize the data in real-time.
## BECGuiClient
The `gui` object is the main entry point for interacting with the BEC Widgets framework. It is an instance of the [`BECGuiClient`](/api_reference/_autosummary/bec_widgets.cli.client.BECGuiClient) class, which provides methods to create and manage GUI components. Upon BEC startup, a default [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance named *bec* is automatically launched.
A launcher interface is available via the top menu bar under New → Open Launcher. This opens a window where users can launch a new [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance, an [AutoUpdate](#user.auto_updates) instance, individual widgets or a custom *ui file* created with *BEC Designer*. Alternatively, users can launch a new [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) from the command line:
```python
dock_area = gui.new() # launches a new BECDockArea instance
gui.new('my_dock_area') # launches a new BECDockArea instance with the name 'my_dock_area'
dock_area2 = gui.my_dock_area # Dynamic attribute access to created dock_area
```
``` {note}
If a name is provided, the new dock area will use that name. If the name already exists, an error is raised. If no name is specified, a name will be auto-generated following the pattern *dock_area_ii* where *ii* is the next available number. Named dock areas can be accessed dynamically as attributes of the gui object.
```
## BECDockArea
The `bec.gui` object is your entry point to BEC Widgets. It is a [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) instance that can be composed of multiple [`BECDock`](/api_reference/_autosummary/bec_widgets.cli.client.BECDock)s that can be attached / detached to the main area. These docks allow users to freely arrange and customize the widgets they add to the gui, providing a flexible and customizable interface to visualize data.
The [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea) is a versatile container for quickly building customized GUIs. It supports adding new widgets either through the CLI or directly via toolbar actions. Widgets must be added into [`BECDock`](/api_reference/_autosummary/bec_widgets.cli.client.BECDock) instances, which serve as the individual containers. These docks can be arranged freely, detached from the main window, and used as floating panels.
**Schema of the BECDockArea**
From the CLI, you can create new docks like this:
![BECDockArea.png](BECDockArea.png)
```python
dock_area = gui.new()
dock = dock_area.new(name='my_dock_area')
dock = gui.new().new()
```
<!-- **Schema of the BECDockArea**
![BECDockArea.png](BECDockArea.png) -->
## Widgets
Widgets are the building blocks of the BEC Widgets framework. They are the visual components that allow users to interact with the data and control the behavior of the application. Each dock can contain multiple widgets, albeit we recommend for most use cases a single widget per dock. BEC Widgets provides a set of core widgets (cf. [widgets](#user.widgets)). More widgets can be added by the users, and we invite you to explore the [developer documentation](developer.widgets) to learn how to create custom widgets.
For the introduction given here, we will focus on the `BECFigure` widget, as it is the most commonly used widget for visualizing data from BEC. The same access pattern can be used for all other widgets.
**BECFigure**
The [`BECFigure`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure) widget is one of the core widgets developed for BEC and can be used to visualize different plot types, such as [1D waveforms](user.widgets.waveform_1d), [2D scatter plots](user.widgets.scatter_2d), [position maps](user.widgets.motor_map) and [2D images](user.widgets.image_2d).
If BEC Widgets is installed, the default behaviour of BEC is to automatically add a BECFigure Widget to the existing GUI instance. This widget is directly accessible via the `fig` object from the client. Moreover, a best-effort attempt is made to automatically determine the best plot type based on the currently performed scan. This behaviour can be changed or disabled by the user. For more details, please refer to the [auto update](user.auto_updates) section.
For the introduction given here, we will focus on the plotting widgets of BECWidgets.
<!-- We also provide two methods [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot), [`image()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.image) and [`motor_map()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.motor_map) as shortcuts to add a plot, image or motor map to the BECFigure. -->
**Waveform Plot**
The [`BECWaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform) is a widget that can be used to visualize 1D waveform data, i.e. to plot data of a monitor against a motor position. The method [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot) of BECFigure adds a BECWaveForm widget to the figure, and returns the plot object.
The [`WaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm) is a widget that can be used to visualize 1D waveform data, i.e. to plot data of a monitor against a motor position. The method [`plot()`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm.rst#bec_widgets.cli.client.WaveForm.plot) returns the plot object.
```python
plt = fig.plot(x_name='samx', y_name='bpm4i')
plt = gui.new().new().new(gui.available_widgets.Waveform)
plt.plot(x_name='samx', y_name='bpm4i')
```
Here, we create a new plot with a subscription to the devices `samx` and `bpm4i` and assign the plot to the object `plt`. We can now use this object to further customize the plot, e.g. changing the title ([`set_title()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_title)), axis labels ([`set_x_label()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_x_label)) or limits ([`set_x_lim()`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveform.rst#bec_widgets.cli.client.BECWaveform.set_x_lim)). We invite you to explore the API of the BECWaveForm in the [documentation](user.widgets.waveform_1d) or directly in the command line.
Here, we create a new plot with a subscription to the devices `samx` and `bpm4i` and assign the plot to the object `plt`. We can now use this object to further customize the plot, e.g. changing the title ([`plt.title = 'my title' `](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.title)), axis labels ([`plt.x_label = 'my x label'`](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.x_label))
<!-- or limits ([`set_x_lim()`](/api_reference/_autosummary/bec_widgets.cli.client.Waveform.rst#bec_widgets.cli.client.Waveform.x_lim)). -->
We invite you to explore the API of the WaveForm in the [documentation](user.widgets.waveform_1d) or directly in the command line.
To plot custom data, i.e. data that is not directly available through a scan in BEC, we can use the same method, but provide the data directly to the plot.
```python
plt = fig.plot([1,2,3,4], [1,4,9,16])
plt = gui.new().new().new(gui.available_widgets.Waveform)
#
plt.plot([1,2,3,4], [1,4,9,16])
# or
plt = fig.plot(x=[1,2,3,4], y=[1,4,9,16])
plt.plot(x=np.array([1,2,3,4]), y=np.array([1,4,9,16]))
# or
plt = fig.plot(x=np.array([1,2,3,4]), y=np.array([1,4,9,16]))
# or
plt = fig.plot(np.random.rand(10,2))
plt.plot(np.random.rand(10,2))
# or if you like to receive the custom curve item
curve = plt.plot(x=[1,2,3,4], y=[1,4,9,16])
```
**Scatter Plot**
The [`BECWaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.BECWaveForm) widget can also be used to visualize 2D scatter plots. More details on setting up the scatter plot are available in the widget documentation of the [scatter plot](user.widgets.scatter_2d).
The [`WaveForm`](/api_reference/_autosummary/bec_widgets.cli.client.WaveForm) widget can also be used to visualize 2D scatter plots. More details on setting up the scatter plot are available in the widget documentation of the [scatter plot](user.widgets.scatter_2d).
**Motor Map**
The [`BECMotorMap`](/api_reference/_autosummary/bec_widgets.cli.client.BECMotorMap) widget can be used to visualize the position of motors. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans. More details on setting up the motor map are available in the widget documentation of the [motor map](user.widgets.motor_map).
The [`MotorMap`](/api_reference/_autosummary/bec_widgets.cli.client.MotorMap) widget can be used to visualize the position of motors. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans. More details on setting up the motor map are available in the widget documentation of the [motor map](user.widgets.motor_map).
**Image Plot**
The [`BECImageItem`](/api_reference/_autosummary/bec_widgets.cli.client.BECImageItem) widget can be used to visualize 2D image data for example a camera. More details on setting up the image plot are available in the widget documentation of the [image plot](user.widgets.image).
The [`Image`](/api_reference/_autosummary/bec_widgets.cli.client.Image) widget can be used to visualize 2D image data for example a camera. More details on setting up the image plot are available in the widget documentation of the [image plot](user.widgets.image).
### Useful Commands
We recommend users to explore the API of the widgets by themselves since we assume that the user interface is supposed to be intuitive and self-explanatory. We appreciate feedback from user in order to constantly improve the experience and allow easy access to the gui, widgets and their functionality. We recommend checking the [API documentation](user.api_reference), but also by using BEC Widgets, exploring the available functions and check their dockstrings.
```python
gui.add_dock? # shows the dockstring of the add_dock method
gui.new? # shows the dockstring of the new method
```
In addition, we list below a few useful commands that can be used to interface with the widgets:
```python
gui.panels # returns a dictionary of all docks in the gui
gui.add_dock() # adds a new dock to the gui
gui.windows # Returns a dictionary of all dock areas in the GUI
gui.new() # Adds a new BECDockArea to the GUI
dock = gui.panels['dock_2']
dock.add_widget('BECFigure') # adds a new widget of BECFigure to the dock
dock.widget_list # returns a list of all widgets in the dock
dock_area = gui.windows['bec']
dock = dock_area.new('my_dock') # Adds a new dock named 'my_dock' to the dock area
plt = dock.new(gui.available_widgets.Waveform) # Adds a Waveform widget to the dock
figure = dock.widget_list[0] # assigns the created BECFigure to figure
plt = figure.plot(x_name='samx', y_name='bpm4i') # adds a BECWaveForm plot to the figure
plt.curves # returns a list of all curves in the plot
# Alternative syntax:
# dock_area.my_dock.new(gui.available_widgets.Waveform)
dock.element_list # Returns a list of all widgets in the dock
dock.elements # Equivalent to dock.element_list
plt.curves # Returns a list of all curves in the plot
```
We note that commands can also be chained. For example, `gui.add_dock().add_widget('BECFigure')` will add a new dock to the gui and add a new widget of `BECFigure` to the dock.
We note that commands can also be chained. For example, `gui.new().new().new(gui.available_widgets.Waveform)` will add a new dock_area to the gui and add a new dock with a `Waveform` widget to the dock.
## Composing a larger GUI
The example given above introduces BEC Widgets with its different components, and provides an overview of how to interact with the widgets. Nevertheless, another power aspect of BEC Widgets lies in the ability to compose a larger GUI with multiple docks and widgets. This section aims to provide a tutorial like guide on how to compose a more complex GUI that (A) live-plots a 1D waveform, (B) plots data from a camera, and (C) tracks the positions of two motors.
Let's assume BEC was just started and the `gui` object is available in the client. A single dock is already attached together with a BEC Figure. Let's add the 1D waveform to this dock, change the color of the line to white and add the title *1D Waveform* to the plot.
```python
plt = fig.plot(x_name='samx', y_name='bpm4i')
dock_area = gui.new()
plt = dock_area.new().new(gui.available_widgets.Waveform)
plt.plot(x_name='samx', y_name='bpm4i')
plt.curves[0].set_color(color="white")
plt.set_title('1D Waveform')
plt.title = '1D Waveform'
```
Next, we add 2 new docks to the gui, one to plot the data of a camera and one to track the positions of two motors.
```ipython
cam_widget= gui.add_dock(name="cam_dock").add_widget('BECFigure').image("eiger")
motor_widget = gui.add_dock(name="mot_dock").add_widget('BECFigure').motor_map("samx", "samy")
cam_widget= dock_area.new().new(gui.available_widgets.Image)
cam_widget.image("eiger")
motor_widget = dock_area.new().new(gui.available_widgets.MotorMap)
motor_widget.map("samx", "samy")
```
Note, we chain commands here which is possible since the `add_dock` and `add_widget` methods return the dock and the widget respectively. We can now further customize the widgets by changing the title, axis labels, etc.
Note, we chain commands here which is possible since the `new()` and `new()` methods return the dock and the widget respectively. We can now further customize the widgets by changing the title, axis labels, etc.
```python
cam_widget.set_title("Camera Image Eiger")
cam_widget.set_vrange(vmin=0, vmax=100)
cam_widget.title = "Camera Image Eiger"
cam_widget.vrange = [0, 100]
```
As a final step, we can now add also a RingProgressBar to a new dock, and perform a grid_scan with the motors *samx* and *samy*.
As you see in the example below, all docks are arranged below each other. This is the default behavior of the `add_dock` method. However, the docks can be freely arranged by drag and drop as desired by the user. We invite you to explore this by yourself following the example in the video, and build your custom GUI with BEC Widgets.
As you see in the example below, all docks are arranged below each other. This is the default behavior of the `new()` method. However, the docks can be freely arranged by drag and drop as desired by the user. We invite you to explore this by yourself following the example in the video, and build your custom GUI with BEC Widgets.
```python
prog_bar = gui.add_dock(name="prog_dock").add_widget('RingProgressBar')
prog_bar = dock_area.new().new(gui.available_widgets.RingProgressBar)
prog_bar.set_line_widths(15)
scans.grid_scan(dev.samy, -2, 2, 10, dev.samx, -5, 5, 10, exp_time=0.1, relative=False)
```

View File

@@ -1,5 +1,5 @@
(user.plugin_widgets)=
# PLugin repository widgets
# Plugin repository widgets
## Adding widgets to the plugin repository
@@ -44,7 +44,7 @@ class TestWidget(BECWidget, QWidget):
### Generating the plugin files and RPC client template
To allow the BEC client to communicate with the GUI server and to know which widgets are available,
as well as to allow the Qt Designer to find the available widgets, a code generation tool should be
as well as to allow the BEC Designer to find the available widgets, a code generation tool should be
run to prepare a client file which lists all the available widget classes and functions. Make sure
you are in the BEC python environment where your plugin repository is also installed, and run:

View File

@@ -2,6 +2,16 @@
# User
Welcome to the User section of the BEC Widgets documentation! BEC Widgets is a versatile GUI framework tailored for beamline scientists, enabling efficient and intuitive interaction with beamline experiments. This section is designed to guide both new and experienced users through the essential aspects of utilizing BEC Widgets.
```{toctree}
---
maxdepth: 2
hidden: false
---
customisation.md
plugin_widgets.md
```
```{toctree}
---
maxdepth: 2
@@ -14,7 +24,6 @@ widgets/widgets.md
api_reference/api_reference.md
```
***
````{grid} 2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,105 +0,0 @@
(user.widgets.bec_figure)=
# BECFigure
````{tab} Overview
[`BECFigure`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure) is a robust framework that provides a fast, flexible plotting environment, similar to the Matplotlib figure. With BECFigure, users can dynamically change layouts, add or remove subplots, and customize their plotting environment in real-time. This flexibility makes BECFigure an ideal tool for both rapid prototyping and detailed data visualization.
- **Dynamic Layout Management**: Easily add, remove, and rearrange subplots within `BECFigure`, enabling tailored visualization setups.
- **Widget Integration**: Incorporate various specialized widgets like [`WaveformWidget`](user.widgets.waveform_widget), [`ImageWidget`](user.widgets.image_widget) , and [`MotorMapWidget`](user.widgets.motor_map) into `BECFigure`. Note that these widgets can also be used individually. For more details, please refer to the documentation for each individual widget.
- **Interactive Controls**: Provides interactive tools for zooming, panning, and adjusting plots on the fly, streamlining the data exploration process.
**Schema of the BECFigure components**
![BECFigure.png](BECFigure.png)
````
````{tab} Examples - CLI
In the following examples, we will use `BECIPythonClient` with a predefined `BECDockArea` as the `gui` object. These tutorials focus on how to work with the `BECFigure` framework, such as changing layouts, adding new elements, and accessing them. For more detailed examples of each individual component, please refer to the example sections of each individual widget: [`WaveformWidget`](user.widgets.waveform_widget), [`MotorMapWidget`](user.widgets.motor_map), [`ImageWidget`](user.widgets.image_widget).
## Example 1 - Adding subplots to BECFigure
In this example, we will demonstrate how to add different subplots to a single `BECFigure` widget.
```python
# Add a new dock with BECFigure widget
fig = gui.add_dock().add_widget('BECFigure')
# Add a WaveformWidget to the BECFigure
plt1 = fig.plot(x_name='samx', y_name='bpm4i')
# Add a second WaveformWidget to the BECFigure, specifying new=True to add it as a new subplot
plt2 = fig.plot(x_name='samx', y_name='bpm3i', new=True)
# Add a MotorMapWidget to the BECFigure
mm = fig.motor_map(motor_x='samx', motor_y='samy')
# Add an ImageWidget to the BECFigure
img = fig.image('eiger')
```
```{note}
By default, the [`.plot`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.plot), [`.image`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.image), and [`.motor_map`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.motor_map) methods always find the first widget of that type in the layout and interact with it. If you want to add a new subplot of the same type, you must either specify the coordinates of the new subplot or use the `new=True` keyword argument, as shown above when adding the second WaveformWidget. Additionally, you can directly add a subplot to a specific, unoccupied position in the layout by specifying the `row` and `col` arguments, such as `fig.plot(x_name='samx', y_name='bpm4i', row=1, col=1)`.
```
## Example 2 - Changing the layout of BECFigure
The previous example added four subplots into a single `BECFigure` widget. By default, new widgets are always added to the bottom of the BECFigure. However, you can change the layout of the BECFigure by using the [`change_layout`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.change_layout) method, specifying the number of rows and/or columns.
```python
# Change the layout of the BECFigure to have 4 columns -> 4x1 matrix layout
fig.change_layout(max_columns=4)
# Change the layout of the BECFigure to have 2 rows -> 2x2 matrix layout
fig.change_layout(max_rows=2)
```
## Example 3 - Accessing Subplots in BECFigure
The subplots in BECFigure can be accessed in a similar way to Matplotlib figures using the [`axes`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.axes) property. Each subplot can be accessed by its index coordinates within the layout, specified by the row and column index (starting at 0). In following example, we will access the subplots and modify their titles. The layout is a 2x2 matrix, so the subplots are indexed as follows:
```python
# Access the first subplot in the first row and first column (0, 0)
subplot1 = fig.axes(0, 0)
# Access the second subplot in the first row and second column (0, 1)
subplot2 = fig.axes(0, 1)
# Access the first subplot in the second row and first column (1, 0)
subplot3 = fig.axes(1, 0)
# Example: Set title for the first subplot
subplot1.set_title("Waveform 1")
# Example: Set title for the second subplot
subplot2.set_title("Waveform 2")
# Example: Set title for the third subplot
subplot3.set_title("Motor Map")
```
In this example, we accessed three different subplots based on their row and column positions and modified their titles.
## Example 4 - Removing Subplots from BECFigure
You may want to remove certain subplots from the `BECFigure`. This can be done using the [`remove`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.remove) method, which takes the row and column index of the subplot you want to remove. The [`remove`](/api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst#bec_widgets.cli.client.BECFigure.remove) method could be also called on the subplot itself.
```python
# Remove the subplot in the second row and second column (1, 1)
fig.remove(1, 1)
# Remove the subplot in the first row and first column (0, 0)
fig.remove(0, 0)
# Remove previously accessed subplot plt2 from Example 1
plt2.remove()
```
````
````{tab} API
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECFigure.rst
```
````

View File

@@ -3,7 +3,7 @@
```{tab} Overview
The BEC Progressbar widget is a general purpose progress bar that follows the BEC theme and style. It can be embedded in any application to display the progress of a task or operation.
The [`BECProgressbar`](/api_reference/_autosummary/bec_widgets.cli.client.BECProgressBar) widget is a general purpose progress bar that follows the BEC theme and style. It can be embedded in any application to display the progress of a task or operation.
## Key Features:
- **Modern Design**: The BEC Progressbar widget is designed with a modern and sleek appearance, following the BEC theme.
@@ -18,15 +18,16 @@ The BEC Progressbar widget is a general purpose progress bar that follows the BE
````{tab} Examples
The `BECProgressBar` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `BECProgressBar` widget.
The `BECProgressBar` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`. Below are examples demonstrating how to create and use the `BECProgressBar` widget.
## Example 1 - Adding BEC Status Box to BECDockArea
In this example, we demonstrate how to add a `BECProgressBar` widget to a `BECDockArea`, allowing users to manually set and update the progress states.
```python
# Add a new dock with a BECStatusBox widget
pb = gui.add_dock().add_widget("BECProgressBar")
# Add a new dock with a BEC Progressbar widget
dock_area = gui.new()
pb = dock_area.new().new(gui.available_widgets.BECProgressBar)
pb.set_value(50)
```
@@ -34,6 +35,6 @@ pb.set_value(50)
````{tab} API
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECProgressbar.rst
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECProgressBar.rst
```
````

View File

@@ -15,7 +15,7 @@ The [`BEC Status Box`](/api_reference/_autosummary/bec_widgets.cli.client.BECSta
````{tab} Examples
The `BECStatusBox` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `BECStatusBox` widget.
The `BECStatusBox` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BEC Designer`. Below are examples demonstrating how to create and use the `BECStatusBox` widget.
## Example 1 - Adding BEC Status Box to BECDockArea
@@ -23,7 +23,7 @@ In this example, we demonstrate how to add a `BECStatusBox` widget to a `BECDock
```python
# Add a new dock with a BECStatusBox widget
bec_status_box = gui.add_dock().add_widget("BECStatusBox")
sb = gui.bec.new().new(widget=gui.available_widgets.BECStatusBox)
```
```{hint}

View File

@@ -23,7 +23,7 @@ The `Dark Mode Button` is a toggle control that allows users to switch between l
**Key Features:**
- **Theme Switching**: Enables users to switch between light and dark themes with a single click.
- **Configurable from BECDesigner**: The defaults for the dark mode can be set in the BECDesigner, allowing users to customize the startup appearance of the GUI.
- **Configurable from BEC Designer**: The defaults for the dark mode can be set in the BEC Designer, allowing users to customize the startup appearance of the GUI.
## Color Button
@@ -48,7 +48,7 @@ The `Colormap Button` is a custom widget that displays the current colormap and,
- **Current Colormap Display**: Shows the name and a gradient icon of the current colormap directly on the button.
- **Nested Menu Selection**: Offers a nested menu with categorized colormaps, making it easy to find and select the desired colormap.
- **Signal Emission**: Emits a signal when the colormap changes, providing the new colormap name as a string.
- **Qt Designer Integration**: Exposes properties and signals to be used within Qt Designer, allowing for customization within the designer interface.
- **BEC Designer Integration**: Exposes properties and signals to be used within BEC Designer, allowing for customization within the designer interface.
- **Resizable and Styled**: Features adjustable size policies and styles to match the look and feel of standard `QPushButton` widgets, including rounded edges.
`````
@@ -63,12 +63,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import DarkModeButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setLayout(QVBoxLayout(self))
# Create and add the DarkModeButton to the layout
self.dark_mode_button = DarkModeButton()
self.dark_mode_button = DarkModeButton(parent=self)
self.layout().addWidget(self.dark_mode_button)
# Example of how this custom GUI might be used:
@@ -83,12 +83,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import ColorButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setLayout(QVBoxLayout(self))
# Create and add the ColorButton to the layout
self.color_button = ColorButton()
self.color_button = ColorButton(self)
self.layout().addWidget(self.color_button)
# Example of how this custom GUI might be used:
@@ -103,12 +103,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import ColormapSelector
class MyGui(QWidget):
def __init__(self):
super().__init__()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setLayout(QVBoxLayout(self))
# Create and add the ColormapSelector to the layout
self.colormap_selector = ColormapSelector()
self.colormap_selector = ColormapSelector(self)
self.layout().addWidget(self.colormap_selector)
# Example of how this custom GUI might be used:
@@ -123,12 +123,12 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import ColormapButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setLayout(QVBoxLayout(self))
# Create and add the ColormapButton to the layout
self.colormap_button = ColormapButton()
self.colormap_button = ColormapButton(self)
self.layout().addWidget(self.colormap_button)
# Connect the signal to handle colormap changes

View File

@@ -2,7 +2,7 @@
# Queue Control Buttons
`````{tab} Overview
```{tab} Overview
This section consolidates various buttons designed to manage the BEC scan queue, providing essential controls for operations like stopping, resuming, aborting, and resetting the scan queue.
## Stop Button
@@ -37,98 +37,41 @@ The `Reset Button` is used to reset the scan queue. It prompts the user for conf
- **Queue Reset**: Resets the entire scan queue.
- **Confirmation Dialog**: Prompts the user to confirm the reset action to prevent accidental resets.
- **Toolbar and Button Options**: Can be configured as a toolbar button or a standard push button.
`````
```
````{tab} Examples
`````{tab} Examples
Integrating these buttons into a BEC GUI layout is straightforward. The following examples demonstrate how to embed these buttons within a custom GUI layout using `QtWidgets`.
### Example 1 - Embedding a Stop Button in a Custom GUI Layout
```python
from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import StopButton
import sys
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout(self))
self.setLayout(QVBoxLayout())
# Create and add the StopButton to the layout
self.stop_button = StopButton()
self.layout().addWidget(self.stop_button)
# Example of how this custom GUI might be used:
app = QApplication([])
my_gui = MyGui()
my_gui.show()
app.exec_()
```
### Example 2 - Embedding a Resume Button in a Custom GUI Layout
```python
from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import ResumeButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
# Create and add the ResumeButton to the layout
self.resume_button = ResumeButton()
self.layout().addWidget(self.resume_button)
# Example of how this custom GUI might be used:
my_gui = MyGui()
my_gui.show()
```
### Example 3 - Adding an Abort Button
```python
from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import AbortButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout(self))
# Create and add the AbortButton to the layout
self.abort_button = AbortButton()
self.layout().addWidget(self.abort_button)
# Example of how this custom GUI might be used:
my_gui = MyGui()
my_gui.show()
```
### Example 4 - Adding a Reset Queue Button
```python
from qtpy.QtWidgets import QWidget, QVBoxLayout
from bec_widgets.widgets.buttons import ResetButton
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout(self))
# Create and add the ResetButton to the layout
self.reset_button = ResetButton()
self.layout().addWidget(self.reset_button)
# Example of how this custom GUI might be used:
my_gui = MyGui()
my_gui.show()
```
````
`ResumeButton`, `ResetButton`, and `AbortButton` may be used in an exactly analogous way.
````{tab} API
```{eval-rst}
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.StopButton.rst
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ResumeButton.rst
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.AbortButton.rst
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.ResetButton.rst
```
````
`````

View File

@@ -4,7 +4,7 @@
````{tab} Overview
The `Device Browser` widget provides a user-friendly interface for browsing through all available devices in the current BEC session. As it supports drag functionality, users can easily drag and drop device into other widgets or applications.
The [`Device Browser`](/api_reference/_autosummary/bec_widgets.cli.client.DeviceBrowser) widget provides a user-friendly interface for browsing through all available devices in the current BEC session. As it supports drag functionality, users can easily drag and drop device into other widgets or applications.
```{note}
The `Device Browser` widget is currently under development. Other widgets may not support drag and drop functionality yet.
@@ -24,7 +24,10 @@ In this example, we demonstrate how to add a `DeviceBrowser` widget to a `BECDoc
```python
# Add a new dock with a DeviceBrowser widget
browser = gui.add_dock().add_widget("DeviceBrowser")
dock_area = gui.new()
browser = dock_area.new("device_browser").new(gui.available_widgets.DeviceBrowser)
# You can also access the DeviceBrowser widget directly from the dock_area
dock_area.device_browser.DeviceBrowser
```
````

View File

@@ -4,7 +4,7 @@
````{tab} Overview
The `Device Input Widgets` consist of two primary widgets: `DeviceLineEdit` and `DeviceComboBox`. Both widgets are designed to facilitate the selection of devices within the BEC environment, allowing users to filter, search, and select devices dynamically. These widgets are highly customizable and can be integrated into a GUI either through direct code instantiation or by using `QtDesigner`.
The `Device Input Widgets` consist of two primary widgets: `DeviceLineEdit` and `DeviceComboBox`. Both widgets are designed to facilitate the selection of devices within the BEC environment, allowing users to filter, search, and select devices dynamically. These widgets are highly customizable and can be integrated into a GUI either through direct code instantiation or by using `BEC Designer`.
## DeviceLineEdit
The `DeviceLineEdit` widget provides a line edit interface with autocomplete functionality for device names, making it easier for users to quickly search and select devices.
@@ -19,7 +19,7 @@ The `DeviceComboBox` widget offers a dropdown interface for device selection, pr
- **Real-Time Autocomplete (LineEdit)**: The `DeviceLineEdit` widget supports real-time autocomplete, helping users find devices faster.
- **Real-Time Input Validation (LineEdit)**: User input is validated in real-time with a red border around the `DeviceLineEdit` indicating an invalid input.
- **Dropdown Selection (ComboBox)**: The `DeviceComboBox` widget displays devices in a dropdown list, making selection straightforward.
- **QtDesigner Integration**: Both widgets can be added as custom widgets in `QtDesigner` or instantiated directly in code.
- **BEC Designer Integration**: Both widgets can be added as custom widgets in `BEC Designer` or instantiated directly in code.
## Screenshot
```{figure} /assets/widget_screenshots/device_inputs.png
@@ -29,7 +29,7 @@ The `DeviceComboBox` widget offers a dropdown interface for device selection, pr
````{tab} Examples
Both `DeviceLineEdit` and `DeviceComboBox` can be integrated within a GUI application through direct code instantiation or by using `QtDesigner`. Below are examples demonstrating how to create and use these widgets.
Both `DeviceLineEdit` and `DeviceComboBox` can be integrated within a GUI application through direct code instantiation or by using `BEC Designer`. Below are examples demonstrating how to create and use these widgets.
## Example 1 - Creating a DeviceLineEdit in Code
@@ -45,12 +45,12 @@ from bec_lib.device import ReadoutPriority
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
class MyGui(QWidget):
def __init__(self):
super().__init__()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
# Create and add the DeviceLineEdit to the layout
self.device_line_edit = DeviceLineEdit(device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
self.device_line_edit = DeviceLineEdit(parent=self, device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
self.layout().addWidget(self.device_line_edit)
# Example of how this custom GUI might be used:
@@ -71,12 +71,12 @@ from bec_lib.device import ReadoutPriority
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
class MyGui(QWidget):
def __init__(self):
super().__init__()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
# Create and add the DeviceComboBox to the layout
self.device_combobox = DeviceComboBox(device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
self.device_combobox = DeviceComboBox(parent=self, device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
self.layout().addWidget(self.device_combobox)
# Example of how this custom GUI might be used:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 322 KiB

View File

@@ -9,7 +9,7 @@
- **Flexible Dock Management**: Easily add, remove, and rearrange docks within `BECDockArea`, providing a customized layout for different tasks.
- **State Persistence**: Save and restore the state of the dock area, enabling consistent user experiences across sessions.
- **Dock Customization**: Add docks with customizable positions, names, and behaviors, such as floating or closable docks.
- **Integration with Widgets**: Integrate various widgets like [`WaveformWidget`](user.widgets.waveform_widget), [`ImageWidget`](user.widgets.image_widget), and [`MotorMapWidget`](user.widgets.motor_map) into `BECDockArea`, either as standalone tools or as part of a more complex interface.
- **Integration with Widgets**: Integrate various widgets like [`WaveformWidget`](user.widgets.waveform_widget), [`ImageWidget`](user.widgets.image_widget), and [`MotorMapWidget`](user.widgets.motor_map) into [`BECDockArea`](/api_reference/_autosummary/bec_widgets.cli.client.BECDockArea), either as standalone tools or as part of a more complex interface.
**BEC Dock Area Components Schema**
@@ -24,14 +24,18 @@ In the following examples, we will use `BECIPythonClient` as the main object to
In this example, we will demonstrate how to add different docks to a single `BECDockArea` widget. New docks are always added to the bottom of the dock area by default; however, you can specify the position of the dock by using the `position` and `relative_to` arguments.
```python
# Add a new dock with a WaveformWidget to the BECDockArea
dock1 = gui.add_dock(name="Waveform Dock", widget="BECWaveformWidget")
# Create a new dock_area from GUI object
dock_area = gui.new()
# Add a new dock with a Waveform to the BECDockArea
dock_area.new(name="waveform_dock", widget="Waveform")
dock1 = dock_area.waveform_dock # dynamic namespace was created
# Add a second dock with a MotorMapWidget to the BECDockArea to the right of the first dock
dock2 = gui.add_dock(name="Motor Map Dock", widget="BECMotorMapWidget",relative_to="Waveform Dock", position="right")
dock2 = dock_area.new(name="motor_dock", widget="MotorMap",relative_to="Waveform Dock", position="right")
# Add a third dock with an ImageWidget to the BECDockArea, placing it on bottom of the dock area
dock3 = gui.add_dock(name="Image Dock", widget="BECImageWidget")
dock3 = dock_area.new(name="image_dock", widget="Image")
```
```{hint}
@@ -44,18 +48,26 @@ Docks can be accessed by their name or by the dock object. The dock object can b
```python
# All docks can be accessed by their name from the panels dictionary
gui.panels
dock_area.panels
# Output
{'Waveform Dock': <BECDock object at 0x168b983d0>,
'Motor Map Dock': <BECDock object at 0x13a969250>,
'Image Dock': <BECDock object at 0x13f267950>}
# Access the dock by its name
dock1 = gui.panels["Waveform Dock"]
{'waveform_dock': <BECDock with name: waveform_dock>,
'motor_dock': <BECDock with name: motor_dock>,
'image_dock': <BECDock with name: image_dock>}
# Access all docks from the dock area via list
dock_area.panel_list
# Access the widget object of the dock
waveform_widget = dock1.widget_list[0]
# Access through dynamic namespace mapping
dock_area.waveform_dock
dock_area.motor_dock
dock_area.image_dock
# If objects were closed, we will keep a refernce that will indicate that the dock was deleted
# Try closing the window with the dock_area via mouse click on x
dock_area
# Output
<Deleted widget with gui_id BECDockArea_2025_04_24_14_28_11_742887>
```
## Example 3 - Detaching and Attaching Docks in BECDockArea
@@ -64,11 +76,10 @@ Docks in `BECDockArea` can be detached (floated) or reattached to the main dock
```python
# Detach the dock named "Waveform Dock"
gui.detach_dock(dock_name="Waveform Dock")
# Docks can be also detached by the dock object
dock2.detach()
dock3.detach()
dock_area.detach_dock("waveform_dock")
# Alternatively, you can use the dock object to detach the dock
dock1 = dock_area.waveform_dock
dock1.detach()
# Docks can be individually reattached to the main dock area
dock2.attach()
@@ -87,13 +98,13 @@ Docks can be removed from the dock area by their name or by the dock object. The
```python
# Removing docks by their name
gui.remove_dock(dock_name="Waveform Dock")
# Removing docks by the dock object
dock2.remove()
dock_area.delete("waveform_dock")
# Alternatively, you can use the dock object to remove the dock
dock1 = dock_area.motor_dock
dock1.remove()
# Removing all docks from the dock area
gui.clear_all()
gui.delete_all()
```
```{warning}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 MiB

View File

@@ -1,104 +1,81 @@
(user.widgets.image_widget)=
# Image Widget
# Image widget
````{tab} Overview
The Image Widget is a versatile tool designed for visualizing both 1D and 2D data, such as camera images or waveform data, in real-time. Directly integrated with the `BEC` framework, it can display live data streams from connected detectors or other data sources within the current `BEC` session. The widget provides advanced customization options for color maps and scale bars, allowing users to tailor the visualization to their specific needs.
The Image widget is a versatile tool designed for visualizing both 1D and 2D data, such as camera images or waveform data, in real-time. Directly integrated with the `BEC` framework, it can display live data streams from connected detectors or other data sources within the current `BEC` session. The widget provides advanced customization options for color maps and scale bars, allowing users to tailor the visualization to their specific needs.
## Key Features:
- **Flexible Integration**: The widget can be integrated into both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`.
- **Flexible Integration**: The widget can be integrated into [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BEC Designer`.
- **Live Data Visualization**: Real-time plotting of both 1D and 2D data from detectors or other data sources, provided that a data stream is available in the BEC session.
- **Support for Multiple Monitor Types**: The Image Widget supports different monitor types (`'1d'` and `'2d'`), allowing visualization of various data dimensions.
- **Support for Multiple Monitor Types**: The Image widget supports different monitor types (`'1d'` and `'2d'`), allowing visualization of various data dimensions. It can automatically determine the best way to visualise the data based on the shape of the data source.
- **Customizable Color Maps and Scale Bars**: Users can customize the appearance of images with various color maps and adjust scale bars to better interpret the visualized data.
- **Real-time Image Processing**: Apply real-time image processing techniques directly within the widget to enhance the quality or analyze specific aspects of the data, such as rotation, logarithmic scaling, and Fast Fourier Transform (FFT).
- **Data Export**: Export visualized data to various formats such as PNG, TIFF, or H5 for further analysis or reporting.
- **Interactive Controls**: Offers interactive controls for zooming, panning, and adjusting the visual properties of the images on the fly.
## Monitor Types
The Image Widget can handle different types of data, specified by the `monitor_type` parameter:
- **1D Monitor (`monitor_type='1d'`)**: Used for visualizing 1D waveform data. The widget collects incoming 1D data arrays and constructs a 2D image by stacking them, adjusting for varying lengths if necessary.
- **2D Monitor (`monitor_type='2d'`)**: Used for visualizing 2D image data directly from detectors like cameras.
By specifying the appropriate `monitor_type`, you can configure the Image Widget to handle data from different detectors and sources.
![Image 2D](./image_plot.gif)
![Image 2D](./image.gif)
````
````{tab} Examples - CLI
`ImageWidget` can be embedded in both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`. The command-line API is the same for all cases.
`ImageWidget` can be embedded in [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BEC Designer`. The command-line API is the same for all cases.
## Example 1 - Visualizing 2D Image Data from a Detector
In this example, we demonstrate how to add an `ImageWidget` to a [`BECFigure`](user.widgets.bec_figure) to visualize live 2D image data from a connected camera detector.
In this example, we demonstrate how to add an `ImageWidget` to a [`BECDockArea`](user.widgets.bec_dock_area) to visualize live 2D image data from a connected camera detector.
```python
# Add a new dock with BECFigure widget
fig = gui.add_dock().add_widget('BECFigure')
dock_area = gui.new()
img_widget = dock_area.new().new(gui.available_widgets.Image)
# Add an ImageWidget to the BECFigure for a 2D detector
img_widget = fig.image(monitor='eiger', monitor_type='2d')
img_widget.set_title("Camera Image - Eiger Detector")
img_widget.image(monitor='eiger', monitor_type='2d')
img_widget.title = "Camera Image - Eiger Detector"
```
## Example 2 - Visualizing 1D Waveform Data from a Detector
This example demonstrates how to set up the Image Widget to visualize 1D waveform data from a detector, such as a line detector or a spectrometer. The widget will stack incoming 1D data arrays to construct a 2D image.
This example demonstrates how to set up the Image widget to visualize 1D waveform data from a detector, such as a line detector or a spectrometer. The widget will stack incoming 1D data arrays to construct a 2D image.
```python
# Add an ImageWidget to the BECFigure for a 1D detector
img_widget = fig.image(monitor='line_detector', monitor_type='1d')
img_widget.set_title("Line Detector Data")
# Add a new dock with BECFigure widget
dock_area = gui.new()
img_widget = dock_area.new().new(gui.available_widgets.Image)
# Add an ImageWidget to the BECFigure for a 2D detector
img_widget.image(monitor='waveform', monitor_type='1d')
img_widget.title = "Line Detector Data"
# Optional: Set the color map and value range
img_widget.set_colormap("plasma")
img_widget.set_vrange(vmin=0, vmax=100)
img_widget.colormap = "plasma"
img_widget.vrange= [0, 100]
```
## Example 3 - Adding Image Widget as a Dock in BECDockArea
## Example 3 - Real-time Image Processing
Adding an `ImageWidget` into a [`BECDockArea`](user.widgets.bec_dock_area) is similar to adding any other widget. The widget has the same API as the one in [`BECFigure`](user.widgets.bec_figure); however, as an independent widget outside of `BECFigure`, it has its own toolbar, allowing users to configure the widget without needing CLI commands.
The `Image` provides real-time image processing capabilities, such as rotating, scaling, applying logarithmic scaling, and performing FFT on the displayed images. The following example demonstrates how to apply these transformations to an image.
```python
# Add an ImageWidget to the BECDockArea for a 2D detector
img_widget = gui.add_dock().add_widget('BECImageWidget')
# Visualize live data from a camera with a specified value range
img_widget.image(monitor='eiger', monitor_type='2d')
img_widget.set_vrange(vmin=0, vmax=100)
```
## Example 4 - Customizing Image Display
This example demonstrates how to customize the color map and scale bar for an image being visualized in an `ImageWidget`.
```python
# Set the color map and adjust the value range
img_widget.set_colormap("viridis")
img_widget.set_vrange(vmin=10, vmax=200)
```
## Example 5 - Real-time Image Processing
The `ImageWidget` provides real-time image processing capabilities, such as rotating, scaling, applying logarithmic scaling, and performing FFT on the displayed images. The following example demonstrates how to apply these transformations to an image.
```python
# Rotate the image by 90 degrees
img_widget.set_rotation(deg_90=1)
# Rotate the image by 90 degrees (1,2,3,4 are multiplied by 90 degrees)
img_widget.num_rotation_90 = 1
# Transpose the image
img_widget.set_transpose(enable=True)
img_widget.transpose = True
# Apply FFT to the image
img_widget.set_fft(enable=True)
img_widget.fft = True
# Set logarithmic scaling for the image display
img_widget.set_log(enable=True)
```
img_widget.log = True
# Set autorange for the image color map
img_widget.autorange = True
img_widget.autorange_mode = 'mean'# or 'max'
```
<!--
## Example 6 - Setting Up for Different Detectors
The Image Widget can be configured for different detectors by specifying the correct monitor name and monitor type. Here's how to set it up for various detectors:
@@ -121,13 +98,13 @@ img_widget.set_title("Line Detector Data")
```{note}
Since the Image Widget does not have prior information about the shape of incoming data, it is essential to specify the correct `monitor_type` when setting up the widget. This ensures that the data is processed and displayed correctly.
```
``` -->
````
````{tab} API
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECImageWidget.rst
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.Image.rst
```
````

View File

@@ -4,14 +4,14 @@
````{tab} Overview
The [`LMFit Dialog`](/api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog) is a widget that is developed to be used together with the [`BECWaveformWidget`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget). The `BECWaveformWidget` allows user to submit a fit request to BEC's [DAP server](https://bec.readthedocs.io/en/latest/developer/getting_started/architecture.html) choosing from a selection of [LMFit models](https://lmfit.github.io/lmfit-py/builtin_models.html#) to fit monitored data sources. The `LMFit Dialog` provides an interface to monitor these fits, including statistics and fit parameters in real time.
Within the `BECWaveformWidget`, the dialog is accessible via the toolbar and will be automatically linked to the current waveform widget. For a more customised use, we can embed the `LMFit Dialog` in a larger GUI using the *BEC Designer*. In this case, one has to connect the [`update_summary_tree`](/api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.rst#bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.update_summary_tree) slot of the LMFit Dialog to the [`dap_summary_update`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.rst#bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.dap_summary_update) signal of the BECWaveformWidget to ensure its functionality.
The [`LMFit Dialog`](/api_reference/_autosummary/bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog.LMFitDialog) is a widget that is developed to be used together with the [`Waveform`](/api_reference/_autosummary/bec_widgets.widgets.plots.waveform.waveform.Waveform) widget. The `Waveform` widget allows user to submit a fit request to BEC's [DAP server](https://bec.readthedocs.io/en/latest/developer/getting_started/architecture.html) choosing from a selection of [LMFit models](https://lmfit.github.io/lmfit-py/builtin_models.html#) to fit monitored data sources. The `LMFit Dialog` provides an interface to monitor these fits, including statistics and fit parameters in real time.
Within the `Waveform` widget, the dialog is accessible via the toolbar and will be automatically linked to the current waveform widget. For a more customised use, we can embed the `LMFit Dialog` in a larger GUI using the *BEC Designer*. In this case, one has to connect the [`update_summary_tree`](/api_reference/_autosummary/bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog.LMFitDialog.rst#bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.update_summary_tree) slot of the LMFit Dialog to the [`dap_summary_update`](/api_reference/_autosummary/bec_widgets.widgets.plots.waveform.waveform_widget.Waveform.rst#bec_widgets.widgets.plots.waveform.waveform.Waveform.dap_summary_update) signal of the Waveform widget to ensure its functionality.
## Key Features:
- **Fit Summary**: Display updates on LMFit DAP processes and fit statistics.
- **Fit Parameter**: Display current fit parameter.
- **BECWaveformWidget Integration**: Directly connect to BECWaveformWidget to display fit statistics and parameters.
- **`Waveform` Widget Integration**: Directly connect to `Waveform` widget to display fit statistics and parameters.
```{figure} /assets/widget_screenshots/lmfit_dialog.png
---
name: lmfit_dialog
@@ -20,15 +20,15 @@ LMFit Dialog
```
````
````{tab} Connect in BEC Designer
The `LMFit Dialog` widget can be connected to a `BECWaveformWidget` to display fit statistics and parameters from the LMFit DAP process hooked up to the waveform widget. You can use the signal/slot editor from the BEC Designer to connect the `dap_summary_update` signal of the BECWaveformWidget to the `update_summary_tree` slot of the LMFit Dialog.
The `LMFit Dialog` widget can be connected to a `Waveform` widget to display fit statistics and parameters from the LMFit DAP process hooked up to the waveform widget. You can use the signal/slot editor from the BEC Designer to connect the `dap_summary_update` signal of the `Waveform` widget to the `update_summary_tree` slot of the LMFit Dialog.
```{figure} /assets/widget_screenshots/lmfit_dialog_connect.png
````
````{tab} Connect in Python
It is also possible to directly connect the `dap_summary_update` signal of the BECWaveformWidget to the `update_summary_tree` slot of the LMFit Dialog in Python.
It is also possible to directly connect the `dap_summary_update` signal of the `Waveform` widget to the `update_summary_tree` slot of the LMFit Dialog in Python.
```python
waveform = BECWaveformWidget(...)
waveform = Waveform(...)
lmfit_dialog = LMFitDialog(...)
waveform.dap_summary_update.connect(lmfit_dialog.update_summary_tree)

View File

@@ -7,7 +7,7 @@
The Motor Map Widget is a specialized tool for tracking and visualizing the positions of motors in real-time. This widget is crucial for applications requiring precise alignment and movement tracking during scans. It provides an intuitive way to monitor motor trajectories, ensuring accurate positioning throughout the scanning process.
## Key Features:
- **Flexible Integration**: The widget can be integrated into both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`.
- **Flexible Integration**: The widget can be integrated into a [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BEC Designer`.
- **Real-time Motor Position Visualization**: Tracks motor positions in real-time and visually represents motor trajectories.
- **Customizable Visual Elements**: The appearance of all widget components is fully customizable, including scatter size and background values.
- **Interactive Controls**: Interactive controls for zooming, panning, and adjusting the visual properties of motor trajectories on the fly.
@@ -16,51 +16,39 @@ The Motor Map Widget is a specialized tool for tracking and visualizing the posi
````
````{tab} Examples CLI
`MotorMapWidget` can be embedded in both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`. However, the command-line API is the same for all cases.
`MotorMapWidget` can be embedded in [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BEC Designer`. However, the command-line API is the same for all cases.
## Example 1 - Adding Motor Map Widget to BECFigure
## Example 1 - Adding Motor Map Widget as a Dock in BECDockArea
In this example, we will demonstrate how to add two different `MotorMapWidgets` into a single [`BECFigure`](user.widgets.bec_figure) widget.
```python
# Add new dock with BECFigure widget
fig = gui.add_dock().add_widget('BECFigure')
# Add two WaveformWidgets to the BECFigure
mm1 = fig.motor_map(motor_x='samx', motor_y='samy')
mm2 = fig.motor_map(motor_x='aptrx', motor_y='aptry',new=True)
```
## Example 2 - Adding Motor Map Widget as a Dock in BECDockArea
Adding `MotorMapWidget` into a [`BECDockArea`](user.widgets.bec_dock_area) is similar to adding any other widget. The widget has the same API as the one in BECFigure; however, as an independent widget outside BECFigure, it has its own toolbar, allowing users to configure the widget without needing CLI commands.
Adding `MotorMapWidget` into a [`BECDockArea`](user.widgets.bec_dock_area) is similar to adding any other widget.
```python
# Add new MotorMaps to the BECDockArea
mm1 = gui.add_dock().add_widget('BECMotorMapWidget')
mm2 = gui.add_dock().add_widget('BECMotorMapWidget')
dock_area = gui.new()
mm1 = dock_area.new().new(gui.available_widgets.MotorMap)
mm2 = dock_area.new().new(gui.available_widgets.MotorMap)
# Add signals to the MotorMaps
mm1.change_motors(motor_x='samx', motor_y='samy')
mm2.change_motors(motor_x='aptrx', motor_y='aptry')
mm1.map(x_name='samx', y_name='samy')
mm2.map(x_name='aptrx', y_name='aptry')
```
## Example 3 - Customizing Motor Map Display
## Example 2 - Customizing Motor Map Display
The `MotorMapWidget` allows customization of its visual elements to better suit the needs of your application. Below is an example of how to adjust the scatter size, set background values, and limit the number of points displayed from the position buffer.
```python
# Set scatter size
mm1.set_scatter_size(scatter_size=5)
mm1.scatter_size = 10
# Set background value
mm1.set_background_value(background_value=0)
# Set background value (between 0 and 100)
mm1.background_value = 0
# Limit the number of points displayed and saved in the position buffer
mm1.set_max_points(max_points=500)
mm1.max_points = 500
```
## Example 4 - Changing Motors and Resetting History
## Example 3 - Changing Motors and Resetting History
You can dynamically change the motors being tracked and reset the history of the motor trajectories during the session.
@@ -69,12 +57,12 @@ You can dynamically change the motors being tracked and reset the history of the
mm1.reset_history()
# Change the motors being tracked
mm1.change_motors(motor_x='aptrx', motor_y='aptry')
mm1.map(x_name='aptrx', y_name='aptry')
```
````
````{tab} API
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECMotorMap.rst
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.MotorMap.rst
```
````

Some files were not shown because too many files have changed in this diff Show More