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

Compare commits

...

87 Commits

Author SHA1 Message Date
1bfb358829 feat: available resources basic structure 2025-08-21 15:47:41 +02:00
591c2a7438 feat: initial commit DeviceConfigWidget 2025-08-18 14:10:12 +02:00
semantic-release
a2f8880459 2.35.0
Automatically generated by python-semantic-release
2025-08-14 07:16:53 +00:00
926d722955 feat(property_manager): property manager widget 2025-08-14 09:16:04 +02:00
44ba7201b4 build: PySide6 upgraded to 6.9.0 2025-08-12 19:56:29 +02:00
semantic-release
0717426db2 2.34.0
Automatically generated by python-semantic-release
2025-08-07 13:39:47 +00:00
f4af6ebc5f fix: use better source for plugin repo name 2025-08-07 15:39:07 +02:00
a923f12c97 feat: autoformat compiled file and add docs 2025-08-07 15:39:07 +02:00
a5a7607a83 tests: add tests for widget creator 2025-08-07 15:39:07 +02:00
9de548446b fix: plugin widget import machinery
- lazy import client so plugin widgets can import BECWidgets which use
  it indirectly
- exclude classes originating from bec_widgets core from plugin
  discovery
- better errors
2025-08-07 15:39:07 +02:00
49ac7decf7 feat(plugin manager): add cli commands 2025-08-07 15:39:07 +02:00
semantic-release
092bed38fa 2.33.3
Automatically generated by python-semantic-release
2025-07-31 11:10:38 +00:00
50c84a766a refactor(scan-history): add spinner for loading time of history 2025-07-31 13:09:47 +02:00
d22a3317ba refactor: use client callback for scan history reload 2025-07-31 13:09:47 +02:00
6df1d0c31f fix(scan-history-view): account for async loading of scan history 2025-07-31 13:09:47 +02:00
946752a4b0 refactor(scan-history): fix insert logic; cleanup 2025-07-31 13:09:47 +02:00
c1f62ad6cb refactor: make ids a set, cleanup 2025-07-31 13:09:47 +02:00
a5adf3a97d refactor: improve scan history performance on loading full scan lists 2025-07-31 13:09:47 +02:00
semantic-release
76e3e0b60f 2.33.2
Automatically generated by python-semantic-release
2025-07-31 07:27:50 +00:00
f18eeb9c5d fix: don't warn on empty DeviceEdit init 2025-07-31 09:26:59 +02:00
32ce8e2818 fix: remove config, directly set device+signal 2025-07-31 09:26:59 +02:00
23413cffab fix: delete choice dialog on close 2025-07-31 09:26:59 +02:00
David Perl
4bbb8fa519 fix: display short lists in SignalDisplay 2025-07-31 09:26:59 +02:00
semantic-release
a972369a72 2.33.1
Automatically generated by python-semantic-release
2025-07-31 06:50:30 +00:00
cd81e7f9ba fix(cli): ensure guis are not started twice 2025-07-31 08:49:48 +02:00
semantic-release
e2b8118f67 2.33.0
Automatically generated by python-semantic-release
2025-07-29 13:24:20 +00:00
5f925ba4e3 build: update bec and qtmonaco min dependencies 2025-07-29 15:23:36 +02:00
fc68d2cf2d feat(monaco): add insert, delete and lsp header 2025-07-29 15:23:36 +02:00
627b49b33a feat(monaco): add vim mode 2025-07-29 15:23:36 +02:00
a51ef04cdf fix(monaco): forward text changed signal 2025-07-29 15:23:36 +02:00
40f4bce285 test(web console): add tests for the web console 2025-07-29 15:23:36 +02:00
2b9fe6c959 feat(web console): add signal to indicate when the js backend is initialized 2025-07-29 15:23:36 +02:00
c2e16429c9 feat(web console): add set_readonly method 2025-07-29 15:23:36 +02:00
semantic-release
85ce2aa136 2.32.0
Automatically generated by python-semantic-release
2025-07-29 13:09:07 +00:00
fd5af01842 feat(dock area): add screenshot toolbar action 2025-07-29 15:08:17 +02:00
8a214c8978 feat(rpc_timeout): add decorator to override the rpc timeout 2025-07-29 15:08:17 +02:00
semantic-release
f3214445f2 2.31.3
Automatically generated by python-semantic-release
2025-07-29 12:57:40 +00:00
6bf84aea25 fix(waveform): fallback mechanism for auto mode to use index if scan_report_devices are not available 2025-07-29 14:56:54 +02:00
semantic-release
aace071f11 2.31.2
Automatically generated by python-semantic-release
2025-07-29 12:05:13 +00:00
bf86a030a0 fix(bec widgets): always call cleanup of child widgets on cleanup 2025-07-29 14:04:24 +02:00
semantic-release
358c979bf2 2.31.1
Automatically generated by python-semantic-release
2025-07-29 09:19:55 +00:00
c1bdc506e8 fix(image_base): fix cleanup of uninitialized image layer 2025-07-29 11:19:07 +02:00
semantic-release
4febfb79df 2.31.0
Automatically generated by python-semantic-release
2025-07-29 07:02:55 +00:00
0854175acb test(launch_window): MainWindow raise test removed, features is supported now 2025-07-29 09:01:01 +02:00
e090ac49b7 fix(launch_window): logic for custom main window apps adjusted 2025-07-29 09:01:01 +02:00
e4521d9528 feat(bec_main_window): plugin and rpc created 2025-07-29 09:01:01 +02:00
1d0490fff4 fix(bec_main_window): main window have unified status bar on macOS 2025-07-29 09:01:01 +02:00
10cbb9a05c refactor(widgets): all plugins regenerated 2025-07-29 09:01:01 +02:00
7073e75adf fix(scan_progressbar): added kwargs to init 2025-07-29 09:01:01 +02:00
e42ffd7c01 fix(color_button_native): removed BECWidget inheritance 2025-07-29 09:01:01 +02:00
2bd6d00899 fix(decimal_spinbox): removed BECWidget inheritance 2025-07-29 09:01:01 +02:00
c2a918ef4b fix(plugin_utils): plugins can be created from QWidgets, no need for BECWidget base class for plugin creation 2025-07-29 09:01:01 +02:00
6bbf5126cf fix(widgets): added missing __init__ files 2025-07-29 09:01:01 +02:00
728d4efd96 fix(utils): plugin template createWidget do not initialise widgets by default 2025-07-29 09:01:01 +02:00
semantic-release
7926969996 2.30.6
Automatically generated by python-semantic-release
2025-07-26 12:44:29 +00:00
61e5bde15f fix(waveform): autorange is applied with 150ms delay after curve is added 2025-07-26 14:43:51 +02:00
semantic-release
c8aa770de3 2.30.5
Automatically generated by python-semantic-release
2025-07-25 17:44:39 +00:00
4d5df9608a refactor(positioner-box): cleanup, accept float precision 2025-07-25 19:43:52 +02:00
b718b438ba fix(positioner-box): Test to fix handling of none integer values for precision 2025-07-25 19:43:52 +02:00
semantic-release
2f978c93c4 2.30.4
Automatically generated by python-semantic-release
2025-07-25 10:18:28 +00:00
b4e0664011 fix(cli): remove stderr from cli output when not using rpc 2025-07-25 12:17:44 +02:00
semantic-release
45fbf4015d 2.30.3
Automatically generated by python-semantic-release
2025-07-23 08:01:36 +00:00
David Perl
0d81bdd4dd fix: cleanup subscriptions in device browser 2025-07-23 10:00:43 +02:00
semantic-release
bb4c30ad80 2.30.2
Automatically generated by python-semantic-release
2025-07-23 06:57:35 +00:00
3fd09fceef test(test_plotting_framework_e2e): added test for waveform with passing device from dev container 2025-07-23 08:56:52 +02:00
8eb8225a7f fix: factor out device name function and add test 2025-07-23 08:56:52 +02:00
491d04467c fix(rpc_base): rpc_call wrapper passes full_name for Devices indeed of name 2025-07-23 08:56:52 +02:00
semantic-release
3bcff75107 2.30.1
Automatically generated by python-semantic-release
2025-07-22 18:19:10 +00:00
608590c542 fix: ignore KeyError in SignalLabel 2025-07-22 20:18:28 +02:00
semantic-release
012f7cf970 2.30.0
Automatically generated by python-semantic-release
2025-07-22 14:24:47 +00:00
cd17a4aad9 fix(signal_label): rewrite reading selection logic 2025-07-22 15:24:03 +01:00
f0dc992586 fix(signal_label): use read() instead of get() for init 2025-07-22 15:24:03 +01:00
fd1f9941e0 chore: update client.py 2025-07-22 15:24:03 +01:00
3384ca02bd fix(device_browser): display signal for signals 2025-07-22 15:24:03 +01:00
959cedbbd5 fix(signal_label): update signal from dialog correctly 2025-07-22 15:24:03 +01:00
ca4f97503b feat(signal_label): property to display array data or not 2025-07-22 15:24:03 +01:00
22beadcad0 fix(signal_label): show all signals by default 2025-07-22 15:24:03 +01:00
b9af36a4f1 fix(device_signal_display): don't read omitted 2025-07-22 15:24:03 +01:00
semantic-release
bdff736aa2 2.29.0
Automatically generated by python-semantic-release
2025-07-22 11:39:06 +00:00
7cda2ed846 refactor(notification_banner): BECNotificationBroker done as singleton to sync all windows in the session 2025-07-22 13:38:23 +02:00
cd9d22d0b4 feat(notification_banner): notification centre for alarms implemented into BECMainWindow 2025-07-22 13:38:23 +02:00
semantic-release
37b80e16a0 2.28.0
Automatically generated by python-semantic-release
2025-07-21 12:23:48 +00:00
7f0098f153 feat: save and load config from devicebrowser 2025-07-21 14:23:01 +02:00
8489ef4a69 feat: remove and readd device for config changes 2025-07-21 14:23:01 +02:00
13976557fb feat: disable editing while scan active 2025-07-21 14:23:01 +02:00
semantic-release
06ad87ce0a 2.27.1
Automatically generated by python-semantic-release
2025-07-17 13:22:03 +00:00
00e3713181 fix(image_roi_tree): rois signals are disconnected when roi tree widget is closed 2025-07-17 15:21:11 +02:00
149 changed files with 6157 additions and 467 deletions

View File

@@ -1,6 +1,334 @@
# CHANGELOG
## v2.35.0 (2025-08-14)
### Build System
- Pyside6 upgraded to 6.9.0
([`44ba720`](https://github.com/bec-project/bec_widgets/commit/44ba7201b4914d63281bbed5e62d07e5c240595a))
### Features
- **property_manager**: Property manager widget
([`926d722`](https://github.com/bec-project/bec_widgets/commit/926d7229559d189d382fe034b3afbc544e709efa))
## v2.34.0 (2025-08-07)
### Bug Fixes
- Plugin widget import machinery
([`9de5484`](https://github.com/bec-project/bec_widgets/commit/9de548446b9975c0f692757c66ffa07b9a849f15))
- lazy import client so plugin widgets can import BECWidgets which use it indirectly - exclude
classes originating from bec_widgets core from plugin discovery - better errors
- Use better source for plugin repo name
([`f4af6eb`](https://github.com/bec-project/bec_widgets/commit/f4af6ebc5fabf5b62ec87b580476d93d52690b08))
### Features
- Autoformat compiled file and add docs
([`a923f12`](https://github.com/bec-project/bec_widgets/commit/a923f12c974192909222fcada9eca97325866d74))
- **plugin manager**: Add cli commands
([`49ac7de`](https://github.com/bec-project/bec_widgets/commit/49ac7decf7d4cf461e6437f7285dc6967ee36d96))
## v2.33.3 (2025-07-31)
### Bug Fixes
- **scan-history-view**: Account for async loading of scan history
([`6df1d0c`](https://github.com/bec-project/bec_widgets/commit/6df1d0c31fb58c25b01e95e2247277ff2dd5d00e))
### Refactoring
- Improve scan history performance on loading full scan lists
([`a5adf3a`](https://github.com/bec-project/bec_widgets/commit/a5adf3a97d9ff05cef833445c1e6cd8f35a9a2fa))
- Make ids a set, cleanup
([`c1f62ad`](https://github.com/bec-project/bec_widgets/commit/c1f62ad6cb00d9b392a8e0b6247f5260dfb37256))
- Use client callback for scan history reload
([`d22a331`](https://github.com/bec-project/bec_widgets/commit/d22a3317baeccfcc4e074dcef4e3912301d210c5))
- **scan-history**: Add spinner for loading time of history
([`50c84a7`](https://github.com/bec-project/bec_widgets/commit/50c84a766a2b021768fb2c0e8ee00b8e5f058ba7))
- **scan-history**: Fix insert logic; cleanup
([`946752a`](https://github.com/bec-project/bec_widgets/commit/946752a4b05804c2f59cb5c21e4c1d11709a7d44))
## v2.33.2 (2025-07-31)
### Bug Fixes
- Delete choice dialog on close
([`23413cf`](https://github.com/bec-project/bec_widgets/commit/23413cffabe721e35bb5bb726ec34d74dc4ffe05))
- Display short lists in SignalDisplay
([`4bbb8fa`](https://github.com/bec-project/bec_widgets/commit/4bbb8fa519e8a90eebfcfa34e157493c9baa7880))
- Don't warn on empty DeviceEdit init
([`f18eeb9`](https://github.com/bec-project/bec_widgets/commit/f18eeb9c5dccbd9348b6ee6d1477a8b7925d40fc))
- Remove config, directly set device+signal
([`32ce8e2`](https://github.com/bec-project/bec_widgets/commit/32ce8e2818ceacda87e48399e3ed4df0cabb2335))
## v2.33.1 (2025-07-31)
### Bug Fixes
- **cli**: Ensure guis are not started twice
([`cd81e7f`](https://github.com/bec-project/bec_widgets/commit/cd81e7f9ba40be23f6b930d250f743276720b277))
## v2.33.0 (2025-07-29)
### Bug Fixes
- **monaco**: Forward text changed signal
([`a51ef04`](https://github.com/bec-project/bec_widgets/commit/a51ef04cdf0ac8abdb7008d78b13c75b86ce9e06))
### Build System
- Update bec and qtmonaco min dependencies
([`5f925ba`](https://github.com/bec-project/bec_widgets/commit/5f925ba4e3840219e4473d6346ece6746076f718))
### Features
- **monaco**: Add insert, delete and lsp header
([`fc68d2c`](https://github.com/bec-project/bec_widgets/commit/fc68d2cf2d6b161d8e3b9fc9daf6185d9197deba))
- **monaco**: Add vim mode
([`627b49b`](https://github.com/bec-project/bec_widgets/commit/627b49b33a30e45b2bfecb57f090eecfa31af09d))
- **web console**: Add set_readonly method
([`c2e1642`](https://github.com/bec-project/bec_widgets/commit/c2e16429c91de7cc0e672ba36224e9031c1c4234))
- **web console**: Add signal to indicate when the js backend is initialized
([`2b9fe6c`](https://github.com/bec-project/bec_widgets/commit/2b9fe6c9590c8d18b7542307273176e118828681))
### Testing
- **web console**: Add tests for the web console
([`40f4bce`](https://github.com/bec-project/bec_widgets/commit/40f4bce2854bcf333ce261229bd1703b80ced538))
## v2.32.0 (2025-07-29)
### Features
- **dock area**: Add screenshot toolbar action
([`fd5af01`](https://github.com/bec-project/bec_widgets/commit/fd5af0184279400ca6d8e5d2042f31be88d180f3))
- **rpc_timeout**: Add decorator to override the rpc timeout
([`8a214c8`](https://github.com/bec-project/bec_widgets/commit/8a214c897899d0d94d5f262591a001c127d1b155))
## v2.31.3 (2025-07-29)
### Bug Fixes
- **waveform**: Fallback mechanism for auto mode to use index if scan_report_devices are not
available
([`6bf84ae`](https://github.com/bec-project/bec_widgets/commit/6bf84aea2508ff01fe201c045ec055684da88593))
## v2.31.2 (2025-07-29)
### Bug Fixes
- **bec widgets**: Always call cleanup of child widgets on cleanup
([`bf86a03`](https://github.com/bec-project/bec_widgets/commit/bf86a030a08b325a08e031ff71d0716a2f2f122b))
## v2.31.1 (2025-07-29)
### Bug Fixes
- **image_base**: Fix cleanup of uninitialized image layer
([`c1bdc50`](https://github.com/bec-project/bec_widgets/commit/c1bdc506e8099f178acdccbe0e1109deeeaaca38))
## v2.31.0 (2025-07-29)
### Bug Fixes
- **bec_main_window**: Main window have unified status bar on macOS
([`1d0490f`](https://github.com/bec-project/bec_widgets/commit/1d0490fff428d51f2cdb7d35a954a7cd62cbb65c))
- **color_button_native**: Removed BECWidget inheritance
([`e42ffd7`](https://github.com/bec-project/bec_widgets/commit/e42ffd7c015a026d8e0967ac6b5866cbbea7bfed))
- **decimal_spinbox**: Removed BECWidget inheritance
([`2bd6d00`](https://github.com/bec-project/bec_widgets/commit/2bd6d0089955172134afb4d39939890026ed43f0))
- **launch_window**: Logic for custom main window apps adjusted
([`e090ac4`](https://github.com/bec-project/bec_widgets/commit/e090ac49b72fa15ebf1c09164ff3c6de577cb939))
- **plugin_utils**: Plugins can be created from QWidgets, no need for BECWidget base class for
plugin creation
([`c2a918e`](https://github.com/bec-project/bec_widgets/commit/c2a918ef4b77ccd7fa43d1bc0b907d55a17a6c95))
- **scan_progressbar**: Added kwargs to init
([`7073e75`](https://github.com/bec-project/bec_widgets/commit/7073e75adf0eeb81f4f8e27eb99fc1b7a395c751))
- **utils**: Plugin template createWidget do not initialise widgets by default
([`728d4ef`](https://github.com/bec-project/bec_widgets/commit/728d4efd9646ffcecd7d1a2f70988a7d7c799124))
- **widgets**: Added missing __init__ files
([`6bbf512`](https://github.com/bec-project/bec_widgets/commit/6bbf5126cf586063ed08d6cd489d6a9af28eac35))
### Features
- **bec_main_window**: Plugin and rpc created
([`e4521d9`](https://github.com/bec-project/bec_widgets/commit/e4521d95286bbc598c3c05f357d247d950477b71))
### Refactoring
- **widgets**: All plugins regenerated
([`10cbb9a`](https://github.com/bec-project/bec_widgets/commit/10cbb9a05cb96a791448caff4ffc4115b76146d7))
### Testing
- **launch_window**: Mainwindow raise test removed, features is supported now
([`0854175`](https://github.com/bec-project/bec_widgets/commit/0854175acbda1d4de71358aec028539552a26448))
## v2.30.6 (2025-07-26)
### Bug Fixes
- **waveform**: Autorange is applied with 150ms delay after curve is added
([`61e5bde`](https://github.com/bec-project/bec_widgets/commit/61e5bde15f0e1ebe185ddbe81cd71ad581ae6009))
## v2.30.5 (2025-07-25)
### Bug Fixes
- **positioner-box**: Test to fix handling of none integer values for precision
([`b718b43`](https://github.com/bec-project/bec_widgets/commit/b718b438bacff6eb6cd6015f1a67dcf75c05dce4))
### Refactoring
- **positioner-box**: Cleanup, accept float precision
([`4d5df96`](https://github.com/bec-project/bec_widgets/commit/4d5df9608a9438b9f6d7508c323eb3772e53f37d))
## v2.30.4 (2025-07-25)
### Bug Fixes
- **cli**: Remove stderr from cli output when not using rpc
([`b4e0664`](https://github.com/bec-project/bec_widgets/commit/b4e0664011682cae9966aa2632210a6b60e11714))
## v2.30.3 (2025-07-23)
### Bug Fixes
- Cleanup subscriptions in device browser
([`0d81bdd`](https://github.com/bec-project/bec_widgets/commit/0d81bdd4ddb4ec474a414b107cbc7fc865253934))
## v2.30.2 (2025-07-23)
### Bug Fixes
- Factor out device name function and add test
([`8eb8225`](https://github.com/bec-project/bec_widgets/commit/8eb8225a7f56014d6093aa142b3a5d071837982e))
- **rpc_base**: Rpc_call wrapper passes full_name for Devices indeed of name
([`491d044`](https://github.com/bec-project/bec_widgets/commit/491d04467c8ce4e116d61e614895d1dcc6b4b201))
### Testing
- **test_plotting_framework_e2e**: Added test for waveform with passing device from dev container
([`3fd09fc`](https://github.com/bec-project/bec_widgets/commit/3fd09fceef2ffa7e7c3eee20176304bafb00d0db))
## v2.30.1 (2025-07-22)
### Bug Fixes
- Ignore KeyError in SignalLabel
([`608590c`](https://github.com/bec-project/bec_widgets/commit/608590c5421368d5bba0e4b0f5187d90cac323be))
## v2.30.0 (2025-07-22)
### Bug Fixes
- **device_browser**: Display signal for signals
([`3384ca0`](https://github.com/bec-project/bec_widgets/commit/3384ca02bdb5a2798ad3339ecf3e2ba7c121e28f))
- **device_signal_display**: Don't read omitted
([`b9af36a`](https://github.com/bec-project/bec_widgets/commit/b9af36a4f1c91e910d4fc738b17b90e92287a7e3))
- **signal_label**: Rewrite reading selection logic
([`cd17a4a`](https://github.com/bec-project/bec_widgets/commit/cd17a4aad905296eb0460ecc27e5920f5c2e8fe5))
- **signal_label**: Show all signals by default
([`22beadc`](https://github.com/bec-project/bec_widgets/commit/22beadcad061b328c986414f30fef57b64bad693))
- **signal_label**: Update signal from dialog correctly
([`959cedb`](https://github.com/bec-project/bec_widgets/commit/959cedbbd5a123eef5f3370287bf6476c48caab9))
- **signal_label**: Use read() instead of get() for init
([`f0dc992`](https://github.com/bec-project/bec_widgets/commit/f0dc99258607a5cc8af51686d01f7fd54ae2779f))
### Chores
- Update client.py
([`fd1f994`](https://github.com/bec-project/bec_widgets/commit/fd1f9941e046b7ae1e247dde39c20bcbc37ac189))
### Features
- **signal_label**: Property to display array data or not
([`ca4f975`](https://github.com/bec-project/bec_widgets/commit/ca4f97503bf06363e8e8a5d494a9857223da4104))
## v2.29.0 (2025-07-22)
### Features
- **notification_banner**: Notification centre for alarms implemented into BECMainWindow
([`cd9d22d`](https://github.com/bec-project/bec_widgets/commit/cd9d22d0b40d633af76cb1188b57feb7b6a5dbf2))
### Refactoring
- **notification_banner**: Becnotificationbroker done as singleton to sync all windows in the
session
([`7cda2ed`](https://github.com/bec-project/bec_widgets/commit/7cda2ed846d3c27799f4f15f6c5c667631b1ca55))
## v2.28.0 (2025-07-21)
### Features
- Disable editing while scan active
([`1397655`](https://github.com/bec-project/bec_widgets/commit/13976557fbdb71a1161029521d81a655d25dd134))
- Remove and readd device for config changes
([`8489ef4`](https://github.com/bec-project/bec_widgets/commit/8489ef4a69d69b39648b1a9270012f14f95c6121))
- Save and load config from devicebrowser
([`7f0098f`](https://github.com/bec-project/bec_widgets/commit/7f0098f1533d419cc75801c4d6cbea485c7bbf94))
## v2.27.1 (2025-07-17)
### Bug Fixes
- **image_roi_tree**: Rois signals are disconnected when roi tree widget is closed
([`00e3713`](https://github.com/bec-project/bec_widgets/commit/00e3713181916a432e4e9dec8a0d80205914cf77))
## v2.27.0 (2025-07-17)
### Features

View File

@@ -31,7 +31,7 @@ from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, UILaunchWindow
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
if TYPE_CHECKING: # pragma: no cover
@@ -395,20 +395,24 @@ class LaunchWindow(BECMainWindow):
if isinstance(result_widget, BECMainWindow):
result_widget.show()
else:
window = BECMainWindow()
window = BECMainWindowNoRPC()
window.setCentralWidget(result_widget)
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
window.show()
return result_widget
def _launch_custom_ui_file(self, ui_file: str | None) -> BECMainWindow:
# Load the custom UI file
"""
Load a custom .ui file. If the top-level widget is a MainWindow subclass,
instantiate it directly; otherwise, embed it in a UILaunchWindow.
"""
if ui_file is None:
raise ValueError("UI file must be provided for custom UI file launch.")
filename = os.path.basename(ui_file).split(".")[0]
WidgetContainerUtils.raise_for_invalid_name(filename)
# Parse the UI to detect top-level widget class
tree = ET.parse(ui_file)
root = tree.getroot()
# Check if the top-level widget is a QMainWindow
@@ -416,19 +420,22 @@ class LaunchWindow(BECMainWindow):
if widget is None:
raise ValueError("No widget found in the UI file.")
if widget.attrib.get("class") == "QMainWindow":
raise ValueError(
"Loading a QMainWindow from a UI file is currently not supported. "
"If you need this, please contact the BEC team or create a ticket on gitlab.psi.ch/bec/bec_widgets."
)
# Load the UI into a widget
loader = UILoader(None)
loaded = loader.loader(ui_file)
# Display the UI in a BECMainWindow
if isinstance(loaded, BECMainWindow):
window = loaded
window.object_name = filename
else:
window = BECMainWindow(object_name=filename)
window.setCentralWidget(loaded)
window = UILaunchWindow(object_name=filename)
QApplication.processEvents()
result_widget = UILoader(window).loader(ui_file)
window.setCentralWidget(result_widget)
window.setWindowTitle(f"BEC - {window.object_name}")
window.setWindowTitle(f"BEC - {filename}")
window.show()
logger.info(f"Object name of new instance: {result_widget.objectName()}, {window.gui_id}")
logger.info(f"Launched custom UI: {filename}, type: {type(window).__name__}")
return window
def _launch_auto_update(self, auto_update: str) -> AutoUpdates:
@@ -451,7 +458,7 @@ class LaunchWindow(BECMainWindow):
WidgetContainerUtils.raise_for_invalid_name(name)
window = BECMainWindow()
window = BECMainWindowNoRPC()
widget_instance = widget(root_widget=True, object_name=name)
assert isinstance(widget_instance, QWidget)

View File

@@ -12,7 +12,7 @@ from typing import Literal, Optional
from bec_lib.logger import bec_logger
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module
logger = bec_logger.logger
@@ -29,6 +29,7 @@ class _WidgetsEnumType(str, enum.Enum):
_Widgets = {
"AbortButton": "AbortButton",
"BECDockArea": "BECDockArea",
"BECMainWindow": "BECMainWindow",
"BECProgressBar": "BECProgressBar",
"BECQueue": "BECQueue",
"BECStatusBox": "BECStatusBox",
@@ -48,11 +49,13 @@ _Widgets = {
"PositionerBox": "PositionerBox",
"PositionerBox2D": "PositionerBox2D",
"PositionerControlLine": "PositionerControlLine",
"PositionerGroup": "PositionerGroup",
"ResetButton": "ResetButton",
"ResumeButton": "ResumeButton",
"RingProgressBar": "RingProgressBar",
"SBBMonitor": "SBBMonitor",
"ScanControl": "ScanControl",
"ScanProgressBar": "ScanProgressBar",
"ScatterWaveform": "ScatterWaveform",
"SignalComboBox": "SignalComboBox",
"SignalLabel": "SignalLabel",
@@ -411,6 +414,13 @@ class BECDockArea(RPCBase):
dict: The state of the dock area.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@rpc_call
def restore_state(
self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom"
@@ -425,6 +435,14 @@ class BECDockArea(RPCBase):
"""
class BECMainWindow(RPCBase):
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class BECProgressBar(RPCBase):
"""A custom progress bar with smooth transitions. The displayed text can be customized using a template."""
@@ -1415,6 +1433,13 @@ class Heatmap(RPCBase):
Minimum decimal places for crosshair when dynamic precision is enabled.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@property
@rpc_call
def color_map(self) -> "str":
@@ -1953,6 +1978,13 @@ class Image(RPCBase):
Minimum decimal places for crosshair when dynamic precision is enabled.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@property
@rpc_call
def color_map(self) -> "str":
@@ -2437,6 +2469,26 @@ class MonacoWidget(RPCBase):
Get the current text from the Monaco editor.
"""
@rpc_call
def insert_text(self, text: str, line: int | None = None, column: int | None = None) -> None:
"""
Insert text at the current cursor position or at a specified line and column.
Args:
text (str): The text to insert.
line (int, optional): The line number (1-based) to insert the text at. Defaults to None.
column (int, optional): The column number (1-based) to insert the text at. Defaults to None.
"""
@rpc_call
def delete_line(self, line: int | None = None) -> None:
"""
Delete a line in the Monaco editor.
Args:
line (int, optional): The line number (1-based) to delete. If None, the current line will be deleted.
"""
@rpc_call
def set_language(self, language: str) -> None:
"""
@@ -2510,6 +2562,34 @@ class MonacoWidget(RPCBase):
enabled (bool): If True, the minimap will be enabled; otherwise, it will be disabled.
"""
@rpc_call
def set_vim_mode_enabled(self, enabled: bool) -> None:
"""
Enable or disable Vim mode in the Monaco editor.
Args:
enabled (bool): If True, Vim mode will be enabled; otherwise, it will be disabled.
"""
@rpc_call
def set_lsp_header(self, header: str) -> None:
"""
Set the LSP (Language Server Protocol) header for the Monaco editor.
The header is used to provide context for language servers but is not displayed in the editor.
Args:
header (str): The LSP header to set.
"""
@rpc_call
def get_lsp_header(self) -> str:
"""
Get the current LSP header set in the Monaco editor.
Returns:
str: The LSP header.
"""
class MotorMap(RPCBase):
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
@@ -2785,6 +2865,13 @@ class MotorMap(RPCBase):
The font size of the legend font.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@property
@rpc_call
def color(self) -> "tuple":
@@ -3190,6 +3277,13 @@ class MultiWaveform(RPCBase):
Minimum decimal places for crosshair when dynamic precision is enabled.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@property
@rpc_call
def highlighted_index(self):
@@ -3404,6 +3498,13 @@ class PositionerBox(RPCBase):
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
class PositionerBox2D(RPCBase):
"""Simple Widget to control two positioners in box form"""
@@ -3426,6 +3527,13 @@ class PositionerBox2D(RPCBase):
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
class PositionerControlLine(RPCBase):
"""A widget that controls a single device."""
@@ -3439,6 +3547,13 @@ class PositionerControlLine(RPCBase):
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
class PositionerGroup(RPCBase):
"""Simple Widget to control a positioner in box form"""
@@ -3897,6 +4012,13 @@ class ScanControl(RPCBase):
Cleanup the BECConnector
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
class ScanProgressBar(RPCBase):
"""Widget to display a progress bar that is hooked up to the scan progress of a scan."""
@@ -4205,6 +4327,13 @@ class ScatterWaveform(RPCBase):
Minimum decimal places for crosshair when dynamic precision is enabled.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@property
@rpc_call
def main_curve(self) -> "ScatterCurve":
@@ -4378,6 +4507,76 @@ class SignalLabel(RPCBase):
Show the button to select the signal to display
"""
@property
@rpc_call
def show_hinted_signals(self) -> "bool":
"""
In the signal selection menu, show hinted signals
"""
@show_hinted_signals.setter
@rpc_call
def show_hinted_signals(self) -> "bool":
"""
In the signal selection menu, show hinted signals
"""
@property
@rpc_call
def show_normal_signals(self) -> "bool":
"""
In the signal selection menu, show normal signals
"""
@show_normal_signals.setter
@rpc_call
def show_normal_signals(self) -> "bool":
"""
In the signal selection menu, show normal signals
"""
@property
@rpc_call
def show_config_signals(self) -> "bool":
"""
In the signal selection menu, show config signals
"""
@show_config_signals.setter
@rpc_call
def show_config_signals(self) -> "bool":
"""
In the signal selection menu, show config signals
"""
@property
@rpc_call
def display_array_data(self) -> "bool":
"""
Displays the full data from array signals if set to True.
"""
@display_array_data.setter
@rpc_call
def display_array_data(self) -> "bool":
"""
Displays the full data from array signals if set to True.
"""
@property
@rpc_call
def max_list_display_len(self) -> "int":
"""
For small lists, the max length to display
"""
@max_list_display_len.setter
@rpc_call
def max_list_display_len(self) -> "int":
"""
For small lists, the max length to display
"""
class SignalLineEdit(RPCBase):
"""Line edit widget for device input with autocomplete for device names."""
@@ -4453,14 +4652,6 @@ class TextBox(RPCBase):
"""
class UILaunchWindow(RPCBase):
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class VSCodeEditor(RPCBase):
"""A widget to display the VSCode editor."""
@@ -4774,6 +4965,13 @@ class Waveform(RPCBase):
Minimum decimal places for crosshair when dynamic precision is enabled.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@property
@rpc_call
def curves(self) -> "list[Curve]":

View File

@@ -14,18 +14,21 @@ from typing import TYPE_CHECKING, Literal, TypeAlias, cast
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import lazy_import_from
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
from rich.console import Console
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
import bec_widgets.cli.client as client
else:
GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
client = lazy_import("bec_widgets.cli.client")
logger = bec_logger.logger
@@ -51,7 +54,7 @@ def _filter_output(output: str) -> str:
def _get_output(process, logger) -> None:
log_func = {process.stdout: logger.debug, process.stderr: logger.error}
log_func = {process.stdout: logger.debug, process.stderr: logger.info}
stream_buffer = {process.stdout: [], process.stderr: []}
try:
os.set_blocking(process.stdout.fileno(), False)
@@ -151,8 +154,10 @@ def wait_for_server(client: BECGuiClient):
raise RuntimeError("GUI is not alive")
try:
if client._gui_started_event.wait(timeout=timeout):
client._gui_started_timer.cancel()
client._gui_started_timer.join()
if client._gui_started_timer is not None:
# cancel the timer, we are done
client._gui_started_timer.cancel()
client._gui_started_timer.join()
else:
raise TimeoutError("Could not connect to GUI server")
finally:
@@ -261,13 +266,20 @@ class BECGuiClient(RPCBase):
def start(self, wait: bool = False) -> None:
"""Start the GUI server."""
logger.warning("Using <gui>.start() is deprecated, use <gui>.show() instead.")
return self._start(wait=wait)
def show(self):
"""Show the GUI window."""
def show(self, wait=True) -> None:
"""
Show the GUI window.
If the GUI server is not running, it will be started.
Args:
wait(bool): Whether to wait for the server to start. Defaults to True.
"""
if self._check_if_server_is_alive():
return self._show_all()
return self.start(wait=True)
return self._start(wait=wait)
def hide(self):
"""Hide the GUI window."""
@@ -382,6 +394,9 @@ class BECGuiClient(RPCBase):
"""
Start the GUI server, and execute callback when it is launched
"""
if self._gui_is_alive():
self._gui_started_event.set()
return
if self._process is None or self._process.poll() is not None:
logger.success("GUI starting...")
self._startup_timeout = 5
@@ -524,7 +539,7 @@ if __name__ == "__main__": # pragma: no cover
# Test the client_utils.py module
gui = BECGuiClient()
gui.start(wait=True)
gui.show(wait=True)
gui.new().new(widget="Waveform")
time.sleep(10)
finally:

View File

@@ -53,7 +53,7 @@ from __future__ import annotations
{base_imports}
from bec_lib.logger import bec_logger
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
{"from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module" if self._base else ""}
logger = bec_logger.logger
@@ -180,7 +180,10 @@ class {class_name}(RPCBase):"""
f"Method {method} not found in class {cls.__name__}. "
f"Please check the USER_ACCESS list."
)
if hasattr(obj, "__rpc_timeout__"):
timeout = {"value": obj.__rpc_timeout__}
else:
timeout = {}
if isinstance(obj, (property, QtProperty)):
# for the cli, we can map qt properties to regular properties
if is_property_setter:
@@ -205,14 +208,26 @@ class {class_name}(RPCBase):"""
def {method}{str(sig_overload)}: ...
"""
self.content += """
@rpc_call"""
self.content += f"""
{self._rpc_call(timeout)}"""
self.content += f"""
def {method}{str(sig)}:
\"\"\"
{doc}
\"\"\""""
def _rpc_call(self, timeout_info: dict[str, float | None]):
"""
Decorator to mark a method as an RPC call.
This is used to generate the client code for the method.
"""
if not timeout_info:
return "@rpc_call"
timeout = timeout_info.get("value", None)
return f"""
@rpc_timeout({timeout})
@rpc_call"""
def write(self, file_name: str):
"""
Write the content to a file, automatically formatted with black.

View File

@@ -7,6 +7,7 @@ from functools import wraps
from typing import TYPE_CHECKING, Any, cast
from bec_lib.client import BECClient
from bec_lib.device import DeviceBaseWithConfig
from bec_lib.endpoints import MessageEndpoints
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
@@ -24,6 +25,43 @@ else:
# pylint: disable=protected-access
def _name_arg(arg):
if isinstance(arg, DeviceBaseWithConfig):
# if dev.<device> is passed to GUI, it passes full_name
if hasattr(arg, "full_name"):
return arg.full_name
elif hasattr(arg, "name"):
return arg.name
return arg
def _transform_args_kwargs(args, kwargs) -> tuple[tuple, dict]:
return tuple(_name_arg(arg) for arg in args), {k: _name_arg(v) for k, v in kwargs.items()}
def rpc_timeout(timeout):
"""
A decorator to set a timeout for an RPC call.
Args:
timeout: The timeout in seconds.
Returns:
The decorated function.
"""
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if "timeout" not in kwargs:
kwargs["timeout"] = timeout
return func(self, *args, **kwargs)
return wrapper
return decorator
def rpc_call(func):
"""
A decorator for calling a function on the server.
@@ -47,15 +85,7 @@ def rpc_call(func):
return None # func(*args, **kwargs)
caller_frame = caller_frame.f_back
out = []
for arg in args:
if hasattr(arg, "name"):
arg = arg.name
out.append(arg)
args = tuple(out)
for key, val in kwargs.items():
if hasattr(val, "name"):
kwargs[key] = val.name
args, kwargs = _transform_args_kwargs(args, kwargs)
if not self._root._gui_is_alive():
raise RuntimeError("GUI is not alive")
return self._run_rpc(func.__name__, *args, **kwargs)

View File

@@ -161,8 +161,6 @@ class BECConnector:
# 2) Enforce unique objectName among siblings with the same BECConnector parent
self.setParent(parent)
if isinstance(self.parent(), QObject) and hasattr(self, "cleanup"):
self.parent().destroyed.connect(self._run_cleanup_on_deleted_parent)
# Error popups
self.error_utility = ErrorPopupUtility()
@@ -186,24 +184,6 @@ class BECConnector:
except:
logger.error(f"Error getting parent_id for {self.__class__.__name__}")
def _run_cleanup_on_deleted_parent(self) -> None:
"""
Run cleanup on the deleted parent.
This method is called when the parent is deleted.
"""
if not hasattr(self, "cleanup"):
return
try:
if not self._destroyed:
self.cleanup()
self._destroyed = True
except Exception:
content = traceback.format_exc()
logger.info(
"Failed to run cleanup on deleted parent. "
f"This is not necessarily an error as the parent may be deleted before the child and includes already a cleanup. The following exception was raised:\n{content}"
)
def change_object_name(self, name: str) -> None:
"""
Change the object name of the widget. Unregister old name and register the new one.

View File

@@ -38,9 +38,11 @@ def _loaded_submodules_from_specs(
try:
submodule.__loader__.exec_module(submodule)
except Exception as e:
logger.error(
f"Error loading plugin {submodule}: \n{''.join(traceback.format_exception(e))}"
)
exception_text = "".join(traceback.format_exception(e))
if "(most likely due to a circular import)" in exception_text:
logger.warning(f"Circular import encountered while loading {submodule}")
else:
logger.error(f"Error loading plugin {submodule}: \n{exception_text}")
yield submodule
@@ -59,7 +61,8 @@ def _get_widgets_from_module(module: ModuleType) -> BECClassContainer:
module,
predicate=lambda item: inspect.isclass(item)
and issubclass(item, BECWidget)
and item is not BECWidget,
and item is not BECWidget
and not item.__module__.startswith("bec_widgets"),
)
return BECClassContainer(
BECClassInfo(name=k, module=module.__name__, file=module.__loader__.get_filename(), obj=v)

View File

@@ -0,0 +1,86 @@
import traceback
from pathlib import Path
from typing import Annotated
import copier
import typer
from bec_lib.logger import bec_logger
from bec_lib.plugin_helper import plugin_repo_path
from bec_lib.utils.plugin_manager._constants import ANSWER_KEYS
from bec_lib.utils.plugin_manager._util import existing_data, git_stage_files, make_commit
from bec_widgets.utils.bec_plugin_manager.edit_ui import open_and_watch_ui_editor
logger = bec_logger.logger
_app = typer.Typer(rich_markup_mode="rich")
def _commit_added_widget(repo: Path, name: str):
git_stage_files(repo, [".copier-answers.yml"])
git_stage_files(repo / repo.name / "bec_widgets" / "widgets" / name, [])
make_commit(repo, f"plugin-manager added new widget: {name}")
logger.info(f"Committing new widget {name}")
def _widget_exists(widget_list: list[dict[str, str | bool]], name: str):
return name in [w["name"] for w in widget_list]
def _editor_cb(ctx: typer.Context, value: bool):
if value and not ctx.params["use_ui"]:
raise typer.BadParameter("Can only open the editor if creating a .ui file!")
return value
_bold_blue = "\033[34m\033[1m"
_off = "\033[0m"
_USE_UI_MSG = "Generate a .ui file for use in bec-designer."
_OPEN_DESIGNER_MSG = f"""This app can watch for changes and recompile them to a python file imported to the widget whenever it is saved.
To open this editor independently, you can use {_bold_blue}bec-plugin-manager edit-ui [widget_name]{_off}.
Open the created widget .ui file in bec-designer now?"""
@_app.command()
def widget(
name: Annotated[str, typer.Argument(help="Enter a name for your widget in snake_case")],
use_ui: Annotated[bool, typer.Option(prompt=_USE_UI_MSG, help=_USE_UI_MSG)] = True,
open_editor: Annotated[
bool, typer.Option(prompt=_OPEN_DESIGNER_MSG, help=_OPEN_DESIGNER_MSG, callback=_editor_cb)
] = True,
):
"""Create a new widget plugin with the given name.
If [bold white]use_ui[/bold white] is set, a bec-designer .ui file will also be created. If \
[bold white]open_editor[/bold white] is additionally set, the .ui file will be opened in \
bec-designer and the compiled python version will be updated when changes are made and saved."""
if (formatted_name := name.lower().replace("-", "_")) != name:
logger.warning(f"Adjusting widget name from {name} to {formatted_name}")
if not formatted_name.isidentifier():
logger.error(
f"{name} is not a valid name for a widget (even after converting to {formatted_name}) - please enter something in snake_case"
)
exit(-1)
logger.info(f"Adding new widget {formatted_name} to the template...")
try:
repo = Path(plugin_repo_path())
plugin_data = existing_data(repo, [ANSWER_KEYS.VERSION, ANSWER_KEYS.WIDGETS])
if _widget_exists(plugin_data[ANSWER_KEYS.WIDGETS], formatted_name):
logger.error(f"Widget {formatted_name} already exists!")
exit(-1)
plugin_data[ANSWER_KEYS.WIDGETS].append({"name": formatted_name, "use_ui": use_ui})
copier.run_update(
repo,
data=plugin_data,
defaults=True,
unsafe=True,
overwrite=True,
vcs_ref=plugin_data[ANSWER_KEYS.VERSION],
)
_commit_added_widget(repo, formatted_name)
except Exception:
logger.error(traceback.format_exc())
logger.error("exiting...")
exit(-1)
logger.success(f"Added widget {formatted_name}!")
if open_editor:
open_and_watch_ui_editor(formatted_name)

View File

@@ -0,0 +1,136 @@
import re
import subprocess
from pathlib import Path
from bec_lib.logger import bec_logger
from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
from watchdog.events import (
DirCreatedEvent,
DirModifiedEvent,
DirMovedEvent,
FileCreatedEvent,
FileModifiedEvent,
FileMovedEvent,
FileSystemEvent,
FileSystemEventHandler,
)
from watchdog.observers import Observer
from bec_widgets.utils.bec_designer import open_designer
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
from bec_widgets.utils.plugin_utils import get_custom_classes
logger = bec_logger.logger
class RecompileHandler(FileSystemEventHandler):
def __init__(self, in_file: Path, out_file: Path) -> None:
super().__init__()
self.in_file = str(in_file)
self.out_file = str(out_file)
self._pyside_import_re = re.compile(r"from PySide6\.(.*) import ")
self._widget_import_re = re.compile(
r"^from ([a-zA-Z_]*) import ([a-zA-Z_]*)$", re.MULTILINE
)
self._widget_modules = {
c.name: c.module for c in (get_custom_classes("bec_widgets") + get_all_plugin_widgets())
}
def on_created(self, event: DirCreatedEvent | FileCreatedEvent) -> None:
self.recompile(event)
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
self.recompile(event)
def on_moved(self, event: DirMovedEvent | FileMovedEvent) -> None:
self.recompile(event)
def recompile(self, event: FileSystemEvent) -> None:
if event.src_path == self.in_file or event.dest_path == self.in_file:
self._recompile()
def _recompile(self):
logger.success(".ui file modified, recompiling...")
code = subprocess.call(
["pyside6-uic", "--absolute-imports", self.in_file, "-o", self.out_file]
)
logger.success(f"compilation exited with code {code}")
if code != 0:
return
self._add_comment_to_file()
logger.success("updating imports...")
self._update_imports()
logger.success("formatting...")
code = subprocess.call(
["black", "--line-length=100", "--skip-magic-trailing-comma", self.out_file]
)
if code != 0:
logger.error(f"Error while running black on {self.out_file}, code: {code}")
return
code = subprocess.call(
[
"isort",
"--line-length=100",
"--profile=black",
"--multi-line=3",
"--trailing-comma",
self.out_file,
]
)
if code != 0:
logger.error(f"Error while running isort on {self.out_file}, code: {code}")
return
logger.success("done!")
def _add_comment_to_file(self):
with open(self.out_file, "r+") as f:
initial = f.read()
f.seek(0)
f.write(f"# Generated from {self.in_file} by bec-plugin-manager - do not edit! \n")
f.write(
"# Use 'bec-plugin-manager edit-ui [widget_name]' to make changes, and this file will be updated accordingly. \n\n"
)
f.write(initial)
def _update_imports(self):
with open(self.out_file, "r+") as f:
initial = f.read()
f.seek(0)
qtpy_imports = re.sub(
self._pyside_import_re, lambda ob: f"from qtpy.{ob.group(1)} import ", initial
)
print(self._widget_modules)
print(re.findall(self._widget_import_re, qtpy_imports))
widget_imports = re.sub(
self._widget_import_re,
lambda ob: (
f"from {module} import {ob.group(2)}"
if (module := self._widget_modules.get(ob.group(2))) is not None
else ob.group(1)
),
qtpy_imports,
)
f.write(widget_imports)
f.truncate()
def open_and_watch_ui_editor(widget_name: str):
logger.info(f"Opening the editor for {widget_name}, and watching")
repo = Path(plugin_repo_path())
widget_dir = repo / plugin_package_name() / "bec_widgets" / "widgets" / widget_name
ui_file = widget_dir / f"{widget_name}.ui"
ui_outfile = widget_dir / f"{widget_name}_ui.py"
logger.info(
f"Opening the editor for {widget_name}, and watching {ui_file} for changes. Whenever you save the file, it will be recompiled to {ui_outfile}"
)
recompile_handler = RecompileHandler(ui_file, ui_outfile)
observer = Observer()
observer.schedule(recompile_handler, str(ui_file.parent))
observer.start()
try:
open_designer([str(ui_file)])
finally:
observer.stop()
observer.join()
logger.info("Editing session ended, exiting...")

View File

@@ -1,15 +1,19 @@
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
import darkdetect
import shiboken6
from bec_lib.logger import bec_logger
from qtpy.QtCore import QObject, Slot
from qtpy.QtWidgets import QApplication
from qtpy.QtCore import QObject
from qtpy.QtWidgets import QApplication, QFileDialog, QWidget
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.colors import set_theme
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.rpc_decorator import rpc_timeout
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.widgets.containers.dock import BECDock
@@ -87,7 +91,7 @@ class BECWidget(BECConnector):
theme = "dark"
self.apply_theme(theme)
@Slot(str)
@SafeSlot(str)
def apply_theme(self, theme: str):
"""
Apply the theme to the widget.
@@ -96,12 +100,43 @@ class BECWidget(BECConnector):
theme(str, optional): The theme to be applied.
"""
@SafeSlot()
@SafeSlot(str)
@rpc_timeout(None)
def screenshot(self, file_name: str | None = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
if not isinstance(self, QWidget):
logger.error("Cannot take screenshot of non-QWidget instance")
return
screenshot = self.grab()
if file_name is None:
file_name, _ = QFileDialog.getSaveFileName(
self,
"Save Screenshot",
f"bec_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png",
"PNG Files (*.png);;JPEG Files (*.jpg *.jpeg);;All Files (*)",
)
if not file_name:
return
screenshot.save(file_name)
logger.info(f"Screenshot saved to {file_name}")
def cleanup(self):
"""Cleanup the widget."""
with RPCRegister.delayed_broadcast():
# All widgets need to call super().cleanup() in their cleanup method
logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
self.rpc_register.remove_rpc(self)
children = self.findChildren(BECWidget)
for child in children:
if not shiboken6.isValid(child):
# If the child is not valid, it means it has already been deleted
continue
child.close()
child.deleteLater()
def closeEvent(self, event):
"""Wrap the close even to ensure the rpc_register is cleaned up."""

View File

@@ -259,12 +259,3 @@ class CompactPopupWidget(QWidget):
@expand_popup.setter
def expand_popup(self, popup: bool):
self._expand_popup = popup
def closeEvent(self, event):
# Called by Qt, on closing - since the children widgets can be
# BECWidgets, it is good to explicitely call 'close' on them,
# to ensure proper resources cleanup
for child in self.container.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
child.close()
super().closeEvent(event)

View File

@@ -28,6 +28,10 @@ class EntryValidator:
if not available_entries:
available_entries = [name]
# edge case for if name is passed instead of full_name, should not happen
if entry in signals_dict:
entry = signals_dict[entry].get("obj_name", entry)
if entry is None or entry == "":
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
if entry not in available_entries:

View File

@@ -7,7 +7,7 @@ from qtpy.QtCore import QObject
from bec_widgets.utils.name_utils import pascal_to_snake
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
EXCLUDED_PLUGINS = ["BECConnector", "BECDock"]
_PARENT_ARG_REGEX = r".__init__\(\s*(?:parent\)|parent=parent,?|parent,?)"
_SELF_PARENT_ARG_REGEX = r".__init__\(\s*self,\s*(?:parent\)|parent=parent,?|parent,?)"
SUPER_INIT_REGEX = re.compile(r"super\(\)" + _PARENT_ARG_REGEX, re.MULTILINE)

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
{widget_import}
@@ -20,6 +21,8 @@ class {plugin_name_pascal}Plugin(QDesignerCustomWidgetInterface): # pragma: no
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = {plugin_name_pascal}(parent)
return t

View File

@@ -201,7 +201,7 @@ def get_custom_classes(repo_name: str) -> BECClassContainer:
class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj)
if issubclass(obj, BECConnector):
class_info.is_connector = True
if issubclass(obj, BECWidget):
if issubclass(obj, QWidget) or issubclass(obj, BECWidget):
class_info.is_widget = True
if len(subs) == 1 and (
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)

View File

@@ -0,0 +1,694 @@
from __future__ import annotations
from qtpy.QtCore import QLocale, QMetaEnum, Qt, QTimer
from qtpy.QtGui import QColor, QCursor, QFont, QIcon, QPalette
from qtpy.QtWidgets import (
QCheckBox,
QColorDialog,
QComboBox,
QDoubleSpinBox,
QFileDialog,
QFontDialog,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QMenu,
QPushButton,
QSizePolicy,
QSpinBox,
QToolButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
class PropertyEditor(QWidget):
def __init__(self, target: QWidget, parent: QWidget | None = None, show_only_bec: bool = True):
super().__init__(parent)
self._target = target
self._bec_only = show_only_bec
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Name row
name_row = QHBoxLayout()
name_row.addWidget(QLabel("Name:"))
self.name_edit = QLineEdit(target.objectName())
self.name_edit.setEnabled(False) # TODO implement with RPC broadcast
name_row.addWidget(self.name_edit)
layout.addLayout(name_row)
# BEC only checkbox
filter_row = QHBoxLayout()
self.chk_show_qt = QCheckBox("Show Qt properties")
self.chk_show_qt.setChecked(False)
filter_row.addWidget(self.chk_show_qt)
filter_row.addStretch(1)
layout.addLayout(filter_row)
self.chk_show_qt.toggled.connect(lambda checked: self.set_show_only_bec(not checked))
# Main tree widget
self.tree = QTreeWidget(self)
self.tree.setColumnCount(2)
self.tree.setHeaderLabels(["Property", "Value"])
self.tree.setAlternatingRowColors(True)
self.tree.setRootIsDecorated(False)
layout.addWidget(self.tree)
self._build()
def _class_chain(self):
chain = []
mo = self._target.metaObject()
while mo is not None:
chain.append(mo)
mo = mo.superClass()
return chain
def set_show_only_bec(self, flag: bool):
self._bec_only = flag
self._build()
def _set_equal_columns(self):
header = self.tree.header()
header.setSectionResizeMode(0, QHeaderView.Interactive)
header.setSectionResizeMode(1, QHeaderView.Interactive)
w = self.tree.viewport().width() or self.tree.width()
if w > 0:
half = max(1, w // 2)
self.tree.setColumnWidth(0, half)
self.tree.setColumnWidth(1, w - half)
def _build(self):
self.tree.clear()
for mo in self._class_chain():
class_name = mo.className()
if self._bec_only and not self._is_bec_metaobject(mo):
continue
group_item = QTreeWidgetItem(self.tree, [class_name])
group_item.setFirstColumnSpanned(True)
start = mo.propertyOffset()
end = mo.propertyCount()
for i in range(start, end):
prop = mo.property(i)
if (
not prop.isReadable()
or not prop.isWritable()
or not prop.isStored()
or not prop.isDesignable()
):
continue
name = prop.name()
if name == "objectName":
continue
value = self._target.property(name)
self._add_property_row(group_item, name, value, prop)
if group_item.childCount() == 0:
idx = self.tree.indexOfTopLevelItem(group_item)
self.tree.takeTopLevelItem(idx)
self.tree.expandAll()
QTimer.singleShot(0, self._set_equal_columns)
def _enum_int(self, obj) -> int:
return int(getattr(obj, "value", obj))
def _make_sizepolicy_editor(self, name: str, sp):
if not isinstance(sp, QSizePolicy):
return None
wrap = QWidget(self)
row = QHBoxLayout(wrap)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(4)
h_combo = QComboBox(wrap)
v_combo = QComboBox(wrap)
hs = QSpinBox(wrap)
vs = QSpinBox(wrap)
for b in (hs, vs):
b.setRange(0, 16777215)
policies = [
(QSizePolicy.Fixed, "Fixed"),
(QSizePolicy.Minimum, "Minimum"),
(QSizePolicy.Maximum, "Maximum"),
(QSizePolicy.Preferred, "Preferred"),
(QSizePolicy.Expanding, "Expanding"),
(QSizePolicy.MinimumExpanding, "MinExpanding"),
(QSizePolicy.Ignored, "Ignored"),
]
for pol, text in policies:
h_combo.addItem(text, self._enum_int(pol))
v_combo.addItem(text, self._enum_int(pol))
def _set_current(combo, val):
idx = combo.findData(self._enum_int(val))
if idx >= 0:
combo.setCurrentIndex(idx)
_set_current(h_combo, sp.horizontalPolicy())
_set_current(v_combo, sp.verticalPolicy())
hs.setValue(sp.horizontalStretch())
vs.setValue(sp.verticalStretch())
def apply_changes():
hp = QSizePolicy.Policy(h_combo.currentData())
vp = QSizePolicy.Policy(v_combo.currentData())
nsp = QSizePolicy(hp, vp)
nsp.setHorizontalStretch(hs.value())
nsp.setVerticalStretch(vs.value())
self._target.setProperty(name, nsp)
h_combo.currentIndexChanged.connect(lambda _=None: apply_changes())
v_combo.currentIndexChanged.connect(lambda _=None: apply_changes())
hs.valueChanged.connect(lambda _=None: apply_changes())
vs.valueChanged.connect(lambda _=None: apply_changes())
row.addWidget(h_combo)
row.addWidget(v_combo)
row.addWidget(hs)
row.addWidget(vs)
return wrap
def _make_locale_editor(self, name: str, loc):
if not isinstance(loc, QLocale):
return None
wrap = QWidget(self)
row = QHBoxLayout(wrap)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(4)
lang_combo = QComboBox(wrap)
country_combo = QComboBox(wrap)
for lang in QLocale.Language:
try:
lang_int = self._enum_int(lang)
except Exception:
continue
if lang_int < 0:
continue
name_txt = QLocale.languageToString(QLocale.Language(lang_int))
lang_combo.addItem(name_txt, lang_int)
def populate_countries():
country_combo.blockSignals(True)
country_combo.clear()
for terr in QLocale.Country:
try:
terr_int = self._enum_int(terr)
except Exception:
continue
if terr_int < 0:
continue
text = QLocale.countryToString(QLocale.Country(terr_int))
country_combo.addItem(text, terr_int)
cur_country = self._enum_int(loc.country())
idx = country_combo.findData(cur_country)
if idx >= 0:
country_combo.setCurrentIndex(idx)
country_combo.blockSignals(False)
cur_lang = self._enum_int(loc.language())
idx = lang_combo.findData(cur_lang)
if idx >= 0:
lang_combo.setCurrentIndex(idx)
populate_countries()
def apply_locale():
lang = QLocale.Language(int(lang_combo.currentData()))
country = QLocale.Country(int(country_combo.currentData()))
self._target.setProperty(name, QLocale(lang, country))
lang_combo.currentIndexChanged.connect(lambda _=None: populate_countries())
lang_combo.currentIndexChanged.connect(lambda _=None: apply_locale())
country_combo.currentIndexChanged.connect(lambda _=None: apply_locale())
row.addWidget(lang_combo)
row.addWidget(country_combo)
return wrap
def _make_icon_editor(self, name: str, icon):
btn = QPushButton(self)
btn.setText("Choose…")
if isinstance(icon, QIcon) and not icon.isNull():
btn.setIcon(icon)
def pick():
path, _ = QFileDialog.getOpenFileName(
self, "Select Icon", "", "Images (*.png *.jpg *.jpeg *.bmp *.svg)"
)
if path:
ic = QIcon(path)
self._target.setProperty(name, ic)
btn.setIcon(ic)
btn.clicked.connect(pick)
return btn
def _spin_pair(self, ints: bool = True):
box1 = QSpinBox(self) if ints else QDoubleSpinBox(self)
box2 = QSpinBox(self) if ints else QDoubleSpinBox(self)
if ints:
box1.setRange(-10_000_000, 10_000_000)
box2.setRange(-10_000_000, 10_000_000)
else:
for b in (box1, box2):
b.setDecimals(6)
b.setRange(-1e12, 1e12)
b.setSingleStep(0.1)
row = QHBoxLayout()
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(4)
wrap = QWidget(self)
wrap.setLayout(row)
row.addWidget(box1)
row.addWidget(box2)
return wrap, box1, box2
def _spin_quad(self, ints: bool = True):
s = QSpinBox if ints else QDoubleSpinBox
boxes = [s(self) for _ in range(4)]
if ints:
for b in boxes:
b.setRange(-10_000_000, 10_000_000)
else:
for b in boxes:
b.setDecimals(6)
b.setRange(-1e12, 1e12)
b.setSingleStep(0.1)
row = QHBoxLayout()
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(4)
wrap = QWidget(self)
wrap.setLayout(row)
for b in boxes:
row.addWidget(b)
return wrap, boxes
def _make_font_editor(self, name: str, value):
btn = QPushButton(self)
if isinstance(value, QFont):
btn.setText(f"{value.family()}, {value.pointSize()}pt")
else:
btn.setText("Select font…")
def pick():
ok, font = QFontDialog.getFont(
value if isinstance(value, QFont) else QFont(), self, "Select Font"
)
if ok:
self._target.setProperty(name, font)
btn.setText(f"{font.family()}, {font.pointSize()}pt")
btn.clicked.connect(pick)
return btn
def _make_color_editor(self, initial: QColor, apply_cb):
btn = QPushButton(self)
if isinstance(initial, QColor):
btn.setText(initial.name())
btn.setStyleSheet(f"background:{initial.name()};")
else:
btn.setText("Select color…")
def pick():
col = QColorDialog.getColor(
initial if isinstance(initial, QColor) else QColor(), self, "Select Color"
)
if col.isValid():
apply_cb(col)
btn.setText(col.name())
btn.setStyleSheet(f"background:{col.name()};")
btn.clicked.connect(pick)
return btn
def _apply_palette_color(
self,
name: str,
pal: QPalette,
group: QPalette.ColorGroup,
role: QPalette.ColorRole,
col: QColor,
):
pal.setColor(group, role, col)
self._target.setProperty(name, pal)
def _make_palette_editor(self, name: str, pal: QPalette):
if not isinstance(pal, QPalette):
return None
wrap = QWidget(self)
row = QHBoxLayout(wrap)
row.setContentsMargins(0, 0, 0, 0)
group_combo = QComboBox(wrap)
role_combo = QComboBox(wrap)
pick_btn = self._make_color_editor(
pal.color(QPalette.Active, QPalette.WindowText),
lambda col: self._apply_palette_color(
name, pal, QPalette.Active, QPalette.WindowText, col
),
)
groups = [
(QPalette.Active, "Active"),
(QPalette.Inactive, "Inactive"),
(QPalette.Disabled, "Disabled"),
]
for g, label in groups:
group_combo.addItem(label, int(getattr(g, "value", g)))
roles = [
(QPalette.WindowText, "WindowText"),
(QPalette.Window, "Window"),
(QPalette.Base, "Base"),
(QPalette.AlternateBase, "AlternateBase"),
(QPalette.ToolTipBase, "ToolTipBase"),
(QPalette.ToolTipText, "ToolTipText"),
(QPalette.Text, "Text"),
(QPalette.Button, "Button"),
(QPalette.ButtonText, "ButtonText"),
(QPalette.BrightText, "BrightText"),
(QPalette.Highlight, "Highlight"),
(QPalette.HighlightedText, "HighlightedText"),
]
for r, label in roles:
role_combo.addItem(label, int(getattr(r, "value", r)))
def rewire_button():
g = QPalette.ColorGroup(int(group_combo.currentData()))
r = QPalette.ColorRole(int(role_combo.currentData()))
col = pal.color(g, r)
while row.count() > 2:
w = row.takeAt(2).widget()
if w:
w.deleteLater()
btn = self._make_color_editor(
col, lambda c: self._apply_palette_color(name, pal, g, r, c)
)
row.addWidget(btn)
group_combo.currentIndexChanged.connect(lambda _: rewire_button())
role_combo.currentIndexChanged.connect(lambda _: rewire_button())
row.addWidget(group_combo)
row.addWidget(role_combo)
row.addWidget(pick_btn)
return wrap
def _make_cursor_editor(self, name: str, value):
combo = QComboBox(self)
shapes = [
(Qt.ArrowCursor, "Arrow"),
(Qt.IBeamCursor, "IBeam"),
(Qt.WaitCursor, "Wait"),
(Qt.CrossCursor, "Cross"),
(Qt.UpArrowCursor, "UpArrow"),
(Qt.SizeAllCursor, "SizeAll"),
(Qt.PointingHandCursor, "PointingHand"),
(Qt.ForbiddenCursor, "Forbidden"),
(Qt.WhatsThisCursor, "WhatsThis"),
(Qt.BusyCursor, "Busy"),
]
current_shape = None
if isinstance(value, QCursor):
try:
enum_val = value.shape()
current_shape = int(getattr(enum_val, "value", enum_val))
except Exception:
current_shape = None
for shape, text in shapes:
combo.addItem(text, int(getattr(shape, "value", shape)))
if current_shape is not None:
idx = combo.findData(current_shape)
if idx >= 0:
combo.setCurrentIndex(idx)
def apply_index(i):
shape_val = int(combo.itemData(i))
self._target.setProperty(name, QCursor(Qt.CursorShape(shape_val)))
combo.currentIndexChanged.connect(apply_index)
return combo
def _add_property_row(self, parent: QTreeWidgetItem, name: str, value, prop):
item = QTreeWidgetItem(parent, [name, ""])
editor = self._make_editor(name, value, prop)
if editor is not None:
self.tree.setItemWidget(item, 1, editor)
else:
item.setText(1, repr(value))
def _is_bec_metaobject(self, mo) -> bool:
cname = mo.className()
for cls in type(self._target).mro():
if getattr(cls, "__name__", None) == cname:
mod = getattr(cls, "__module__", "")
return mod.startswith("bec_widgets")
return False
def _enum_text(self, meta_enum: QMetaEnum, value_int: int) -> str:
if not meta_enum.isFlag():
key = meta_enum.valueToKey(value_int)
return key.decode() if isinstance(key, (bytes, bytearray)) else (key or str(value_int))
parts = []
for i in range(meta_enum.keyCount()):
k = meta_enum.key(i)
v = meta_enum.value(i)
if value_int & v:
k = k.decode() if isinstance(k, (bytes, bytearray)) else k
parts.append(k)
return " | ".join(parts) if parts else "0"
def _enum_value_to_int(self, meta_enum: QMetaEnum, value) -> int:
try:
return int(value)
except Exception:
pass
v = getattr(value, "value", None)
if isinstance(v, (int,)):
return int(v)
n = getattr(value, "name", None)
if isinstance(n, str):
res = meta_enum.keyToValue(n)
if res != -1:
return int(res)
s = str(value)
parts = [p.strip() for p in s.replace(",", "|").split("|")]
keys = []
for p in parts:
if "." in p:
p = p.split(".")[-1]
keys.append(p)
keystr = "|".join(keys)
try:
res = meta_enum.keysToValue(keystr)
if res != -1:
return int(res)
except Exception:
pass
return 0
def _make_enum_editor(self, name: str, value, prop):
meta_enum = prop.enumerator()
current = self._enum_value_to_int(meta_enum, value)
if not meta_enum.isFlag():
combo = QComboBox(self)
for i in range(meta_enum.keyCount()):
key = meta_enum.key(i)
key = key.decode() if isinstance(key, (bytes, bytearray)) else key
combo.addItem(key, meta_enum.value(i))
idx = combo.findData(current)
if idx < 0:
txt = self._enum_text(meta_enum, current)
idx = combo.findText(txt)
combo.setCurrentIndex(max(idx, 0))
def apply_index(i):
v = combo.itemData(i)
self._target.setProperty(name, int(v))
combo.currentIndexChanged.connect(apply_index)
return combo
btn = QToolButton(self)
btn.setText(self._enum_text(meta_enum, current))
btn.setPopupMode(QToolButton.InstantPopup)
menu = QMenu(btn)
actions = []
for i in range(meta_enum.keyCount()):
key = meta_enum.key(i)
key = key.decode() if isinstance(key, (bytes, bytearray)) else key
act = menu.addAction(key)
act.setCheckable(True)
act.setChecked(bool(current & meta_enum.value(i)))
actions.append(act)
btn.setMenu(menu)
def apply_flags():
flags = 0
for i, act in enumerate(actions):
if act.isChecked():
flags |= meta_enum.value(i)
self._target.setProperty(name, int(flags))
btn.setText(self._enum_text(meta_enum, flags))
menu.triggered.connect(lambda _a: apply_flags())
return btn
def _make_editor(self, name: str, value, prop):
from qtpy.QtCore import QPoint, QPointF, QRect, QRectF, QSize, QSizeF
if prop.isEnumType():
return self._make_enum_editor(name, value, prop)
if isinstance(value, QColor):
return self._make_color_editor(value, lambda col: self._target.setProperty(name, col))
if isinstance(value, QFont):
return self._make_font_editor(name, value)
if isinstance(value, QPalette):
return self._make_palette_editor(name, value)
if isinstance(value, QCursor):
return self._make_cursor_editor(name, value)
if isinstance(value, QSizePolicy):
ed = self._make_sizepolicy_editor(name, value)
if ed is not None:
return ed
if isinstance(value, QLocale):
ed = self._make_locale_editor(name, value)
if ed is not None:
return ed
if isinstance(value, QIcon):
ed = self._make_icon_editor(name, value)
if ed is not None:
return ed
if isinstance(value, QSize):
wrap, w, h = self._spin_pair(ints=True)
w.setValue(value.width())
h.setValue(value.height())
w.valueChanged.connect(
lambda _: self._target.setProperty(name, QSize(w.value(), h.value()))
)
h.valueChanged.connect(
lambda _: self._target.setProperty(name, QSize(w.value(), h.value()))
)
return wrap
if isinstance(value, QSizeF):
wrap, w, h = self._spin_pair(ints=False)
w.setValue(value.width())
h.setValue(value.height())
w.valueChanged.connect(
lambda _: self._target.setProperty(name, QSizeF(w.value(), h.value()))
)
h.valueChanged.connect(
lambda _: self._target.setProperty(name, QSizeF(w.value(), h.value()))
)
return wrap
if isinstance(value, QPoint):
wrap, x, y = self._spin_pair(ints=True)
x.setValue(value.x())
y.setValue(value.y())
x.valueChanged.connect(
lambda _: self._target.setProperty(name, QPoint(x.value(), y.value()))
)
y.valueChanged.connect(
lambda _: self._target.setProperty(name, QPoint(x.value(), y.value()))
)
return wrap
if isinstance(value, QPointF):
wrap, x, y = self._spin_pair(ints=False)
x.setValue(value.x())
y.setValue(value.y())
x.valueChanged.connect(
lambda _: self._target.setProperty(name, QPointF(x.value(), y.value()))
)
y.valueChanged.connect(
lambda _: self._target.setProperty(name, QPointF(x.value(), y.value()))
)
return wrap
if isinstance(value, QRect):
wrap, boxes = self._spin_quad(ints=True)
for b, v in zip(boxes, (value.x(), value.y(), value.width(), value.height())):
b.setValue(v)
def apply_rect():
self._target.setProperty(
name,
QRect(boxes[0].value(), boxes[1].value(), boxes[2].value(), boxes[3].value()),
)
for b in boxes:
b.valueChanged.connect(lambda _=None: apply_rect())
return wrap
if isinstance(value, QRectF):
wrap, boxes = self._spin_quad(ints=False)
for b, v in zip(boxes, (value.x(), value.y(), value.width(), value.height())):
b.setValue(v)
def apply_rectf():
self._target.setProperty(
name,
QRectF(boxes[0].value(), boxes[1].value(), boxes[2].value(), boxes[3].value()),
)
for b in boxes:
b.valueChanged.connect(lambda _=None: apply_rectf())
return wrap
if isinstance(value, bool):
w = QCheckBox(self)
w.setChecked(bool(value))
w.toggled.connect(lambda v: self._target.setProperty(name, v))
return w
if isinstance(value, int) and not isinstance(value, bool):
w = QSpinBox(self)
w.setRange(-10_000_000, 10_000_000)
w.setValue(int(value))
w.valueChanged.connect(lambda v: self._target.setProperty(name, v))
return w
if isinstance(value, float):
w = QDoubleSpinBox(self)
w.setDecimals(6)
w.setRange(-1e12, 1e12)
w.setSingleStep(0.1)
w.setValue(float(value))
w.valueChanged.connect(lambda v: self._target.setProperty(name, v))
return w
if isinstance(value, str):
w = QLineEdit(self)
w.setText(value)
w.editingFinished.connect(lambda: self._target.setProperty(name, w.text()))
return w
return None
class DemoApp(QWidget): # pragma: no cover:
def __init__(self):
super().__init__()
layout = QHBoxLayout(self)
# Create a BECWidget instance example
waveform = self.create_waveform()
# property editor for the BECWidget
property_editor = PropertyEditor(waveform, show_only_bec=True)
layout.addWidget(waveform)
layout.addWidget(property_editor)
def create_waveform(self):
"""Create a new waveform widget."""
from bec_widgets.widgets.plots.waveform.waveform import Waveform
waveform = Waveform(parent=self)
waveform.title = "New Waveform"
waveform.x_label = "X Axis"
waveform.y_label = "Y Axis"
return waveform
if __name__ == "__main__": # pragma: no cover:
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
demo = DemoApp()
demo.setWindowTitle("Property Editor Demo")
demo.resize(1200, 800)
demo.show()
sys.exit(app.exec())

View File

@@ -13,3 +13,17 @@ def register_rpc_methods(cls):
if getattr(method, "rpc_public", False):
cls.USER_ACCESS.add(name)
return cls
def rpc_timeout(timeout: float | None):
"""
Decorator to set a timeout for RPC methods.
The actual implementation of timeout handling is within the cli module. This decorator
is solely to inform the generate-cli command about the timeout value.
"""
def decorator(func):
func.__rpc_timeout__ = timeout # Store the timeout value in the function
return func
return decorator

View File

@@ -27,6 +27,7 @@ class AutoUpdates(BECMainWindow):
_default_dock: BECDock
USER_ACCESS = ["enabled", "enabled.setter", "selected_device", "selected_device.setter"]
RPC = True
PLUGIN = False
# enforce that subclasses have the same rpc widget class
rpc_widget_class = "AutoUpdates"

View File

@@ -0,0 +1 @@
{'files': ['dock_area.py']}

View File

@@ -1,22 +1,19 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.containers.dock import BECDockArea
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
DOM_XML = """
<ui language='c++'>
<widget class='BECDockArea' name='dock_area'>
<widget class='BECDockArea' name='bec_dock_area'>
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -24,6 +21,8 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = BECDockArea(parent)
return t
@@ -31,13 +30,13 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "BEC Plots"
return "BEC Containers"
def icon(self):
return designer_material_icon(BECDockArea.ICON_NAME)
def includeFile(self):
return "dock_area"
return "bec_dock_area"
def initialize(self, form_editor):
self._form_editor = form_editor
@@ -52,7 +51,7 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "BECDockArea"
def toolTip(self):
return "BECDockArea"
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -389,6 +389,7 @@ class BECDock(BECWidget, Dock):
if widget in self.widgets:
self.widgets.remove(widget)
widget.close()
widget.deleteLater()
def delete_all(self):
"""

View File

@@ -71,6 +71,7 @@ class BECDockArea(BECWidget, QWidget):
"detach_dock",
"attach_all",
"save_state",
"screenshot",
"restore_state",
]
@@ -267,11 +268,16 @@ class BECDockArea(BECWidget, QWidget):
"restore_state",
MaterialIconAction(icon_name="frame_reload", tooltip="Restore Dock State", parent=self),
)
self.toolbar.components.add_safe(
"screenshot",
MaterialIconAction(icon_name="photo_camera", tooltip="Take Screenshot", parent=self),
)
bundle = ToolbarBundle("dock_actions", self.toolbar.components)
bundle.add_action("attach_all")
bundle.add_action("save_state")
bundle.add_action("restore_state")
bundle.add_action("screenshot")
self.toolbar.add_bundle(bundle)
def _hook_toolbar(self):
@@ -333,6 +339,7 @@ class BECDockArea(BECWidget, QWidget):
self.toolbar.components.get_action("restore_state").action.triggered.connect(
self.restore_state
)
self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot)
@SafeSlot()
def _create_widget_from_toolbar(self, widget_name: str) -> None:

View File

@@ -1 +0,0 @@
{'files': ['dock_area.py','dock.py']}

View File

@@ -6,7 +6,7 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.containers.dock.dock_area_plugin import BECDockAreaPlugin
from bec_widgets.widgets.containers.dock.bec_dock_area_plugin import BECDockAreaPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(BECDockAreaPlugin())

View File

@@ -0,0 +1 @@
{'files': ['main_window.py']}

View File

@@ -0,0 +1,73 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtCore import QSize
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
DOM_XML = """
<ui language='c++'>
<widget class='BECMainWindow' name='bec_main_window'>
</widget>
</ui>
"""
class BECMainWindowPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
# We want to initialize BECMainWindow upon starting designer
t = BECMainWindow(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Containers"
def icon(self):
return designer_material_icon(BECMainWindow.ICON_NAME)
def includeFile(self):
return "bec_main_window"
def initialize(self, form_editor):
import os
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
import bec_widgets
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
QApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
app = QApplication.instance()
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "app_icons", "BEC-General-App.png"),
size=QSize(48, 48),
)
app.setWindowIcon(icon)
self._form_editor = form_editor
def isContainer(self):
return True
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "BECMainWindow"
def toolTip(self):
return "BECMainWindow"
def whatsThis(self):
return self.toolTip()

View File

@@ -19,20 +19,28 @@ from qtpy.QtWidgets import (
import bec_widgets
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.colors import apply_theme, set_theme
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.widgets.containers.main_window.addons.hover_widget import HoverWidget
from bec_widgets.widgets.containers.main_window.addons.notification_center.notification_banner import (
BECNotificationBroker,
NotificationCentre,
NotificationIndicator,
)
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
# Ensure the application does not use the native menu bar on macOS to be consistent with linux development.
QApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
class BECMainWindow(BECWidget, QMainWindow):
RPC = False
PLUGIN = False
RPC = True
PLUGIN = True
SCAN_PROGRESS_WIDTH = 100 # px
STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds
@@ -50,6 +58,14 @@ class BECMainWindow(BECWidget, QMainWindow):
self.app = QApplication.instance()
self.status_bar = self.statusBar()
self.setWindowTitle(window_title)
# Notification Centre overlay
self.notification_centre = NotificationCentre(parent=self) # Notification layer
self.notification_broker = BECNotificationBroker()
self._nc_margin = 16
self._position_notification_centre()
# Init ui
self._init_ui()
self._connect_to_theme_change()
@@ -58,6 +74,34 @@ class BECMainWindow(BECWidget, QMainWindow):
self.display_client_message, MessageEndpoints.client_info()
)
def setCentralWidget(self, widget: QWidget, qt_default: bool = False): # type: ignore[override]
"""
Reimplement QMainWindow.setCentralWidget so that the *main content*
widget always lives on the lower layer of the stacked layout that
hosts our notification overlays.
Args:
widget: The widget that should become the new central content.
qt_default: When *True* the call is forwarded to the base class so
that Qt behaves exactly as the original implementation (used
during __init__ when we first install ``self._full_content``).
"""
super().setCentralWidget(widget)
self.notification_centre.raise_()
self.statusBar().raise_()
def resizeEvent(self, event):
super().resizeEvent(event)
self._position_notification_centre()
def _position_notification_centre(self):
"""Keep the notification panel at a fixed margin top-right."""
if not hasattr(self, "notification_centre"):
return
margin = getattr(self, "_nc_margin", 16) # px
nc = self.notification_centre
nc.move(self.width() - nc.width() - margin, margin)
################################################################################
# MainWindow Elements Initialization
################################################################################
@@ -94,6 +138,26 @@ class BECMainWindow(BECWidget, QMainWindow):
# Add scan_progress bar with display logic
self._add_scan_progress_bar()
# Setup NotificationIndicator to bottom right of the status bar
self._add_notification_indicator()
################################################################################
# Notification indicator and Notification Centre helpers
def _add_notification_indicator(self):
"""
Add the notification indicator to the status bar and hook the signals.
"""
# Add the notification indicator to the status bar
self.notification_indicator = NotificationIndicator(self)
self.status_bar.addPermanentWidget(self.notification_indicator)
# Connect the notification broker to the indicator
self.notification_centre.counts_updated.connect(self.notification_indicator.update_counts)
self.notification_indicator.filter_changed.connect(self.notification_centre.apply_filter)
self.notification_indicator.show_all_requested.connect(self.notification_centre.show_all)
self.notification_indicator.hide_all_requested.connect(self.notification_centre.hide_all)
################################################################################
# Client message status bar widget helpers
@@ -379,12 +443,12 @@ class BECMainWindow(BECWidget, QMainWindow):
@SafeSlot(str)
def change_theme(self, theme: str):
"""
Change the theme of the application.
Change the theme of the application and propagate it to widgets.
Args:
theme(str): The theme to apply, either "light" or "dark".
theme(str): Either "light" or "dark".
"""
apply_theme(theme)
set_theme(theme) # emits theme_updated and applies palette globally
def event(self, event):
if event.type() == QEvent.Type.StatusTip:
@@ -430,15 +494,16 @@ class BECMainWindow(BECWidget, QMainWindow):
super().cleanup()
class UILaunchWindow(BECMainWindow):
RPC = True
class BECMainWindowNoRPC(BECMainWindow):
RPC = False
PLUGIN = False
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
main_window = UILaunchWindow()
main_window = BECMainWindow()
main_window.show()
main_window.resize(800, 600)
sys.exit(app.exec())

View File

@@ -0,0 +1,17 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.containers.main_window.bec_main_window_plugin import (
BECMainWindowPlugin,
)
QPyDesignerCustomWidgetCollection.addCustomWidget(BECMainWindowPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
@@ -20,6 +21,8 @@ class AbortButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = AbortButton(parent)
return t

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.buttons.button_reset.button_reset import ResetButton
@@ -20,6 +21,8 @@ class ResetButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = ResetButton(parent)
return t
@@ -48,7 +51,7 @@ class ResetButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "ResetButton"
def toolTip(self):
return "A button that reset the scan queue."
return "A button that resets the scan queue."
def whatsThis(self):
return self.toolTip()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.buttons.button_resume.button_resume import ResumeButton
@@ -20,6 +21,8 @@ class ResumeButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = ResumeButton(parent)
return t

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
@@ -15,8 +14,6 @@ DOM_XML = """
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -24,6 +21,8 @@ class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = StopButton(parent)
return t

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
PositionIndicator,
@@ -17,8 +16,6 @@ DOM_XML = """
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -26,6 +23,8 @@ class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = PositionIndicator(parent)
return t
@@ -54,7 +53,7 @@ class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
return "PositionIndicator"
def toolTip(self):
return "PositionIndicator"
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -138,7 +138,11 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
signals = msg_content.get("signals", {})
# pylint: disable=protected-access
hinted_signals = self.dev[device]._hints
precision = self.dev[device].precision
precision = getattr(self.dev[device], "precision", 8)
try:
precision = int(precision)
except (TypeError, ValueError):
precision = int(8)
spinner = ui_components["spinner"]
position_indicator = ui_components["position_indicator"]
@@ -178,11 +182,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
spinner.setVisible(False)
if readback_val is not None:
readback.setText(f"{readback_val:.{precision}f}")
text = f"{readback_val:.{precision}f}"
readback.setText(text)
position_emit(readback_val)
if setpoint_val is not None:
setpoint.setText(f"{setpoint_val:.{precision}f}")
text = f"{setpoint_val:.{precision}f}"
setpoint.setText(text)
limits = self.dev[device].limits
limit_update(limits)
@@ -205,10 +211,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
ui["readback"].setToolTip(f"{device} readback")
ui["setpoint"].setToolTip(f"{device} setpoint")
ui["step_size"].setToolTip(f"Step size for {device}")
precision = self.dev[device].precision
if precision is not None:
ui["step_size"].setDecimals(precision)
ui["step_size"].setValue(10**-precision * 10)
precision = getattr(self.dev[device], "precision", 8)
try:
precision = int(precision)
except (TypeError, ValueError):
precision = int(8)
ui["step_size"].setDecimals(precision)
ui["step_size"].setValue(10**-precision * 10)
def _swap_readback_signal_connection(self, slot, old_device, new_device):
self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))

View File

@@ -33,7 +33,7 @@ class PositionerBox(PositionerBoxBase):
PLUGIN = True
RPC = True
USER_ACCESS = ["set_positioner"]
USER_ACCESS = ["set_positioner", "screenshot"]
device_changed = Signal(str, str)
# Signal emitted to inform listeners about a position update
position_update = Signal(float)

View File

@@ -1,12 +1,13 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import (
PositionerBox,
)
DOM_XML = """
<ui language='c++'>
@@ -14,7 +15,6 @@ DOM_XML = """
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
@@ -23,6 +23,8 @@ class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = PositionerBox(parent)
return t
@@ -30,7 +32,7 @@ class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Device Control"
return "BEC Device Control"
def icon(self):
return designer_material_icon(PositionerBox.ICON_NAME)

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
@@ -22,6 +23,8 @@ class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = PositionerBox2D(parent)
return t
@@ -29,7 +32,7 @@ class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Device Control"
return "BEC Device Control"
def icon(self):
return designer_material_icon(PositionerBox2D.ICON_NAME)

View File

@@ -34,7 +34,7 @@ class PositionerBox2D(PositionerBoxBase):
PLUGIN = True
RPC = True
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver", "screenshot"]
device_changed_hor = Signal(str, str)
device_changed_ver = Signal(str, str)

View File

@@ -1,12 +1,13 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_control.positioner_box import PositionerControlLine
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import (
PositionerControlLine,
)
DOM_XML = """
<ui language='c++'>
@@ -14,7 +15,6 @@ DOM_XML = """
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
@@ -23,6 +23,8 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = PositionerControlLine(parent)
return t
@@ -30,7 +32,7 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
return DOM_XML
def group(self):
return "Device Control"
return "BEC Device Control"
def icon(self):
return designer_material_icon(PositionerControlLine.ICON_NAME)
@@ -51,7 +53,7 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
return "PositionerControlLine"
def toolTip(self):
return "A widget that controls a single positioner in line form."
return "A widget that controls a single device."
def whatsThis(self):
return self.toolTip()

View File

@@ -15,7 +15,7 @@ logger = bec_logger.logger
class PositionerGroupBox(QGroupBox):
PLUGIN = True
position_update = Signal(float)
def __init__(self, parent, dev_name):
@@ -45,7 +45,12 @@ class PositionerGroupBox(QGroupBox):
def _on_position_update(self, pos: float):
self.position_update.emit(pos)
self.widget.label = f"%.{self.widget.dev[self.widget.device].precision}f" % pos
precision = getattr(self.widget.dev[self.widget.device], "precision", 8)
try:
precision = int(precision)
except (TypeError, ValueError):
precision = int(8)
self.widget.label = f"{pos:.{precision}f}"
def close(self):
self.widget.close()
@@ -55,6 +60,7 @@ class PositionerGroupBox(QGroupBox):
class PositionerGroup(BECWidget, QWidget):
"""Simple Widget to control a positioner in box form"""
PLUGIN = True
ICON_NAME = "grid_view"
USER_ACCESS = ["set_positioners"]

View File

@@ -1 +1 @@
{'files': ['positioner_group.py']}
{'files': ['positioner_group.py']}

View File

@@ -1,9 +1,8 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_control.positioner_group.positioner_group import (
@@ -16,7 +15,6 @@ DOM_XML = """
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
@@ -25,6 +23,8 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = PositionerGroup(parent)
return t
@@ -32,7 +32,7 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Device Control"
return "BEC Device Control"
def icon(self):
return designer_material_icon(PositionerGroup.ICON_NAME)
@@ -53,7 +53,7 @@ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "PositionerGroup"
def toolTip(self):
return "Container Widget to control positioners in compact form, in a grid"
return "Simple Widget to control a positioner in box form"
def whatsThis(self):
return self.toolTip()

View File

@@ -112,7 +112,9 @@ class DeviceInputBase(BECWidget):
WidgetIO.set_value(widget=self, value=device)
self.config.default = device
else:
logger.warning(f"Device {device} is not in the filtered selection.")
logger.warning(
f"Device {device} is not in the filtered selection of {self}: {self.devices}."
)
@SafeSlot()
def update_devices_from_filters(self):
@@ -131,7 +133,8 @@ class DeviceInputBase(BECWidget):
# Filter based on readout priority
devs = [dev for dev in devs if self._check_readout_filter(dev)]
self.devices = [device.name for device in devs]
self.set_device(current_device)
if current_device != "":
self.set_device(current_device)
@SafeSlot(list)
def set_available_devices(self, devices: list[str]):

View File

@@ -1,3 +1 @@
{
"files": ["device_combobox.py"]
}
{'files': ['device_combobox.py']}

View File

@@ -1,22 +1,19 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
DOM_XML = """
<ui language='c++'>
<widget class='DeviceComboBox' name='device_combobox'>
<widget class='DeviceComboBox' name='device_combo_box'>
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -24,6 +21,8 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = DeviceComboBox(parent)
return t
@@ -37,7 +36,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return designer_material_icon(DeviceComboBox.ICON_NAME)
def includeFile(self):
return "device_combobox"
return "device_combo_box"
def initialize(self, form_editor):
self._form_editor = form_editor
@@ -52,7 +51,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "DeviceComboBox"
def toolTip(self):
return "Device ComboBox Example for BEC Widgets"
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -1,3 +1 @@
{
"files": ["device_line_edit.py"]
}
{'files': ['device_line_edit.py']}

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
DeviceLineEdit,
@@ -17,8 +16,6 @@ DOM_XML = """
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -26,6 +23,8 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = DeviceLineEdit(parent)
return t
@@ -54,7 +53,7 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "DeviceLineEdit"
def toolTip(self):
return "Device LineEdit Example for BEC Widgets with autocomplete."
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
@@ -20,6 +21,8 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = SignalComboBox(parent)
return t
@@ -48,7 +51,7 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "SignalComboBox"
def toolTip(self):
return "Signal ComboBox Example for BEC Widgets with autocomplete."
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit import (
@@ -22,6 +23,8 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = SignalLineEdit(parent)
return t
@@ -50,7 +53,7 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "SignalLineEdit"
def toolTip(self):
return "Signal LineEdit Example for BEC Widgets with autocomplete."
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,3 @@
from .available_device_resources import AvailableDeviceResources
__all__ = ["AvailableDeviceResources"]

View File

@@ -0,0 +1,61 @@
from random import randint
from typing import Iterable
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QListWidgetItem, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.control.device_manager.components.available_device_resources.available_device_resources_ui import (
Ui_availableDeviceResources,
)
from bec_widgets.widgets.control.device_manager.components.available_device_resources.device_resource_backend import (
HashableDevice,
get_backend,
)
from bec_widgets.widgets.control.device_manager.components.available_device_resources.device_tag_group import (
DeviceTagGroup,
)
class AvailableDeviceResources(BECWidget, QWidget, Ui_availableDeviceResources):
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
self.setupUi(self)
self._backend = get_backend()
self._items: dict[str, tuple[QListWidgetItem, DeviceTagGroup]] = {}
self.refresh_full_list()
def refresh_full_list(self):
self.tag_groups_list.clear()
self._items = {}
for tag_group, devices in self._backend.tag_groups.items():
item = QListWidgetItem(self.tag_groups_list)
tag_group_widget = DeviceTagGroup(self.tag_groups_list, tag_group, devices)
self.tag_groups_list.setItemWidget(item, tag_group_widget)
self.tag_groups_list.addItem(item)
self._items[tag_group] = (item, tag_group_widget)
item.setSizeHint(QSize(tag_group_widget.width(), tag_group_widget.height()))
def set_devices_state(self, devices: Iterable[HashableDevice], included: bool):
for _, tag_group in self._items.values():
for device in devices:
tag_group.set_item_state(hash(device), included)
def resizeEvent(self, event):
super().resizeEvent(event)
for list_item, tag_group_widget in self._items.values():
list_item.setSizeHint(tag_group_widget.sizeHint())
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = AvailableDeviceResources()
widget.set_devices_state(
list(filter(lambda _: randint(0, 1) == 1, widget._backend.all_devices)), True
)
widget.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,27 @@
from qtpy.QtCore import QMetaObject, Qt
from qtpy.QtWidgets import QAbstractItemView, QListView, QListWidget, QVBoxLayout
class Ui_availableDeviceResources(object):
def setupUi(self, availableDeviceResources):
if not availableDeviceResources.objectName():
availableDeviceResources.setObjectName("availableDeviceResources")
self.verticalLayout = QVBoxLayout(availableDeviceResources)
self.verticalLayout.setObjectName("verticalLayout")
self.tag_groups_list = QListWidget(availableDeviceResources)
self.tag_groups_list.setObjectName("tag_groups_list")
self.tag_groups_list.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.tag_groups_list.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.tag_groups_list.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.tag_groups_list.setMovement(QListView.Movement.Static)
self.tag_groups_list.setSpacing(2)
self.tag_groups_list.setDragDropMode(QListWidget.DragDropMode.DragOnly)
self.tag_groups_list.setDragEnabled(True)
self.tag_groups_list.setAcceptDrops(False)
self.tag_groups_list.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
availableDeviceResources.setMinimumWidth(250)
availableDeviceResources.resize(250, availableDeviceResources.height())
self.verticalLayout.addWidget(self.tag_groups_list)
QMetaObject.connectSlotsByName(availableDeviceResources)

View File

@@ -0,0 +1,143 @@
from __future__ import annotations
import operator
from functools import reduce
from glob import glob
from pathlib import Path
from textwrap import dedent
from typing import AbstractSet, Protocol
from bec_lib.atlas_models import Device
from bec_lib.bec_yaml_loader import yaml_load
from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
from pydantic import model_validator
class HashableDevice(Device):
source_files: set[str] = set()
names: set[str] = set()
@model_validator(mode="after")
def add_name(self) -> HashableDevice:
self.names.add(self.name)
return self
def as_normal_device(self):
return Device.model_validate(self)
def __hash__(self) -> int:
config_values = sorted(
(str(kv) for kv in self.deviceConfig.items()) if self.deviceConfig else []
)
return (reduce(operator.add, (self.name, self.deviceClass, *config_values))).__hash__()
def __eq__(self, value: object) -> bool:
if not isinstance(value, self.__class__):
return False
if hash(self) == hash(value):
return True
return False
def rich_text(self) -> str:
return dedent(
f"""
<b><u><h2> {self.name}: </h2></u></b>
<table>
<tr><td> description: </td><td><i> {self.description} </i></td></tr>
<tr><td> config: </td><td><i> {self.deviceConfig} </i></td></tr>
<tr><td> enabled: </td><td><i> {self.enabled} </i></td></tr>
<tr><td> read only: </td><td><i> {self.readOnly} </i></td></tr>
</table>
"""
)
def add_sources(self, other: HashableDevice):
self.source_files.update(other.source_files)
def add_tags(self, other: HashableDevice):
self.deviceTags.update(other.deviceTags)
def add_names(self, other: HashableDevice):
self.names.update(other.names)
class _HashableDeviceSet(set):
def __or__(self, value: AbstractSet) -> _HashableDeviceSet:
for item in self:
if item in value:
for other_item in value:
if other_item == item:
item.add_sources(other_item)
item.add_tags(other_item)
item.add_names(other_item)
for other_item in value:
if other_item not in self:
self.add(other_item)
return self
class DeviceResourceBackend(Protocol):
@property
def tag_groups(self) -> dict[str, set[HashableDevice]]:
"""A dictionary of all availble devices separated by tag groups. The same device may
appear more than once (in different groups)."""
...
@property
def all_devices(self) -> set[HashableDevice]:
"""A set of all availble devices. The same device may not appear more than once."""
...
def tags(self) -> set[str]:
"""Returns a set of all the tags in all available devices."""
...
def tag_group(self, tag: str) -> set[HashableDevice]:
"""Returns a set of the devices in the tag group with the given key."""
...
class _ConfigFileBackend(DeviceResourceBackend):
def __init__(self) -> None:
self._raw_device_set: set[HashableDevice] = self._get_config_from_files(
Path(plugin_repo_path()) / plugin_package_name() / "device_configs/"
)
self._tag_groups = self._get_tag_groups()
def _get_config_from_files(self, dir: Path):
files = glob("*.yaml", root_dir=dir, recursive=True)
def devices_from_file(file: str):
data = yaml_load(str(dir / file))
return set(
HashableDevice.model_validate(
dev | {"name": name, "source_files": {str(dir / file)}}
)
for name, dev in data.items()
)
return reduce(operator.or_, map(devices_from_file, files))
def _get_tag_groups(self) -> dict[str, set[HashableDevice]]:
return {
tag: set(filter(lambda dev: tag in dev.deviceTags, self._raw_device_set))
for tag in self.tags()
}
@property
def tag_groups(self):
return self._tag_groups
@property
def all_devices(self):
return self._raw_device_set
def tags(self) -> set[str]:
return reduce(operator.or_, (dev.deviceTags for dev in self._raw_device_set))
def tag_group(self, tag: str) -> set[HashableDevice]:
return self.tag_groups[tag]
def get_backend() -> DeviceResourceBackend:
return _ConfigFileBackend()

View File

@@ -0,0 +1,163 @@
from textwrap import dedent
from typing import Callable, NamedTuple
from bec_qthemes import material_icon
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QFrame, QHBoxLayout, QLabel, QListWidgetItem, QVBoxLayout, QWidget
from bec_widgets.widgets.control.device_manager.components.available_device_resources.device_resource_backend import (
HashableDevice,
)
from bec_widgets.widgets.control.device_manager.components.available_device_resources.device_tag_group_item_ui import (
Ui_DeviceTagGroup,
)
DEVICE_HASH_ROLE = 101
class _DeviceEntryWidget(QFrame):
_grid_size = QSize(120, 80)
def __init__(self, device_spec: HashableDevice, parent=None, **kwargs):
super().__init__(parent, **kwargs)
self._device_spec = device_spec
self.included: bool = False
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setFrameShadow(QFrame.Shadow.Raised)
self._layout = QVBoxLayout()
self._layout.setContentsMargins(5, 5, 5, 5)
self.setLayout(self._layout)
self.setMinimumSize(self._grid_size)
self.setup_title_layout(device_spec)
self.setToolTip(device_spec.rich_text())
self.details = QLabel(f"Tags:\n{', '.join(device_spec.deviceTags)}")
self.details.setStyleSheet("QLabel { font-size: 8pt; }")
self.details.setWordWrap(True)
self._layout.addWidget(self.details)
def setup_title_layout(self, device_spec: HashableDevice):
self._title_layout = QHBoxLayout()
self._title_container = QWidget(parent=self)
self._title_container.setLayout(self._title_layout)
self.title = QLabel(device_spec.name)
self.title.setToolTip(device_spec.name)
self.title.setStyleSheet(self.title_style("#FF0000"))
self._title_layout.addWidget(self.title)
self._title_layout.addStretch(1)
self._warning_label = QLabel()
self._title_layout.addWidget(self._warning_label)
self._layout.addWidget(self._title_container)
def check_and_display_warning(self):
if len(self._device_spec.names) == 1 and len(self._device_spec.source_files) == 1:
self._warning_label.setText("")
self._warning_label.setToolTip("")
else:
self._warning_label.setPixmap(material_icon("warning", color="#FFAA00"))
self._warning_label.setToolTip(
dedent(
f"""
{f"Device has multiple names! Please check! \n names: {self._device_spec.names}" if len(self._device_spec.names)>1 else ""}
{f"Device found in multiple source files! Please check! \n files: {self._device_spec.names}" if len(self._device_spec.names)>1 else ""}
"""
)
)
@property
def device_hash(self):
return hash(self._device_spec)
def title_style(self, color: str) -> str:
return f"QLabel {{ color: {color}; font-weight: bold; font-size: 10pt; }}"
def setTitle(self, text: str):
self.title.setText(text)
def set_included(self, included: bool):
self.included = included
self.title.setStyleSheet(self.title_style("#00FF00" if included else "#FF0000"))
class _DeviceEntry(NamedTuple):
list_item: QListWidgetItem
widget: _DeviceEntryWidget
class DeviceTagGroup(QWidget, Ui_DeviceTagGroup):
def __init__(
self, parent=None, name: str = "TagGroupTitle", data: set[HashableDevice] = set(), **kwargs
):
super().__init__(parent=parent, **kwargs)
self.setupUi(self)
self.device_list.setGridSize(_DeviceEntryWidget._grid_size)
self.title.setText(name)
self._devices: dict[str, _DeviceEntry] = {}
for device in data:
self._add_item(device)
self.device_list.sortItems()
def _add_item(self, device: HashableDevice):
item = QListWidgetItem(self.device_list)
widget = _DeviceEntryWidget(device, self)
item.setSizeHint(QSize(widget.width(), widget.height()))
self.device_list.setItemWidget(item, widget)
self.device_list.addItem(item)
self._devices[device.name] = _DeviceEntry(item, widget)
def set_item_state(self, /, device_hash: int, included: bool):
for dev in self._devices.values():
if dev.widget.device_hash == device_hash:
dev.widget.set_included(included)
self._update_num_included()
def _update_num_included(self):
n_included = sum(int(dev.widget.included) for dev in self._devices.values())
if n_included == 0:
color = "#FF0000"
elif n_included == len(self._devices):
color = "#00FF00"
else:
color = "#FFAA00"
self.n_included.setText(f"{n_included} / {len(self._devices)}")
self.n_included.setStyleSheet(f"QLabel {{ color: {color}; }}")
def resizeEvent(self, event):
super().resizeEvent(event)
self.setMinimumHeight(self.sizeHint().height())
self.setMaximumHeight(self.sizeHint().height())
def __repr__(self) -> str:
return f"{self.__class__.__name__}: {self.title.text()}"
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = DeviceTagGroup(name="Tag group 1")
for item in [
HashableDevice(
**{
"name": f"test_device_{i}",
"deviceClass": "TestDeviceClass",
"readoutPriority": "baseline",
"enabled": True,
}
)
for i in range(5)
]:
widget._add_item(item)
widget._update_num_included()
widget.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,135 @@
import math
from functools import partial
from bec_qthemes import material_icon
from qtpy.QtCore import QMetaObject, QSize, Qt
from qtpy.QtWidgets import (
QAbstractItemView,
QFrame,
QHBoxLayout,
QLabel,
QListView,
QListWidget,
QSizePolicy,
QSpacerItem,
QToolButton,
QVBoxLayout,
)
class AutoHeightListWidget(QListWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setViewMode(QListView.ViewMode.IconMode)
self.setResizeMode(QListView.ResizeMode.Adjust)
self.setWrapping(True)
self.setUniformItemSizes(True)
self.setMovement(QListView.Movement.Static)
self.setAcceptDrops(False)
self.setDragEnabled(True)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSpacing(5)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
def resizeEvent(self, event):
super().resizeEvent(event)
self.setMinimumHeight(self._calcSize().height())
self.setMaximumHeight(self._calcSize().height())
def sizeHint(self):
return self._calcSize()
def minimumSizeHint(self):
return self._calcSize()
def _calcSize(self):
if self.count() == 0:
return super().sizeHint()
grid = self.gridSize()
if not grid.isValid():
grid = QSize(100, 100) # fallback
items_per_row = max(1, self.viewport().width() // grid.width())
rows = math.ceil(self.count() / items_per_row)
height = rows * grid.height() + 2 * self.frameWidth()
return QSize(self.viewport().width(), height)
class Ui_DeviceTagGroup(object):
def setupUi(self, DeviceTagGroup):
if not DeviceTagGroup.objectName():
DeviceTagGroup.setObjectName("DeviceTagGroup")
DeviceTagGroup.setMinimumWidth(150)
self.verticalLayout = QVBoxLayout(DeviceTagGroup)
self.verticalLayout.setObjectName("verticalLayout")
self.frame = QFrame(DeviceTagGroup)
self.frame.setObjectName("frame")
self.frame.setFrameShape(QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.verticalLayout_2 = QVBoxLayout(self.frame)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.title = QLabel(self.frame)
self.title.setObjectName("title")
self.horizontalLayout.addWidget(self.title)
self.n_included = QLabel(self.frame, text="...")
self.n_included.setObjectName("n_included")
self.horizontalLayout.addWidget(self.n_included)
self.horizontalSpacer = QSpacerItem(
40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
)
self.horizontalLayout.addItem(self.horizontalSpacer)
self.delete_tag_button = QToolButton(self.frame)
self.delete_tag_button.setObjectName("delete_tag_button")
self.horizontalLayout.addWidget(self.delete_tag_button)
self.remove_from_composition_button = QToolButton(self.frame)
self.remove_from_composition_button.setObjectName("remove_from_composition_button")
self.horizontalLayout.addWidget(self.remove_from_composition_button)
self.add_to_composition_button = QToolButton(self.frame)
self.add_to_composition_button.setObjectName("add_to_composition_button")
self.horizontalLayout.addWidget(self.add_to_composition_button)
self.remove_all_button = QToolButton(self.frame)
self.remove_all_button.setObjectName("remove_all_from_composition_button")
self.horizontalLayout.addWidget(self.remove_all_button)
self.add_all_button = QToolButton(self.frame)
self.add_all_button.setObjectName("add_all_to_composition_button")
self.horizontalLayout.addWidget(self.add_all_button)
self.verticalLayout_2.addLayout(self.horizontalLayout)
self.device_list = AutoHeightListWidget(self.frame)
self.device_list.setObjectName("device_list")
self.verticalLayout_2.addWidget(self.device_list)
self.verticalLayout.addWidget(self.frame)
self.set_icons()
QMetaObject.connectSlotsByName(DeviceTagGroup)
def set_icons(self):
icon = partial(material_icon, size=(15, 15), convert_to_pixmap=False)
self.delete_tag_button.setIcon(icon("delete"))
self.delete_tag_button.setToolTip("Delete tag group")
self.remove_from_composition_button.setIcon(icon("remove"))
self.remove_from_composition_button.setToolTip("Remove selected from composition")
self.add_to_composition_button.setIcon(icon("add"))
self.add_to_composition_button.setToolTip("Add selected to composition")
self.remove_all_button.setIcon(icon("chips"))
self.remove_all_button.setToolTip("Remove all with this tag from composition")
self.add_all_button.setIcon(icon("add_box"))
self.add_all_button.setToolTip("Add all with this tag to composition")

View File

@@ -0,0 +1,32 @@
"""
This module provides an implementation for the device config view.
The widget is the entry point for users to edit device configurations.
"""
from qtpy import QtWidgets
from bec_widgets.utils.bec_widget import BECWidget
class DeviceConfigView(BECWidget, QtWidgets.QTreeWidget):
"""
DeviceConfigView is a widget that allows users to view device configurations.
It inherits from BECWidget and QtWidgets.QTreeWidget.
"""
RPC = False
PLUGIN = False
def __init__(self, parent=None):
super().__init__(parent)
self.column_header
self.setup_ui()
def setup_ui(self):
"""
Set up the user interface for the device configuration view.
This method initializes the layout and widgets needed for displaying
and editing device configurations.
"""
# Initialize layout and widgets here
pass

View File

@@ -45,6 +45,7 @@ class ScanControl(BECWidget, QWidget):
Widget to submit new scans to the queue.
"""
USER_ACCESS = ["remove", "screenshot"]
PLUGIN = True
ICON_NAME = "tune"
ARG_BOX_POSITION: int = 2

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
@@ -20,6 +21,8 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = ScanControl(parent)
return t
@@ -27,7 +30,7 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Device Control"
return "BEC Device Control"
def icon(self):
return designer_material_icon(ScanControl.ICON_NAME)
@@ -48,7 +51,7 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "ScanControl"
def toolTip(self):
return "ScanControl"
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
@@ -20,6 +21,8 @@ class DapComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = DapComboBox(parent)
return t

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
@@ -20,6 +21,8 @@ class LMFitDialogPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = LMFitDialog(parent)
return t
@@ -48,7 +51,7 @@ class LMFitDialogPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "LMFitDialog"
def toolTip(self):
return "LMFitDialog"
return "Dialog for displaying the fit summary and params for LMFit DAP processes"
def whatsThis(self):
return self.toolTip()

View File

@@ -1,6 +1,7 @@
from typing import Literal
import qtmonaco
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils.bec_widget import BECWidget
@@ -12,11 +13,14 @@ class MonacoWidget(BECWidget, QWidget):
A simple Monaco editor widget
"""
text_changed = Signal(str)
PLUGIN = True
ICON_NAME = "code"
USER_ACCESS = [
"set_text",
"get_text",
"insert_text",
"delete_line",
"set_language",
"get_language",
"set_theme",
@@ -25,6 +29,9 @@ class MonacoWidget(BECWidget, QWidget):
"set_cursor",
"current_cursor",
"set_minimap_enabled",
"set_vim_mode_enabled",
"set_lsp_header",
"get_lsp_header",
]
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
@@ -36,6 +43,7 @@ class MonacoWidget(BECWidget, QWidget):
self.editor = qtmonaco.Monaco(self)
layout.addWidget(self.editor)
self.setLayout(layout)
self.editor.text_changed.connect(self.text_changed.emit)
self.editor.initialized.connect(self.apply_theme)
def apply_theme(self, theme: str | None = None) -> None:
@@ -65,6 +73,26 @@ class MonacoWidget(BECWidget, QWidget):
"""
return self.editor.get_text()
def insert_text(self, text: str, line: int | None = None, column: int | None = None) -> None:
"""
Insert text at the current cursor position or at a specified line and column.
Args:
text (str): The text to insert.
line (int, optional): The line number (1-based) to insert the text at. Defaults to None.
column (int, optional): The column number (1-based) to insert the text at. Defaults to None.
"""
self.editor.insert_text(text, line, column)
def delete_line(self, line: int | None = None) -> None:
"""
Delete a line in the Monaco editor.
Args:
line (int, optional): The line number (1-based) to delete. If None, the current line will be deleted.
"""
self.editor.delete_line(line)
def set_cursor(
self,
line: int,
@@ -154,6 +182,34 @@ class MonacoWidget(BECWidget, QWidget):
"""
self.editor.clear_highlighted_lines()
def set_vim_mode_enabled(self, enabled: bool) -> None:
"""
Enable or disable Vim mode in the Monaco editor.
Args:
enabled (bool): If True, Vim mode will be enabled; otherwise, it will be disabled.
"""
self.editor.set_vim_mode_enabled(enabled)
def set_lsp_header(self, header: str) -> None:
"""
Set the LSP (Language Server Protocol) header for the Monaco editor.
The header is used to provide context for language servers but is not displayed in the editor.
Args:
header (str): The LSP header to set.
"""
self.editor.set_lsp_header(header)
def get_lsp_header(self) -> str:
"""
Get the current LSP header set in the Monaco editor.
Returns:
str: The LSP header.
"""
return self.editor.get_lsp_header()
if __name__ == "__main__": # pragma: no cover
qapp = QApplication([])

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
@@ -20,6 +21,8 @@ class MonacoWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = MonacoWidget(parent)
return t

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.editors.sbb_monitor.sbb_monitor import SBBMonitor
@@ -20,6 +21,8 @@ class SBBMonitorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = SBBMonitor(parent)
return t
@@ -27,7 +30,7 @@ class SBBMonitorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return ""
return "BEC Utils"
def icon(self):
return designer_material_icon(SBBMonitor.ICON_NAME)

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
@@ -20,6 +21,8 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = ScanMetadata(parent)
return t
@@ -27,7 +30,7 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return ""
return "BEC Input Widgets"
def icon(self):
return designer_material_icon(ScanMetadata.ICON_NAME)
@@ -48,7 +51,7 @@ class ScanMetadataPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "ScanMetadata"
def toolTip(self):
return "Dynamically generates a form for inclusion of metadata for a scan."
return "ScanMetadata"
def whatsThis(self):
return self.toolTip()

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.editors.text_box.text_box import TextBox
@@ -14,7 +13,6 @@ DOM_XML = """
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
@@ -23,6 +21,8 @@ class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = TextBox(parent)
return t
@@ -51,7 +51,7 @@ class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "TextBox"
def toolTip(self):
return "TextBox"
return "A widget that displays text in plain and HTML format"
def whatsThis(self):
return self.toolTip()

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
@@ -15,8 +14,6 @@ DOM_XML = """
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -24,6 +21,8 @@ class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = VSCodeEditor(parent)
return t

View File

@@ -6,11 +6,12 @@ import time
from bec_lib.logger import bec_logger
from louie.saferef import safe_ref
from qtpy.QtCore import QUrl, qInstallMessageHandler
from qtpy.QtCore import QTimer, QUrl, Signal, qInstallMessageHandler
from qtpy.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty
logger = bec_logger.logger
@@ -165,11 +166,16 @@ class WebConsole(BECWidget, QWidget):
A simple widget to display a website
"""
_js_callback = Signal(bool)
initialized = Signal()
PLUGIN = True
ICON_NAME = "terminal"
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self._startup_cmd = "bec --nogui"
self._is_initialized = False
_web_console_registry.register(self)
self._token = _web_console_registry._token
layout = QVBoxLayout()
@@ -181,6 +187,48 @@ class WebConsole(BECWidget, QWidget):
layout.addWidget(self.browser)
self.setLayout(layout)
self.page.setUrl(QUrl(f"http://localhost:{_web_console_registry._server_port}"))
self._startup_timer = QTimer()
self._startup_timer.setInterval(500)
self._startup_timer.timeout.connect(self._check_page_ready)
self._startup_timer.start()
self._js_callback.connect(self._on_js_callback)
def _check_page_ready(self):
"""
Check if the page is ready and stop the timer if it is.
"""
if self.page.isLoading():
return
self.page.runJavaScript("window.term !== undefined", self._js_callback.emit)
def _on_js_callback(self, ready: bool):
"""
Callback for when the JavaScript is ready.
"""
if not ready:
return
self._is_initialized = True
self._startup_timer.stop()
if self._startup_cmd:
self.write(self._startup_cmd)
self.initialized.emit()
@SafeProperty(str)
def startup_cmd(self):
"""
Get the startup command for the web console.
"""
return self._startup_cmd
@startup_cmd.setter
def startup_cmd(self, cmd: str):
"""
Set the startup command for the web console.
"""
if not isinstance(cmd, str):
raise ValueError("Startup command must be a string.")
self._startup_cmd = cmd
def write(self, data: str, send_return: bool = True):
"""
@@ -213,10 +261,19 @@ class WebConsole(BECWidget, QWidget):
"document.querySelector('textarea.xterm-helper-textarea').dispatchEvent(new KeyboardEvent('keypress', {charCode: 3}))"
)
def set_readonly(self, readonly: bool):
"""
Set the web console to read-only mode.
"""
if not isinstance(readonly, bool):
raise ValueError("Readonly must be a boolean.")
self.setEnabled(not readonly)
def cleanup(self):
"""
Clean up the registry by removing any instances that are no longer valid.
"""
self._startup_timer.stop()
_web_console_registry.unregister(self)
super().cleanup()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.editors.web_console.web_console import WebConsole
@@ -20,6 +21,8 @@ class WebConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = WebConsole(parent)
return t
@@ -27,7 +30,7 @@ class WebConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "BEC Console"
return "BEC Developer"
def icon(self):
return designer_material_icon(WebConsole.ICON_NAME)

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.editors.website.website import WebsiteWidget
@@ -14,7 +13,6 @@ DOM_XML = """
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
@@ -23,6 +21,8 @@ class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = WebsiteWidget(parent)
return t

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.games.minesweeper import Minesweeper
@@ -20,6 +21,8 @@ class MinesweeperPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = Minesweeper(parent)
return t

View File

@@ -115,6 +115,7 @@ class Heatmap(ImageBase):
"auto_range_y.setter",
"minimal_crosshair_precision",
"minimal_crosshair_precision.setter",
"screenshot",
# ImageView Specific Settings
"color_map",
"color_map.setter",

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
@@ -20,6 +21,8 @@ class HeatmapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = Heatmap(parent)
return t
@@ -27,7 +30,7 @@ class HeatmapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Plot Widgets"
return "BEC Plots"
def icon(self):
return designer_material_icon(Heatmap.ICON_NAME)

View File

@@ -91,6 +91,7 @@ class Image(ImageBase):
"auto_range_y.setter",
"minimal_crosshair_precision",
"minimal_crosshair_precision.setter",
"screenshot",
# ImageView Specific Settings
"color_map",
"color_map.setter",

View File

@@ -1034,7 +1034,8 @@ class ImageBase(PlotBase):
if self.y_roi is not None:
self.y_roi.cleanup_pyqtgraph()
self.layer_manager.clear()
self.layer_manager = None
if self.layer_manager is not None:
self.layer_manager.clear()
self.layer_manager = None
super().cleanup()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.plots.image.image import Image
@@ -20,6 +21,8 @@ class ImagePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = Image(parent)
return t
@@ -27,7 +30,7 @@ class ImagePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Plot Widgets"
return "BEC Plots"
def icon(self):
return designer_material_icon(Image.ICON_NAME)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import math
from typing import TYPE_CHECKING
from bec_lib import bec_logger
from bec_qthemes import material_icon
from qtpy.QtCore import QEvent, Qt
from qtpy.QtGui import QColor
@@ -39,6 +40,9 @@ if TYPE_CHECKING:
from bec_widgets.widgets.plots.image.image import Image
logger = bec_logger.logger
class ROILockButton(QToolButton):
"""Keeps its icon and checked state in sync with a single ROI."""
@@ -447,6 +451,18 @@ class ROIPropertyTree(BECWidget, QWidget):
def cleanup(self):
self.cmap.close()
self.cmap.deleteLater()
if self.controller and hasattr(self.controller, "rois"):
for roi in self.controller.rois: # disconnect all signals from ROIs
try:
if isinstance(roi, RectangularROI):
roi.edgesChanged.disconnect()
else:
roi.centerChanged.disconnect()
roi.penChanged.disconnect()
roi.nameChanged.disconnect()
except (RuntimeError, TypeError) as e:
logger.error(f"Failed to disconnect roi qt signal: {e}")
super().cleanup()

View File

@@ -128,6 +128,7 @@ class MotorMap(PlotBase):
"y_log.setter",
"legend_label_size",
"legend_label_size.setter",
"screenshot",
# motor_map specific
"color",
"color.setter",

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
@@ -20,6 +21,8 @@ class MotorMapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = MotorMap(parent)
return t
@@ -27,7 +30,7 @@ class MotorMapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Plot Widgets"
return "BEC Plots"
def icon(self):
return designer_material_icon(MotorMap.ICON_NAME)

View File

@@ -96,6 +96,7 @@ class MultiWaveform(PlotBase):
"legend_label_size.setter",
"minimal_crosshair_precision",
"minimal_crosshair_precision.setter",
"screenshot",
# MultiWaveform Specific RPC Access
"highlighted_index",
"highlighted_index.setter",

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
@@ -20,6 +21,8 @@ class MultiWaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = MultiWaveform(parent)
return t
@@ -27,7 +30,7 @@ class MultiWaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Plot Widgets"
return "BEC Plots"
def icon(self):
return designer_material_icon(MultiWaveform.ICON_NAME)

View File

@@ -84,6 +84,7 @@ class ScatterWaveform(PlotBase):
"legend_label_size.setter",
"minimal_crosshair_precision",
"minimal_crosshair_precision.setter",
"screenshot",
# Scatter Waveform Specific RPC Access
"main_curve",
"color_map",

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
@@ -20,6 +21,8 @@ class ScatterWaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = ScatterWaveform(parent)
return t
@@ -27,7 +30,7 @@ class ScatterWaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Plot Widgets"
return "BEC Plots"
def icon(self):
return designer_material_icon(ScatterWaveform.ICON_NAME)

View File

@@ -105,6 +105,7 @@ class Waveform(PlotBase):
"legend_label_size.setter",
"minimal_crosshair_precision",
"minimal_crosshair_precision.setter",
"screenshot",
# Waveform Specific RPC Access
"curves",
"x_mode",
@@ -908,6 +909,10 @@ class Waveform(PlotBase):
self.roi_enable.emit(True) # Enable the ROI toolbar action
self.request_dap() # Request DAP update directly without blocking proxy
QTimer.singleShot(
150, self.auto_range
) # autorange with a delay to ensure the plot is updated
return curve
def _add_curve_object(self, name: str, config: CurveConfig) -> Curve:
@@ -1630,18 +1635,25 @@ class Waveform(PlotBase):
# 4.2 If there are sync curves, use the first device from the scan report
else:
try:
x_name = self._ensure_str_list(
scan_report_devices = self._ensure_str_list(
self.scan_item.metadata["bec"]["scan_report_devices"]
)[0]
except:
x_name = self.scan_item.status_message.info["scan_report_devices"][0]
x_entry = self.entry_validator.validate_signal(x_name, None)
if access_key == "val":
x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, None)
)
except Exception:
scan_report_devices = self.scan_item.status_message.info.get(
"scan_report_devices", []
)
if not scan_report_devices:
x_data = None
new_suffix = " (auto: index)"
else:
entry_obj = data.get(x_name, {}).get(x_entry)
x_data = entry_obj.read()["value"] if entry_obj else None
new_suffix = f" (auto: {x_name}-{x_entry})"
x_name = scan_report_devices[0]
x_entry = self.entry_validator.validate_signal(x_name, None)
if access_key == "val":
x_data = data.get(x_name, {}).get(x_entry, {}).get(access_key, None)
else:
entry_obj = data.get(x_name, {}).get(x_entry)
x_data = entry_obj.read()["value"] if entry_obj else None
new_suffix = f" (auto: {x_name}-{x_entry})"
self._update_x_label_suffix(new_suffix)
return x_data

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.plots.waveform.waveform import Waveform
@@ -20,6 +21,8 @@ class WaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = Waveform(parent)
return t
@@ -27,7 +30,7 @@ class WaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return "Plot Widgets"
return "BEC Plots"
def icon(self):
return designer_material_icon(Waveform.ICON_NAME)

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import BECProgressBar
@@ -20,6 +21,8 @@ class BECProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = BECProgressBar(parent)
return t

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
@@ -14,7 +13,6 @@ DOM_XML = """
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class RingProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
@@ -23,6 +21,8 @@ class RingProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = RingProgressBar(parent)
return t
@@ -51,7 +51,7 @@ class RingProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "RingProgressBar"
def toolTip(self):
return "RingProgressBar"
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
@@ -20,6 +21,8 @@ class ScanProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = ScanProgressBar(parent)
return t
@@ -48,7 +51,7 @@ class ScanProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return "ScanProgressBar"
def toolTip(self):
return "A progress bar that is hooked up to the scan progress of a scan."
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -124,11 +124,14 @@ class ScanProgressBar(BECWidget, QWidget):
"""
ICON_NAME = "timelapse"
PLUGIN = True
progress_started = Signal()
progress_finished = Signal()
def __init__(self, parent=None, client=None, config=None, gui_id=None, one_line_design=False):
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id)
def __init__(
self, parent=None, client=None, config=None, gui_id=None, one_line_design=False, **kwargs
):
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
self.get_bec_shortcuts()
ui_file = os.path.join(

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
@@ -15,8 +14,6 @@ DOM_XML = """
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -24,6 +21,8 @@ class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = BECQueue(parent)
return t

View File

@@ -1,10 +1,9 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
import bec_widgets
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
@@ -15,8 +14,6 @@ DOM_XML = """
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class BECStatusBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
@@ -24,6 +21,8 @@ class BECStatusBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = BECStatusBox(parent)
return t

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