Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba76d6bb86 | ||
| 2304c9f849 | |||
| c6e48ec1fe | |||
|
|
f837129023 | ||
| 940ee6552c | |||
|
|
86b60b4aed | ||
| 14dd8c5b29 | |||
| b039933405 | |||
|
|
d8c80293c7 | ||
| 40c9fea35f | |||
| 5d4b86e1c6 | |||
|
|
5681c0cbd1 | ||
| 91959e82de | |||
| 5eb15b785f | |||
| 6fb20552ff | |||
| 0350833f36 | |||
| acb79020d4 | |||
|
|
9c6ba6ae73 | ||
| 4f5448cf51 | |||
|
|
6f0182115f | ||
| 7469c892c8 | |||
|
|
cb45527f3e | ||
| f9a889fc6d | |||
| 2ab12ed60a | |||
|
|
98c68e9ff4 | ||
| 19f4e407e0 | |||
| a23841b255 | |||
| 6982711fea | |||
| 0015f0e2d6 | |||
| af9655de0c | |||
| e4121a01cb | |||
| a69d2870e2 | |||
| e3d0a7bbf9 | |||
| 523cc43572 | |||
| 261578796f | |||
| 0b9b1a3c89 | |||
| 9801d2769e | |||
|
|
dfccf97a99 | ||
| 9ef1d1c9ac | |||
| b23695167a | |||
|
|
92cc808d65 | ||
| 3a22392780 | |||
| f5f1f6c304 | |||
| 923867947f | |||
|
|
91260bb579 | ||
| 8dc892df0a | |||
| 8c5ef26843 | |||
|
|
b681b13a33 | ||
| 499b6b9a12 | |||
| 94ce92f5b0 | |||
| 49268e3829 | |||
| 908dbc1760 | |||
|
|
d7e6506a27 | ||
| c5e9ed6e42 | |||
| b207e45a67 | |||
| 8bf4842788 | |||
|
|
49b9bfc9d3 | ||
| 04cfb1edf1 | |||
| efa276358b | |||
| f084e2514b | |||
|
|
7cd0b3630e | ||
| dc0c825fd5 | |||
| 1dcfeb6cfc | |||
| f554f3c167 | |||
| 0f9953e8fd | |||
| 63c24f97a3 | |||
| efe90eb163 | |||
| 281cb27d8b | |||
| 5c740371d8 | |||
| 28ee3856be | |||
| 7cc0726398 | |||
| e039304fd3 | |||
|
|
6fa7ca8f09 | ||
| b2f7d3c5f3 | |||
| e3b5e338bf | |||
| c8e614b575 | |||
|
|
8e44ca1ad0 | ||
| 286ad7196b | |||
|
|
adef25f4e2 | ||
| 60f7d54e2b | |||
| dd932dd8f3 | |||
| d3c1a1b2ed | |||
| 7ea4a482e7 | |||
| 9045323049 | |||
| d15b22250f | |||
|
|
5557bfe717 | ||
| a8576c164c |
267
CHANGELOG.md
@@ -1,165 +1,172 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.109.1 (2024-09-09)
|
||||
|
||||
### Fix
|
||||
## v1.3.2 (2024-11-05)
|
||||
|
||||
* fix: refactor textbox widget, remove inheritance, adhere to bec style; closes #324 ([`b0d786b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b0d786b991677c0846a0c6ba3f2252d48d94ccaa))
|
||||
### Bug Fixes
|
||||
|
||||
## v0.109.0 (2024-09-06)
|
||||
* fix(plot_base): legend text color is changed when changing dark-light theme ([`2304c9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2304c9f8497c1ab1492f3e6690bb79b0464c0df8))
|
||||
|
||||
### Feature
|
||||
### Build System
|
||||
|
||||
* feat(accent colors): added helper function to get all accent colors ([`84a59f7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/84a59f70eed6d8a3c3aeeabc77a5f9ea4e864f61))
|
||||
* build: PySide6 version fixed 6.7.2 ([`c6e48ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c6e48ec1fe5aaee6a7c7a6f930f1520cd439cdb2))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(theme): fixed theme access for themecontainer ([`de303f0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/de303f0227fc9d3a74a0410f1e7999ac5132273c))
|
||||
## v1.3.1 (2024-10-31)
|
||||
|
||||
## v0.108.0 (2024-09-06)
|
||||
### Bug Fixes
|
||||
|
||||
* fix(ophyd_kind_util): Kind enums are imported from the bec widget util class ([`940ee65`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/940ee6552c1ee8d9b4e4a74c62351f2e133ab678))
|
||||
|
||||
|
||||
## v1.3.0 (2024-10-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix(colors): extend color map validation for matplotlib and colorcet maps (if available) ([`14dd8c5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/14dd8c5b2947c92f6643b888d71975e4e8d4ee88))
|
||||
|
||||
### Features
|
||||
|
||||
* feat(colormap_button): colormap button with menu to select colormap filtered by the colormap type ([`b039933`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b039933405e2fbe92bd81bd0748e79e8d443a741))
|
||||
|
||||
|
||||
## v1.2.0 (2024-10-25)
|
||||
|
||||
### Features
|
||||
|
||||
* feat(colors): evenly spaced color generation + new golden ratio calculation ([`40c9fea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40c9fea35f869ef52e05948dd1989bcd99f602e0))
|
||||
|
||||
### Refactoring
|
||||
|
||||
* refactor: add bec_lib version to statusbox ([`5d4b86e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d4b86e1c6e1800051afce4f991153e370767fa6))
|
||||
|
||||
|
||||
## v1.1.0 (2024-10-25)
|
||||
|
||||
### Features
|
||||
|
||||
* feat: add filter i/o utility class ([`0350833`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0350833f36e0a7cadce4173f9b1d1fbfdf985375))
|
||||
|
||||
### Refactoring
|
||||
|
||||
* refactor: do not flush selection upon receiving config update; allow widgetIO to receive kwargs to be able to use get_value to receive string instead of int for QComboBox ([`91959e8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/91959e82de8586934af3ebb5aaa0923930effc51))
|
||||
|
||||
* refactor: allow to set selection in DeviceInput; automatic update of selection on device config update; cleanup ([`5eb15b7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5eb15b785f12e30eb8ccbc56d4ad9e759a4cf5eb))
|
||||
|
||||
* refactor: cleanup, added device_signal for signal inputs ([`6fb2055`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6fb20552ff57978f4aeb79fd7f062f8d6b5581e7))
|
||||
|
||||
### Testing
|
||||
|
||||
* test(scan_control): tests added for grid_scan to ensure scan_args signal validity ([`acb7902`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/acb79020d4be546efc001ff47b6f5cdba2ee9375))
|
||||
|
||||
|
||||
## v1.0.2 (2024-10-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix(scan_control): scan args signal fixed to emit list instead of hardcoded structure ([`4f5448c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f5448cf51a204e077af162c7f0aed1f1a60e57a))
|
||||
|
||||
|
||||
## v1.0.1 (2024-10-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix(waveform): added support for live_data and data access ([`7469c89`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7469c892c8076fc09e61f173df6920c551241cec))
|
||||
|
||||
|
||||
## v1.0.0 (2024-10-18)
|
||||
|
||||
### Breaking
|
||||
|
||||
* feat!: ability to disable scatter from waveform & compatible crosshair with down sampling ([`2ab12ed`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2ab12ed60abb995abc381d9330fdcf399796d9e5))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix(crosshair): downsample clear markers ([`f9a889f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f9a889fc6d380b9e587edcb465203122ea0bffc1))
|
||||
|
||||
|
||||
## v0.119.0 (2024-10-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix: fix syntax due to change of api for simulated devices ([`19f4e40`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/19f4e407e00ee242973ca4c3f90e4e41a4d3e315))
|
||||
|
||||
* fix: remove wrongly scoped test ([`a23841b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a23841b2553dc7162da943715d58275c7dc39ed9))
|
||||
|
||||
* fix: rename 'compact' property -> 'compact_view' ([`6982711`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6982711fea5fb8a73845ed7c0692e3ec53ef7871))
|
||||
|
||||
* fix: Alignment 1D update, make app window a main window (in .ui file) ([`0015f0e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0015f0e2d62adc02d3ef334e1f6dbb2d0288fec6))
|
||||
|
||||
* fix: set (Minimum, Fixed) size policy on Stop button ([`523cc43`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/523cc435725b10b7d59a4477a1aaa24a1f3e37a2))
|
||||
|
||||
### Features
|
||||
|
||||
* feat: new PositionerGroup widget ([`af9655d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af9655de0c541092437accfbaa779628a2f48ccb))
|
||||
|
||||
* feat: add 'expand_popup' property to CompactPopupWidget
|
||||
|
||||
This property tells if expand should show a popup (by default), or
|
||||
if the widget should expand in-place ([`e4121a0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e4121a01cb6b8d496e630cd43bc642b994b8f310))
|
||||
|
||||
* feat: PositionerBox with a popup view ([`2615787`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/261578796f1de8ca9cab9b91659bc1484f7aa89d))
|
||||
|
||||
* feat: emit 'device_selected' and 'scan_axis' from scan control widget ([`0b9b1a3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0b9b1a3c89a98505079f7d4078915b7bbfaa1e23))
|
||||
|
||||
* feat: new 'device_selected' signals to ScanControl, ScanGroupBox, DeviceLineEdit ([`9801d27`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9801d2769eb0ee95c94ec0c011e1dac1407142ae))
|
||||
|
||||
### Refactoring
|
||||
|
||||
* refactor: redesign of scan selection and scan control boxes ([`a69d287`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a69d2870e2b3539739781d741b27b8599c0f4abd))
|
||||
|
||||
* refactor: move add/remove bundle to scan group box ([`e3d0a7b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e3d0a7bbf9918dc16eb7227a178c310256ce570d))
|
||||
|
||||
|
||||
## v0.118.0 (2024-10-13)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(progressbar): added docs ([`7d07cea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7d07cea946f9c884477b01bebfb60b332ff09e0a))
|
||||
* docs(sphinx-build): adjusted pyside verion ([`b236951`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b23695167ab969f754a058ffdccca2b40f00a008))
|
||||
|
||||
### Feature
|
||||
### Features
|
||||
|
||||
* feat(progressbar): added bec progressbar ([`f6d1d0b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6d1d0bbe3ba30a3b7291cd36a1f7f8e6bd5b895))
|
||||
* feat(image): image widget can take data from monitor_1d endpoint ([`9ef1d1c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9ef1d1c9ac2178d9fa2e655942208f8abbdf5c1b))
|
||||
|
||||
* feat(generate_cli): added support for property and qproperty setter ([`a52182d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a52182dca978833bfc3fad755c596d3a2ef45c42))
|
||||
|
||||
## v0.107.0 (2024-09-06)
|
||||
## v0.117.1 (2024-10-11)
|
||||
|
||||
### Documentation
|
||||
### Bug Fixes
|
||||
|
||||
* docs: extend waveform docs ([`e6976dc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6976dc15105209852090a00a97b7cda723142e9))
|
||||
* fix(FPS): qtimer cleanup leaking ([`3a22392`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3a2239278075de7489ad10a58c31d7d89715e221))
|
||||
|
||||
### Feature
|
||||
### Unknown
|
||||
|
||||
* feat: add roi select for dap, allow automatic clear curves on plot request ([`7bdca84`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7bdca8431496fe6562d2c28f5a6af869d1a2e654))
|
||||
* feature(vscode): added support for vscode instructions ([`f5f1f6c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f5f1f6c304b890dc162e8653005233bce4ea82e4))
|
||||
|
||||
### Refactor
|
||||
* feature(vscode): support for controlling vscode from widgets ([`9238679`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/923867947f62db026ac0378c30ef62c883596058))
|
||||
|
||||
* refactor: change style to bec_accent_colors ([`bd126dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bd126dddbbec3e6c448cce263433d328d577c5c0))
|
||||
|
||||
### Test
|
||||
## v0.117.0 (2024-10-11)
|
||||
|
||||
* test: add tests, including extension to end-2-end test ([`b1aff6d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b1aff6d791ff847eb2f628e66ccaa4672fdeea08))
|
||||
### Features
|
||||
|
||||
## v0.106.0 (2024-09-05)
|
||||
* feat(utils): FPS counter utility based on the viewBox updates, integrated to waveform and image widget ([`8c5ef26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8c5ef268430d5243ac05fcbbdb6b76ad24ac5735))
|
||||
|
||||
### Feature
|
||||
### Unknown
|
||||
|
||||
* feat(plot_base): toggle to switch outer axes for plotting widgets ([`06d7741`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06d7741622aea8556208cd17cae521c37333f8b6))
|
||||
* tests(plot_base): tests extended ([`8dc892d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8dc892df0a47ccbdd812555b7c5775a455a23ede))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: use DAPComboBox in curve_dialog selection ([`998a745`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/998a7451335b1b35c3e18691d3bab8d882e2d30b))
|
||||
## v0.116.0 (2024-10-11)
|
||||
|
||||
### Test
|
||||
### Features
|
||||
|
||||
* test: fix tests ([`6b15abc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b15abcc73170cb49292741a619a08ee615e6250))
|
||||
* feat: UI changes to have top toolbar with compact popup widgets (fix issue #360) ([`499b6b9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/499b6b9a12efd931b5728b519404c41a7e29e4d6))
|
||||
|
||||
## v0.105.0 (2024-09-04)
|
||||
* feat: adapt BECQueue and BECStatusBox widgets to use CompactPopupWidget ([`94ce92f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/94ce92f5b054d25ea3bb7976c1f75e14b78b9edc))
|
||||
|
||||
### Feature
|
||||
* feat: add 'CompactPopupWidget' container widget
|
||||
|
||||
* feat: add dap_combobox ([`cc691d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cc691d4039bde710e78f362d2f0e712f9e8f196f))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: cleanup and renaming of slot/signals ([`0fd5cee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0fd5cee77611b6645326eaefa68455ea8de26597))
|
||||
|
||||
* refactor(logger): changed prints to logger calls ([`3a5d7d0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3a5d7d07966ab9b38ba33bda0bed38c30f500c66))
|
||||
|
||||
## v0.104.0 (2024-09-04)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(scan_control): docs extended ([`730e25f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/730e25fd3a8be156603005982bfd2a2c2b16dff1))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(scan_control): scan control remember the previously set parameters and shares kwarg settings across scans ([`d28f9b0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d28f9b04c45385179353cc247221ec821dcaa29b))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(scan_control): SafeSlot applied to run_scan to avoid faulty scan requests ([`9047916`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/90479167fb5cae393c884e71a80fcfdb48a76427))
|
||||
|
||||
* fix(scan_control): scan parameters can be loaded from the last executed scan from redis ([`ec3bc8b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ec3bc8b5194c680b847d3306c41eef4638ccfcc7))
|
||||
|
||||
* fix(toggle): state can be determined with the widget initialisation ([`2cd9c7f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2cd9c7f5854f158468e53b5b29ec31b1ff1e00e6))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(scan_control): scan control layout adjusted ([`85dcbda`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/85dcbdaa88fe77aeea7012bfc16f10c4f873f75e))
|
||||
|
||||
* refactor(scan_control): basic pydantic config added ([`fe8dc55`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fe8dc55eb102c51c34bf9606690914da53b5ac02))
|
||||
|
||||
### Test
|
||||
|
||||
* test(scan_control): tests extended for getting kwargs between scan switching and getting parameters from redis ([`b07e677`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b07e67715c9284e9bf36056ba4ba8068f60cbaf3))
|
||||
|
||||
* test(conftest): only run cleanup checks if test passed ([`26920f8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/26920f8482bdb35ece46df37232af50ab9cab463))
|
||||
|
||||
## v0.103.0 (2024-09-04)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: prefill variables for manual pipeline start ([`158c19e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/158c19eda771562a325fd59405f9fd4cb9a17ed6))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(vscode): open vscode on a free port ([`52da835`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/52da835803f2453096a8b7df23bee5fdf93ae2bb))
|
||||
|
||||
* feat(website): added method to wait until the webpage is loaded ([`9be19d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9be19d4abebad08c5fc6bea936dd97475fe8f628))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(theme): fixed segfault for webengineview for auto updates ([`9866075`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9866075100577948755b563dc7b7dc4cdc60d040))
|
||||
|
||||
### Test
|
||||
|
||||
* test(webview): fixed tests after refactoring ([`d5eb30c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d5eb30cd7df4cb0dc3275dd362768afc211eaf2d))
|
||||
|
||||
* test(vscode): popen call does not have to be the only one ([`39f98ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39f98ec223ba8b59e478ac788c08c59ffe886b4e))
|
||||
|
||||
## v0.102.0 (2024-09-04)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(buttons): buttons section of docs split to appearance and queue buttons ([`047aa26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/047aa26a60220c826cc1375cf81daf11d1f3ab5c))
|
||||
|
||||
* docs(tests): added tests tutorial for widget ([`18d8561`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/18d8561c965d149a7662085f7dbe2a39a8c4a475))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(queue): BECQueue controls extended with Resume, Stop, Abort, Reset buttons ([`0d7c10e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0d7c10e670e4937787e1afaa19ca8259ac752486))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(queue_reset_button): queue reset has to be confirmed with msgBox ([`9dd43aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9dd43aa1fd3991368002605df4389a7a7271011b))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(tests): positioner box test changed to use create_widget fixture ([`df5eff3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df5eff3147c79ff0278e6a5a09c8f73d5236aed3))
|
||||
|
||||
## v0.101.0 (2024-09-02)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: add Dap dialog widget ([`9781b77`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9781b77de27b2810fbb1047a61b1832dd186db01))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: add docs, cleanup ([`61ecf49`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61ecf491e52bfbfa0d5a84764a9095310659043d))
|
||||
|
||||
## v0.100.0 (2024-09-01)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(becwidget): improvements to the bec widget base class docs; fixed type hint import for sphinx ([`99d5e8e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99d5e8e71c7f89a53d7967126f4056dde005534c))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(pyqt slot): removed slot decorator to avoid problems with pyqt6 ([`6c1f89a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6c1f89ad39b7240ab1d1c1123422b99ae195bf01))
|
||||
Makes it easy to write widgets which can have a compact
|
||||
representation with LED-like global state indicator,
|
||||
with the possibility to display a popup dialog with more
|
||||
complete UI ([`49268e3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/49268e3829406d70b09e4d88989812f5578e46f4))
|
||||
|
||||
0
bec_widgets/applications/__init__.py
Normal file
0
bec_widgets/applications/alignment/__init__.py
Normal file
209
bec_widgets/applications/alignment/alignment_1d/alignment_1d.py
Normal file
@@ -0,0 +1,209 @@
|
||||
""" This module contains the GUI for the 1D alignment application.
|
||||
It is a preliminary version of the GUI, which will be added to the main branch and steadily updated to be improved.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from bec_lib.device import Positioner as BECPositioner
|
||||
from bec_lib.device import Signal as BECSignal
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QSize, Signal
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QDoubleSpinBox,
|
||||
QMainWindow,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
)
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.qt_utils.toolbar import WidgetAction
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.widgets.bec_progressbar.bec_progressbar import BECProgressBar
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
from bec_widgets.widgets.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.positioner_group.positioner_group import PositionerGroup
|
||||
from bec_widgets.widgets.stop_button.stop_button import StopButton
|
||||
from bec_widgets.widgets.toggle.toggle import ToggleSwitch
|
||||
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class Alignment1D:
|
||||
"""Alignment GUI to perform 1D scans"""
|
||||
|
||||
def __init__(self, client=None, gui_id: Optional[str] = None) -> None:
|
||||
"""Initialization
|
||||
|
||||
Args:
|
||||
config: Configuration of the application.
|
||||
client: BEC client object.
|
||||
gui_id: GUI ID.
|
||||
"""
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
QApplication.instance().aboutToQuit.connect(self.close)
|
||||
self.dev = self.client.device_manager.devices
|
||||
|
||||
self._accent_colors = get_accent_colors()
|
||||
self.ui_file = "alignment_1d.ui"
|
||||
self.ui = None
|
||||
self.progress_bar = None
|
||||
self.waveform = None
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialise the UI from QT Designer file"""
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader(None).loader(os.path.join(current_path, self.ui_file))
|
||||
# Customize the plotting widget
|
||||
self.waveform = self.ui.findChild(BECWaveformWidget, "bec_waveform_widget")
|
||||
self._customise_bec_waveform_widget()
|
||||
# Setup comboboxes for motor and signal selection
|
||||
# FIXME after changing the filtering in the combobox
|
||||
self._setup_signal_combobox()
|
||||
# Setup motor indicator
|
||||
self._setup_motor_indicator()
|
||||
# Setup progress bar
|
||||
self._setup_progress_bar()
|
||||
# Add actions buttons
|
||||
self._customise_buttons()
|
||||
# Hook scaninfo updates
|
||||
self.bec_dispatcher.connect_slot(self.scan_status_callback, MessageEndpoints.scan_status())
|
||||
|
||||
def show(self):
|
||||
return self.ui.show()
|
||||
|
||||
##############################
|
||||
############ SLOTS ###########
|
||||
##############################
|
||||
|
||||
@Slot(dict, dict)
|
||||
def scan_status_callback(self, content: dict, _) -> None:
|
||||
"""This slot allows to enable/disable the UI critical components when a scan is running"""
|
||||
if content["status"] in ["open"]:
|
||||
self.enable_ui(False)
|
||||
elif content["status"] in ["aborted", "halted", "closed"]:
|
||||
self.enable_ui(True)
|
||||
|
||||
@Slot(tuple)
|
||||
def move_to_center(self, move_request: tuple) -> None:
|
||||
"""Move the selected motor to the center"""
|
||||
motor = self.ui.device_combobox.currentText()
|
||||
if move_request[0] in ["center", "center1", "center2"]:
|
||||
pos = move_request[1]
|
||||
self.dev.get(motor).move(float(pos), relative=False)
|
||||
|
||||
@Slot()
|
||||
def reset_progress_bar(self) -> None:
|
||||
"""Reset the progress bar"""
|
||||
self.progress_bar.set_value(0)
|
||||
self.progress_bar.set_minimum(0)
|
||||
|
||||
@Slot(dict, dict)
|
||||
def update_progress_bar(self, content: dict, _) -> None:
|
||||
"""Hook to update the progress bar
|
||||
|
||||
Args:
|
||||
content: Content of the scan progress message.
|
||||
metadata: Metadata of the message.
|
||||
"""
|
||||
if content["max_value"] == 0:
|
||||
self.progress_bar.set_value(0)
|
||||
return
|
||||
self.progress_bar.set_maximum(content["max_value"])
|
||||
self.progress_bar.set_value(content["value"])
|
||||
|
||||
@Slot()
|
||||
def clear_queue(self) -> None:
|
||||
"""Clear the scan queue"""
|
||||
self.queue.request_queue_reset()
|
||||
|
||||
##############################
|
||||
######## END OF SLOTS ########
|
||||
##############################
|
||||
|
||||
def enable_ui(self, enable: bool) -> None:
|
||||
"""Enable or disable the UI components"""
|
||||
# Enable/disable motor and signal selection
|
||||
self.ui.device_combobox_2.setEnabled(enable)
|
||||
# Enable/disable DAP selection
|
||||
self.ui.dap_combo_box.setEnabled(enable)
|
||||
# Enable/disable Scan Button
|
||||
# self.ui.scan_button.setEnabled(enable)
|
||||
# Disable move to buttons in LMFitDialog
|
||||
self.ui.findChild(LMFitDialog).set_actions_enabled(enable)
|
||||
|
||||
def _customise_buttons(self) -> None:
|
||||
"""Add action buttons for the Action Control.
|
||||
In addition, we are adding a callback to also clear the queue to the stop button
|
||||
to ensure that upon clicking the button, no scans from another client may be queued
|
||||
which would be confusing without the queue widget.
|
||||
"""
|
||||
fit_dialog = self.ui.findChild(LMFitDialog)
|
||||
fit_dialog.active_action_list = ["center", "center1", "center2"]
|
||||
fit_dialog.move_action.connect(self.move_to_center)
|
||||
stop_button = self.ui.findChild(StopButton)
|
||||
stop_button.button.setText("Stop and Clear Queue")
|
||||
stop_button.button.clicked.connect(self.clear_queue)
|
||||
|
||||
def _customise_bec_waveform_widget(self) -> None:
|
||||
"""Customise the BEC Waveform Widget, i.e. clear the toolbar"""
|
||||
self.waveform.toolbar.clear()
|
||||
|
||||
def _setup_motor_indicator(self) -> None:
|
||||
"""Setup the arrow item"""
|
||||
self.waveform.waveform.tick_item.add_to_plot()
|
||||
positioner_box = self.ui.findChild(PositionerGroup)
|
||||
positioner_box.position_update.connect(self.waveform.waveform.tick_item.set_position)
|
||||
self.waveform.waveform.tick_item.set_position(0)
|
||||
|
||||
def _setup_signal_combobox(self) -> None:
|
||||
"""Setup signal selection"""
|
||||
# FIXME after changing the filtering in the combobox
|
||||
signals = [name for name in self.dev if isinstance(self.dev.get(name), BECSignal)]
|
||||
self.ui.device_combobox_2.setCurrentText(signals[0])
|
||||
self.ui.device_combobox_2.set_device_filter("Signal")
|
||||
|
||||
def _setup_progress_bar(self) -> None:
|
||||
"""Setup progress bar"""
|
||||
# FIXME once the BECScanProgressBar is implemented
|
||||
self.progress_bar = self.ui.findChild(BECProgressBar, "bec_progress_bar")
|
||||
self.progress_bar.set_value(0)
|
||||
self.ui.bec_waveform_widget.new_scan.connect(self.reset_progress_bar)
|
||||
self.bec_dispatcher.connect_slot(self.update_progress_bar, MessageEndpoints.scan_progress())
|
||||
|
||||
def close(self):
|
||||
logger.info("Disconnecting", repr(self.bec_dispatcher))
|
||||
self.bec_dispatcher.disconnect_all()
|
||||
logger.info("Shutting down BEC Client", repr(self.client))
|
||||
self.client.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "alignment_1d.png"), size=QSize(48, 48)
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
window = Alignment1D()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
615
bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui
Normal file
@@ -0,0 +1,615 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>mainWindow</class>
|
||||
<widget class="QMainWindow" name="mainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1611</width>
|
||||
<height>1019</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Alignment tool</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="widget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="DarkModeButton" name="dark_mode_button"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECStatusBox" name="bec_status_box">
|
||||
<property name="compact_view" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="label" stdset="0">
|
||||
<string>BEC Servers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECQueue" name="bec_queue">
|
||||
<property name="compact_view" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radioButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>SLS Light On</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radioButton_3">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BEAMLINE Checks</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="autoExclusive">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="StopButton" name="stop_button">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECProgressBar" name="bec_progress_bar">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="ControlTab">
|
||||
<attribute name="title">
|
||||
<string>Alignment Control</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_4" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="ScanControl" name="scan_control">
|
||||
<property name="current_scan" stdset="0">
|
||||
<string>line_scan</string>
|
||||
</property>
|
||||
<property name="hide_arg_box" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="hide_scan_selection_combobox" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_add_remove_buttons" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PositionerGroup" name="positioner_group"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>4</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_2" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Monitor</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DeviceComboBox" name="device_combobox_2"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>LMFit Model</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DapComboBox" name="dap_combo_box"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Enable ROI</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ToggleSwitch" name="toggle_switch">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>3</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Activate linear region select for LMFit</string>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LayoutDirection::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_8">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECWaveformWidget" name="bec_waveform_widget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="clear_curves_on_plot_update" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="LMFitDialog" name="lm_fit_dialog">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>190</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="always_show_latest" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_curve_selection" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="hide_summary" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Logbook</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="WebsiteWidget" name="website_widget">
|
||||
<property name="url" stdset="0">
|
||||
<string>https://scilog.psi.ch/login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DapComboBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dap_combo_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>StopButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>stop_button</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WebsiteWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>website_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECQueue</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_queue</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ScanControl</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>scan_control</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECProgressBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_progress_bar</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DarkModeButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dark_mode_button</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PositionerGroup</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>positioner_group</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECWaveformWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_waveform_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DeviceComboBox</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>device_combobox</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>LMFitDialog</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>lm_fit_dialog</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECStatusBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_status_box</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>toggle_switch</sender>
|
||||
<signal>enabled(bool)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>toogle_roi_select(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1042</x>
|
||||
<y>212</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>322</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>bec_waveform_widget</sender>
|
||||
<signal>dap_summary_update(QVariantMap,QVariantMap)</signal>
|
||||
<receiver>lm_fit_dialog</receiver>
|
||||
<slot>update_summary_tree(QVariantMap,QVariantMap)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1099</x>
|
||||
<y>258</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1157</x>
|
||||
<y>929</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>device_combobox_2</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>plot(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>577</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>427</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>device_combobox_2</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>dap_combo_box</receiver>
|
||||
<slot>select_y_axis(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>577</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>909</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>dap_combo_box</sender>
|
||||
<signal>new_dap_config(QString,QString,QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>add_dap(QString,QString,QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>909</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1416</x>
|
||||
<y>447</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>positioner_group</receiver>
|
||||
<slot>set_positioners(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>230</x>
|
||||
<y>306</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>187</x>
|
||||
<y>926</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>bec_waveform_widget</receiver>
|
||||
<slot>set_x(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>187</x>
|
||||
<y>356</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>972</x>
|
||||
<y>509</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>scan_control</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>dap_combo_box</receiver>
|
||||
<slot>select_x_axis(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>187</x>
|
||||
<y>356</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>794</x>
|
||||
<y>202</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
84
bec_widgets/applications/bec_app.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Launcher for BEC GUI Applications
|
||||
|
||||
Application must be located in bec_widgets/applications ;
|
||||
in order for the launcher to find the application, it has to be put in
|
||||
a subdirectory with the same name as the main Python module:
|
||||
|
||||
/bec_widgets/applications
|
||||
├── alignment
|
||||
│ └── alignment_1d
|
||||
│ └── alignment_1d.py
|
||||
├── other_app
|
||||
└── other_app.py
|
||||
|
||||
The tree above would contain 2 applications, alignment_1d and other_app.
|
||||
|
||||
The Python module for the application must have `if __name__ == "__main__":`
|
||||
in order for the launcher to execute it (it is run with `python -m`).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
MODULE_PATH = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def find_apps(base_dir: str) -> list[str]:
|
||||
matching_modules = []
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
parent_dir = os.path.basename(root)
|
||||
|
||||
for file in files:
|
||||
if file.endswith(".py") and file != "__init__.py":
|
||||
file_name_without_ext = os.path.splitext(file)[0]
|
||||
|
||||
if file_name_without_ext == parent_dir:
|
||||
rel_path = os.path.relpath(root, base_dir)
|
||||
module_path = rel_path.replace(os.sep, ".")
|
||||
|
||||
module_name = f"{module_path}.{file_name_without_ext}"
|
||||
matching_modules.append((file_name_without_ext, module_name))
|
||||
|
||||
return matching_modules
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="BEC application launcher")
|
||||
|
||||
parser.add_argument("-m", "--module", type=str, help="The module to run (string argument).")
|
||||
|
||||
# Add a positional argument for the module, which acts as a fallback if -m is not provided
|
||||
parser.add_argument(
|
||||
"positional_module",
|
||||
nargs="?", # This makes the positional argument optional
|
||||
help="Positional argument that is treated as module if -m is not specified.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
# If the -m/--module is not provided, fallback to the positional argument
|
||||
module = args.module if args.module else args.positional_module
|
||||
|
||||
if module:
|
||||
for app_name, app_module in find_apps(MODULE_PATH):
|
||||
if module in (app_name, app_module):
|
||||
print("Starting:", app_name)
|
||||
python_executable = sys.executable
|
||||
|
||||
# Replace the current process with the new Python module
|
||||
os.execvp(
|
||||
python_executable,
|
||||
[python_executable, "-m", f"bec_widgets.applications.{app_module}"],
|
||||
)
|
||||
print(f"Error: cannot find application {module}")
|
||||
|
||||
# display list of apps
|
||||
print("Available applications:")
|
||||
for app, _ in find_apps(MODULE_PATH):
|
||||
print(f" - {app}")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
BIN
bec_widgets/assets/app_icons/alignment_1d.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#EA3323">
|
||||
<path d="M479.85-265.87q19.8 0 32.69-12.46 12.9-12.46 12.9-32.26 0-19.8-12.75-32.98-12.74-13.17-32.54-13.17-19.8 0-32.69 13.16-12.9 13.15-12.9 32.95 0 19.8 12.75 32.28 12.74 12.48 32.54 12.48Zm-36.46-166.56h79.22v-262.61h-79.22v262.61Zm36.95 366.56q-86.2 0-161.5-32.39-75.3-32.4-131.74-88.84-56.44-56.44-88.84-131.73-32.39-75.3-32.39-161.59t32.39-161.67q32.4-75.37 88.75-131.34t131.69-88.62q75.34-32.65 161.67-32.65 86.34 0 161.78 32.61 75.45 32.6 131.37 88.5 55.93 55.89 88.55 131.45 32.63 75.56 32.63 161.87 0 86.29-32.65 161.58t-88.62 131.48q-55.97 56.18-131.42 88.76-75.46 32.58-161.67 32.58Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 718 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#EA3323">
|
||||
<path d="m759.04-283.09-63.13-62q49.31-9.43 84.44-46.02 35.13-36.59 35.13-86.14 0-55.49-39.42-95.08-39.43-39.58-94.93-39.58h-153.3v-79.79h152.74q89.28 0 151.7 62.71Q894.7-566.28 894.7-477q0 63.7-38.26 115.96-38.27 52.26-97.4 77.95ZM596.83-443.61l-65.66-66.78h110.05v66.78h-44.39ZM804.96-56 58.48-802.48 106-850l746.48 746.48L804.96-56ZM443.22-265.87H279.43q-89.28 0-151.7-62.42Q65.3-390.72 65.3-480q0-72.57 43.09-129.54 43.09-56.98 112.09-76.07l70.13 70.7h-11.18q-55.73 0-95.32 39.3-39.59 39.31-39.59 95.61t39.66 95.61q39.66 39.3 95.5 39.3h163.54v79.22ZM319.35-446.61v-66.78h77.3l66.78 66.78H319.35Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 721 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFF55">
|
||||
<path d="M478.3-145.87q-138.65 0-236.39-97.74-97.74-97.74-97.74-236.25t97.74-236.68q97.74-98.16 236.39-98.16 88.4 0 155.45 35.76 67.04 35.76 115.86 98.9V-814.7h66.78v274.92H540.91V-606h165.74q-38.56-57.74-95.3-93.33-56.74-35.58-133.05-35.58-106.88 0-180.89 73.98-74.02 73.99-74.02 180.83 0 106.84 74.02 180.93 74.02 74.08 180.91 74.08 80.16 0 147.74-46.08 67.59-46.09 95.16-121.83H803q-29.56 110.65-119.67 178.89-90.1 68.24-205.03 68.24Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 559 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#75FB4C">
|
||||
<path d="m419.87-289.52 289.22-289.22-57.31-56.87L419.87-403.7 304.96-518.61l-56.31 56.87 171.22 172.22Zm60.21 223.65q-85.47 0-161.01-32.39-75.53-32.4-131.97-88.84-56.44-56.44-88.84-131.89-32.39-75.46-32.39-160.93 0-86.47 32.39-162.01 32.4-75.53 88.75-131.5t131.85-88.62q75.5-32.65 161.01-32.65 86.52 0 162.12 32.61 75.61 32.6 131.53 88.5 55.93 55.89 88.55 131.45Q894.7-566.58 894.7-480q0 85.55-32.65 161.07-32.65 75.53-88.62 131.9-55.97 56.37-131.42 88.77-75.46 32.39-161.93 32.39Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 604 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#F19E39">
|
||||
<path d="M27.56-112.65 480-894.7l452.44 782.05H27.56Zm456.62-125.48q13.15 0 22.61-9.64 9.47-9.65 9.47-22.8t-9.64-22.33q-9.65-9.19-22.8-9.19t-22.61 9.36q-9.47 9.36-9.47 22.51 0 13.15 9.64 22.62 9.65 9.47 22.8 9.47ZM454-348h60v-219.48h-60V-348Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 364 B |
@@ -1,5 +1,7 @@
|
||||
# This file was automatically generated by generate_cli.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
@@ -14,6 +16,7 @@ class Widgets(str, enum.Enum):
|
||||
"""
|
||||
|
||||
AbortButton = "AbortButton"
|
||||
BECColorMapWidget = "BECColorMapWidget"
|
||||
BECDock = "BECDock"
|
||||
BECDockArea = "BECDockArea"
|
||||
BECFigure = "BECFigure"
|
||||
@@ -29,12 +32,16 @@ class Widgets(str, enum.Enum):
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
LMFitDialog = "LMFitDialog"
|
||||
PositionIndicator = "PositionIndicator"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerControlLine = "PositionerControlLine"
|
||||
PositionerGroup = "PositionerGroup"
|
||||
ResetButton = "ResetButton"
|
||||
ResumeButton = "ResumeButton"
|
||||
RingProgressBar = "RingProgressBar"
|
||||
ScanControl = "ScanControl"
|
||||
SignalComboBox = "SignalComboBox"
|
||||
SignalLineEdit = "SignalLineEdit"
|
||||
StopButton = "StopButton"
|
||||
TextBox = "TextBox"
|
||||
VSCodeEditor = "VSCodeEditor"
|
||||
@@ -59,6 +66,15 @@ class AbortButton(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECColorMapWidget(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def colormap(self):
|
||||
"""
|
||||
Get the current colormap name.
|
||||
"""
|
||||
|
||||
|
||||
class BECCurve(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
@@ -549,6 +565,7 @@ class BECFigure(RPCBase):
|
||||
def image(
|
||||
self,
|
||||
monitor: "str" = None,
|
||||
monitor_type: "Literal['1d', '2d']" = "2d",
|
||||
color_bar: "Literal['simple', 'full']" = "full",
|
||||
color_map: "str" = "magma",
|
||||
data: "np.ndarray" = None,
|
||||
@@ -853,6 +870,7 @@ class BECImageShow(RPCBase):
|
||||
def image(
|
||||
self,
|
||||
monitor: "str",
|
||||
monitor_type: "Literal['1d', '2d']" = "2d",
|
||||
color_map: "Optional[str]" = "magma",
|
||||
color_bar: "Optional[Literal['simple', 'full']]" = "full",
|
||||
downsample: "Optional[bool]" = True,
|
||||
@@ -865,6 +883,7 @@ class BECImageShow(RPCBase):
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
monitor_type(Literal["1d","2d"]): The type of monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
@@ -1132,6 +1151,15 @@ class BECImageShow(RPCBase):
|
||||
y(bool): Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self, enable: "bool" = True):
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock):
|
||||
"""
|
||||
@@ -1168,6 +1196,7 @@ class BECImageWidget(RPCBase):
|
||||
def image(
|
||||
self,
|
||||
monitor: "str",
|
||||
monitor_type: "Optional[Literal['1d', '2d']]" = "2d",
|
||||
color_map: "Optional[str]" = "magma",
|
||||
color_bar: "Optional[Literal['simple', 'full']]" = "full",
|
||||
downsample: "Optional[bool]" = True,
|
||||
@@ -1327,6 +1356,15 @@ class BECImageWidget(RPCBase):
|
||||
y_grid(bool): Visibility of the y-axis grid.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self, enabled: "bool"):
|
||||
"""
|
||||
Enable the FPS monitor of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock: "bool"):
|
||||
"""
|
||||
@@ -1663,6 +1701,15 @@ class BECPlotBase(RPCBase):
|
||||
show(bool): Show the outer axes.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self, enable: "bool" = True):
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock):
|
||||
"""
|
||||
@@ -2066,6 +2113,24 @@ class BECWaveform(RPCBase):
|
||||
colormap(str, optional): Scale the colors of curves to colormap. If None, use the default color palette.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_scatter(self, enable: "bool"):
|
||||
"""
|
||||
Enable/Disable scatter plot on all curves.
|
||||
|
||||
Args:
|
||||
enable(bool): If True, enable scatter markers; if False, disable them.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self, enable: "bool" = True):
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock):
|
||||
"""
|
||||
@@ -2371,6 +2436,24 @@ class BECWaveformWidget(RPCBase):
|
||||
y_grid(bool): Visibility of the y-axis grid.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self, enabled: "bool"):
|
||||
"""
|
||||
Enable the FPS monitor of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_scatter(self, enabled: "bool"):
|
||||
"""
|
||||
Enable the scatter plot of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the scatter plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock: "bool"):
|
||||
"""
|
||||
@@ -2521,6 +2604,24 @@ class DeviceLineEdit(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class DeviceSignalInputBase(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
|
||||
class LMFitDialog(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -2539,6 +2640,45 @@ class LMFitDialog(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_range(self, min_value: float, max_value: float):
|
||||
"""
|
||||
Set the range of the position indicator
|
||||
|
||||
Args:
|
||||
min_value(float): Minimum value of the range
|
||||
max_value(float): Maximum value of the range
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def vertical(self):
|
||||
"""
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def indicator_width(self):
|
||||
"""
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rounded_corners(self):
|
||||
"""
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox(RPCBase):
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
@@ -2561,6 +2701,16 @@ class PositionerControlLine(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class PositionerGroup(RPCBase):
|
||||
@rpc_call
|
||||
def set_positioners(self, device_names: "str"):
|
||||
"""
|
||||
Redraw grid with positioners from device_names string
|
||||
|
||||
Device names must be separated by space
|
||||
"""
|
||||
|
||||
|
||||
class ResetButton(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -2894,6 +3044,42 @@ class ScanControl(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class SignalComboBox(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
|
||||
class SignalLineEdit(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
|
||||
class StopButton(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
|
||||
@@ -31,6 +31,7 @@ else:
|
||||
class ClientGenerator:
|
||||
def __init__(self):
|
||||
self.header = """# This file was automatically generated by generate_cli.py\n
|
||||
from __future__ import annotations
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
|
||||
self.d1 = self.dock.add_dock(name="dock_1", position="right")
|
||||
self.im = self.d1.add_widget("BECImageWidget")
|
||||
self.im.image("eiger")
|
||||
self.im.image("waveform", "1d")
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.wf = self.d2.add_widget("BECWaveformWidget", row=0, col=0)
|
||||
|
||||
268
bec_widgets/qt_utils/compact_popup.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Property, Qt, Signal
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
|
||||
|
||||
class LedLabel(QLabel):
|
||||
success_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.145, y1:0.16, x2:1, y2:1, stop:0 %s, stop:1 %s);"
|
||||
emergency_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.145, y1:0.16, x2:0.92, y2:0.988636, stop:0 %s, stop:1 %s);"
|
||||
warning_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.232, y1:0.272, x2:0.98, y2:0.959773, stop:0 %s, stop:1 %s);"
|
||||
default_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.04, y1:0.0565909, x2:0.799, y2:0.795, stop:0 %s, stop:1 %s);"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.palette = get_accent_colors()
|
||||
if self.palette is None:
|
||||
# no theme!
|
||||
self.palette = SimpleNamespace(
|
||||
default=QColor("blue"),
|
||||
success=QColor("green"),
|
||||
warning=QColor("orange"),
|
||||
emergency=QColor("red"),
|
||||
)
|
||||
self.setState("default")
|
||||
self.setFixedSize(20, 20)
|
||||
|
||||
def setState(self, state: str):
|
||||
match state:
|
||||
case "success":
|
||||
r, g, b, a = self.palette.success.getRgb()
|
||||
self.setStyleSheet(
|
||||
LedLabel.success_led
|
||||
% (
|
||||
f"rgba({r},{g},{b},{a})",
|
||||
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
|
||||
)
|
||||
)
|
||||
case "default":
|
||||
r, g, b, a = self.palette.default.getRgb()
|
||||
self.setStyleSheet(
|
||||
LedLabel.default_led
|
||||
% (
|
||||
f"rgba({r},{g},{b},{a})",
|
||||
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
|
||||
)
|
||||
)
|
||||
case "warning":
|
||||
r, g, b, a = self.palette.warning.getRgb()
|
||||
self.setStyleSheet(
|
||||
LedLabel.warning_led
|
||||
% (
|
||||
f"rgba({r},{g},{b},{a})",
|
||||
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
|
||||
)
|
||||
)
|
||||
case "emergency":
|
||||
r, g, b, a = self.palette.emergency.getRgb()
|
||||
self.setStyleSheet(
|
||||
LedLabel.emergency_led
|
||||
% (
|
||||
f"rgba({r},{g},{b},{a})",
|
||||
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
|
||||
)
|
||||
)
|
||||
case unknown_state:
|
||||
raise ValueError(
|
||||
f"Unknown state {repr(unknown_state)}, must be one of default, success, warning or emergency"
|
||||
)
|
||||
|
||||
|
||||
class PopupDialog(QDialog):
|
||||
def __init__(self, content_widget):
|
||||
self.parent = content_widget.parent()
|
||||
self.content_widget = content_widget
|
||||
|
||||
super().__init__(self.parent)
|
||||
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
|
||||
self.content_widget.setParent(self)
|
||||
QVBoxLayout(self)
|
||||
self.layout().addWidget(self.content_widget)
|
||||
self.content_widget.setVisible(True)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.content_widget.setVisible(False)
|
||||
self.content_widget.setParent(self.parent)
|
||||
self.done(True)
|
||||
|
||||
|
||||
class CompactPopupWidget(QWidget):
|
||||
"""Container widget, that can display its content or have a compact form,
|
||||
in this case clicking on a small button pops the contained widget up.
|
||||
|
||||
In the compact form, a LED-like indicator shows a status indicator.
|
||||
"""
|
||||
|
||||
expand = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None, layout=QVBoxLayout):
|
||||
super().__init__(parent)
|
||||
|
||||
self._popup_window = None
|
||||
self._expand_popup = True
|
||||
|
||||
QVBoxLayout(self)
|
||||
self.compact_view_widget = QWidget(self)
|
||||
self.compact_view_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
QHBoxLayout(self.compact_view_widget)
|
||||
self.compact_view_widget.layout().setSpacing(0)
|
||||
self.compact_view_widget.layout().setContentsMargins(0, 0, 0, 0)
|
||||
self.compact_view_widget.layout().addSpacerItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
)
|
||||
self.compact_label = QLabel(self.compact_view_widget)
|
||||
self.compact_status = LedLabel(self.compact_view_widget)
|
||||
self.compact_show_popup = QPushButton(self.compact_view_widget)
|
||||
self.compact_show_popup.setFlat(True)
|
||||
self.compact_show_popup.setIcon(
|
||||
material_icon(icon_name="expand_content", size=(10, 10), convert_to_pixmap=False)
|
||||
)
|
||||
self.compact_view_widget.layout().addWidget(self.compact_label)
|
||||
self.compact_view_widget.layout().addWidget(self.compact_status)
|
||||
self.compact_view_widget.layout().addWidget(self.compact_show_popup)
|
||||
self.compact_view_widget.setVisible(False)
|
||||
self.layout().addWidget(self.compact_view_widget)
|
||||
self.container = QWidget(self)
|
||||
self.layout().addWidget(self.container)
|
||||
self.container.setVisible(True)
|
||||
layout(self.container)
|
||||
self.layout = self.container.layout()
|
||||
|
||||
self.compact_show_popup.clicked.connect(self.show_popup)
|
||||
|
||||
def set_global_state(self, state: str):
|
||||
"""Set the LED-indicator state
|
||||
|
||||
The LED indicator represents the 'global' state. State can be one of the
|
||||
following: "default", "success", "warning", "emergency"
|
||||
"""
|
||||
self.compact_status.setState(state)
|
||||
|
||||
def show_popup(self):
|
||||
"""Display the contained widgets in a popup dialog"""
|
||||
if self._expand_popup:
|
||||
# show popup
|
||||
self._popup_window = PopupDialog(self.container)
|
||||
self._popup_window.show()
|
||||
self._popup_window.finished.connect(lambda: self.expand.emit(False))
|
||||
self.expand.emit(True)
|
||||
else:
|
||||
if self.compact_view:
|
||||
# expand in place
|
||||
self.compact_view = False
|
||||
self.compact_view_widget.setVisible(True)
|
||||
self.compact_label.setVisible(False)
|
||||
self.compact_status.setVisible(False)
|
||||
self.compact_show_popup.setIcon(
|
||||
material_icon(
|
||||
icon_name="collapse_content", size=(10, 10), convert_to_pixmap=False
|
||||
)
|
||||
)
|
||||
self.expand.emit(True)
|
||||
else:
|
||||
# back to compact form
|
||||
self.compact_label.setVisible(True)
|
||||
self.compact_status.setVisible(True)
|
||||
self.compact_show_popup.setIcon(
|
||||
material_icon(
|
||||
icon_name="expand_content", size=(10, 10), convert_to_pixmap=False
|
||||
)
|
||||
)
|
||||
self.compact_view = True
|
||||
self.expand.emit(False)
|
||||
|
||||
def setSizePolicy(self, size_policy1, size_policy2=None):
|
||||
# setting size policy on the compact popup widget will set
|
||||
# the policy for the container, and for itself
|
||||
if size_policy2 is None:
|
||||
# assuming first form: setSizePolicy(QSizePolicy)
|
||||
self.container.setSizePolicy(size_policy1)
|
||||
QWidget.setSizePolicy(self, size_policy1)
|
||||
else:
|
||||
self.container.setSizePolicy(size_policy1, size_policy2)
|
||||
QWidget.setSizePolicy(self, size_policy1, size_policy2)
|
||||
|
||||
def addWidget(self, widget):
|
||||
"""Add a widget to the popup container
|
||||
|
||||
The popup container corresponds to the "full view" (not compact)
|
||||
The widget is reparented to the container, and added to the container layout
|
||||
"""
|
||||
widget.setParent(self.container)
|
||||
self.container.layout().addWidget(widget)
|
||||
|
||||
@Property(bool)
|
||||
def compact_view(self):
|
||||
return self.compact_label.isVisible()
|
||||
|
||||
@compact_view.setter
|
||||
def compact_view(self, set_compact: bool):
|
||||
"""Sets the compact form
|
||||
|
||||
If set_compact is True, the compact view is displayed ; otherwise,
|
||||
the full view is displayed. This is handled by toggling visibility of
|
||||
the container widget or the compact view widget.
|
||||
"""
|
||||
if set_compact:
|
||||
self.compact_view_widget.setVisible(True)
|
||||
self.container.setVisible(False)
|
||||
QWidget.setSizePolicy(self, QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
else:
|
||||
self.compact_view_widget.setVisible(False)
|
||||
self.container.setVisible(True)
|
||||
QWidget.setSizePolicy(self, self.container.sizePolicy())
|
||||
if self.parentWidget():
|
||||
self.parentWidget().adjustSize()
|
||||
else:
|
||||
self.adjustSize()
|
||||
|
||||
@Property(str)
|
||||
def label(self):
|
||||
return self.compact_label.text()
|
||||
|
||||
@label.setter
|
||||
def label(self, compact_label_text: str):
|
||||
"""Set the label text associated to the compact view"""
|
||||
self.compact_label.setText(compact_label_text)
|
||||
|
||||
@Property(str)
|
||||
def tooltip(self):
|
||||
return self.compact_label.toolTip()
|
||||
|
||||
@tooltip.setter
|
||||
def tooltip(self, tooltip: str):
|
||||
"""Set the tooltip text associated to the compact view"""
|
||||
self.compact_label.setToolTip(tooltip)
|
||||
self.compact_status.setToolTip(tooltip)
|
||||
|
||||
@Property(bool)
|
||||
def expand_popup(self):
|
||||
return self._expand_popup
|
||||
|
||||
@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()
|
||||
183
bec_widgets/qt_utils/palette_viewer.py
Normal file
@@ -0,0 +1,183 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors, get_theme_palette
|
||||
from bec_widgets.widgets.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
|
||||
class PaletteViewer(BECWidget, QWidget):
|
||||
"""
|
||||
This class is a widget that displays current palette colors.
|
||||
"""
|
||||
|
||||
ICON_NAME = "palette"
|
||||
|
||||
def __init__(self, *args, parent=None, **kwargs):
|
||||
super().__init__(*args, theme_update=True, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.setFixedSize(400, 600)
|
||||
layout = QVBoxLayout(self)
|
||||
dark_mode_button = DarkModeButton(self)
|
||||
layout.addWidget(dark_mode_button)
|
||||
|
||||
# Create a scroll area to hold the color boxes
|
||||
scroll_area = QScrollArea(self)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
# Create a frame to hold the color boxes
|
||||
self.frame = QFrame(self)
|
||||
self.frame_layout = QGridLayout(self.frame)
|
||||
self.frame_layout.setSpacing(0)
|
||||
self.frame_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
scroll_area.setWidget(self.frame)
|
||||
layout.addWidget(scroll_area)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.update_palette()
|
||||
|
||||
def apply_theme(self, theme) -> None:
|
||||
"""
|
||||
Apply the theme to the widget.
|
||||
|
||||
Args:
|
||||
theme (str): The theme to apply.
|
||||
"""
|
||||
self.update_palette()
|
||||
|
||||
def clear_palette(self) -> None:
|
||||
"""
|
||||
Clear the palette colors from the frame.
|
||||
Recursively removes all widgets and layouts in the frame layout.
|
||||
"""
|
||||
# Iterate over all items in the layout in reverse to safely remove them
|
||||
for i in reversed(range(self.frame_layout.count())):
|
||||
item = self.frame_layout.itemAt(i)
|
||||
|
||||
# If the item is a layout, clear its contents
|
||||
if isinstance(item, QHBoxLayout):
|
||||
# Recursively remove all widgets from the layout
|
||||
for j in reversed(range(item.count())):
|
||||
widget = item.itemAt(j).widget()
|
||||
if widget:
|
||||
item.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
self.frame_layout.removeItem(item)
|
||||
|
||||
# If the item is a widget, remove and delete it
|
||||
elif item.widget():
|
||||
widget = item.widget()
|
||||
self.frame_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
||||
def update_palette(self) -> None:
|
||||
"""
|
||||
Update the palette colors in the frame.
|
||||
"""
|
||||
self.clear_palette()
|
||||
palette_label = QLabel("Palette Colors (e.g. palette.windowText().color())")
|
||||
palette_label.setStyleSheet("font-weight: bold;")
|
||||
self.frame_layout.addWidget(palette_label, 0, 0)
|
||||
|
||||
palette = get_theme_palette()
|
||||
# Add the palette colors (roles) to the frame
|
||||
palette_roles = [
|
||||
palette.windowText,
|
||||
palette.toolTipText,
|
||||
palette.placeholderText,
|
||||
palette.text,
|
||||
palette.buttonText,
|
||||
palette.highlight,
|
||||
palette.link,
|
||||
palette.light,
|
||||
palette.midlight,
|
||||
palette.mid,
|
||||
palette.shadow,
|
||||
palette.button,
|
||||
palette.brightText,
|
||||
palette.toolTipBase,
|
||||
palette.alternateBase,
|
||||
palette.dark,
|
||||
palette.base,
|
||||
palette.window,
|
||||
palette.highlightedText,
|
||||
palette.linkVisited,
|
||||
]
|
||||
|
||||
offset = 1
|
||||
for i, pal in enumerate(palette_roles):
|
||||
i += offset
|
||||
color = pal().color()
|
||||
label_layout = QHBoxLayout()
|
||||
color_label = QLabel(f"{pal().color().name()} ({pal.__name__})")
|
||||
background_label = self.background_label_with_clipboard(color)
|
||||
label_layout.addWidget(color_label)
|
||||
label_layout.addWidget(background_label)
|
||||
self.frame_layout.addLayout(label_layout, i, 0)
|
||||
|
||||
# add a horizontal spacer
|
||||
spacer = QLabel()
|
||||
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self.frame_layout.addWidget(spacer, i + 1, 0)
|
||||
|
||||
accent_colors_label = QLabel("Accent Colors (e.g. accent_colors.default)")
|
||||
accent_colors_label.setStyleSheet("font-weight: bold;")
|
||||
self.frame_layout.addWidget(accent_colors_label, i + 2, 0)
|
||||
|
||||
accent_colors = get_accent_colors()
|
||||
items = [
|
||||
(accent_colors.default, "default"),
|
||||
(accent_colors.success, "success"),
|
||||
(accent_colors.warning, "warning"),
|
||||
(accent_colors.emergency, "emergency"),
|
||||
(accent_colors.highlight, "highlight"),
|
||||
]
|
||||
|
||||
offset = len(palette_roles) + 2
|
||||
for i, (color, name) in enumerate(items):
|
||||
i += offset
|
||||
label_layout = QHBoxLayout()
|
||||
color_label = QLabel(f"{color.name()} ({name})")
|
||||
background_label = self.background_label_with_clipboard(color)
|
||||
label_layout.addWidget(color_label)
|
||||
label_layout.addWidget(background_label)
|
||||
self.frame_layout.addLayout(label_layout, i + 2, 0)
|
||||
|
||||
def background_label_with_clipboard(self, color) -> QLabel:
|
||||
"""
|
||||
Create a label with a background color that copies the color to the clipboard when clicked.
|
||||
|
||||
Args:
|
||||
color (QColor): The color to display in the background.
|
||||
|
||||
Returns:
|
||||
QLabel: The label with the background color.
|
||||
"""
|
||||
button = QLabel()
|
||||
button.setStyleSheet(f"QLabel {{ background-color: {color.name()}; }}")
|
||||
button.setToolTip("Click to copy color to clipboard")
|
||||
button.setCursor(Qt.PointingHandCursor)
|
||||
button.mousePressEvent = lambda event: QApplication.clipboard().setText(color.name())
|
||||
return button
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
viewer = PaletteViewer()
|
||||
viewer.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -7,9 +7,18 @@ from collections import defaultdict
|
||||
from typing import Literal
|
||||
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QAction, QColor, QIcon
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
|
||||
from qtpy.QtWidgets import (
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMenu,
|
||||
QSizePolicy,
|
||||
QToolBar,
|
||||
QToolButton,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import bec_widgets
|
||||
|
||||
@@ -154,20 +163,52 @@ class WidgetAction(ToolBarAction):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label: str | None = None, widget: QWidget = None):
|
||||
super().__init__()
|
||||
def __init__(self, label: str | None = None, widget: QWidget = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label = label
|
||||
self.widget = widget
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(5)
|
||||
|
||||
if self.label is not None:
|
||||
label = QLabel(f"{self.label}")
|
||||
layout.addWidget(label)
|
||||
label_widget = QLabel(f"{self.label}")
|
||||
label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
layout.addWidget(label_widget)
|
||||
|
||||
if isinstance(self.widget, QComboBox):
|
||||
self.widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
||||
|
||||
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.widget.setSizePolicy(size_policy)
|
||||
|
||||
self.widget.setMinimumWidth(self.calculate_minimum_width(self.widget))
|
||||
|
||||
else:
|
||||
self.widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
|
||||
layout.addWidget(self.widget)
|
||||
toolbar.addWidget(widget)
|
||||
|
||||
toolbar.addWidget(container)
|
||||
|
||||
@staticmethod
|
||||
def calculate_minimum_width(combo_box: QComboBox) -> int:
|
||||
"""
|
||||
Calculate the minimum width required to display the longest item in the combo box.
|
||||
|
||||
Args:
|
||||
combo_box (QComboBox): The combo box to calculate the width for.
|
||||
|
||||
Returns:
|
||||
int: The calculated minimum width in pixels.
|
||||
"""
|
||||
font_metrics = combo_box.fontMetrics()
|
||||
max_width = max(font_metrics.width(combo_box.itemText(i)) for i in range(combo_box.count()))
|
||||
return max_width + 60
|
||||
|
||||
|
||||
class ExpandableMenuAction(ToolBarAction):
|
||||
|
||||
0
bec_widgets/tests/__init__.py
Normal file
226
bec_widgets/tests/utils.py
Normal file
@@ -0,0 +1,226 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from bec_lib.device import Device as BECDevice
|
||||
from bec_lib.device import Positioner as BECPositioner
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from bec_lib.devicemanager import DeviceContainer
|
||||
|
||||
|
||||
class FakeDevice(BECDevice):
|
||||
"""Fake minimal positioner class for testing."""
|
||||
|
||||
def __init__(self, name, enabled=True, readout_priority=ReadoutPriority.MONITORED):
|
||||
super().__init__(name=name)
|
||||
self._enabled = enabled
|
||||
self.signals = {self.name: {"value": 1.0}}
|
||||
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
|
||||
self._readout_priority = readout_priority
|
||||
self._config = {
|
||||
"readoutPriority": "baseline",
|
||||
"deviceClass": "ophyd.Device",
|
||||
"deviceConfig": {},
|
||||
"deviceTags": ["user device"],
|
||||
"enabled": enabled,
|
||||
"readOnly": False,
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
@property
|
||||
def readout_priority(self):
|
||||
return self._readout_priority
|
||||
|
||||
@readout_priority.setter
|
||||
def readout_priority(self, value):
|
||||
self._readout_priority = value
|
||||
|
||||
@property
|
||||
def limits(self) -> tuple[float, float]:
|
||||
return self._limits
|
||||
|
||||
@limits.setter
|
||||
def limits(self, value: tuple[float, float]):
|
||||
self._limits = value
|
||||
|
||||
def __contains__(self, item):
|
||||
return item == self.name
|
||||
|
||||
@property
|
||||
def _hints(self):
|
||||
return [self.name]
|
||||
|
||||
def set_value(self, fake_value: float = 1.0) -> None:
|
||||
"""
|
||||
Setup fake value for device readout
|
||||
Args:
|
||||
fake_value(float): Desired fake value
|
||||
"""
|
||||
self.signals[self.name]["value"] = fake_value
|
||||
|
||||
def describe(self) -> dict:
|
||||
"""
|
||||
Get the description of the device
|
||||
Returns:
|
||||
dict: Description of the device
|
||||
"""
|
||||
return self.description
|
||||
|
||||
|
||||
class FakePositioner(BECPositioner):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
enabled=True,
|
||||
limits=None,
|
||||
read_value=1.0,
|
||||
readout_priority=ReadoutPriority.MONITORED,
|
||||
):
|
||||
super().__init__(name=name)
|
||||
# self.limits = limits if limits is not None else [0.0, 0.0]
|
||||
self.read_value = read_value
|
||||
self.setpoint_value = read_value
|
||||
self.motor_is_moving_value = 0
|
||||
self._enabled = enabled
|
||||
self._limits = limits
|
||||
self._readout_priority = readout_priority
|
||||
self.signals = {self.name: {"value": 1.0}}
|
||||
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
|
||||
self._config = {
|
||||
"readoutPriority": "baseline",
|
||||
"deviceClass": "ophyd_devices.SimPositioner",
|
||||
"deviceConfig": {"delay": 1, "tolerance": 0.01, "update_frequency": 400},
|
||||
"deviceTags": ["user motors"],
|
||||
"enabled": enabled,
|
||||
"readOnly": False,
|
||||
"name": self.name,
|
||||
}
|
||||
self._info = {
|
||||
"signals": {
|
||||
"readback": {"kind_str": "5"}, # hinted
|
||||
"setpoint": {"kind_str": "1"}, # normal
|
||||
"velocity": {"kind_str": "2"}, # config
|
||||
}
|
||||
}
|
||||
self.signals = {
|
||||
self.name: {"value": self.read_value},
|
||||
f"{self.name}_setpoint": {"value": self.setpoint_value},
|
||||
f"{self.name}_motor_is_moving": {"value": self.motor_is_moving_value},
|
||||
}
|
||||
|
||||
@property
|
||||
def readout_priority(self):
|
||||
return self._readout_priority
|
||||
|
||||
@readout_priority.setter
|
||||
def readout_priority(self, value):
|
||||
self._readout_priority = value
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value: bool):
|
||||
self._enabled = value
|
||||
|
||||
@property
|
||||
def limits(self) -> tuple[float, float]:
|
||||
return self._limits
|
||||
|
||||
@limits.setter
|
||||
def limits(self, value: tuple[float, float]):
|
||||
self._limits = value
|
||||
|
||||
def __contains__(self, item):
|
||||
return item == self.name
|
||||
|
||||
@property
|
||||
def _hints(self):
|
||||
return [self.name]
|
||||
|
||||
def set_value(self, fake_value: float = 1.0) -> None:
|
||||
"""
|
||||
Setup fake value for device readout
|
||||
Args:
|
||||
fake_value(float): Desired fake value
|
||||
"""
|
||||
self.read_value = fake_value
|
||||
|
||||
def describe(self) -> dict:
|
||||
"""
|
||||
Get the description of the device
|
||||
Returns:
|
||||
dict: Description of the device
|
||||
"""
|
||||
return self.description
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
return 3
|
||||
|
||||
def set_read_value(self, value):
|
||||
self.read_value = value
|
||||
|
||||
def read(self):
|
||||
return self.signals
|
||||
|
||||
def set_limits(self, limits):
|
||||
self.limits = limits
|
||||
|
||||
def move(self, value, relative=False):
|
||||
"""Simulates moving the device to a new position."""
|
||||
if relative:
|
||||
self.read_value += value
|
||||
else:
|
||||
self.read_value = value
|
||||
# Respect the limits
|
||||
self.read_value = max(min(self.read_value, self.limits[1]), self.limits[0])
|
||||
|
||||
@property
|
||||
def readback(self):
|
||||
return MagicMock(get=MagicMock(return_value=self.read_value))
|
||||
|
||||
|
||||
class Positioner(FakePositioner):
|
||||
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
|
||||
|
||||
def __init__(self, name="test", limits=None, read_value=1.0):
|
||||
super().__init__(name, limits, read_value)
|
||||
|
||||
|
||||
class Device(FakeDevice):
|
||||
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
|
||||
|
||||
def __init__(self, name, enabled=True):
|
||||
super().__init__(name, enabled)
|
||||
|
||||
|
||||
class DMMock:
|
||||
def __init__(self):
|
||||
self.devices = DeviceContainer()
|
||||
self.enabled_devices = [device for device in self.devices if device.enabled]
|
||||
|
||||
def add_devives(self, devices: list):
|
||||
for device in devices:
|
||||
self.devices[device.name] = device
|
||||
|
||||
|
||||
DEVICES = [
|
||||
FakePositioner("samx", limits=[-10, 10], read_value=2.0),
|
||||
FakePositioner("samy", limits=[-5, 5], read_value=3.0),
|
||||
FakePositioner("samz", limits=[-8, 8], read_value=4.0),
|
||||
FakePositioner("aptrx", limits=None, read_value=4.0),
|
||||
FakePositioner("aptry", limits=None, read_value=5.0),
|
||||
FakeDevice("gauss_bpm"),
|
||||
FakeDevice("gauss_adc1"),
|
||||
FakeDevice("gauss_adc2"),
|
||||
FakeDevice("gauss_adc3"),
|
||||
FakeDevice("bpm4i"),
|
||||
FakeDevice("bpm3a"),
|
||||
FakeDevice("bpm3i"),
|
||||
FakeDevice("eiger", readout_priority=ReadoutPriority.ASYNC),
|
||||
FakeDevice("waveform1d"),
|
||||
FakeDevice("async_device", readout_priority=ReadoutPriority.ASYNC),
|
||||
Positioner("test", limits=[-10, 10], read_value=2.0),
|
||||
Device("test_device"),
|
||||
]
|
||||
54
bec_widgets/utils/bec_signal_proxy.py
Normal file
@@ -0,0 +1,54 @@
|
||||
""" This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
||||
Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is
|
||||
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
||||
analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy
|
||||
will allow you to decide by yourself when to unblock and execute the callback again."""
|
||||
|
||||
from pyqtgraph import SignalProxy
|
||||
from qtpy.QtCore import Signal, Slot
|
||||
|
||||
|
||||
class BECSignalProxy(SignalProxy):
|
||||
"""Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored
|
||||
|
||||
Args:
|
||||
*args: Arguments to pass to the SignalProxy class
|
||||
rateLimit (int): The rateLimit of the proxy
|
||||
**kwargs: Keyword arguments to pass to the SignalProxy class
|
||||
|
||||
Example:
|
||||
>>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)"""
|
||||
|
||||
is_blocked = Signal(bool)
|
||||
|
||||
def __init__(self, *args, rateLimit=25, **kwargs):
|
||||
super().__init__(*args, rateLimit=rateLimit, **kwargs)
|
||||
self._blocking = False
|
||||
self.old_args = None
|
||||
self.new_args = None
|
||||
|
||||
@property
|
||||
def blocked(self):
|
||||
"""Returns if the proxy is blocked"""
|
||||
return self._blocking
|
||||
|
||||
@blocked.setter
|
||||
def blocked(self, value: bool):
|
||||
self._blocking = value
|
||||
self.is_blocked.emit(value)
|
||||
|
||||
def signalReceived(self, *args):
|
||||
"""Receive signal, store the args and call signalReceived from the parent class if not blocked"""
|
||||
self.new_args = args
|
||||
if self.blocked is True:
|
||||
return
|
||||
self.blocked = True
|
||||
self.old_args = args
|
||||
super().signalReceived(*args)
|
||||
|
||||
@Slot()
|
||||
def unblock_proxy(self):
|
||||
"""Unblock the proxy, and call the signalReceived method in case there was an update of the args."""
|
||||
self.blocked = False
|
||||
if self.new_args != self.old_args:
|
||||
self.signalReceived(*self.new_args)
|
||||
@@ -107,9 +107,98 @@ class Colors:
|
||||
angles.append(angle)
|
||||
return angles
|
||||
|
||||
@staticmethod
|
||||
def set_theme_offset(theme: Literal["light", "dark"] | None = None, offset=0.2) -> tuple:
|
||||
"""
|
||||
Set the theme offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
|
||||
|
||||
Args:
|
||||
theme(str): The theme to be applied.
|
||||
offset(float): Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
|
||||
|
||||
Returns:
|
||||
tuple: Tuple of min_pos and max_pos.
|
||||
|
||||
Raises:
|
||||
ValueError: If theme_offset is not between 0 and 1.
|
||||
"""
|
||||
|
||||
if offset < 0 or offset > 1:
|
||||
raise ValueError("theme_offset must be between 0 and 1")
|
||||
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
if hasattr(app, "theme"):
|
||||
theme = app.theme.theme
|
||||
|
||||
if theme == "light":
|
||||
min_pos = 0.0
|
||||
max_pos = 1 - offset
|
||||
else:
|
||||
min_pos = 0.0 + offset
|
||||
max_pos = 1.0
|
||||
|
||||
return min_pos, max_pos
|
||||
|
||||
@staticmethod
|
||||
def evenly_spaced_colors(
|
||||
colormap: str,
|
||||
num: int,
|
||||
format: Literal["QColor", "HEX", "RGB"] = "QColor",
|
||||
theme_offset=0.2,
|
||||
theme: Literal["light", "dark"] | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
Extract `num` colors from the specified colormap, evenly spaced along its range,
|
||||
and return them in the specified format.
|
||||
|
||||
Args:
|
||||
colormap (str): Name of the colormap.
|
||||
num (int): Number of requested colors.
|
||||
format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
|
||||
theme_offset (float): Has to be between 0-1. Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
|
||||
theme (Literal['light', 'dark'] | None): The theme to be applied. Overrides the QApplication theme if specified.
|
||||
|
||||
Returns:
|
||||
list: List of colors in the specified format.
|
||||
|
||||
Raises:
|
||||
ValueError: If theme_offset is not between 0 and 1.
|
||||
"""
|
||||
if theme_offset < 0 or theme_offset > 1:
|
||||
raise ValueError("theme_offset must be between 0 and 1")
|
||||
|
||||
cmap = pg.colormap.get(colormap)
|
||||
min_pos, max_pos = Colors.set_theme_offset(theme, theme_offset)
|
||||
|
||||
# Generate positions that are evenly spaced within the acceptable range
|
||||
if num == 1:
|
||||
positions = np.array([(min_pos + max_pos) / 2])
|
||||
else:
|
||||
positions = np.linspace(min_pos, max_pos, num)
|
||||
|
||||
# Sample colors from the colormap at the calculated positions
|
||||
colors = cmap.map(positions, mode="float")
|
||||
color_list = []
|
||||
|
||||
for color in colors:
|
||||
if format.upper() == "HEX":
|
||||
color_list.append(QColor.fromRgbF(*color).name())
|
||||
elif format.upper() == "RGB":
|
||||
color_list.append(tuple((np.array(color) * 255).astype(int)))
|
||||
elif format.upper() == "QCOLOR":
|
||||
color_list.append(QColor.fromRgbF(*color))
|
||||
else:
|
||||
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
||||
return color_list
|
||||
|
||||
@staticmethod
|
||||
def golden_angle_color(
|
||||
colormap: str, num: int, format: Literal["QColor", "HEX", "RGB"] = "QColor"
|
||||
colormap: str,
|
||||
num: int,
|
||||
format: Literal["QColor", "HEX", "RGB"] = "QColor",
|
||||
theme_offset=0.2,
|
||||
theme: Literal["dark", "light"] | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
Extract num colors from the specified colormap following golden angle distribution and return them in the specified format.
|
||||
@@ -118,45 +207,39 @@ class Colors:
|
||||
colormap (str): Name of the colormap.
|
||||
num (int): Number of requested colors.
|
||||
format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
|
||||
theme_offset (float): Has to be between 0-1. Offset to avoid colors too close to white or black with light or dark theme respectively for pyqtgraph plot background.
|
||||
|
||||
Returns:
|
||||
list: List of colors in the specified format.
|
||||
|
||||
Raises:
|
||||
ValueError: If the number of requested colors is greater than the number of colors in the colormap.
|
||||
ValueError: If theme_offset is not between 0 and 1.
|
||||
"""
|
||||
cmap = pg.colormap.get(colormap)
|
||||
cmap_colors = cmap.getColors(mode="float")
|
||||
if num > len(cmap_colors):
|
||||
raise ValueError(
|
||||
f"Number of colors requested ({num}) is greater than the number of colors in the colormap ({len(cmap_colors)})"
|
||||
)
|
||||
angles = Colors.golden_ratio(len(cmap_colors))
|
||||
color_selection = np.round(np.interp(angles, (-np.pi, np.pi), (0, len(cmap_colors))))
|
||||
colors = []
|
||||
ii = 0
|
||||
while len(colors) < num:
|
||||
color_index = int(color_selection[ii])
|
||||
color = cmap_colors[color_index]
|
||||
app = QApplication.instance()
|
||||
if hasattr(app, "theme") and app.theme.theme == "light":
|
||||
background = 255
|
||||
else:
|
||||
background = 0
|
||||
if np.abs(np.mean(color[:3] * 255) - background) < 50:
|
||||
ii += 1
|
||||
continue
|
||||
|
||||
cmap = pg.colormap.get(colormap)
|
||||
phi = (1 + np.sqrt(5)) / 2 # Golden ratio
|
||||
golden_angle_conjugate = 1 - (1 / phi) # Approximately 0.38196601125
|
||||
|
||||
min_pos, max_pos = Colors.set_theme_offset(theme, theme_offset)
|
||||
|
||||
# Generate positions within the acceptable range
|
||||
positions = np.mod(np.arange(num) * golden_angle_conjugate, 1)
|
||||
positions = min_pos + positions * (max_pos - min_pos)
|
||||
|
||||
# Sample colors from the colormap at the calculated positions
|
||||
colors = cmap.map(positions, mode="float")
|
||||
color_list = []
|
||||
|
||||
for color in colors:
|
||||
if format.upper() == "HEX":
|
||||
colors.append(QColor.fromRgbF(*color).name())
|
||||
color_list.append(QColor.fromRgbF(*color).name())
|
||||
elif format.upper() == "RGB":
|
||||
colors.append(tuple((np.array(color) * 255).astype(int)))
|
||||
color_list.append(tuple((np.array(color) * 255).astype(int)))
|
||||
elif format.upper() == "QCOLOR":
|
||||
colors.append(QColor.fromRgbF(*color))
|
||||
color_list.append(QColor.fromRgbF(*color))
|
||||
else:
|
||||
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
||||
ii += 1
|
||||
return colors
|
||||
return color_list
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgba(hex_color: str, alpha=255) -> tuple:
|
||||
@@ -385,7 +468,7 @@ class Colors:
|
||||
return color
|
||||
|
||||
@staticmethod
|
||||
def validate_color_map(color_map: str) -> str:
|
||||
def validate_color_map(color_map: str, return_error: bool = True) -> str | bool:
|
||||
"""
|
||||
Validate the colormap input if it is supported by pyqtgraph. Can be used in any pydantic model as a field validator. If validation fails it prints all available colormaps from pyqtgraph instance.
|
||||
|
||||
@@ -393,13 +476,24 @@ class Colors:
|
||||
color_map(str): The colormap to be validated.
|
||||
|
||||
Returns:
|
||||
str: The validated colormap.
|
||||
str: The validated colormap, if colormap is valid.
|
||||
bool: False, if colormap is invalid.
|
||||
|
||||
Raises:
|
||||
PydanticCustomError: If colormap is invalid.
|
||||
"""
|
||||
available_colormaps = pg.colormap.listMaps()
|
||||
available_pg_maps = pg.colormap.listMaps()
|
||||
available_mpl_maps = pg.colormap.listMaps("matplotlib")
|
||||
available_mpl_colorcet = pg.colormap.listMaps("colorcet")
|
||||
|
||||
available_colormaps = available_pg_maps + available_mpl_maps + available_mpl_colorcet
|
||||
if color_map not in available_colormaps:
|
||||
raise PydanticCustomError(
|
||||
"unsupported colormap",
|
||||
f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.",
|
||||
{"wrong_value": color_map},
|
||||
)
|
||||
if return_error:
|
||||
raise PydanticCustomError(
|
||||
"unsupported colormap",
|
||||
f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.",
|
||||
{"wrong_value": color_map},
|
||||
)
|
||||
else:
|
||||
return False
|
||||
return color_map
|
||||
|
||||
@@ -8,6 +8,14 @@ from qtpy.QtCore import QObject, Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
|
||||
class NonDownsamplingScatterPlotItem(pg.ScatterPlotItem):
|
||||
def setDownsampling(self, ds=None, auto=None, method=None):
|
||||
pass
|
||||
|
||||
def setClipToView(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class Crosshair(QObject):
|
||||
positionChanged = pyqtSignal(tuple)
|
||||
positionClicked = pyqtSignal(tuple)
|
||||
@@ -47,6 +55,7 @@ class Crosshair(QObject):
|
||||
self.plot_item.ctrl.derivativeCheck.checkStateChanged.connect(self.check_derivatives)
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.ctrl.downsampleSpin.valueChanged.connect(self.clear_markers)
|
||||
|
||||
# Initialize markers
|
||||
self.marker_moved_1d = {}
|
||||
@@ -64,7 +73,7 @@ class Crosshair(QObject):
|
||||
continue
|
||||
pen = item.opts["pen"]
|
||||
color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
|
||||
marker_moved = pg.ScatterPlotItem(
|
||||
marker_moved = NonDownsamplingScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
||||
)
|
||||
marker_moved.skip_auto_range = True
|
||||
@@ -73,7 +82,7 @@ class Crosshair(QObject):
|
||||
|
||||
# Create glowing effect markers for clicked events
|
||||
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
marker_clicked = NonDownsamplingScatterPlotItem(
|
||||
size=size,
|
||||
pen=pg.mkPen(None),
|
||||
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
|
||||
|
||||
156
bec_widgets/utils/filter_io.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Module for handling filter I/O operations in BEC Widgets for input fields.
|
||||
These operations include filtering device/signal names and/or device types.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QStringListModel
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter, QLineEdit
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class WidgetFilterHandler(ABC):
|
||||
"""Abstract base class for widget filter handlers"""
|
||||
|
||||
@abstractmethod
|
||||
def set_selection(self, widget, selection: list) -> None:
|
||||
"""Set the filtered_selection for the widget
|
||||
|
||||
Args:
|
||||
selection (list): Filtered selection of items
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def check_input(self, widget, text: str) -> bool:
|
||||
"""Check if the input text is in the filtered selection
|
||||
|
||||
Args:
|
||||
widget: Widget instance
|
||||
text (str): Input text
|
||||
|
||||
Returns:
|
||||
bool: True if the input text is in the filtered selection
|
||||
"""
|
||||
|
||||
|
||||
class LineEditFilterHandler(WidgetFilterHandler):
|
||||
"""Handler for QLineEdit widget"""
|
||||
|
||||
def set_selection(self, widget: QLineEdit, selection: list) -> None:
|
||||
"""Set the selection for the widget to the completer model
|
||||
|
||||
Args:
|
||||
widget (QLineEdit): The QLineEdit widget
|
||||
selection (list): Filtered selection of items
|
||||
"""
|
||||
if not isinstance(widget.completer, QCompleter):
|
||||
completer = QCompleter(widget)
|
||||
widget.setCompleter(completer)
|
||||
widget.completer.setModel(QStringListModel(selection, widget))
|
||||
|
||||
def check_input(self, widget: QLineEdit, text: str) -> bool:
|
||||
"""Check if the input text is in the filtered selection
|
||||
|
||||
Args:
|
||||
widget (QLineEdit): The QLineEdit widget
|
||||
text (str): Input text
|
||||
|
||||
Returns:
|
||||
bool: True if the input text is in the filtered selection
|
||||
"""
|
||||
model = widget.completer.model()
|
||||
model_data = [model.data(model.index(i)) for i in range(model.rowCount())]
|
||||
return text in model_data
|
||||
|
||||
|
||||
class ComboBoxFilterHandler(WidgetFilterHandler):
|
||||
"""Handler for QComboBox widget"""
|
||||
|
||||
def set_selection(self, widget: QComboBox, selection: list) -> None:
|
||||
"""Set the selection for the widget to the completer model
|
||||
|
||||
Args:
|
||||
widget (QComboBox): The QComboBox widget
|
||||
selection (list): Filtered selection of items
|
||||
"""
|
||||
widget.clear()
|
||||
widget.addItems(selection)
|
||||
|
||||
def check_input(self, widget: QComboBox, text: str) -> bool:
|
||||
"""Check if the input text is in the filtered selection
|
||||
|
||||
Args:
|
||||
widget (QComboBox): The QComboBox widget
|
||||
text (str): Input text
|
||||
|
||||
Returns:
|
||||
bool: True if the input text is in the filtered selection
|
||||
"""
|
||||
return text in [widget.itemText(i) for i in range(widget.count())]
|
||||
|
||||
|
||||
class FilterIO:
|
||||
"""Public interface to set filters for input widgets.
|
||||
It supports the list of widgets stored in class attribute _handlers.
|
||||
"""
|
||||
|
||||
_handlers = {QLineEdit: LineEditFilterHandler, QComboBox: ComboBoxFilterHandler}
|
||||
|
||||
@staticmethod
|
||||
def set_selection(widget, selection: list, ignore_errors=True):
|
||||
"""
|
||||
Retrieve value from the widget instance.
|
||||
|
||||
Args:
|
||||
widget: Widget instance.
|
||||
selection(list): List of filtered selection items.
|
||||
ignore_errors(bool, optional): Whether to ignore if no handler is found.
|
||||
"""
|
||||
handler_class = FilterIO._find_handler(widget)
|
||||
if handler_class:
|
||||
return handler_class().set_selection(widget=widget, selection=selection)
|
||||
if not ignore_errors:
|
||||
raise ValueError(
|
||||
f"No matching handler for widget type: {type(widget)} in handler list {FilterIO._handlers}"
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def check_input(widget, text: str, ignore_errors=True):
|
||||
"""
|
||||
Check if the input text is in the filtered selection.
|
||||
|
||||
Args:
|
||||
widget: Widget instance.
|
||||
text(str): Input text.
|
||||
ignore_errors(bool, optional): Whether to ignore if no handler is found.
|
||||
|
||||
Returns:
|
||||
bool: True if the input text is in the filtered selection.
|
||||
"""
|
||||
handler_class = FilterIO._find_handler(widget)
|
||||
if handler_class:
|
||||
return handler_class().check_input(widget=widget, text=text)
|
||||
if not ignore_errors:
|
||||
raise ValueError(
|
||||
f"No matching handler for widget type: {type(widget)} in handler list {FilterIO._handlers}"
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find_handler(widget):
|
||||
"""
|
||||
Find the appropriate handler for the widget by checking its base classes.
|
||||
|
||||
Args:
|
||||
widget: Widget instance.
|
||||
|
||||
Returns:
|
||||
handler_class: The handler class if found, otherwise None.
|
||||
"""
|
||||
for base in type(widget).__mro__:
|
||||
if base in FilterIO._handlers:
|
||||
return FilterIO._handlers[base]
|
||||
return None
|
||||
84
bec_widgets/utils/fps_counter.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
This module provides a utility class for counting and reporting frames per second (FPS) in a PyQtGraph application.
|
||||
|
||||
Classes:
|
||||
FPSCounter: A class that monitors the paint events of a `ViewBox` to calculate and emit FPS values.
|
||||
|
||||
Usage:
|
||||
The `FPSCounter` class can be used to monitor the rendering performance of a `ViewBox` in a PyQtGraph application.
|
||||
It connects to the `ViewBox`'s paint event and calculates the FPS over a specified interval, emitting the FPS value
|
||||
at regular intervals.
|
||||
|
||||
Example:
|
||||
from qtpy import QtWidgets, QtCore
|
||||
import pyqtgraph as pg
|
||||
from fps_counter import FPSCounter
|
||||
|
||||
app = pg.mkQApp("FPS Counter Example")
|
||||
win = pg.GraphicsLayoutWidget()
|
||||
win.show()
|
||||
|
||||
vb = pg.ViewBox()
|
||||
plot_item = pg.PlotItem(viewBox=vb)
|
||||
win.addItem(plot_item)
|
||||
|
||||
fps_counter = FPSCounter(vb)
|
||||
fps_counter.sigFpsUpdate.connect(lambda fps: print(f"FPS: {fps:.2f}"))
|
||||
|
||||
sys.exit(app.exec_())
|
||||
"""
|
||||
|
||||
from time import perf_counter
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy import QtCore
|
||||
|
||||
|
||||
class FPSCounter(QtCore.QObject):
|
||||
"""
|
||||
A utility class for counting and reporting frames per second (FPS).
|
||||
|
||||
This class connects to a `ViewBox`'s paint event to count the number of
|
||||
frames rendered and calculates the FPS over a specified interval. It emits
|
||||
a signal with the FPS value at regular intervals.
|
||||
|
||||
Attributes:
|
||||
sigFpsUpdate (QtCore.Signal): Signal emitted with the FPS value.
|
||||
view_box (pg.ViewBox): The `ViewBox` instance to monitor.
|
||||
"""
|
||||
|
||||
sigFpsUpdate = QtCore.Signal(float)
|
||||
|
||||
def __init__(self, view_box):
|
||||
super().__init__()
|
||||
self.view_box = view_box
|
||||
self.view_box.sigPaint.connect(self.increment_count)
|
||||
self.count = 0
|
||||
self.last_update = perf_counter()
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.calculate_fps)
|
||||
self.timer.start(1000)
|
||||
|
||||
def increment_count(self):
|
||||
"""
|
||||
Increment the frame count when the `ViewBox` is painted.
|
||||
"""
|
||||
self.count += 1
|
||||
|
||||
def calculate_fps(self):
|
||||
"""
|
||||
Calculate the frames per second (FPS) based on the number of frames
|
||||
"""
|
||||
now = perf_counter()
|
||||
elapsed = now - self.last_update
|
||||
fps = self.count / elapsed if elapsed > 0 else 0.0
|
||||
self.last_update = now
|
||||
self.count = 0
|
||||
self.sigFpsUpdate.emit(fps)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up the FPS counter by stopping the timer and disconnecting the signal.
|
||||
"""
|
||||
self.timer.stop()
|
||||
self.timer.timeout.disconnect(self.calculate_fps)
|
||||
@@ -23,11 +23,14 @@ class LinearRegionWrapper(QObject):
|
||||
self, plot_item: pg.PlotItem, color: QColor = None, hover_color: QColor = None, parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.is_log_x = None
|
||||
self._edge_width = 2
|
||||
self.plot_item = plot_item
|
||||
self.linear_region_selector = pg.LinearRegionItem()
|
||||
self.proxy = None
|
||||
self.change_roi_color((color, hover_color))
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
||||
|
||||
# Slot for changing the color of the region selector (edge and fill)
|
||||
@Slot(tuple)
|
||||
@@ -63,9 +66,18 @@ class LinearRegionWrapper(QObject):
|
||||
self.plot_item.removeItem(self.linear_region_selector)
|
||||
|
||||
def _region_change_proxy(self):
|
||||
"""Emit the region change signal"""
|
||||
region = self.linear_region_selector.getRegion()
|
||||
self.region_changed.emit(region)
|
||||
"""Emit the region change signal. If the plot is in log mode, convert the region to log."""
|
||||
x_low, x_high = self.linear_region_selector.getRegion()
|
||||
if self.is_log_x:
|
||||
x_low = 10**x_low
|
||||
x_high = 10**x_high
|
||||
self.region_changed.emit((x_low, x_high))
|
||||
|
||||
@Slot()
|
||||
def check_log(self):
|
||||
"""Check if the plot is in log mode."""
|
||||
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
||||
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
|
||||
26
bec_widgets/utils/ophyd_kind_util.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from enum import IntFlag
|
||||
|
||||
try:
|
||||
|
||||
from enum import KEEP
|
||||
|
||||
class IFBase(IntFlag, boundary=KEEP): ...
|
||||
|
||||
except ImportError:
|
||||
|
||||
IFBase = IntFlag
|
||||
|
||||
|
||||
class Kind(IFBase):
|
||||
"""
|
||||
This is used in the .kind attribute of all OphydObj (Signals, Devices).
|
||||
|
||||
A Device examines its components' .kind atttribute to decide whether to
|
||||
traverse it in read(), read_configuration(), or neither. Additionally, if
|
||||
decides whether to include its name in `hints['fields']`.
|
||||
"""
|
||||
|
||||
omitted = 0b000
|
||||
normal = 0b001
|
||||
config = 0b010
|
||||
hinted = 0b101 # Notice that bool(hinted & normal) is True.
|
||||
247
bec_widgets/utils/plot_indicator_items.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""Module to create an arrow item for a pyqtgraph plot"""
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QObject, QPointF, Signal, Slot
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class BECIndicatorItem(QObject):
|
||||
|
||||
def __init__(self, plot_item: pg.PlotItem = None, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.accent_colors = get_accent_colors()
|
||||
self.plot_item = plot_item
|
||||
self._item_on_plot = False
|
||||
self._pos = None
|
||||
self.is_log_x = False
|
||||
self.is_log_y = False
|
||||
|
||||
@property
|
||||
def item_on_plot(self) -> bool:
|
||||
"""Returns if the item is on the plot"""
|
||||
return self._item_on_plot
|
||||
|
||||
@item_on_plot.setter
|
||||
def item_on_plot(self, value: bool) -> None:
|
||||
self._item_on_plot = value
|
||||
|
||||
def add_to_plot(self) -> None:
|
||||
"""Add the item to the plot"""
|
||||
raise NotImplementedError("Method add_to_plot not implemented")
|
||||
|
||||
def remove_from_plot(self) -> None:
|
||||
"""Remove the item from the plot"""
|
||||
raise NotImplementedError("Method remove_from_plot not implemented")
|
||||
|
||||
def set_position(self, pos) -> None:
|
||||
"""This method should implement the logic to set the position of the
|
||||
item on the plot. Depending on the child class, the position can be
|
||||
a tuple (x,y) or a single value, i.e. x position where y position is fixed.
|
||||
"""
|
||||
raise NotImplementedError("Method set_position not implemented")
|
||||
|
||||
def check_log(self):
|
||||
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
||||
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
||||
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
||||
self.set_position(self._pos)
|
||||
|
||||
|
||||
class BECTickItem(BECIndicatorItem):
|
||||
"""Class to create a tick item which can be added to a pyqtgraph plot.
|
||||
The tick item will be added to the layout of the plot item and can be used to indicate
|
||||
a position"""
|
||||
|
||||
position_changed = Signal(float)
|
||||
position_changed_str = Signal(str)
|
||||
|
||||
def __init__(self, plot_item: pg.PlotItem = None, parent=None):
|
||||
super().__init__(plot_item=plot_item, parent=parent)
|
||||
self.tick_item = pg.TickSliderItem(
|
||||
parent=parent, allowAdd=False, allowRemove=False, orientation="bottom"
|
||||
)
|
||||
self.tick_item.skip_auto_range = True
|
||||
self.tick = None
|
||||
self._pos = 0.0
|
||||
self._range = [0, 1]
|
||||
|
||||
@Slot(float)
|
||||
def set_position(self, pos: float) -> None:
|
||||
"""Set the x position of the tick item
|
||||
|
||||
Args:
|
||||
pos (float): The position of the tick item.
|
||||
"""
|
||||
if self.is_log_x is True:
|
||||
pos = pos if pos > 0 else 1e-10
|
||||
pos = np.log10(pos)
|
||||
self._pos = pos
|
||||
view_box = self.plot_item.getViewBox() # Ensure you're accessing the correct view box
|
||||
view_range = view_box.viewRange()[0]
|
||||
self.update_range(self.plot_item.vb, view_range)
|
||||
self.position_changed.emit(pos)
|
||||
self.position_changed_str.emit(str(pos))
|
||||
|
||||
@Slot()
|
||||
def update_range(self, _, view_range: tuple[float, float]) -> None:
|
||||
"""Update the range of the tick item
|
||||
|
||||
Args:
|
||||
vb (pg.ViewBox): The view box.
|
||||
viewRange (tuple): The view range.
|
||||
"""
|
||||
if self._pos < view_range[0] or self._pos > view_range[1]:
|
||||
self.tick_item.setVisible(False)
|
||||
else:
|
||||
self.tick_item.setVisible(True)
|
||||
|
||||
if self.tick_item.isVisible():
|
||||
origin = self.tick_item.tickSize / 2.0
|
||||
length = self.tick_item.length
|
||||
|
||||
length_with_padding = length + self.tick_item.tickSize + 2
|
||||
|
||||
self._range = view_range
|
||||
tick_with_padding = (self._pos - view_range[0]) / (view_range[1] - view_range[0])
|
||||
tick_value = (tick_with_padding * length_with_padding - origin) / length
|
||||
self.tick_item.setTickValue(self.tick, tick_value)
|
||||
|
||||
def add_to_plot(self):
|
||||
"""Add the tick item to the view box or plot item."""
|
||||
if self.plot_item is None:
|
||||
return
|
||||
|
||||
self.plot_item.layout.addItem(self.tick_item, 2, 1)
|
||||
self.tick_item.setOrientation("top")
|
||||
self.tick = self.tick_item.addTick(0, movable=False, color=self.accent_colors.highlight)
|
||||
self.update_tick_pos_y()
|
||||
self.plot_item.vb.sigXRangeChanged.connect(self.update_range)
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.vb.geometryChanged.connect(self.update_tick_pos_y)
|
||||
self.item_on_plot = True
|
||||
|
||||
@Slot()
|
||||
def update_tick_pos_y(self):
|
||||
"""Update tick position, while respecting the tick_item coordinates"""
|
||||
pos = self.tick.pos()
|
||||
pos = self.tick_item.mapToParent(pos)
|
||||
new_pos = self.plot_item.vb.geometry().bottom()
|
||||
new_pos = self.tick_item.mapFromParent(QPointF(pos.x(), new_pos))
|
||||
self.tick.setPos(new_pos)
|
||||
|
||||
def remove_from_plot(self):
|
||||
"""Remove the tick item from the view box or plot item."""
|
||||
if self.plot_item is not None and self.item_on_plot is True:
|
||||
self.plot_item.vb.sigXRangeChanged.disconnect(self.update_range)
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.disconnect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.disconnect(self.check_log)
|
||||
if self.plot_item.layout is not None:
|
||||
self.plot_item.layout.removeItem(self.tick_item)
|
||||
self.item_on_plot = False
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Cleanup the item"""
|
||||
self.remove_from_plot()
|
||||
if self.tick_item is not None:
|
||||
self.tick_item.close()
|
||||
self.tick_item.deleteLater()
|
||||
self.tick_item = None
|
||||
|
||||
|
||||
class BECArrowItem(BECIndicatorItem):
|
||||
"""Class to create an arrow item which can be added to a pyqtgraph plot.
|
||||
It can be either added directly to a view box or a plot item.
|
||||
To add the arrow item to a view box or plot item, use the add_to_plot method.
|
||||
|
||||
Args:
|
||||
view_box (pg.ViewBox | pg.PlotItem): The view box or plot item to which the arrow item should be added.
|
||||
parent (QObject): The parent object.
|
||||
|
||||
Signals:
|
||||
position_changed (tuple[float, float]): Signal emitted when the position of the arrow item has changed.
|
||||
position_changed_str (tuple[str, str]): Signal emitted when the position of the arrow item has changed.
|
||||
"""
|
||||
|
||||
# Signal to emit if the position of the arrow item has changed
|
||||
position_changed = Signal(tuple)
|
||||
position_changed_str = Signal(tuple)
|
||||
|
||||
def __init__(self, plot_item: pg.PlotItem = None, parent=None):
|
||||
super().__init__(plot_item=plot_item, parent=parent)
|
||||
self.arrow_item = pg.ArrowItem(parent=parent)
|
||||
self.arrow_item.skip_auto_range = True
|
||||
self._pos = (0, 0)
|
||||
self.arrow_item.setVisible(False)
|
||||
|
||||
@Slot(dict)
|
||||
def set_style(self, style: dict) -> None:
|
||||
"""Set the style of the arrow item
|
||||
|
||||
Args:
|
||||
style (dict): The style of the arrow item. Dictionary with key,
|
||||
value pairs which are accepted from the pg.ArrowItem.setStyle method.
|
||||
"""
|
||||
self.arrow_item.setStyle(**style)
|
||||
|
||||
@Slot(tuple)
|
||||
def set_position(self, pos: tuple[float, float]) -> None:
|
||||
"""Set the position of the arrow item
|
||||
|
||||
Args:
|
||||
pos (tuple): The position of the arrow item as a tuple (x, y).
|
||||
"""
|
||||
self._pos = pos
|
||||
pos_x = pos[0]
|
||||
pos_y = pos[1]
|
||||
if self.is_log_x is True:
|
||||
pos_x = np.log10(pos_x) if pos_x > 0 else 1e-10
|
||||
view_box = self.plot_item.getViewBox() # Ensure you're accessing the correct view box
|
||||
view_range = view_box.viewRange()[0]
|
||||
# Avoid values outside the view range in the negative direction. Otherwise, there is
|
||||
# a buggy behaviour of the arrow item and it appears at the wrong position.
|
||||
if pos_x < view_range[0]:
|
||||
pos_x = view_range[0]
|
||||
if self.is_log_y is True:
|
||||
pos_y = np.log10(pos_y) if pos_y > 0 else 1e-10
|
||||
|
||||
self.arrow_item.setPos(pos_x, pos_y)
|
||||
self.position_changed.emit(self._pos)
|
||||
self.position_changed_str.emit((str(self._pos[0]), str(self._pos[1])))
|
||||
|
||||
def add_to_plot(self):
|
||||
"""Add the arrow item to the view box or plot item."""
|
||||
if not self.arrow_item:
|
||||
logger.warning(f"Arrow item was already destroyed, cannot be created")
|
||||
return
|
||||
|
||||
self.arrow_item.setStyle(
|
||||
angle=-90,
|
||||
pen=pg.mkPen(self.accent_colors.emergency, width=1),
|
||||
brush=pg.mkBrush(self.accent_colors.highlight),
|
||||
headLen=20,
|
||||
)
|
||||
self.arrow_item.setVisible(True)
|
||||
if self.plot_item is not None:
|
||||
self.plot_item.addItem(self.arrow_item)
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
||||
self.item_on_plot = True
|
||||
|
||||
def remove_from_plot(self):
|
||||
"""Remove the arrow item from the view box or plot item."""
|
||||
if self.plot_item is not None and self.item_on_plot is True:
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.disconnect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.disconnect(self.check_log)
|
||||
self.plot_item.removeItem(self.arrow_item)
|
||||
self.item_on_plot = False
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Cleanup the item"""
|
||||
self.remove_from_plot()
|
||||
self.arrow_item = None
|
||||
@@ -1,5 +1,6 @@
|
||||
# pylint: disable=no-name-in-module
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Literal
|
||||
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -20,7 +21,7 @@ class WidgetHandler(ABC):
|
||||
"""Abstract base class for all widget handlers."""
|
||||
|
||||
@abstractmethod
|
||||
def get_value(self, widget: QWidget):
|
||||
def get_value(self, widget: QWidget, **kwargs):
|
||||
"""Retrieve value from the widget instance."""
|
||||
|
||||
@abstractmethod
|
||||
@@ -31,7 +32,7 @@ class WidgetHandler(ABC):
|
||||
class LineEditHandler(WidgetHandler):
|
||||
"""Handler for QLineEdit widgets."""
|
||||
|
||||
def get_value(self, widget: QLineEdit) -> str:
|
||||
def get_value(self, widget: QLineEdit, **kwargs) -> str:
|
||||
return widget.text()
|
||||
|
||||
def set_value(self, widget: QLineEdit, value: str) -> None:
|
||||
@@ -41,7 +42,9 @@ class LineEditHandler(WidgetHandler):
|
||||
class ComboBoxHandler(WidgetHandler):
|
||||
"""Handler for QComboBox widgets."""
|
||||
|
||||
def get_value(self, widget: QComboBox) -> int:
|
||||
def get_value(self, widget: QComboBox, as_string: bool = False, **kwargs) -> int | str:
|
||||
if as_string is True:
|
||||
return widget.currentText()
|
||||
return widget.currentIndex()
|
||||
|
||||
def set_value(self, widget: QComboBox, value: int | str) -> None:
|
||||
@@ -54,7 +57,7 @@ class ComboBoxHandler(WidgetHandler):
|
||||
class TableWidgetHandler(WidgetHandler):
|
||||
"""Handler for QTableWidget widgets."""
|
||||
|
||||
def get_value(self, widget: QTableWidget) -> list:
|
||||
def get_value(self, widget: QTableWidget, **kwargs) -> list:
|
||||
return [
|
||||
[
|
||||
widget.item(row, col).text() if widget.item(row, col) else ""
|
||||
@@ -73,7 +76,7 @@ class TableWidgetHandler(WidgetHandler):
|
||||
class SpinBoxHandler(WidgetHandler):
|
||||
"""Handler for QSpinBox and QDoubleSpinBox widgets."""
|
||||
|
||||
def get_value(self, widget):
|
||||
def get_value(self, widget, **kwargs):
|
||||
return widget.value()
|
||||
|
||||
def set_value(self, widget, value):
|
||||
@@ -83,7 +86,7 @@ class SpinBoxHandler(WidgetHandler):
|
||||
class CheckBoxHandler(WidgetHandler):
|
||||
"""Handler for QCheckBox widgets."""
|
||||
|
||||
def get_value(self, widget):
|
||||
def get_value(self, widget, **kwargs):
|
||||
return widget.isChecked()
|
||||
|
||||
def set_value(self, widget, value):
|
||||
@@ -93,7 +96,7 @@ class CheckBoxHandler(WidgetHandler):
|
||||
class LabelHandler(WidgetHandler):
|
||||
"""Handler for QLabel widgets."""
|
||||
|
||||
def get_value(self, widget):
|
||||
def get_value(self, widget, **kwargs):
|
||||
return widget.text()
|
||||
|
||||
def set_value(self, widget, value):
|
||||
@@ -114,7 +117,7 @@ class WidgetIO:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_value(widget, ignore_errors=False):
|
||||
def get_value(widget, ignore_errors=False, **kwargs):
|
||||
"""
|
||||
Retrieve value from the widget instance.
|
||||
|
||||
@@ -124,7 +127,7 @@ class WidgetIO:
|
||||
"""
|
||||
handler_class = WidgetIO._find_handler(widget)
|
||||
if handler_class:
|
||||
return handler_class().get_value(widget) # Instantiate the handler
|
||||
return handler_class().get_value(widget, **kwargs) # Instantiate the handler
|
||||
if not ignore_errors:
|
||||
raise ValueError(f"No handler for widget type: {type(widget)}")
|
||||
return None
|
||||
|
||||
@@ -1,38 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from bec_lib.device import ComputedSignal, Device, Positioner, ReadoutPriority
|
||||
from bec_lib.device import Signal as BECSignal
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.filter_io import FilterIO
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class BECDeviceFilter(enum.Enum):
|
||||
"""Filter for the device classes."""
|
||||
|
||||
DEVICE = "Device"
|
||||
POSITIONER = "Positioner"
|
||||
SIGNAL = "Signal"
|
||||
COMPUTED_SIGNAL = "ComputedSignal"
|
||||
|
||||
|
||||
class DeviceInputConfig(ConnectionConfig):
|
||||
device_filter: str | list[str] | None = None
|
||||
device_filter: list[BECDeviceFilter] = []
|
||||
readout_filter: list[ReadoutPriority] = []
|
||||
devices: list[str] = []
|
||||
default: str | None = None
|
||||
arg_name: str | None = None
|
||||
apply_filter: bool = True
|
||||
|
||||
|
||||
class DeviceInputBase(BECWidget):
|
||||
"""
|
||||
Mixin class for device input widgets. This class provides methods to get the device list and device object based
|
||||
on the current text of the widget.
|
||||
Mixin base class for device input widgets.
|
||||
It allows to filter devices from BEC based on
|
||||
device class and readout priority.
|
||||
"""
|
||||
|
||||
def __init__(self, client=None, config=None, gui_id=None):
|
||||
_device_handler = {
|
||||
BECDeviceFilter.DEVICE: Device,
|
||||
BECDeviceFilter.POSITIONER: Positioner,
|
||||
BECDeviceFilter.SIGNAL: BECSignal,
|
||||
BECDeviceFilter.COMPUTED_SIGNAL: ComputedSignal,
|
||||
}
|
||||
|
||||
_filter_handler = {
|
||||
BECDeviceFilter.DEVICE: "filter_to_device",
|
||||
BECDeviceFilter.POSITIONER: "filter_to_positioner",
|
||||
BECDeviceFilter.SIGNAL: "filter_to_signal",
|
||||
BECDeviceFilter.COMPUTED_SIGNAL: "filter_to_computed_signal",
|
||||
ReadoutPriority.MONITORED: "readout_monitored",
|
||||
ReadoutPriority.BASELINE: "readout_baseline",
|
||||
ReadoutPriority.ASYNC: "readout_async",
|
||||
ReadoutPriority.CONTINUOUS: "readout_continuous",
|
||||
ReadoutPriority.ON_REQUEST: "readout_on_request",
|
||||
}
|
||||
|
||||
def __init__(self, client=None, config=None, gui_id: str = None):
|
||||
|
||||
if config is None:
|
||||
config = DeviceInputConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = DeviceInputConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, theme_update=True)
|
||||
self.get_bec_shortcuts()
|
||||
self._device_filter = None
|
||||
self._device_filter = []
|
||||
self._readout_filter = []
|
||||
self._devices = []
|
||||
|
||||
@property
|
||||
### QtSlots ###
|
||||
|
||||
@Slot(str)
|
||||
def set_device(self, device: str):
|
||||
"""
|
||||
Set the device.
|
||||
|
||||
Args:
|
||||
device (str): Default name.
|
||||
"""
|
||||
if self.validate_device(device) is True:
|
||||
WidgetIO.set_value(widget=self, value=device)
|
||||
self.config.default = device
|
||||
else:
|
||||
logger.warning(f"Device {device} is not in the filtered selection.")
|
||||
|
||||
@Slot()
|
||||
def update_devices_from_filters(self):
|
||||
"""Update the devices based on the current filter selection
|
||||
in self.device_filter and self.readout_filter. If apply_filter is False,
|
||||
it will not apply the filters, store the filter settings and return.
|
||||
"""
|
||||
current_device = WidgetIO.get_value(widget=self, as_string=True)
|
||||
self.config.device_filter = self.device_filter
|
||||
self.config.readout_filter = self.readout_filter
|
||||
if self.apply_filter is False:
|
||||
return
|
||||
all_dev = self.dev.enabled_devices
|
||||
# Filter based on device class
|
||||
devs = [dev for dev in all_dev if self._check_device_filter(dev)]
|
||||
# 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)
|
||||
|
||||
@Slot(list)
|
||||
def set_available_devices(self, devices: list[str]):
|
||||
"""
|
||||
Set the devices. If a device in the list is not valid, it will not be considered.
|
||||
|
||||
Args:
|
||||
devices (list[str]): List of devices.
|
||||
"""
|
||||
self.apply_filter = False
|
||||
self.devices = devices
|
||||
|
||||
### QtProperties ###
|
||||
|
||||
@Property(
|
||||
"QStringList",
|
||||
doc="List of devices. If updated, it will disable the apply filters property.",
|
||||
)
|
||||
def devices(self) -> list[str]:
|
||||
"""
|
||||
Get the list of devices.
|
||||
Get the list of devices for the applied filters.
|
||||
|
||||
Returns:
|
||||
list[str]: List of devices.
|
||||
@@ -40,84 +135,258 @@ class DeviceInputBase(BECWidget):
|
||||
return self._devices
|
||||
|
||||
@devices.setter
|
||||
def devices(self, value: list[str]):
|
||||
"""
|
||||
Set the list of devices.
|
||||
|
||||
Args:
|
||||
value: List of devices.
|
||||
"""
|
||||
def devices(self, value: list):
|
||||
self._devices = value
|
||||
self.config.devices = value
|
||||
FilterIO.set_selection(widget=self, selection=value)
|
||||
|
||||
def set_device_filter(self, device_filter: str | list[str]):
|
||||
@Property(str)
|
||||
def default(self):
|
||||
"""Get the default device name. If set through this property, it will update only if the device is within the filtered selection."""
|
||||
return self.config.default
|
||||
|
||||
@default.setter
|
||||
def default(self, value: str):
|
||||
if self.validate_device(value) is False:
|
||||
return
|
||||
self.config.default = value
|
||||
WidgetIO.set_value(widget=self, value=value)
|
||||
|
||||
@Property(bool)
|
||||
def apply_filter(self):
|
||||
"""Apply the filters on the devices."""
|
||||
return self.config.apply_filter
|
||||
|
||||
@apply_filter.setter
|
||||
def apply_filter(self, value: bool):
|
||||
self.config.apply_filter = value
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def filter_to_device(self):
|
||||
"""Include devices in filters."""
|
||||
return BECDeviceFilter.DEVICE in self.device_filter
|
||||
|
||||
@filter_to_device.setter
|
||||
def filter_to_device(self, value: bool):
|
||||
if value is True and BECDeviceFilter.DEVICE not in self.device_filter:
|
||||
self._device_filter.append(BECDeviceFilter.DEVICE)
|
||||
if value is False and BECDeviceFilter.DEVICE in self.device_filter:
|
||||
self._device_filter.remove(BECDeviceFilter.DEVICE)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def filter_to_positioner(self):
|
||||
"""Include devices of type Positioner in filters."""
|
||||
return BECDeviceFilter.POSITIONER in self.device_filter
|
||||
|
||||
@filter_to_positioner.setter
|
||||
def filter_to_positioner(self, value: bool):
|
||||
if value is True and BECDeviceFilter.POSITIONER not in self.device_filter:
|
||||
self._device_filter.append(BECDeviceFilter.POSITIONER)
|
||||
if value is False and BECDeviceFilter.POSITIONER in self.device_filter:
|
||||
self._device_filter.remove(BECDeviceFilter.POSITIONER)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def filter_to_signal(self):
|
||||
"""Include devices of type Signal in filters."""
|
||||
return BECDeviceFilter.SIGNAL in self.device_filter
|
||||
|
||||
@filter_to_signal.setter
|
||||
def filter_to_signal(self, value: bool):
|
||||
if value is True and BECDeviceFilter.SIGNAL not in self.device_filter:
|
||||
self._device_filter.append(BECDeviceFilter.SIGNAL)
|
||||
if value is False and BECDeviceFilter.SIGNAL in self.device_filter:
|
||||
self._device_filter.remove(BECDeviceFilter.SIGNAL)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def filter_to_computed_signal(self):
|
||||
"""Include devices of type ComputedSignal in filters."""
|
||||
return BECDeviceFilter.COMPUTED_SIGNAL in self.device_filter
|
||||
|
||||
@filter_to_computed_signal.setter
|
||||
def filter_to_computed_signal(self, value: bool):
|
||||
if value is True and BECDeviceFilter.COMPUTED_SIGNAL not in self.device_filter:
|
||||
self._device_filter.append(BECDeviceFilter.COMPUTED_SIGNAL)
|
||||
if value is False and BECDeviceFilter.COMPUTED_SIGNAL in self.device_filter:
|
||||
self._device_filter.remove(BECDeviceFilter.COMPUTED_SIGNAL)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def readout_monitored(self):
|
||||
"""Include devices with readout priority Monitored in filters."""
|
||||
return ReadoutPriority.MONITORED in self.readout_filter
|
||||
|
||||
@readout_monitored.setter
|
||||
def readout_monitored(self, value: bool):
|
||||
if value is True and ReadoutPriority.MONITORED not in self.readout_filter:
|
||||
self._readout_filter.append(ReadoutPriority.MONITORED)
|
||||
if value is False and ReadoutPriority.MONITORED in self.readout_filter:
|
||||
self._readout_filter.remove(ReadoutPriority.MONITORED)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def readout_baseline(self):
|
||||
"""Include devices with readout priority Baseline in filters."""
|
||||
return ReadoutPriority.BASELINE in self.readout_filter
|
||||
|
||||
@readout_baseline.setter
|
||||
def readout_baseline(self, value: bool):
|
||||
if value is True and ReadoutPriority.BASELINE not in self.readout_filter:
|
||||
self._readout_filter.append(ReadoutPriority.BASELINE)
|
||||
if value is False and ReadoutPriority.BASELINE in self.readout_filter:
|
||||
self._readout_filter.remove(ReadoutPriority.BASELINE)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def readout_async(self):
|
||||
"""Include devices with readout priority Async in filters."""
|
||||
return ReadoutPriority.ASYNC in self.readout_filter
|
||||
|
||||
@readout_async.setter
|
||||
def readout_async(self, value: bool):
|
||||
if value is True and ReadoutPriority.ASYNC not in self.readout_filter:
|
||||
self._readout_filter.append(ReadoutPriority.ASYNC)
|
||||
if value is False and ReadoutPriority.ASYNC in self.readout_filter:
|
||||
self._readout_filter.remove(ReadoutPriority.ASYNC)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def readout_continuous(self):
|
||||
"""Include devices with readout priority continuous in filters."""
|
||||
return ReadoutPriority.CONTINUOUS in self.readout_filter
|
||||
|
||||
@readout_continuous.setter
|
||||
def readout_continuous(self, value: bool):
|
||||
if value is True and ReadoutPriority.CONTINUOUS not in self.readout_filter:
|
||||
self._readout_filter.append(ReadoutPriority.CONTINUOUS)
|
||||
if value is False and ReadoutPriority.CONTINUOUS in self.readout_filter:
|
||||
self._readout_filter.remove(ReadoutPriority.CONTINUOUS)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def readout_on_request(self):
|
||||
"""Include devices with readout priority OnRequest in filters."""
|
||||
return ReadoutPriority.ON_REQUEST in self.readout_filter
|
||||
|
||||
@readout_on_request.setter
|
||||
def readout_on_request(self, value: bool):
|
||||
if value is True and ReadoutPriority.ON_REQUEST not in self.readout_filter:
|
||||
self._readout_filter.append(ReadoutPriority.ON_REQUEST)
|
||||
if value is False and ReadoutPriority.ON_REQUEST in self.readout_filter:
|
||||
self._readout_filter.remove(ReadoutPriority.ON_REQUEST)
|
||||
self.update_devices_from_filters()
|
||||
|
||||
### Python Methods and Properties ###
|
||||
|
||||
@property
|
||||
def device_filter(self) -> list[object]:
|
||||
"""Get the list of filters to apply on the devices."""
|
||||
return self._device_filter
|
||||
|
||||
@property
|
||||
def readout_filter(self) -> list[str]:
|
||||
"""Get the list of filters to apply on the devices"""
|
||||
return self._readout_filter
|
||||
|
||||
def get_available_filters(self) -> list:
|
||||
"""Get the available filters."""
|
||||
return [entry for entry in BECDeviceFilter]
|
||||
|
||||
def get_readout_priority_filters(self) -> list:
|
||||
"""Get the available readout priority filters."""
|
||||
return [entry for entry in ReadoutPriority]
|
||||
|
||||
def set_device_filter(
|
||||
self, filter_selection: str | BECDeviceFilter | list[str] | list[BECDeviceFilter]
|
||||
):
|
||||
"""
|
||||
Set the device filter.
|
||||
Set the device filter. If None is passed, no filters are applied and all devices included.
|
||||
|
||||
Args:
|
||||
device_filter(str): Device filter, name of the device class.
|
||||
filter_selection (str | list[str]): Device filters. It is recommended to make an enum for the filters.
|
||||
"""
|
||||
self.validate_device_filter(device_filter)
|
||||
self.config.device_filter = device_filter
|
||||
self._device_filter = device_filter
|
||||
filters = None
|
||||
if isinstance(filter_selection, list):
|
||||
filters = [self._filter_handler.get(entry) for entry in filter_selection]
|
||||
if isinstance(filter_selection, str) or isinstance(filter_selection, BECDeviceFilter):
|
||||
filters = [self._filter_handler.get(filter_selection)]
|
||||
if filters is None or any([entry is None for entry in filters]):
|
||||
logger.warning(f"Device filter {filter_selection} is not in the device filter list.")
|
||||
return
|
||||
for entry in filters:
|
||||
setattr(self, entry, True)
|
||||
|
||||
def set_default_device(self, default_device: str):
|
||||
def set_readout_priority_filter(
|
||||
self, filter_selection: str | ReadoutPriority | list[str] | list[ReadoutPriority]
|
||||
):
|
||||
"""
|
||||
Set the default device.
|
||||
Set the readout priority filter. If None is passed, all filters are included.
|
||||
|
||||
Args:
|
||||
default_device(str): Default device name.
|
||||
filter_selection (str | list[str]): Readout priority filters.
|
||||
"""
|
||||
self.validate_device(default_device)
|
||||
self.config.default = default_device
|
||||
filters = None
|
||||
if isinstance(filter_selection, list):
|
||||
filters = [self._filter_handler.get(entry) for entry in filter_selection]
|
||||
if isinstance(filter_selection, str) or isinstance(filter_selection, ReadoutPriority):
|
||||
filters = [self._filter_handler.get(filter_selection)]
|
||||
if filters is None or any([entry is None for entry in filters]):
|
||||
logger.warning(
|
||||
f"Readout priority filter {filter_selection} is not in the readout priority list."
|
||||
)
|
||||
return
|
||||
for entry in filters:
|
||||
setattr(self, entry, True)
|
||||
|
||||
def get_device_list(self, filter: str | list[str] | None = None) -> list[str]:
|
||||
"""
|
||||
Get the list of device names based on the filter of current BEC client.
|
||||
def _check_device_filter(
|
||||
self, device: Device | BECSignal | ComputedSignal | Positioner
|
||||
) -> bool:
|
||||
"""Check if filter for device type is applied or not.
|
||||
|
||||
Args:
|
||||
filter(str|None): Class name filter to apply on the device list.
|
||||
device(Device | Signal | ComputedSignal | Positioner): Device object.
|
||||
"""
|
||||
return all(isinstance(device, self._device_handler[entry]) for entry in self.device_filter)
|
||||
|
||||
def _check_readout_filter(
|
||||
self, device: Device | BECSignal | ComputedSignal | Positioner
|
||||
) -> bool:
|
||||
"""Check if filter for readout priority is applied or not.
|
||||
|
||||
Args:
|
||||
device(Device | Signal | ComputedSignal | Positioner): Device object.
|
||||
"""
|
||||
return device.readout_priority in self.readout_filter
|
||||
|
||||
def get_device_object(self, device: str) -> object:
|
||||
"""
|
||||
Get the device object based on the device name.
|
||||
|
||||
Args:
|
||||
device(str): Device name.
|
||||
|
||||
Returns:
|
||||
devices(list[str]): List of device names.
|
||||
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
||||
"""
|
||||
all_devices = self.dev.enabled_devices
|
||||
if filter is not None:
|
||||
self.validate_device_filter(filter)
|
||||
if isinstance(filter, str):
|
||||
filter = [filter]
|
||||
devices = [device.name for device in all_devices if device.__class__.__name__ in filter]
|
||||
else:
|
||||
devices = [device.name for device in all_devices]
|
||||
return devices
|
||||
self.validate_device(device)
|
||||
dev = getattr(self.dev, device.lower(), None)
|
||||
if dev is None:
|
||||
raise ValueError(
|
||||
f"Device {device} is not found in the device manager {self.dev} as enabled device."
|
||||
)
|
||||
return dev
|
||||
|
||||
def get_available_filters(self):
|
||||
def validate_device(self, device: str) -> bool:
|
||||
"""
|
||||
Get the available device classes which can be used as filters.
|
||||
"""
|
||||
all_devices = self.dev.enabled_devices
|
||||
filters = {device.__class__.__name__ for device in all_devices}
|
||||
return filters
|
||||
|
||||
def validate_device_filter(self, filter: str | list[str]) -> None:
|
||||
"""
|
||||
Validate the device filter if the class name is present in the current BEC instance.
|
||||
|
||||
Args:
|
||||
filter(str|list[str]): Class name to use as a device filter.
|
||||
"""
|
||||
if isinstance(filter, str):
|
||||
filter = [filter]
|
||||
available_filters = self.get_available_filters()
|
||||
for f in filter:
|
||||
if f not in available_filters:
|
||||
raise ValueError(f"Device filter {f} is not valid.")
|
||||
|
||||
def validate_device(self, device: str) -> None:
|
||||
"""
|
||||
Validate the device if it is present in current BEC instance.
|
||||
Validate the device if it is present in the filtered device selection.
|
||||
|
||||
Args:
|
||||
device(str): Device to validate.
|
||||
"""
|
||||
if device not in self.get_device_list(self.config.device_filter):
|
||||
raise ValueError(f"Device {device} is not valid.")
|
||||
all_devs = [dev.name for dev in self.dev.enabled_devices]
|
||||
if device in self.devices and device in all_devs:
|
||||
return True
|
||||
return False
|
||||
|
||||
280
bec_widgets/widgets/base_classes/device_signal_input_base.py
Normal file
@@ -0,0 +1,280 @@
|
||||
from bec_lib.callback_handler import EventType
|
||||
from bec_lib.device import Signal
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, Slot
|
||||
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.filter_io import FilterIO
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DeviceSignalInputBaseConfig(ConnectionConfig):
|
||||
"""Configuration class for DeviceSignalInputBase."""
|
||||
|
||||
signal_filter: str | list[str] | None = None
|
||||
default: str | None = None
|
||||
arg_name: str | None = None
|
||||
device: str | None = None
|
||||
signals: list[str] | None = None
|
||||
|
||||
|
||||
class DeviceSignalInputBase(BECWidget):
|
||||
"""
|
||||
Mixin base class for device signal input widgets.
|
||||
Mixin class for device signal input widgets. This class provides methods to get the device signal list and device
|
||||
signal object based on the current text of the widget.
|
||||
"""
|
||||
|
||||
_filter_handler = {
|
||||
Kind.hinted: "include_hinted_signals",
|
||||
Kind.normal: "include_normal_signals",
|
||||
Kind.config: "include_config_signals",
|
||||
}
|
||||
|
||||
def __init__(self, client=None, config=None, gui_id: str = None):
|
||||
if config is None:
|
||||
config = DeviceSignalInputBaseConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = DeviceSignalInputBaseConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
|
||||
self._device = None
|
||||
self.get_bec_shortcuts()
|
||||
self._signal_filter = []
|
||||
self._signals = []
|
||||
self._hinted_signals = []
|
||||
self._normal_signals = []
|
||||
self._config_signals = []
|
||||
self.bec_dispatcher.client.callbacks.register(
|
||||
EventType.DEVICE_UPDATE, self.update_signals_from_filters
|
||||
)
|
||||
|
||||
### Qt Slots ###
|
||||
|
||||
@Slot(str)
|
||||
def set_signal(self, signal: str):
|
||||
"""
|
||||
Set the signal.
|
||||
|
||||
Args:
|
||||
signal (str): signal name.
|
||||
"""
|
||||
if self.validate_signal(signal) is True:
|
||||
WidgetIO.set_value(widget=self, value=signal)
|
||||
self.config.default = signal
|
||||
else:
|
||||
logger.warning(
|
||||
f"Signal {signal} not found for device {self.device} and filtered selection {self.signal_filter}."
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
def set_device(self, device: str | None):
|
||||
"""
|
||||
Set the device. If device is not valid, device will be set to None which happpens
|
||||
|
||||
Args:
|
||||
device(str): device name.
|
||||
"""
|
||||
if self.validate_device(device) is False:
|
||||
self._device = None
|
||||
else:
|
||||
self._device = device
|
||||
self.update_signals_from_filters()
|
||||
|
||||
@Slot(dict, dict)
|
||||
@Slot()
|
||||
def update_signals_from_filters(
|
||||
self, content: dict | None = None, metadata: dict | None = None
|
||||
):
|
||||
"""Update the filters for the device signals based on list in self.signal_filter.
|
||||
In addition, store the hinted, normal and config signals in separate lists to allow
|
||||
customisation within QLineEdit.
|
||||
|
||||
Note:
|
||||
Signal and ComputedSignals have no signals. The naming convention follows the device name.
|
||||
"""
|
||||
self.config.signal_filter = self.signal_filter
|
||||
# pylint: disable=protected-access
|
||||
self._hinted_signals = []
|
||||
self._normal_signals = []
|
||||
self._config_signals = []
|
||||
if self.validate_device(self._device) is False:
|
||||
self._device = None
|
||||
self.config.device = self._device
|
||||
return
|
||||
device = self.get_device_object(self._device)
|
||||
# See above convention for Signals and ComputedSignals
|
||||
if isinstance(device, Signal):
|
||||
self._signals = [self._device]
|
||||
FilterIO.set_selection(widget=self, selection=[self._device])
|
||||
return
|
||||
device_info = device._info["signals"]
|
||||
if Kind.hinted in self.signal_filter:
|
||||
hinted_signals = [
|
||||
signal
|
||||
for signal, signal_info in device_info.items()
|
||||
if (signal_info.get("kind_str", None) == str(Kind.hinted.value))
|
||||
]
|
||||
self._hinted_signals = hinted_signals
|
||||
if Kind.normal in self.signal_filter:
|
||||
normal_signals = [
|
||||
signal
|
||||
for signal, signal_info in device_info.items()
|
||||
if (signal_info.get("kind_str", None) == str(Kind.normal.value))
|
||||
]
|
||||
self._normal_signals = normal_signals
|
||||
if Kind.config in self.signal_filter:
|
||||
config_signals = [
|
||||
signal
|
||||
for signal, signal_info in device_info.items()
|
||||
if (signal_info.get("kind_str", None) == str(Kind.config.value))
|
||||
]
|
||||
self._config_signals = config_signals
|
||||
self._signals = self._hinted_signals + self._normal_signals + self._config_signals
|
||||
FilterIO.set_selection(widget=self, selection=self.signals)
|
||||
|
||||
### Qt Properties ###
|
||||
|
||||
@Property(str)
|
||||
def device(self) -> str:
|
||||
"""Get the selected device."""
|
||||
if self._device is None:
|
||||
return ""
|
||||
return self._device
|
||||
|
||||
@device.setter
|
||||
def device(self, value: str):
|
||||
"""Set the device and update the filters, only allow devices present in the devicemanager."""
|
||||
self._device = value
|
||||
self.config.device = value
|
||||
self.update_signals_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def include_hinted_signals(self):
|
||||
"""Include hinted signals in filters."""
|
||||
return Kind.hinted in self.signal_filter
|
||||
|
||||
@include_hinted_signals.setter
|
||||
def include_hinted_signals(self, value: bool):
|
||||
if value:
|
||||
self._signal_filter.append(Kind.hinted)
|
||||
else:
|
||||
self._signal_filter.remove(Kind.hinted)
|
||||
self.update_signals_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def include_normal_signals(self):
|
||||
"""Include normal signals in filters."""
|
||||
return Kind.normal in self.signal_filter
|
||||
|
||||
@include_normal_signals.setter
|
||||
def include_normal_signals(self, value: bool):
|
||||
if value:
|
||||
self._signal_filter.append(Kind.normal)
|
||||
else:
|
||||
self._signal_filter.remove(Kind.normal)
|
||||
self.update_signals_from_filters()
|
||||
|
||||
@Property(bool)
|
||||
def include_config_signals(self):
|
||||
"""Include config signals in filters."""
|
||||
return Kind.config in self.signal_filter
|
||||
|
||||
@include_config_signals.setter
|
||||
def include_config_signals(self, value: bool):
|
||||
if value:
|
||||
self._signal_filter.append(Kind.config)
|
||||
else:
|
||||
self._signal_filter.remove(Kind.config)
|
||||
self.update_signals_from_filters()
|
||||
|
||||
### Properties and Methods ###
|
||||
|
||||
@property
|
||||
def signals(self) -> list[str]:
|
||||
"""
|
||||
Get the list of device signals for the applied filters.
|
||||
|
||||
Returns:
|
||||
list[str]: List of device signals.
|
||||
"""
|
||||
return self._signals
|
||||
|
||||
@signals.setter
|
||||
def signals(self, value: list[str]):
|
||||
self._signals = value
|
||||
self.config.signals = value
|
||||
FilterIO.set_selection(widget=self, selection=value)
|
||||
|
||||
@property
|
||||
def signal_filter(self) -> list[str]:
|
||||
"""Get the list of filters to apply on the device signals."""
|
||||
return self._signal_filter
|
||||
|
||||
def get_available_filters(self) -> list[str]:
|
||||
"""Get the available filters."""
|
||||
return [entry for entry in self._filter_handler]
|
||||
|
||||
def set_filter(self, filter_selection: str | list[str]):
|
||||
"""
|
||||
Set the device filter. If None, all devices are included.
|
||||
|
||||
Args:
|
||||
filter_selection (str | list[str]): Device filters from BECDeviceFilter and BECReadoutPriority.
|
||||
"""
|
||||
filters = None
|
||||
if isinstance(filter_selection, list):
|
||||
filters = [self._filter_handler.get(entry) for entry in filter_selection]
|
||||
if isinstance(filter_selection, str):
|
||||
filters = [self._filter_handler.get(filter_selection)]
|
||||
if filters is None:
|
||||
return
|
||||
for entry in filters:
|
||||
setattr(self, entry, True)
|
||||
|
||||
def get_device_object(self, device: str) -> object | None:
|
||||
"""
|
||||
Get the device object based on the device name.
|
||||
|
||||
Args:
|
||||
device(str): Device name.
|
||||
|
||||
Returns:
|
||||
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
||||
"""
|
||||
self.validate_device(device)
|
||||
dev = getattr(self.dev, device.lower(), None)
|
||||
if dev is None:
|
||||
logger.warning(f"Device {device} not found in devicemanager.")
|
||||
return None
|
||||
return dev
|
||||
|
||||
def validate_device(self, device: str | None, raise_on_false: bool = False) -> bool:
|
||||
"""
|
||||
Validate the device if it is present in current BEC instance.
|
||||
|
||||
Args:
|
||||
device(str): Device to validate.
|
||||
"""
|
||||
if device in self.dev:
|
||||
return True
|
||||
if raise_on_false is True:
|
||||
raise ValueError(f"Device {device} not found in devicemanager.")
|
||||
return False
|
||||
|
||||
def validate_signal(self, signal: str) -> bool:
|
||||
"""
|
||||
Validate the signal if it is present in the device signals.
|
||||
|
||||
Args:
|
||||
signal(str): Signal to validate.
|
||||
"""
|
||||
if signal in self.signals:
|
||||
return True
|
||||
return False
|
||||
@@ -2,10 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Property, Qt, Slot
|
||||
from qtpy.QtCore import Property, Qt, Signal, Slot
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QHeaderView, QLabel, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.qt_utils.toolbar import ModularToolBar, SeparatorAction, WidgetAction
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
@@ -15,7 +16,7 @@ from bec_widgets.widgets.button_resume.button_resume import ResumeButton
|
||||
from bec_widgets.widgets.stop_button.stop_button import StopButton
|
||||
|
||||
|
||||
class BECQueue(BECWidget, QWidget):
|
||||
class BECQueue(BECWidget, CompactPopupWidget):
|
||||
"""
|
||||
Widget to display the BEC queue.
|
||||
"""
|
||||
@@ -31,6 +32,8 @@ class BECQueue(BECWidget, QWidget):
|
||||
"COMPLETED": "blue",
|
||||
}
|
||||
|
||||
queue_busy = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
@@ -40,22 +43,24 @@ class BECQueue(BECWidget, QWidget):
|
||||
refresh_upon_start: bool = True,
|
||||
):
|
||||
super().__init__(client, config, gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Set up the toolbar
|
||||
self.set_toolbar()
|
||||
|
||||
# Set up the table
|
||||
self.table = QTableWidget(self)
|
||||
self.layout.addWidget(self.table)
|
||||
# self.layout.addWidget(self.table)
|
||||
self.table.setColumnCount(4)
|
||||
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status", "Cancel"])
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||
|
||||
self.addWidget(self.table)
|
||||
self.label = "BEC Queue"
|
||||
self.tooltip = "BEC Queue status"
|
||||
|
||||
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
|
||||
self.reset_content()
|
||||
if refresh_upon_start:
|
||||
@@ -78,7 +83,7 @@ class BECQueue(BECWidget, QWidget):
|
||||
target_widget=self,
|
||||
)
|
||||
|
||||
self.layout.addWidget(self.toolbar)
|
||||
self.addWidget(self.toolbar)
|
||||
|
||||
@Property(bool)
|
||||
def hide_toolbar(self):
|
||||
@@ -109,6 +114,9 @@ class BECQueue(BECWidget, QWidget):
|
||||
Refresh the queue.
|
||||
"""
|
||||
msg = self.client.connector.get(MessageEndpoints.scan_queue_status())
|
||||
if msg is None:
|
||||
# msg is None if no scan has been run yet (fresh start)
|
||||
return
|
||||
self.update_queue(msg.content, msg.metadata)
|
||||
|
||||
@Slot(dict, dict)
|
||||
@@ -152,6 +160,13 @@ class BECQueue(BECWidget, QWidget):
|
||||
if scan_ids:
|
||||
scan_ids = ", ".join(scan_ids)
|
||||
self.set_row(index, scan_numbers, scan_types, status, scan_ids)
|
||||
busy = (
|
||||
False
|
||||
if all(item.get("status") in ("STOPPED", "COMPLETED", "IDLE") for item in queue_info)
|
||||
else True
|
||||
)
|
||||
self.set_global_state("warning" if busy else "default")
|
||||
self.queue_busy.emit(busy)
|
||||
|
||||
def format_item(self, content: str, status=False) -> QTableWidgetItem:
|
||||
"""
|
||||
|
||||
@@ -13,8 +13,8 @@ from bec_lib.utils.import_utils import lazy_import_from
|
||||
from qtpy.QtCore import QObject, QTimer, Signal, Slot
|
||||
from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.bec_status_box.status_item import StatusItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -64,7 +64,7 @@ class BECServiceStatusMixin(QObject):
|
||||
self._service_update_timer.deleteLater()
|
||||
|
||||
|
||||
class BECStatusBox(BECWidget, QWidget):
|
||||
class BECStatusBox(BECWidget, CompactPopupWidget):
|
||||
"""An autonomous widget to display the status of BEC services.
|
||||
|
||||
Args:
|
||||
@@ -83,15 +83,13 @@ class BECStatusBox(BECWidget, QWidget):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
box_name: str = "BEC Server",
|
||||
box_name: str = "BEC Servers",
|
||||
client: BECClient = None,
|
||||
bec_service_status_mixin: BECServiceStatusMixin = None,
|
||||
gui_id: str = None,
|
||||
):
|
||||
super().__init__(client=client, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.tree = QTreeWidget(self)
|
||||
self.layout = QHBoxLayout(self)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QHBoxLayout)
|
||||
|
||||
self.box_name = box_name
|
||||
self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None})
|
||||
@@ -100,11 +98,13 @@ class BECStatusBox(BECWidget, QWidget):
|
||||
bec_service_status_mixin = BECServiceStatusMixin(self, client=self.client)
|
||||
self.bec_service_status = bec_service_status_mixin
|
||||
|
||||
self.label = box_name
|
||||
self.tooltip = "BEC servers health status"
|
||||
self.init_ui()
|
||||
self.bec_service_status.services_update.connect(self.update_service_status)
|
||||
self.bec_core_state.connect(self.update_top_item_status)
|
||||
self.tree.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
|
||||
self.layout.addWidget(self.tree)
|
||||
self.addWidget(self.tree)
|
||||
|
||||
def init_ui(self) -> None:
|
||||
"""Init the UI for the BECStatusBox widget, should only take place once."""
|
||||
@@ -121,6 +121,7 @@ class BECStatusBox(BECWidget, QWidget):
|
||||
|
||||
def init_ui_tree_widget(self) -> None:
|
||||
"""Initialise the tree widget for the status box."""
|
||||
self.tree = QTreeWidget(self)
|
||||
self.tree.setHeaderHidden(True)
|
||||
# TODO probably here is a problem still with setting the stylesheet
|
||||
self.tree.setStyleSheet(
|
||||
@@ -163,6 +164,7 @@ class BECStatusBox(BECWidget, QWidget):
|
||||
status (BECStatus): The state of the core services.
|
||||
"""
|
||||
self.status_container[self.box_name]["info"].status = status
|
||||
self.set_global_state("emergency" if status == "NOTCONNECTED" else "success")
|
||||
self.service_update.emit(self.status_container[self.box_name]["info"])
|
||||
|
||||
def _update_status_container(
|
||||
@@ -303,17 +305,15 @@ class BECStatusBox(BECWidget, QWidget):
|
||||
return super().cleanup()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main method to run the BECStatusBox widget."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("dark")
|
||||
set_theme("dark")
|
||||
main_window = BECStatusBox()
|
||||
main_window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -6,11 +6,13 @@ import os
|
||||
from datetime import datetime
|
||||
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Qt, Slot
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtGui import QIcon, QPainter
|
||||
from qtpy.QtWidgets import QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
|
||||
# TODO : Put normal imports back when Pydantic gets faster
|
||||
BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
|
||||
@@ -21,11 +23,11 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
class IconsEnum(enum.Enum):
|
||||
"""Enum class for icons in the status item widget."""
|
||||
|
||||
RUNNING = os.path.join(MODULE_PATH, "assets", "status_icons", "running.svg")
|
||||
BUSY = os.path.join(MODULE_PATH, "assets", "status_icons", "refresh.svg")
|
||||
IDLE = os.path.join(MODULE_PATH, "assets", "status_icons", "warning.svg")
|
||||
ERROR = os.path.join(MODULE_PATH, "assets", "status_icons", "error.svg")
|
||||
NOTCONNECTED = os.path.join(MODULE_PATH, "assets", "status_icons", "not_connected.svg")
|
||||
RUNNING = "done_outline"
|
||||
BUSY = "progress_activity"
|
||||
IDLE = "progress_activity"
|
||||
ERROR = "emergency_home"
|
||||
NOTCONNECTED = "signal_disconnected"
|
||||
|
||||
|
||||
class StatusItem(QWidget):
|
||||
@@ -43,13 +45,13 @@ class StatusItem(QWidget):
|
||||
raise ValueError(
|
||||
"Please initialize the StatusItem with a BECServiceInfoContainer for config, received None."
|
||||
)
|
||||
self.accent_colors = get_accent_colors()
|
||||
self.config = config
|
||||
self.parent = parent
|
||||
self.layout = None
|
||||
self._label = None
|
||||
self._icon = None
|
||||
self.icon_size = (24, 24)
|
||||
|
||||
self._popup_label_ref = {}
|
||||
self.init_ui()
|
||||
|
||||
@@ -97,8 +99,15 @@ class StatusItem(QWidget):
|
||||
|
||||
def set_status(self) -> None:
|
||||
"""Set the status icon for the status item widget."""
|
||||
icon_path = IconsEnum[self.config.status].value
|
||||
icon = QIcon(icon_path)
|
||||
status = self.config.status
|
||||
if status in ["RUNNING", "BUSY"]:
|
||||
color = self.accent_colors.success
|
||||
elif status == "IDLE":
|
||||
color = self.accent_colors.warning
|
||||
elif status in ["ERROR", "NOTCONNECTED"]:
|
||||
color = self.accent_colors.emergency
|
||||
icon = QIcon(material_icon(IconsEnum[status].value, filled=True, color=color))
|
||||
|
||||
self._icon.setPixmap(icon.pixmap(*self.icon_size))
|
||||
self._icon.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
@@ -132,6 +141,7 @@ class StatusItem(QWidget):
|
||||
metrics_text = (
|
||||
f"<b>SERVICE:</b> {self.config.service_name}<br><b>STATUS:</b> {self.config.status}<br>"
|
||||
)
|
||||
metrics_text += f"<b>BEC_LIB VERSION:</b> {self.config.info['version']}<br>"
|
||||
if self.config.metrics:
|
||||
for key, value in self.config.metrics.items():
|
||||
if key == "create_time":
|
||||
|
||||
0
bec_widgets/widgets/colormap_widget/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
{'files': ['colormap_widget.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.colormap_widget.colormap_widget import BECColorMapWidget
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECColorMapWidget' name='bec_color_map_widget'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class BECColorMapWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = BECColorMapWidget(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Buttons"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(BECColorMapWidget.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_color_map_widget"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "BECColorMapWidget"
|
||||
|
||||
def toolTip(self):
|
||||
return "BECColorMapWidget"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
73
bec_widgets/widgets/colormap_widget/colormap_widget.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from pyqtgraph.widgets.ColorMapButton import ColorMapButton
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
|
||||
class BECColorMapWidget(BECWidget, QWidget):
|
||||
colormap_changed_signal = Signal(str)
|
||||
ICON_NAME = "palette"
|
||||
USER_ACCESS = ["colormap"]
|
||||
|
||||
def __init__(self, parent=None, cmap: str = "magma"):
|
||||
super().__init__()
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
# Create the ColorMapButton
|
||||
self.button = ColorMapButton()
|
||||
|
||||
# Set the size policy and minimum width
|
||||
size_policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
|
||||
self.button.setSizePolicy(size_policy)
|
||||
self.button.setMinimumWidth(100)
|
||||
self.button.setMinimumHeight(30)
|
||||
|
||||
# Create the layout
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.button)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Set the initial colormap
|
||||
self.button.setColorMap(cmap)
|
||||
self._cmap = cmap
|
||||
|
||||
# Connect the signal
|
||||
self.button.sigColorMapChanged.connect(self.colormap_changed)
|
||||
|
||||
@Property(str)
|
||||
def colormap(self):
|
||||
"""Get the current colormap name."""
|
||||
return self._cmap
|
||||
|
||||
@colormap.setter
|
||||
def colormap(self, name):
|
||||
"""Set the colormap by name."""
|
||||
if self._cmap != name:
|
||||
if Colors.validate_color_map(name, return_error=False) is False:
|
||||
return
|
||||
self.button.setColorMap(name)
|
||||
self._cmap = name
|
||||
self.colormap_changed_signal.emit(name)
|
||||
|
||||
@Slot()
|
||||
def colormap_changed(self):
|
||||
"""
|
||||
Emit the colormap changed signal with the current colormap selected in the button.
|
||||
"""
|
||||
cmap = self.button.colorMap().name
|
||||
self._cmap = cmap
|
||||
self.colormap_changed_signal.emit(cmap)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
window = BECColorMapWidget()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -0,0 +1,17 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.colormap_widget.bec_color_map_widget_plugin import (
|
||||
BECColorMapWidgetPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECColorMapWidgetPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,24 +1,24 @@
|
||||
"""
|
||||
BECConsole is a Qt widget that runs a Bash shell. The widget can be used and
|
||||
embedded like any other Qt widget.
|
||||
BECConsole is a Qt widget that runs a Bash shell.
|
||||
|
||||
BECConsole is powered by Pyte, a Python based terminal emulator
|
||||
BECConsole VT100 emulation is powered by Pyte,
|
||||
(https://github.com/selectel/pyte).
|
||||
"""
|
||||
|
||||
import collections
|
||||
import fcntl
|
||||
import html
|
||||
import os
|
||||
import pty
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import pyte
|
||||
from pyte.screens import History
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
from qtpy.QtCore import QSize, QSocketNotifier, Qt
|
||||
from qtpy.QtCore import Property as pyqtProperty
|
||||
from qtpy.QtCore import QSize, QSocketNotifier, Qt, QTimer
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtGui import QClipboard, QTextCursor
|
||||
from qtpy.QtGui import QClipboard, QColor, QPalette, QTextCursor
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
@@ -137,13 +137,52 @@ def QtKeyToAscii(event):
|
||||
|
||||
|
||||
class Screen(pyte.HistoryScreen):
|
||||
def __init__(self, stdin_fd, numColumns, numLines, historyLength):
|
||||
super().__init__(numColumns, numLines, historyLength, ratio=1 / numLines)
|
||||
def __init__(self, stdin_fd, cols, rows, historyLength):
|
||||
super().__init__(cols, rows, historyLength, ratio=1 / rows)
|
||||
self._fd = stdin_fd
|
||||
|
||||
def write_process_input(self, data):
|
||||
"""Response to CPR request for example"""
|
||||
os.write(self._fd, data.encode("utf-8"))
|
||||
"""Response to CPR request (for example),
|
||||
this can be for other requests
|
||||
"""
|
||||
try:
|
||||
os.write(self._fd, data.encode("utf-8"))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def resize(self, lines, columns):
|
||||
lines = lines or self.lines
|
||||
columns = columns or self.columns
|
||||
|
||||
if lines == self.lines and columns == self.columns:
|
||||
return # No changes.
|
||||
|
||||
self.dirty.clear()
|
||||
self.dirty.update(range(lines))
|
||||
|
||||
self.save_cursor()
|
||||
if lines < self.lines:
|
||||
if lines <= self.cursor.y:
|
||||
nlines_to_move_up = self.lines - lines
|
||||
for i in range(nlines_to_move_up):
|
||||
line = self.buffer[i] # .pop(0)
|
||||
self.history.top.append(line)
|
||||
self.cursor_position(0, 0)
|
||||
self.delete_lines(nlines_to_move_up)
|
||||
self.restore_cursor()
|
||||
self.cursor.y -= nlines_to_move_up
|
||||
else:
|
||||
self.restore_cursor()
|
||||
|
||||
self.lines, self.columns = lines, columns
|
||||
self.history = History(
|
||||
self.history.top,
|
||||
self.history.bottom,
|
||||
1 / self.lines,
|
||||
self.history.size,
|
||||
self.history.position,
|
||||
)
|
||||
self.set_margins()
|
||||
|
||||
|
||||
class Backend(QtCore.QObject):
|
||||
@@ -155,17 +194,17 @@ class Backend(QtCore.QObject):
|
||||
"""
|
||||
|
||||
# Signals to communicate with ``_TerminalWidget``.
|
||||
startWork = pyqtSignal()
|
||||
dataReady = pyqtSignal(object)
|
||||
processExited = pyqtSignal()
|
||||
|
||||
def __init__(self, fd, numColumns, numLines):
|
||||
def __init__(self, fd, cols, rows):
|
||||
super().__init__()
|
||||
|
||||
# File descriptor that connects to Bash process.
|
||||
self.fd = fd
|
||||
|
||||
# Setup Pyte (hard coded display size for now).
|
||||
self.screen = Screen(self.fd, numColumns, numLines, 10000)
|
||||
self.screen = Screen(self.fd, cols, rows, 10000)
|
||||
self.stream = pyte.ByteStream()
|
||||
self.stream.attach(self.screen)
|
||||
|
||||
@@ -174,12 +213,14 @@ class Backend(QtCore.QObject):
|
||||
|
||||
def _fd_readable(self):
|
||||
"""
|
||||
Poll the Bash output, run it through Pyte, and notify the main applet.
|
||||
Poll the Bash output, run it through Pyte, and notify
|
||||
"""
|
||||
# Read the shell output until the file descriptor is closed.
|
||||
try:
|
||||
out = os.read(self.fd, 2**16)
|
||||
except OSError:
|
||||
self.processExited.emit()
|
||||
self.notifier.setEnabled(False)
|
||||
return
|
||||
|
||||
# Feed output into Pyte's state machine and send the new screen
|
||||
@@ -188,93 +229,256 @@ class Backend(QtCore.QObject):
|
||||
self.dataReady.emit(self.screen)
|
||||
|
||||
|
||||
class BECConsole(QtWidgets.QScrollArea):
|
||||
class BECConsole(QtWidgets.QWidget):
|
||||
"""Container widget for the terminal text area"""
|
||||
|
||||
def __init__(self, parent=None, numLines=50, numColumns=125):
|
||||
ICON_NAME = "terminal"
|
||||
|
||||
def __init__(self, parent=None, cols=132):
|
||||
super().__init__(parent)
|
||||
|
||||
self.innerWidget = QtWidgets.QWidget(self)
|
||||
QHBoxLayout(self.innerWidget)
|
||||
self.innerWidget.layout().setContentsMargins(0, 0, 0, 0)
|
||||
self.term = _TerminalWidget(self, cols, rows=43)
|
||||
self.scroll_bar = QScrollBar(Qt.Vertical, self)
|
||||
# self.scroll_bar.hide()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.addWidget(self.term)
|
||||
layout.addWidget(self.scroll_bar)
|
||||
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||
|
||||
self.term = _TerminalWidget(self.innerWidget, numLines, numColumns)
|
||||
self.term.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.innerWidget.layout().addWidget(self.term)
|
||||
pal = QPalette()
|
||||
self.set_bgcolor(pal.window().color())
|
||||
self.set_fgcolor(pal.windowText().color())
|
||||
self.term.set_scroll_bar(self.scroll_bar)
|
||||
self.set_cmd("bec --nogui")
|
||||
|
||||
self.scroll_bar = QScrollBar(Qt.Vertical, self.term)
|
||||
self.innerWidget.layout().addWidget(self.scroll_bar)
|
||||
self._check_designer_timer = QTimer()
|
||||
self._check_designer_timer.timeout.connect(self.check_designer)
|
||||
self._check_designer_timer.start(1000)
|
||||
|
||||
self.term.set_scroll(self.scroll_bar)
|
||||
def minimumSizeHint(self):
|
||||
size = self.term.sizeHint()
|
||||
size.setWidth(size.width() + self.scroll_bar.width())
|
||||
return size
|
||||
|
||||
self.setWidget(self.innerWidget)
|
||||
def sizeHint(self):
|
||||
return self.minimumSizeHint()
|
||||
|
||||
def start(self, cmd=["bec", "--nogui"], deactivate_ctrl_d=True):
|
||||
def check_designer(self, calls={"n": 0}):
|
||||
calls["n"] += 1
|
||||
if self.term.fd is not None:
|
||||
# already started
|
||||
self._check_designer_timer.stop()
|
||||
elif self.window().windowTitle().endswith("[Preview]"):
|
||||
# assuming Designer preview -> start
|
||||
self._check_designer_timer.stop()
|
||||
self.term.start()
|
||||
elif calls["n"] >= 3:
|
||||
# assuming not in Designer -> stop checking
|
||||
self._check_designer_timer.stop()
|
||||
|
||||
def get_rows(self):
|
||||
return self.term.rows
|
||||
|
||||
def set_rows(self, rows):
|
||||
self.term.rows = rows
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
|
||||
def get_cols(self):
|
||||
return self.term.cols
|
||||
|
||||
def set_cols(self, cols):
|
||||
self.term.cols = cols
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
|
||||
def get_bgcolor(self):
|
||||
return QColor.fromString(self.term.bg_color)
|
||||
|
||||
def set_bgcolor(self, color):
|
||||
self.term.bg_color = color.name(QColor.HexRgb)
|
||||
|
||||
def get_fgcolor(self):
|
||||
return QColor.fromString(self.term.fg_color)
|
||||
|
||||
def set_fgcolor(self, color):
|
||||
self.term.fg_color = color.name(QColor.HexRgb)
|
||||
|
||||
def get_cmd(self):
|
||||
return self.term._cmd
|
||||
|
||||
def set_cmd(self, cmd):
|
||||
self.term._cmd = cmd
|
||||
if self.term.fd is None:
|
||||
# not started yet
|
||||
self.term.clear()
|
||||
self.term.appendHtml(f"<h2>BEC Console - {repr(cmd)}</h2>")
|
||||
|
||||
def start(self, deactivate_ctrl_d=True):
|
||||
self.term.start(deactivate_ctrl_d=deactivate_ctrl_d)
|
||||
|
||||
def push(self, text):
|
||||
"""Push some text to the terminal"""
|
||||
return self.term.push(text)
|
||||
|
||||
cols = pyqtProperty(int, get_cols, set_cols)
|
||||
rows = pyqtProperty(int, get_rows, set_rows)
|
||||
bgcolor = pyqtProperty(QColor, get_bgcolor, set_bgcolor)
|
||||
fgcolor = pyqtProperty(QColor, get_fgcolor, set_fgcolor)
|
||||
cmd = pyqtProperty(str, get_cmd, set_cmd)
|
||||
|
||||
|
||||
class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
"""
|
||||
Start ``Backend`` process and render Pyte output as text.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
|
||||
super().__init__(parent)
|
||||
|
||||
def __init__(self, parent, cols=125, rows=50, **kwargs):
|
||||
# file descriptor to communicate with the subprocess
|
||||
self.fd = None
|
||||
self.backend = None
|
||||
self.lock = threading.Lock()
|
||||
# command to execute
|
||||
self._cmd = None
|
||||
self._cmd = ""
|
||||
# should ctrl-d be deactivated ? (prevent Python exit)
|
||||
self._deactivate_ctrl_d = False
|
||||
|
||||
# Default colors
|
||||
pal = QPalette()
|
||||
self._fg_color = pal.text().color().name()
|
||||
self._bg_color = pal.base().color().name()
|
||||
|
||||
# Specify the terminal size in terms of lines and columns.
|
||||
self.numLines = numLines
|
||||
self.numColumns = numColumns
|
||||
self.output = [""] * numLines
|
||||
self._rows = rows
|
||||
self._cols = cols
|
||||
self.output = collections.deque()
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding)
|
||||
|
||||
# Disable default scrollbars (we use our own, to be set via .set_scroll_bar())
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.scroll_bar = None
|
||||
|
||||
# Use Monospace fonts and disable line wrapping.
|
||||
self.setFont(QtGui.QFont("Courier", 9))
|
||||
self.setFont(QtGui.QFont("Monospace"))
|
||||
self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
|
||||
|
||||
# Disable vertical scrollbar (we use our own, to be set via .set_scroll())
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
|
||||
fmt = QtGui.QFontMetrics(self.font())
|
||||
self._char_width = fmt.width("w")
|
||||
self._char_height = fmt.height()
|
||||
self.setCursorWidth(self._char_width)
|
||||
# self.setStyleSheet("QPlainTextEdit { color: #ffff00; background-color: #303030; } ");
|
||||
char_width = fmt.width("w")
|
||||
self.setCursorWidth(char_width)
|
||||
|
||||
def start(self, deactivate_ctrl_d=False):
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
self.update_stylesheet()
|
||||
|
||||
@property
|
||||
def bg_color(self):
|
||||
return self._bg_color
|
||||
|
||||
@bg_color.setter
|
||||
def bg_color(self, hexcolor):
|
||||
self._bg_color = hexcolor
|
||||
self.update_stylesheet()
|
||||
|
||||
@property
|
||||
def fg_color(self):
|
||||
return self._fg_color
|
||||
|
||||
@fg_color.setter
|
||||
def fg_color(self, hexcolor):
|
||||
self._fg_color = hexcolor
|
||||
self.update_stylesheet()
|
||||
|
||||
def update_stylesheet(self):
|
||||
self.setStyleSheet(
|
||||
f"QPlainTextEdit {{ border: 0; color: {self._fg_color}; background-color: {self._bg_color}; }} "
|
||||
)
|
||||
|
||||
@property
|
||||
def rows(self):
|
||||
return self._rows
|
||||
|
||||
@rows.setter
|
||||
def rows(self, rows: int):
|
||||
if self.backend is None:
|
||||
# not initialized yet, ok to change
|
||||
self._rows = rows
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
else:
|
||||
raise RuntimeError("Cannot change rows after console is started.")
|
||||
|
||||
@property
|
||||
def cols(self):
|
||||
return self._cols
|
||||
|
||||
@cols.setter
|
||||
def cols(self, cols: int):
|
||||
if self.fd is None:
|
||||
# not initialized yet, ok to change
|
||||
self._cols = cols
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
else:
|
||||
raise RuntimeError("Cannot change cols after console is started.")
|
||||
|
||||
def start(self, deactivate_ctrl_d: bool = False):
|
||||
self._deactivate_ctrl_d = deactivate_ctrl_d
|
||||
|
||||
# Start the Bash process
|
||||
self.fd = self.forkShell()
|
||||
self.update_term_size()
|
||||
|
||||
# Create the ``Backend`` object
|
||||
self.backend = Backend(self.fd, self.numColumns, self.numLines)
|
||||
self.backend.dataReady.connect(self.dataReady)
|
||||
# Start the Bash process
|
||||
self.fd = self.fork_shell()
|
||||
|
||||
if self.fd:
|
||||
# Create the ``Backend`` object
|
||||
self.backend = Backend(self.fd, self.cols, self.rows)
|
||||
self.backend.dataReady.connect(self.data_ready)
|
||||
self.backend.processExited.connect(self.process_exited)
|
||||
else:
|
||||
self.process_exited()
|
||||
|
||||
def process_exited(self):
|
||||
self.fd = None
|
||||
self.clear()
|
||||
self.appendHtml(f"<br><h2>{repr(self._cmd)} - Process exited.</h2>")
|
||||
self.setReadOnly(True)
|
||||
|
||||
def data_ready(self, screen):
|
||||
"""Handle new screen: redraw, set scroll bar max and slider, move cursor to its position
|
||||
|
||||
This method is triggered via a signal from ``Backend``.
|
||||
"""
|
||||
self.redraw_screen()
|
||||
self.adjust_scroll_bar()
|
||||
self.move_cursor()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
width = self._char_width * self.numColumns
|
||||
height = self._char_height * self.numLines
|
||||
return QSize(width, height + 20)
|
||||
"""Return minimum size for current cols and rows"""
|
||||
fmt = QtGui.QFontMetrics(self.font())
|
||||
char_width = fmt.width("w")
|
||||
char_height = fmt.height()
|
||||
width = char_width * self.cols
|
||||
height = char_height * self.rows
|
||||
return QSize(width, height)
|
||||
|
||||
def set_scroll(self, scroll):
|
||||
self.scroll = scroll
|
||||
self.scroll.setMinimum(0)
|
||||
self.scroll.valueChanged.connect(self.scroll_value_change)
|
||||
def sizeHint(self):
|
||||
return self.minimumSizeHint()
|
||||
|
||||
def scroll_value_change(self, value, old={"value": 0}):
|
||||
def set_scroll_bar(self, scroll_bar):
|
||||
self.scroll_bar = scroll_bar
|
||||
self.scroll_bar.setMinimum(0)
|
||||
self.scroll_bar.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
def scroll_value_change(self, value, old={"value": -1}):
|
||||
if self.backend is None:
|
||||
return
|
||||
if old["value"] == -1:
|
||||
old["value"] = self.scroll_bar.maximum()
|
||||
if value <= old["value"]:
|
||||
# scroll up
|
||||
# value is number of lines from the start
|
||||
@@ -288,13 +492,35 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
for i in range(nlines):
|
||||
self.backend.screen.next_page()
|
||||
old["value"] = value
|
||||
self.dataReady(self.backend.screen, reset_scroll=False)
|
||||
self.redraw_screen()
|
||||
|
||||
def adjust_scroll_bar(self):
|
||||
sb = self.scroll_bar
|
||||
sb.valueChanged.disconnect(self.scroll_value_change)
|
||||
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
||||
sb.setMaximum(tmp if tmp > 0 else 0)
|
||||
sb.setSliderPosition(tmp if tmp > 0 else 0)
|
||||
# if tmp > 0:
|
||||
# # show scrollbar, but delayed - prevent recursion with widget size change
|
||||
# QTimer.singleShot(0, scrollbar.show)
|
||||
# else:
|
||||
# QTimer.singleShot(0, scrollbar.hide)
|
||||
sb.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
def write(self, data):
|
||||
try:
|
||||
os.write(self.fd, data)
|
||||
except (IOError, OSError):
|
||||
self.process_exited()
|
||||
|
||||
@Slot(object)
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Redirect all keystrokes to the terminal process.
|
||||
"""
|
||||
if self.fd is None:
|
||||
# not started
|
||||
return
|
||||
# Convert the Qt key to the correct ASCII code.
|
||||
if (
|
||||
self._deactivate_ctrl_d
|
||||
@@ -311,15 +537,17 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
# MacOS only: CMD-V handling
|
||||
self._push_clipboard()
|
||||
elif code is not None:
|
||||
os.write(self.fd, code)
|
||||
self.write(code)
|
||||
|
||||
def push(self, text):
|
||||
"""
|
||||
Write 'text' to terminal
|
||||
"""
|
||||
os.write(self.fd, text.encode("utf-8"))
|
||||
self.write(text.encode("utf-8"))
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
if self.fd is None:
|
||||
return
|
||||
menu = self.createStandardContextMenu()
|
||||
for action in menu.actions():
|
||||
# remove all actions except copy and paste
|
||||
@@ -341,7 +569,20 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
clipboard = QApplication.instance().clipboard()
|
||||
self.push(clipboard.text())
|
||||
|
||||
def move_cursor(self):
|
||||
textCursor = self.textCursor()
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
||||
)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
||||
)
|
||||
self.setTextCursor(textCursor)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.fd is None:
|
||||
return
|
||||
if event.button() == Qt.MiddleButton:
|
||||
# push primary selection buffer ("mouse clipboard") to terminal
|
||||
clipboard = QApplication.instance().clipboard()
|
||||
@@ -355,34 +596,34 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
# mouse was used to select text -> nothing to do
|
||||
pass
|
||||
else:
|
||||
# a simple 'click', make cursor going to end
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
||||
)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
||||
)
|
||||
self.setTextCursor(textCursor)
|
||||
self.ensureCursorVisible()
|
||||
# a simple 'click', move scrollbar to end
|
||||
self.scroll_bar.setSliderPosition(self.scroll_bar.maximum())
|
||||
self.move_cursor()
|
||||
return None
|
||||
return super().mouseReleaseEvent(event)
|
||||
|
||||
def dataReady(self, screenData, reset_scroll=True):
|
||||
def redraw_screen(self):
|
||||
"""
|
||||
Render the new screen as text into the widget.
|
||||
Render the screen as formatted text into the widget.
|
||||
"""
|
||||
screen = self.backend.screen
|
||||
|
||||
This method is triggered via a signal from ``Backend``.
|
||||
"""
|
||||
with self.lock:
|
||||
# Clear the widget
|
||||
# Clear the widget
|
||||
if screen.dirty:
|
||||
self.clear()
|
||||
while len(self.output) < (max(screen.dirty) + 1):
|
||||
self.output.append("")
|
||||
while len(self.output) > (max(screen.dirty) + 1):
|
||||
self.output.pop()
|
||||
|
||||
# Prepare the HTML output
|
||||
for line_no in screenData.dirty:
|
||||
for line_no in screen.dirty:
|
||||
line = text = ""
|
||||
style = old_style = ""
|
||||
for ch in screenData.buffer[line_no].values():
|
||||
old_idx = 0
|
||||
for idx, ch in screen.buffer[line_no].items():
|
||||
text += " " * (idx - old_idx - 1)
|
||||
old_idx = idx
|
||||
style = f"{'background-color:%s;' % ansi_colors.get(ch.bg, ansi_colors['black']) if ch.bg!='default' else ''}{'color:%s;' % ansi_colors.get(ch.fg, ansi_colors['white']) if ch.fg!='default' else ''}{'font-weight:bold;' if ch.bold else ''}{'font-style:italic;' if ch.italics else ''}"
|
||||
if style != old_style:
|
||||
if old_style:
|
||||
@@ -396,45 +637,47 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
line += f"<span style={repr(style)}>{html.escape(text, quote=True)}</span>"
|
||||
else:
|
||||
line += html.escape(text, quote=True)
|
||||
# do a check at the cursor position:
|
||||
# it is possible x pos > output line length,
|
||||
# for example if last escape codes are "cursor forward" past end of text,
|
||||
# like IPython does for "..." prompt (in a block, like "for" loop or "while" for example)
|
||||
# In this case, cursor is at 12 but last text output is at 8 -> insert spaces
|
||||
if line_no == screen.cursor.y:
|
||||
llen = len(screen.buffer[line_no])
|
||||
if llen < screen.cursor.x:
|
||||
line += " " * (screen.cursor.x - llen)
|
||||
self.output[line_no] = line
|
||||
# fill the text area with HTML contents in one go
|
||||
self.appendHtml(f"<pre>{chr(10).join(self.output)}</pre>")
|
||||
# done updates, all clean
|
||||
screenData.dirty.clear()
|
||||
# did updates, all clean
|
||||
screen.dirty.clear()
|
||||
|
||||
# Activate cursor
|
||||
textCursor = self.textCursor()
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(QTextCursor.Down, QTextCursor.MoveAnchor, screenData.cursor.y)
|
||||
textCursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, screenData.cursor.x)
|
||||
self.setTextCursor(textCursor)
|
||||
self.ensureCursorVisible()
|
||||
def update_term_size(self):
|
||||
fmt = QtGui.QFontMetrics(self.font())
|
||||
char_width = fmt.width("w")
|
||||
char_height = fmt.height()
|
||||
self._cols = int(self.width() / char_width)
|
||||
self._rows = int(self.height() / char_height)
|
||||
|
||||
# manage scroll
|
||||
if reset_scroll:
|
||||
self.scroll.valueChanged.disconnect(self.scroll_value_change)
|
||||
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
||||
self.scroll.setMaximum(tmp if tmp > 0 else 0)
|
||||
self.scroll.setSliderPosition(len(self.backend.screen.history.top))
|
||||
self.scroll.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
# def resizeEvent(self, event):
|
||||
# with self.lock:
|
||||
# self.numColumns = int(self.width() / self._char_width)
|
||||
# self.numLines = int(self.height() / self._char_height)
|
||||
# self.output = [""] * self.numLines
|
||||
# print("RESIZING TO", self.numColumns, "x", self.numLines)
|
||||
# self.backend.screen.resize(self.numLines, self.numColumns)
|
||||
def resizeEvent(self, event):
|
||||
self.update_term_size()
|
||||
if self.fd:
|
||||
self.backend.screen.resize(self._rows, self._cols)
|
||||
self.redraw_screen()
|
||||
self.adjust_scroll_bar()
|
||||
self.move_cursor()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if not self.fd:
|
||||
return
|
||||
y = event.angleDelta().y()
|
||||
if y > 0:
|
||||
self.backend.screen.prev_page()
|
||||
else:
|
||||
self.backend.screen.next_page()
|
||||
self.dataReady(self.backend.screen, reset_scroll=False)
|
||||
self.redraw_screen()
|
||||
|
||||
def forkShell(self):
|
||||
def fork_shell(self):
|
||||
"""
|
||||
Fork the current process and execute bec in shell.
|
||||
"""
|
||||
@@ -443,32 +686,30 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
except (IOError, OSError):
|
||||
return False
|
||||
if pid == 0:
|
||||
# Safe way to make it work under BSD and Linux
|
||||
try:
|
||||
ls = os.environ["LANG"].split(".")
|
||||
except KeyError:
|
||||
ls = []
|
||||
if len(ls) < 2:
|
||||
ls = ["en_US", "UTF-8"]
|
||||
os.putenv("COLUMNS", str(self.cols))
|
||||
os.putenv("LINES", str(self.rows))
|
||||
os.putenv("TERM", "linux")
|
||||
os.putenv("LANG", ls[0] + ".UTF-8")
|
||||
if not self._cmd:
|
||||
self._cmd = os.environ["SHELL"]
|
||||
cmd = self._cmd
|
||||
if isinstance(cmd, str):
|
||||
cmd = cmd.split()
|
||||
try:
|
||||
os.putenv("COLUMNS", str(self.numColumns))
|
||||
os.putenv("LINES", str(self.numLines))
|
||||
os.putenv("TERM", "linux")
|
||||
os.putenv("LANG", ls[0] + ".UTF-8")
|
||||
if isinstance(self._cmd, str):
|
||||
os.execvp(self._cmd, self._cmd)
|
||||
else:
|
||||
os.execvp(self._cmd[0], self._cmd)
|
||||
# print "child_pid", child_pid, sts
|
||||
os.execvp(cmd[0], cmd)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
# self.proc_finish(sid)
|
||||
os._exit(0)
|
||||
else:
|
||||
# We are in the parent process.
|
||||
# Set file control
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
print("Spawned Bash shell (PID {})".format(pid))
|
||||
return fd
|
||||
|
||||
|
||||
@@ -478,17 +719,13 @@ if __name__ == "__main__":
|
||||
|
||||
from qtpy import QtGui, QtWidgets
|
||||
|
||||
# Terminal size in characters.
|
||||
numLines = 25
|
||||
numColumns = 100
|
||||
|
||||
# Create the Qt application and QBash instance.
|
||||
# Create the Qt application and console.
|
||||
app = QtWidgets.QApplication([])
|
||||
mainwin = QtWidgets.QMainWindow()
|
||||
title = "BECConsole ({}x{})".format(numColumns, numLines)
|
||||
title = "BECConsole"
|
||||
mainwin.setWindowTitle(title)
|
||||
|
||||
console = BECConsole(mainwin, numColumns, numLines)
|
||||
console = BECConsole(mainwin)
|
||||
mainwin.setCentralWidget(console)
|
||||
console.start()
|
||||
|
||||
|
||||
1
bec_widgets/widgets/console/console.pyproject
Normal file
@@ -0,0 +1 @@
|
||||
{'files': ['console.py']}
|
||||
58
bec_widgets/widgets/console/console_plugin.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.console.console import BECConsole
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECConsole' name='bec_console'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class BECConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = BECConsole(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Console"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(BECConsole.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_console"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "BECConsole"
|
||||
|
||||
def toolTip(self):
|
||||
return "A terminal-like vt100 widget."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
15
bec_widgets/widgets/console/register_console.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.console.console_plugin import BECConsolePlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECConsolePlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -124,6 +124,7 @@ class DapComboBox(BECWidget, QWidget):
|
||||
x_axis(str): X axis.
|
||||
"""
|
||||
self.x_axis = x_axis
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_y_axis(self, y_axis: str):
|
||||
@@ -133,6 +134,7 @@ class DapComboBox(BECWidget, QWidget):
|
||||
y_axis(str): Y axis.
|
||||
"""
|
||||
self.y_axis = y_axis
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_fit_model(self, fit_name: str | None):
|
||||
@@ -165,21 +167,18 @@ class DapComboBox(BECWidget, QWidget):
|
||||
return True
|
||||
|
||||
|
||||
# pragma: no cover
|
||||
def main():
|
||||
"""Main function to run the DapComboBox widget."""
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("auto")
|
||||
widget = DapComboBox()
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(DapComboBox())
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
app.exec_()
|
||||
|
||||
@@ -27,7 +27,7 @@ class DapComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Selection Widgets"
|
||||
return "BEC Input Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(DapComboBox.ICON_NAME)
|
||||
|
||||
@@ -31,7 +31,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
return "BEC Input Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(DeviceComboBox.ICON_NAME)
|
||||
|
||||
@@ -1,86 +1,163 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from bec_lib.callback_handler import EventType
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from qtpy.QtCore import QSize, Signal, Slot
|
||||
from qtpy.QtGui import QPainter, QPaintEvent, QPen
|
||||
from qtpy.QtWidgets import QComboBox, QSizePolicy
|
||||
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
|
||||
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.widgets.base_classes.device_input_base import (
|
||||
BECDeviceFilter,
|
||||
DeviceInputBase,
|
||||
DeviceInputConfig,
|
||||
)
|
||||
|
||||
|
||||
class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
"""
|
||||
Line edit widget for device input with autocomplete for device names.
|
||||
Combobox widget for device input with autocomplete for device names.
|
||||
|
||||
Args:
|
||||
parent: Parent widget.
|
||||
client: BEC client object.
|
||||
config: Device input configuration.
|
||||
gui_id: GUI ID.
|
||||
device_filter: Device filter, name of the device class.
|
||||
device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
|
||||
default: Default device name.
|
||||
arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
|
||||
"""
|
||||
|
||||
ICON_NAME = "list_alt"
|
||||
|
||||
device_selected = Signal(str)
|
||||
device_config_update = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
client=None,
|
||||
config: DeviceInputConfig = None,
|
||||
gui_id: str | None = None,
|
||||
device_filter: str | None = None,
|
||||
device_filter: BECDeviceFilter | list[BECDeviceFilter] | None = None,
|
||||
readout_priority_filter: (
|
||||
str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
|
||||
) = None,
|
||||
available_devices: list[str] | None = None,
|
||||
default: str | None = None,
|
||||
arg_name: str | None = None,
|
||||
):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QComboBox.__init__(self, parent=parent)
|
||||
self.setMinimumSize(125, 26)
|
||||
self.populate_combobox()
|
||||
|
||||
if arg_name is not None:
|
||||
self.config.arg_name = arg_name
|
||||
self.arg_name = arg_name
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.setMinimumSize(QSize(100, 0))
|
||||
self._callback_id = None
|
||||
self._is_valid_input = False
|
||||
self._accent_colors = get_accent_colors()
|
||||
# We do not consider the config that is passed here, this produced problems
|
||||
# with QtDesigner, since config and input arguments may differ and resolve properly
|
||||
# Implementing this logic and config recoverage is postponed.
|
||||
# Set available devices if passed
|
||||
if available_devices is not None:
|
||||
self.set_available_devices(available_devices)
|
||||
# Set readout priority filter default is all
|
||||
if readout_priority_filter is not None:
|
||||
self.set_readout_priority_filter(readout_priority_filter)
|
||||
else:
|
||||
self.set_readout_priority_filter(
|
||||
[
|
||||
ReadoutPriority.MONITORED,
|
||||
ReadoutPriority.BASELINE,
|
||||
ReadoutPriority.ASYNC,
|
||||
ReadoutPriority.CONTINUOUS,
|
||||
ReadoutPriority.ON_REQUEST,
|
||||
]
|
||||
)
|
||||
# Device filter default is None
|
||||
if device_filter is not None:
|
||||
self.set_device_filter(device_filter)
|
||||
# Set default device if passed
|
||||
if default is not None:
|
||||
self.set_default_device(default)
|
||||
self.set_device(default)
|
||||
self._callback_id = self.bec_dispatcher.client.callbacks.register(
|
||||
EventType.DEVICE_UPDATE, self.on_device_update
|
||||
)
|
||||
self.device_config_update.connect(self.update_devices_from_filters)
|
||||
self.currentTextChanged.connect(self.check_validity)
|
||||
self.check_validity(self.currentText())
|
||||
|
||||
def set_device_filter(self, device_filter: str):
|
||||
def on_device_update(self, action: str, content: dict) -> None:
|
||||
"""
|
||||
Set the device filter.
|
||||
Callback for device update events. Triggers the device_update signal.
|
||||
|
||||
Args:
|
||||
device_filter(str): Device filter, name of the device class.
|
||||
action (str): The action that triggered the event.
|
||||
content (dict): The content of the config update.
|
||||
"""
|
||||
super().set_device_filter(device_filter)
|
||||
self.populate_combobox()
|
||||
if action in ["add", "remove", "reload"]:
|
||||
self.device_config_update.emit()
|
||||
|
||||
def set_default_device(self, default_device: str):
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
if self._callback_id is not None:
|
||||
self.bec_dispatcher.client.callbacks.remove(self._callback_id)
|
||||
|
||||
def get_current_device(self) -> object:
|
||||
"""
|
||||
Set the default device.
|
||||
|
||||
Args:
|
||||
default_device(str): Default device name.
|
||||
"""
|
||||
super().set_default_device(default_device)
|
||||
self.setCurrentText(default_device)
|
||||
|
||||
def populate_combobox(self):
|
||||
"""Populate the combobox with the devices."""
|
||||
self.devices = self.get_device_list(self.config.device_filter)
|
||||
self.clear()
|
||||
self.addItems(self.devices)
|
||||
|
||||
def get_device(self) -> object:
|
||||
"""
|
||||
Get the selected device object.
|
||||
Get the current device object based on the current value.
|
||||
|
||||
Returns:
|
||||
object: Device object.
|
||||
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
||||
"""
|
||||
device_name = self.currentText()
|
||||
device_obj = getattr(self.dev, device_name.lower(), None)
|
||||
if device_obj is None:
|
||||
raise ValueError(f"Device {device_name} is not found.")
|
||||
return device_obj
|
||||
dev_name = self.currentText()
|
||||
return self.get_device_object(dev_name)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None:
|
||||
"""Extend the paint event to set the border color based on the validity of the input.
|
||||
|
||||
Args:
|
||||
event (PySide6.QtGui.QPaintEvent) : Paint event.
|
||||
"""
|
||||
# logger.info(f"Received paint event: {event} in {self.__class__}")
|
||||
super().paintEvent(event)
|
||||
|
||||
if self._is_valid_input is False and self.isEnabled() is True:
|
||||
painter = QPainter(self)
|
||||
pen = QPen()
|
||||
pen.setWidth(2)
|
||||
pen.setColor(self._accent_colors.emergency)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
|
||||
painter.end()
|
||||
|
||||
@Slot(str)
|
||||
def check_validity(self, input_text: str) -> None:
|
||||
"""
|
||||
Check if the current value is a valid device name.
|
||||
"""
|
||||
if self.validate_device(input_text) is True:
|
||||
self._is_valid_input = True
|
||||
self.device_selected.emit(input_text.lower())
|
||||
else:
|
||||
self._is_valid_input = False
|
||||
self.update()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
combo = DeviceComboBox()
|
||||
combo.devices = ["samx", "dev1", "dev2", "dev3", "dev4"]
|
||||
layout.addWidget(combo)
|
||||
widget.show()
|
||||
app.exec_()
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from bec_lib.callback_handler import EventType
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QSize, Signal, Slot
|
||||
from qtpy.QtGui import QPainter, QPaintEvent, QPen
|
||||
from qtpy.QtWidgets import QApplication, QCompleter, QLineEdit, QSizePolicy
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.widgets.base_classes.device_input_base import (
|
||||
BECDeviceFilter,
|
||||
DeviceInputBase,
|
||||
DeviceInputConfig,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DeviceLineEdit(DeviceInputBase, QLineEdit):
|
||||
@@ -19,11 +24,14 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
|
||||
client: BEC client object.
|
||||
config: Device input configuration.
|
||||
gui_id: GUI ID.
|
||||
device_filter: Device filter, name of the device class.
|
||||
device_filter: Device filter, name of the device class from BECDeviceFilter and ReadoutPriority. Check DeviceInputBase for more details.
|
||||
default: Default device name.
|
||||
arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
|
||||
"""
|
||||
|
||||
device_selected = Signal(str)
|
||||
device_config_update = Signal()
|
||||
|
||||
ICON_NAME = "edit_note"
|
||||
|
||||
def __init__(
|
||||
@@ -32,68 +40,137 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
|
||||
client=None,
|
||||
config: DeviceInputConfig = None,
|
||||
gui_id: str | None = None,
|
||||
device_filter: str | list[str] | None = None,
|
||||
device_filter: BECDeviceFilter | list[BECDeviceFilter] | None = None,
|
||||
readout_priority_filter: (
|
||||
str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
|
||||
) = None,
|
||||
available_devices: list[str] | None = None,
|
||||
default: str | None = None,
|
||||
arg_name: str | None = None,
|
||||
):
|
||||
self._callback_id = None
|
||||
self._is_valid_input = False
|
||||
self._accent_colors = get_accent_colors()
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QLineEdit.__init__(self, parent=parent)
|
||||
|
||||
self.completer = QCompleter(self)
|
||||
self.setCompleter(self.completer)
|
||||
self.populate_completer()
|
||||
|
||||
if arg_name is not None:
|
||||
self.config.arg_name = arg_name
|
||||
self.arg_name = arg_name
|
||||
if device_filter is not None:
|
||||
self.set_device_filter(device_filter)
|
||||
if default is not None:
|
||||
self.set_default_device(default)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.setMinimumSize(QSize(100, 0))
|
||||
|
||||
def set_device_filter(self, device_filter: str | list[str]):
|
||||
# We do not consider the config that is passed here, this produced problems
|
||||
# with QtDesigner, since config and input arguments may differ and resolve properly
|
||||
# Implementing this logic and config recoverage is postponed.
|
||||
# Set available devices if passed
|
||||
if available_devices is not None:
|
||||
self.set_available_devices(available_devices)
|
||||
# Set readout priority filter default is all
|
||||
if readout_priority_filter is not None:
|
||||
self.set_readout_priority_filter(readout_priority_filter)
|
||||
else:
|
||||
self.set_readout_priority_filter(
|
||||
[
|
||||
ReadoutPriority.MONITORED,
|
||||
ReadoutPriority.BASELINE,
|
||||
ReadoutPriority.ASYNC,
|
||||
ReadoutPriority.CONTINUOUS,
|
||||
ReadoutPriority.ON_REQUEST,
|
||||
]
|
||||
)
|
||||
# Device filter default is None
|
||||
if device_filter is not None:
|
||||
self.set_device_filter(device_filter)
|
||||
# Set default device if passed
|
||||
if default is not None:
|
||||
self.set_device(default)
|
||||
self._callback_id = self.bec_dispatcher.client.callbacks.register(
|
||||
EventType.DEVICE_UPDATE, self.on_device_update
|
||||
)
|
||||
self.device_config_update.connect(self.update_devices_from_filters)
|
||||
self.textChanged.connect(self.check_validity)
|
||||
self.check_validity(self.text())
|
||||
|
||||
def on_device_update(self, action: str, content: dict) -> None:
|
||||
"""
|
||||
Set the device filter.
|
||||
Callback for device update events. Triggers the device_update signal.
|
||||
|
||||
Args:
|
||||
device_filter (str | list[str]): Device filter, name of the device class.
|
||||
action (str): The action that triggered the event.
|
||||
content (dict): The content of the config update.
|
||||
"""
|
||||
super().set_device_filter(device_filter)
|
||||
self.populate_completer()
|
||||
if action in ["add", "remove", "reload"]:
|
||||
self.device_config_update.emit()
|
||||
|
||||
def set_default_device(self, default_device: str):
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
if self._callback_id is not None:
|
||||
self.bec_dispatcher.client.callbacks.remove(self._callback_id)
|
||||
|
||||
def get_current_device(self) -> object:
|
||||
"""
|
||||
Set the default device.
|
||||
|
||||
Args:
|
||||
default_device (str): Default device name.
|
||||
"""
|
||||
super().set_default_device(default_device)
|
||||
self.setText(default_device)
|
||||
|
||||
def populate_completer(self):
|
||||
"""Populate the completer with the devices."""
|
||||
self.devices = self.get_device_list(self.config.device_filter)
|
||||
self.completer.setModel(self.create_completer_model(self.devices))
|
||||
|
||||
def create_completer_model(self, devices: list[str]):
|
||||
"""Create a model for the completer."""
|
||||
from qtpy.QtCore import QStringListModel
|
||||
|
||||
return QStringListModel(devices, self)
|
||||
|
||||
def get_device(self) -> object:
|
||||
"""
|
||||
Get the selected device object.
|
||||
Get the current device object based on the current value.
|
||||
|
||||
Returns:
|
||||
object: Device object.
|
||||
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
||||
"""
|
||||
device_name = self.text()
|
||||
device_obj = getattr(self.dev, device_name.lower(), None)
|
||||
if device_obj is None:
|
||||
raise ValueError(f"Device {device_name} is not found.")
|
||||
return device_obj
|
||||
dev_name = self.text()
|
||||
return self.get_device_object(dev_name)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None:
|
||||
"""Extend the paint event to set the border color based on the validity of the input.
|
||||
|
||||
Args:
|
||||
event (PySide6.QtGui.QPaintEvent) : Paint event.
|
||||
"""
|
||||
# logger.info(f"Received paint event: {event} in {self.__class__}")
|
||||
super().paintEvent(event)
|
||||
|
||||
if self._is_valid_input is False and self.isEnabled() is True:
|
||||
painter = QPainter(self)
|
||||
pen = QPen()
|
||||
pen.setWidth(2)
|
||||
pen.setColor(self._accent_colors.emergency)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
|
||||
painter.end()
|
||||
|
||||
@Slot(str)
|
||||
def check_validity(self, input_text: str) -> None:
|
||||
"""
|
||||
Check if the current value is a valid device name.
|
||||
"""
|
||||
if self.validate_device(input_text) is True:
|
||||
self._is_valid_input = True
|
||||
self.device_selected.emit(input_text.lower())
|
||||
else:
|
||||
self._is_valid_input = False
|
||||
self.update()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.widgets.signal_combobox.signal_combobox import SignalComboBox
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
line_edit = DeviceLineEdit()
|
||||
line_edit.filter_to_positioner = True
|
||||
signal_line_edit = SignalComboBox()
|
||||
line_edit.textChanged.connect(signal_line_edit.set_device)
|
||||
line_edit.set_available_devices(["samx", "samy", "samz"])
|
||||
line_edit.set_device("samx")
|
||||
layout.addWidget(line_edit)
|
||||
layout.addWidget(signal_line_edit)
|
||||
widget.show()
|
||||
app.exec_()
|
||||
|
||||
@@ -31,7 +31,7 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
return "BEC Input Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(DeviceLineEdit.ICON_NAME)
|
||||
|
||||
@@ -321,6 +321,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
self,
|
||||
image,
|
||||
monitor: str = None,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
@@ -337,7 +338,13 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
data (np.ndarray): Custom data to display.
|
||||
"""
|
||||
if monitor is not None and data is None:
|
||||
image.image(monitor=monitor, color_map=color_map, vrange=vrange, color_bar=color_bar)
|
||||
image.image(
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_map=color_map,
|
||||
vrange=vrange,
|
||||
color_bar=color_bar,
|
||||
)
|
||||
elif data is not None and monitor is None:
|
||||
image.add_custom_image(
|
||||
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
@@ -355,6 +362,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
def image(
|
||||
self,
|
||||
monitor: str = None,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
@@ -393,6 +401,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
|
||||
@@ -7,10 +7,10 @@ import numpy as np
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from qtpy.QtCore import QThread
|
||||
from qtpy.QtCore import QThread, Slot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
# from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import (
|
||||
@@ -57,6 +57,7 @@ class BECImageShow(BECPlotBase):
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
@@ -79,6 +80,8 @@ class BECImageShow(BECPlotBase):
|
||||
)
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.single_image = single_image
|
||||
self.image_type = "device_monitor_2d"
|
||||
self.scan_id = None
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
self._images = defaultdict(dict)
|
||||
@@ -104,7 +107,7 @@ class BECImageShow(BECPlotBase):
|
||||
|
||||
def find_image_by_monitor(self, item_id: str) -> BECImageItem:
|
||||
"""
|
||||
Find the widget by its gui_id.
|
||||
Find the image item by its gui_id.
|
||||
|
||||
Args:
|
||||
item_id(str): The gui_id of the widget.
|
||||
@@ -229,6 +232,7 @@ class BECImageShow(BECPlotBase):
|
||||
def image(
|
||||
self,
|
||||
monitor: str,
|
||||
monitor_type: Literal["1d", "2d"] = "2d",
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
@@ -242,6 +246,7 @@ class BECImageShow(BECPlotBase):
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
monitor_type(Literal["1d","2d"]): The type of monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
@@ -250,7 +255,12 @@ class BECImageShow(BECPlotBase):
|
||||
Returns:
|
||||
BECImageItem: The image item.
|
||||
"""
|
||||
image_source = "device_monitor_2d"
|
||||
if monitor_type == "1d":
|
||||
image_source = "device_monitor_1d"
|
||||
self.image_type = "device_monitor_1d"
|
||||
elif monitor_type == "2d":
|
||||
image_source = "device_monitor_2d"
|
||||
self.image_type = "device_monitor_2d"
|
||||
|
||||
image_exits = self._check_image_id(monitor, self._images)
|
||||
if image_exits:
|
||||
@@ -291,7 +301,6 @@ class BECImageShow(BECPlotBase):
|
||||
**kwargs,
|
||||
):
|
||||
image_source = "custom"
|
||||
# image_source = "device_monitor_2d"
|
||||
|
||||
image_exits = self._check_image_id(name, self._images)
|
||||
if image_exits:
|
||||
@@ -515,9 +524,58 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
data = msg["data"]
|
||||
device = msg["device"]
|
||||
image = self._images["device_monitor_2d"][device]
|
||||
image.raw_data = data
|
||||
self.process_image(device, image, data)
|
||||
if self.image_type == "device_monitor_1d":
|
||||
image = self._images["device_monitor_1d"][device]
|
||||
current_scan_id = metadata.get("scan_id", None)
|
||||
if current_scan_id is None:
|
||||
return
|
||||
if current_scan_id != self.scan_id:
|
||||
self.reset()
|
||||
self.scan_id = current_scan_id
|
||||
image.image_buffer_list = []
|
||||
image.max_len = 0
|
||||
image_buffer = self.adjust_image_buffer(image, data)
|
||||
image.raw_data = image_buffer
|
||||
self.process_image(device, image, image_buffer)
|
||||
elif self.image_type == "device_monitor_2d":
|
||||
image = self._images["device_monitor_2d"][device]
|
||||
image.raw_data = data
|
||||
self.process_image(device, image, data)
|
||||
|
||||
def adjust_image_buffer(self, image: BECImageItem, new_data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Adjusts the image buffer to accommodate the new data, ensuring that all rows have the same length.
|
||||
|
||||
Args:
|
||||
image: The image object (used to store buffer list and max_len).
|
||||
new_data (np.ndarray): The new incoming 1D waveform data.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The updated image buffer with adjusted shapes.
|
||||
"""
|
||||
new_len = new_data.shape[0]
|
||||
if not hasattr(image, "image_buffer_list"):
|
||||
image.image_buffer_list = []
|
||||
image.max_len = 0
|
||||
|
||||
if new_len > image.max_len:
|
||||
image.max_len = new_len
|
||||
for i in range(len(image.image_buffer_list)):
|
||||
wf = image.image_buffer_list[i]
|
||||
pad_width = image.max_len - wf.shape[0]
|
||||
if pad_width > 0:
|
||||
image.image_buffer_list[i] = np.pad(
|
||||
wf, (0, pad_width), mode="constant", constant_values=0
|
||||
)
|
||||
image.image_buffer_list.append(new_data)
|
||||
else:
|
||||
pad_width = image.max_len - new_len
|
||||
if pad_width > 0:
|
||||
new_data = np.pad(new_data, (0, pad_width), mode="constant", constant_values=0)
|
||||
image.image_buffer_list.append(new_data)
|
||||
|
||||
image_buffer = np.array(image.image_buffer_list)
|
||||
return image_buffer
|
||||
|
||||
@Slot(str, np.ndarray)
|
||||
def update_image(self, device: str, data: np.ndarray):
|
||||
@@ -528,7 +586,7 @@ class BECImageShow(BECPlotBase):
|
||||
device(str): The name of the device.
|
||||
data(np.ndarray): The data to be updated.
|
||||
"""
|
||||
image_to_update = self._images["device_monitor_2d"][device]
|
||||
image_to_update = self._images[self.image_type][device]
|
||||
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
|
||||
|
||||
@Slot(str, ImageStats)
|
||||
@@ -539,7 +597,7 @@ class BECImageShow(BECPlotBase):
|
||||
Args:
|
||||
stats(ImageStats): The statistics of the image.
|
||||
"""
|
||||
image_to_update = self._images["device_monitor_2d"][device]
|
||||
image_to_update = self._images[self.image_type][device]
|
||||
if image_to_update.config.autorange:
|
||||
image_to_update.auto_update_vrange(stats)
|
||||
|
||||
@@ -552,7 +610,7 @@ class BECImageShow(BECPlotBase):
|
||||
data = image.raw_data
|
||||
self.process_image(image_id, image, data)
|
||||
|
||||
def _connect_device_monitor_2d(self, monitor: str):
|
||||
def _connect_device_monitor(self, monitor: str):
|
||||
"""
|
||||
Connect to the device monitor.
|
||||
|
||||
@@ -565,29 +623,36 @@ class BECImageShow(BECPlotBase):
|
||||
except AttributeError:
|
||||
previous_monitor = None
|
||||
if previous_monitor and image_item.connected is True:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(previous_monitor)
|
||||
)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(previous_monitor)
|
||||
)
|
||||
image_item.connected = False
|
||||
if monitor and image_item.connected is False:
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
|
||||
)
|
||||
if self.image_type == "device_monitor_1d":
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(monitor)
|
||||
)
|
||||
elif self.image_type == "device_monitor_2d":
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
|
||||
)
|
||||
image_item.set_monitor(monitor)
|
||||
image_item.connected = True
|
||||
|
||||
def _add_image_object(
|
||||
self, source: str, name: str, config: ImageItemConfig, data=None
|
||||
) -> BECImageItem: # TODO fix types
|
||||
) -> BECImageItem:
|
||||
config.parent_id = self.gui_id
|
||||
if self.single_image is True and len(self.images) > 0:
|
||||
self.remove_image(0)
|
||||
image = BECImageItem(config=config, parent_image=self)
|
||||
self.plot_item.addItem(image)
|
||||
self._images[source][name] = image
|
||||
if source == "device_monitor_2d":
|
||||
self._connect_device_monitor_2d(config.monitor)
|
||||
self._connect_device_monitor(config.monitor)
|
||||
self.config.images[name] = config
|
||||
if data is not None:
|
||||
image.setImage(data)
|
||||
@@ -672,6 +737,9 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
image = self.find_image_by_monitor(image_id)
|
||||
if image:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(image.config.monitor)
|
||||
)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(image.config.monitor)
|
||||
)
|
||||
@@ -680,9 +748,9 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
Clean up the widget.
|
||||
"""
|
||||
for monitor in self._images["device_monitor_2d"]:
|
||||
for monitor in self._images[self.image_type]:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(monitor)
|
||||
)
|
||||
self.images.clear()
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
if vrange is not None:
|
||||
self.color_bar.setLevels(low=vrange[0], high=vrange[1])
|
||||
self.color_bar.setImageItem(self)
|
||||
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
|
||||
self.parent_image.addItem(self.color_bar, row=1, col=1)
|
||||
self.config.color_bar = "simple"
|
||||
elif color_bar_style == "full":
|
||||
# Setting histogram
|
||||
@@ -321,7 +321,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
)
|
||||
|
||||
# Adding histogram to the layout
|
||||
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
|
||||
self.parent_image.addItem(self.color_bar, row=1, col=1)
|
||||
|
||||
# save settings
|
||||
self.config.color_bar = "full"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Literal, Optional
|
||||
|
||||
import bec_qthemes
|
||||
@@ -12,6 +11,8 @@ from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.crosshair import Crosshair
|
||||
from bec_widgets.utils.fps_counter import FPSCounter
|
||||
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -50,6 +51,11 @@ class SubplotConfig(ConnectionConfig):
|
||||
|
||||
|
||||
class BECViewBox(pg.ViewBox):
|
||||
sigPaint = Signal()
|
||||
|
||||
def paint(self, painter, opt, widget):
|
||||
super().paint(painter, opt, widget)
|
||||
self.sigPaint.emit()
|
||||
|
||||
def itemBoundsChanged(self, item):
|
||||
self._itemBoundsCache.pop(item, None)
|
||||
@@ -78,6 +84,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"set_outer_axes",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
@@ -99,12 +106,15 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
|
||||
self.figure = parent_figure
|
||||
|
||||
# self.plot_item = self.addPlot(row=0, col=0)
|
||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(parent=self, enableMenu=True), parent=self)
|
||||
self.addItem(self.plot_item, row=0, col=0)
|
||||
self.addItem(self.plot_item, row=1, col=0)
|
||||
|
||||
self.add_legend()
|
||||
self.crosshair = None
|
||||
self.fps_monitor = None
|
||||
self.fps_label = None
|
||||
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
|
||||
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
|
||||
self._connect_to_theme_change()
|
||||
|
||||
def _connect_to_theme_change(self):
|
||||
@@ -137,6 +147,9 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
self.plot_item.getAxis(axis).setPen(text_pen)
|
||||
self.plot_item.getAxis(axis).setTextPen(text_pen)
|
||||
if self.plot_item.legend is not None:
|
||||
for sample, label in self.plot_item.legend.items:
|
||||
label.setText(label.text, color=palette.text().color())
|
||||
|
||||
def set(self, **kwargs) -> None:
|
||||
"""
|
||||
@@ -376,6 +389,10 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"""
|
||||
self.plot_item.enableAutoRange(axis, enabled)
|
||||
|
||||
############################################################
|
||||
###################### Crosshair ###########################
|
||||
############################################################
|
||||
|
||||
def hook_crosshair(self) -> None:
|
||||
"""Hook the crosshair to all plots."""
|
||||
if self.crosshair is None:
|
||||
@@ -414,6 +431,55 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
self.crosshair.clear_markers()
|
||||
self.crosshair.update_markers()
|
||||
|
||||
############################################################
|
||||
##################### FPS Counter ##########################
|
||||
############################################################
|
||||
|
||||
def update_fps_label(self, fps: float) -> None:
|
||||
"""
|
||||
Update the FPS label.
|
||||
|
||||
Args:
|
||||
fps(float): The frames per second.
|
||||
"""
|
||||
if self.fps_label:
|
||||
self.fps_label.setText(f"FPS: {fps:.2f}")
|
||||
|
||||
def hook_fps_monitor(self):
|
||||
"""Hook the FPS monitor to the plot."""
|
||||
if self.fps_monitor is None:
|
||||
# text_color = self.get_text_color()#TODO later
|
||||
self.fps_monitor = FPSCounter(self.plot_item.vb) # text_color=text_color)
|
||||
self.fps_label = pg.LabelItem(justify="right")
|
||||
self.addItem(self.fps_label, row=0, col=0)
|
||||
|
||||
self.fps_monitor.sigFpsUpdate.connect(self.update_fps_label)
|
||||
|
||||
def unhook_fps_monitor(self, delete_label=True):
|
||||
"""Unhook the FPS monitor from the plot."""
|
||||
if self.fps_monitor is not None:
|
||||
# Remove Monitor
|
||||
self.fps_monitor.cleanup()
|
||||
self.fps_monitor.deleteLater()
|
||||
self.fps_monitor = None
|
||||
if self.fps_label is not None and delete_label:
|
||||
# Remove Label
|
||||
self.removeItem(self.fps_label)
|
||||
self.fps_label.deleteLater()
|
||||
self.fps_label = None
|
||||
|
||||
def enable_fps_monitor(self, enable: bool = True):
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
if enable and self.fps_monitor is None:
|
||||
self.hook_fps_monitor()
|
||||
elif not enable and self.fps_monitor is not None:
|
||||
self.unhook_fps_monitor()
|
||||
|
||||
def export(self):
|
||||
"""Show the Export Dialog of the plot widget."""
|
||||
scene = self.plot_item.scene()
|
||||
@@ -428,6 +494,9 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
def cleanup_pyqtgraph(self):
|
||||
"""Cleanup pyqtgraph items."""
|
||||
self.unhook_crosshair()
|
||||
self.unhook_fps_monitor(delete_label=False)
|
||||
self.tick_item.cleanup()
|
||||
self.arrow_item.cleanup()
|
||||
item = self.plot_item
|
||||
item.vb.menu.close()
|
||||
item.vb.menu.deleteLater()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=too_many_lines
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
@@ -12,7 +13,7 @@ from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from pyqtgraph.exporters import MatplotlibExporter
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.utils import Colors, EntryValidator
|
||||
@@ -71,6 +72,8 @@ class BECWaveform(BECPlotBase):
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"set_colormap",
|
||||
"enable_scatter",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
@@ -165,7 +168,7 @@ class BECWaveform(BECPlotBase):
|
||||
self.roi_select.linear_region_selector.setRegion(region)
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting region {tuple}; Exception raised: {e}")
|
||||
raise ValueError(f"Error setting region {tuple}; Exception raised: {e}")
|
||||
raise ValueError(f"Error setting region {tuple}; Exception raised: {e}") from e
|
||||
|
||||
def _hook_roi(self):
|
||||
"""Hook the linear region selector to the plot."""
|
||||
@@ -219,7 +222,7 @@ class BECWaveform(BECPlotBase):
|
||||
# Reset curves
|
||||
self._curves_data = defaultdict(dict)
|
||||
self._curves = self.plot_item.curves
|
||||
for curve_id, curve_config in self.config.curves.items():
|
||||
for curve_config in self.config.curves.values():
|
||||
self.add_curve_by_config(curve_config)
|
||||
if replot_last_scan:
|
||||
self.scan_history(scan_index=-1)
|
||||
@@ -342,7 +345,7 @@ class BECWaveform(BECPlotBase):
|
||||
Returns:
|
||||
CurveConfig|dict: Configuration of the curve.
|
||||
"""
|
||||
for source, curves in self._curves_data.items():
|
||||
for curves in self._curves_data.values():
|
||||
if curve_id in curves:
|
||||
if dict_output:
|
||||
return curves[curve_id].config.model_dump()
|
||||
@@ -362,13 +365,27 @@ class BECWaveform(BECPlotBase):
|
||||
if isinstance(identifier, int):
|
||||
return self.plot_item.curves[identifier]
|
||||
elif isinstance(identifier, str):
|
||||
for source_type, curves in self._curves_data.items():
|
||||
for curves in self._curves_data.values():
|
||||
if identifier in curves:
|
||||
return curves[identifier]
|
||||
raise ValueError(f"Curve with ID '{identifier}' not found.")
|
||||
else:
|
||||
raise ValueError("Identifier must be either an integer (index) or a string (curve_id).")
|
||||
|
||||
def enable_scatter(self, enable: bool):
|
||||
"""
|
||||
Enable/Disable scatter plot on all curves.
|
||||
|
||||
Args:
|
||||
enable(bool): If True, enable scatter markers; if False, disable them.
|
||||
"""
|
||||
for curve in self.curves:
|
||||
if isinstance(curve, BECCurve):
|
||||
if enable:
|
||||
curve.set_symbol("o") # You can choose any symbol you like
|
||||
else:
|
||||
curve.set_symbol(None)
|
||||
|
||||
def plot(
|
||||
self,
|
||||
arg1: list | np.ndarray | str | None = None,
|
||||
@@ -466,6 +483,10 @@ class BECWaveform(BECPlotBase):
|
||||
- Custom signal name of device from BEC.
|
||||
x_entry(str): Entry of the x signal.
|
||||
"""
|
||||
if not x_name:
|
||||
# this can happen, if executed by a signal from a widget
|
||||
return
|
||||
|
||||
curve_configs = self.config.curves
|
||||
curve_ids = list(curve_configs.keys())
|
||||
curve_configs = list(curve_configs.values())
|
||||
@@ -513,6 +534,7 @@ class BECWaveform(BECPlotBase):
|
||||
|
||||
@Slot()
|
||||
def auto_range(self):
|
||||
"""Manually set auto range of the plotitem"""
|
||||
self.plot_item.autoRange()
|
||||
|
||||
def set_auto_range(self, enabled: bool, axis: str = "xy"):
|
||||
@@ -551,7 +573,6 @@ class BECWaveform(BECPlotBase):
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
curve_source = curve_source
|
||||
curve_id = label or f"Curve {len(self.plot_item.curves) + 1}"
|
||||
|
||||
curve_exits = self._check_curve_id(curve_id, self._curves_data)
|
||||
@@ -630,6 +651,13 @@ class BECWaveform(BECPlotBase):
|
||||
x_name = mode if mode is not None else "best_effort"
|
||||
self.x_axis_mode["name"] = x_name
|
||||
|
||||
if not x_name or not y_name:
|
||||
# can happen if executed from a signal from a widget ;
|
||||
# the code above has to be executed to set some other
|
||||
# variables, but it cannot continue if both names are
|
||||
# not set properly -> exit here
|
||||
return
|
||||
|
||||
# 3. Check - Get entry if not provided and validate
|
||||
x_entry, y_entry, z_entry = self._validate_signal_entries(
|
||||
x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec
|
||||
@@ -725,7 +753,7 @@ class BECWaveform(BECPlotBase):
|
||||
|
||||
if self.x_axis_mode["readout_priority"] == "async":
|
||||
raise ValueError(
|
||||
f"Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
|
||||
"Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
|
||||
)
|
||||
|
||||
if validate_bec is True:
|
||||
@@ -999,7 +1027,7 @@ class BECWaveform(BECPlotBase):
|
||||
Args:
|
||||
curve_id(str): ID of the curve to be removed.
|
||||
"""
|
||||
for source, curves in self._curves_data.items():
|
||||
for curves in self._curves_data.values():
|
||||
if curve_id in curves:
|
||||
curve = curves.pop(curve_id)
|
||||
self.plot_item.removeItem(curve)
|
||||
@@ -1022,7 +1050,7 @@ class BECWaveform(BECPlotBase):
|
||||
self.plot_item.removeItem(curve)
|
||||
del self.config.curves[curve_id]
|
||||
# Remove from self.curve_data
|
||||
for source, curves in self._curves_data.items():
|
||||
for curves in self._curves_data.values():
|
||||
if curve_id in curves:
|
||||
del curves[curve_id]
|
||||
break
|
||||
@@ -1052,7 +1080,7 @@ class BECWaveform(BECPlotBase):
|
||||
if self._curves_data["DAP"]:
|
||||
self.setup_dap(self.old_scan_id, self.scan_id)
|
||||
if self._curves_data["async"]:
|
||||
for curve_id, curve in self._curves_data["async"].items():
|
||||
for curve in self._curves_data["async"].values():
|
||||
self.setup_async(
|
||||
name=curve.config.signals.y.name, entry=curve.config.signals.y.entry
|
||||
)
|
||||
@@ -1184,7 +1212,9 @@ class BECWaveform(BECPlotBase):
|
||||
|
||||
@Slot(dict, dict)
|
||||
def update_dap(self, msg, metadata):
|
||||
self.msg = msg
|
||||
"""Callback for DAP response message."""
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
|
||||
model = msg["dap_request"].content["config"]["class_kwargs"]["model"]
|
||||
|
||||
@@ -1213,7 +1243,7 @@ class BECWaveform(BECPlotBase):
|
||||
metadata(dict): Metadata of the message.
|
||||
"""
|
||||
instruction = metadata.get("async_update")
|
||||
for curve_id, curve in self._curves_data["async"].items():
|
||||
for curve in self._curves_data["async"].values():
|
||||
y_name = curve.config.signals.y.name
|
||||
y_entry = curve.config.signals.y.entry
|
||||
x_name = self._x_axis_mode["name"]
|
||||
@@ -1272,7 +1302,11 @@ class BECWaveform(BECPlotBase):
|
||||
Update the scan curves with the data from the scan segment.
|
||||
"""
|
||||
try:
|
||||
data = self.scan_item.data
|
||||
data = (
|
||||
self.scan_item.live_data
|
||||
if hasattr(self.scan_item, "live_data") # backward compatibility
|
||||
else self.scan_item.data
|
||||
)
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
@@ -1325,8 +1359,14 @@ class BECWaveform(BECPlotBase):
|
||||
list|np.ndarray|None: X data for the curve.
|
||||
"""
|
||||
x_data = None
|
||||
live_data = (
|
||||
self.scan_item.live_data
|
||||
if hasattr(self.scan_item, "live_data")
|
||||
else self.scan_item.data
|
||||
)
|
||||
if self._x_axis_mode["name"] == "timestamp":
|
||||
timestamps = self.scan_item.data[y_name][y_entry].timestamps
|
||||
|
||||
timestamps = live_data[y_name][y_entry].timestamps
|
||||
|
||||
x_data = timestamps
|
||||
return x_data
|
||||
@@ -1337,7 +1377,7 @@ class BECWaveform(BECPlotBase):
|
||||
if self._x_axis_mode["name"] is None or self._x_axis_mode["name"] == "best_effort":
|
||||
if len(self._curves_data["async"]) > 0:
|
||||
x_data = None
|
||||
self._x_axis_mode["label_suffix"] = f" [auto: index]"
|
||||
self._x_axis_mode["label_suffix"] = " [auto: index]"
|
||||
current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
|
||||
self.plot_item.setLabel(
|
||||
"bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
|
||||
@@ -1346,7 +1386,7 @@ class BECWaveform(BECPlotBase):
|
||||
else:
|
||||
x_name = self.scan_item.status_message.info["scan_report_devices"][0]
|
||||
x_entry = self.entry_validator.validate_signal(x_name, None)
|
||||
x_data = self.scan_item.data[x_name][x_entry].val
|
||||
x_data = live_data[x_name][x_entry].val
|
||||
self._x_axis_mode["label_suffix"] = f" [auto: {x_name}-{x_entry}]"
|
||||
current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
|
||||
self.plot_item.setLabel(
|
||||
@@ -1357,7 +1397,7 @@ class BECWaveform(BECPlotBase):
|
||||
x_name = curve.config.signals.x.name
|
||||
x_entry = curve.config.signals.x.entry
|
||||
try:
|
||||
x_data = self.scan_item.data[x_name][x_entry].val
|
||||
x_data = live_data[x_name][x_entry].val
|
||||
except TypeError:
|
||||
x_data = []
|
||||
return x_data
|
||||
@@ -1414,7 +1454,6 @@ class BECWaveform(BECPlotBase):
|
||||
self.scan_signal_update.emit()
|
||||
self.async_signal_update.emit()
|
||||
|
||||
# pylint: ignore: undefined-variable
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict: # | pd.DataFrame:
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
@@ -1484,7 +1523,7 @@ class BECWaveform(BECPlotBase):
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.update_dap, MessageEndpoints.dap_response(self.scan_id)
|
||||
)
|
||||
for curve_id, curve in self._curves_data["async"].items():
|
||||
for curve_id in self._curves_data["async"]:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_async_readback,
|
||||
MessageEndpoints.device_async_readback(self.scan_id, curve_id),
|
||||
|
||||
@@ -42,7 +42,7 @@ class CurveConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the curve.")
|
||||
label: Optional[str] = Field(None, description="The label of the curve.")
|
||||
color: Optional[str | tuple] = Field(None, description="The color of the curve.")
|
||||
symbol: Optional[str] = Field("o", description="The symbol of the curve.")
|
||||
symbol: Optional[str | None] = Field("o", description="The symbol of the curve.")
|
||||
symbol_color: Optional[str | tuple] = Field(
|
||||
None, description="The color of the symbol of the curve."
|
||||
)
|
||||
@@ -201,7 +201,8 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
self.config.symbol = symbol
|
||||
self.apply_config()
|
||||
self.setSymbol(symbol)
|
||||
self.updateItems()
|
||||
|
||||
def set_symbol_color(self, symbol_color: str):
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,8 @@ import sys
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
@@ -13,8 +14,10 @@ from bec_widgets.qt_utils.toolbar import (
|
||||
MaterialIconAction,
|
||||
ModularToolBar,
|
||||
SeparatorAction,
|
||||
WidgetAction,
|
||||
)
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
|
||||
@@ -40,6 +43,7 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
"set_rotation",
|
||||
"set_log",
|
||||
"set_grid",
|
||||
"enable_fps_monitor",
|
||||
"lock_aspect_ratio",
|
||||
]
|
||||
|
||||
@@ -62,11 +66,18 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.fig = BECFigure()
|
||||
self.dim_combo_box = QComboBox()
|
||||
self.dim_combo_box.addItems(["1d", "2d"])
|
||||
self.toolbar = ModularToolBar(
|
||||
actions={
|
||||
"monitor": DeviceSelectionAction(
|
||||
"Monitor:", DeviceComboBox(device_filter="Device")
|
||||
"Monitor:",
|
||||
DeviceComboBox(
|
||||
device_filter=BECDeviceFilter.DEVICE,
|
||||
readout_priority_filter=[ReadoutPriority.ASYNC],
|
||||
),
|
||||
),
|
||||
"monitor_type": WidgetAction(widget=self.dim_combo_box),
|
||||
"connect": MaterialIconAction(icon_name="link", tooltip="Connect Device"),
|
||||
"separator_0": SeparatorAction(),
|
||||
"save": MaterialIconAction(icon_name="save", tooltip="Open Export Dialog"),
|
||||
@@ -104,6 +115,9 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
icon_name="reset_settings", tooltip="Reset Image Settings"
|
||||
),
|
||||
"separator_3": SeparatorAction(),
|
||||
"fps_monitor": MaterialIconAction(
|
||||
icon_name="speed", tooltip="Show FPS Monitor", checkable=True
|
||||
),
|
||||
"axis_settings": MaterialIconAction(
|
||||
icon_name="settings", tooltip="Open Configuration Dialog"
|
||||
),
|
||||
@@ -150,6 +164,7 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["reset"].action.triggered.connect(self.reset_settings)
|
||||
# sepatator
|
||||
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
|
||||
self.toolbar.widgets["fps_monitor"].action.toggled.connect(self.enable_fps_monitor)
|
||||
|
||||
###################################
|
||||
# Dialog Windows
|
||||
@@ -158,7 +173,8 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
def _connect_action(self):
|
||||
monitor_combo = self.toolbar.widgets["monitor"].device_combobox
|
||||
monitor_name = monitor_combo.currentText()
|
||||
self.image(monitor_name)
|
||||
monitor_type = self.toolbar.widgets["monitor_type"].widget.currentText()
|
||||
self.image(monitor=monitor_name, monitor_type=monitor_type)
|
||||
monitor_combo.setStyleSheet("QComboBox { background-color: " "; }")
|
||||
|
||||
def show_axis_settings(self):
|
||||
@@ -177,6 +193,7 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
def image(
|
||||
self,
|
||||
monitor: str,
|
||||
monitor_type: Optional[Literal["1d", "2d"]] = "2d",
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
@@ -190,8 +207,14 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["monitor"].device_combobox.setStyleSheet(
|
||||
"QComboBox {{ background-color: " "; }}"
|
||||
)
|
||||
if self.toolbar.widgets["monitor_type"].widget.currentText() != monitor_type:
|
||||
self.toolbar.widgets["monitor_type"].widget.setCurrentText(monitor_type)
|
||||
self.toolbar.widgets["monitor_type"].widget.setStyleSheet(
|
||||
"QComboBox {{ background-color: " "; }}"
|
||||
)
|
||||
return self._image.image(
|
||||
monitor=monitor,
|
||||
monitor_type=monitor_type,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
downsample=downsample,
|
||||
@@ -450,6 +473,18 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["rectangle_mode"].action.setChecked(False)
|
||||
self._image.plot_item.getViewBox().setMouseMode(pg.ViewBox.PanMode)
|
||||
|
||||
@SafeSlot()
|
||||
def enable_fps_monitor(self, enabled: bool):
|
||||
"""
|
||||
Enable the FPS monitor of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the FPS monitor.
|
||||
"""
|
||||
self._image.enable_fps_monitor(enabled)
|
||||
if self.toolbar.widgets["fps_monitor"].action.isChecked() != enabled:
|
||||
self.toolbar.widgets["fps_monitor"].action.setChecked(enabled)
|
||||
|
||||
def export(self):
|
||||
"""
|
||||
Show the export dialog for the plot widget.
|
||||
@@ -469,6 +504,7 @@ def main(): # pragma: no cover
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = BECImageWidget()
|
||||
widget.image("waveform", "1d")
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import os
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QTreeWidgetItem, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QPushButton, QTreeWidgetItem, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -15,7 +15,10 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
"""Dialog for displaying the fit summary and params for LMFit DAP processes"""
|
||||
|
||||
ICON_NAME = "monitoring"
|
||||
# Signal to emit the currently selected fit curve_id
|
||||
selected_fit = Signal(str)
|
||||
# Signal to emit a move action in form of a tuple (param_name, value)
|
||||
move_action = Signal(tuple)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -49,9 +52,54 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
self.summary_data = {}
|
||||
self._fit_curve_id = None
|
||||
self._deci_precision = 3
|
||||
self._always_show_latest = False
|
||||
self.ui.curve_list.currentItemChanged.connect(self.display_fit_details)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.layout)
|
||||
self._active_actions = []
|
||||
self._enable_actions = True
|
||||
self._move_buttons = []
|
||||
self._accent_colors = get_accent_colors()
|
||||
self.action_buttons = {}
|
||||
|
||||
@property
|
||||
def enable_actions(self) -> bool:
|
||||
"""Property to enable the move to buttons."""
|
||||
return self._enable_actions
|
||||
|
||||
@enable_actions.setter
|
||||
def enable_actions(self, enable: bool):
|
||||
self._enable_actions = enable
|
||||
for button in self.action_buttons.values():
|
||||
button.setEnabled(enable)
|
||||
|
||||
@Property(list)
|
||||
def active_action_list(self) -> list[str]:
|
||||
"""Property to list the names of the fit parameters for which actions should be enabled."""
|
||||
return self._active_actions
|
||||
|
||||
@active_action_list.setter
|
||||
def active_action_list(self, actions: list[str]):
|
||||
self._active_actions = actions
|
||||
|
||||
# This slot needed?
|
||||
@Slot(bool)
|
||||
def set_actions_enabled(self, enable: bool) -> bool:
|
||||
"""Slot to enable the move to buttons.
|
||||
|
||||
Args:
|
||||
enable (bool): Whether to enable the action buttons.
|
||||
"""
|
||||
self.enable_actions = enable
|
||||
|
||||
@Property(bool)
|
||||
def always_show_latest(self):
|
||||
"""Property to indicate if always the latest DAP update is displayed."""
|
||||
return self._always_show_latest
|
||||
|
||||
@always_show_latest.setter
|
||||
def always_show_latest(self, show: bool):
|
||||
self._always_show_latest = show
|
||||
|
||||
@Property(bool)
|
||||
def hide_curve_selection(self):
|
||||
@@ -67,8 +115,36 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
"""
|
||||
self.ui.group_curve_selection.setVisible(not show)
|
||||
|
||||
@Property(bool)
|
||||
def hide_summary(self) -> bool:
|
||||
"""Property for showing the summary."""
|
||||
return not self.ui.group_summary.isVisible()
|
||||
|
||||
@hide_summary.setter
|
||||
def hide_summary(self, show: bool):
|
||||
"""Setter for showing the summary.
|
||||
|
||||
Args:
|
||||
show (bool): Whether to show the summary.
|
||||
"""
|
||||
self.ui.group_summary.setVisible(not show)
|
||||
|
||||
@Property(bool)
|
||||
def hide_parameters(self) -> bool:
|
||||
"""Property for showing the parameters."""
|
||||
return not self.ui.group_parameters.isVisible()
|
||||
|
||||
@hide_parameters.setter
|
||||
def hide_parameters(self, show: bool):
|
||||
"""Setter for showing the parameters.
|
||||
|
||||
Args:
|
||||
show (bool): Whether to show the parameters.
|
||||
"""
|
||||
self.ui.group_parameters.setVisible(not show)
|
||||
|
||||
@property
|
||||
def fit_curve_id(self):
|
||||
def fit_curve_id(self) -> str:
|
||||
"""Property for the currently displayed fit curve_id."""
|
||||
return self._fit_curve_id
|
||||
|
||||
@@ -112,19 +188,34 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
curve_id = metadata.get("curve_id", "")
|
||||
self.summary_data.update({curve_id: data})
|
||||
self.refresh_curve_list()
|
||||
if self.fit_curve_id is None:
|
||||
if self.fit_curve_id is None or self.always_show_latest is True:
|
||||
self.fit_curve_id = curve_id
|
||||
if curve_id != self.fit_curve_id:
|
||||
return
|
||||
if data is None:
|
||||
return
|
||||
self.ui.summary_tree.clear()
|
||||
chi_squared = data.get("chisqr", 0.0)
|
||||
if isinstance(chi_squared, float) or isinstance(chi_squared, int):
|
||||
chi_squared = f"{chi_squared:.{self._deci_precision}f}"
|
||||
else:
|
||||
chi_squared = "None"
|
||||
reduced_chi_squared = data.get("redchi", 0.0)
|
||||
if isinstance(reduced_chi_squared, float) or isinstance(reduced_chi_squared, int):
|
||||
reduced_chi_squared = f"{reduced_chi_squared:.{self._deci_precision}f}"
|
||||
else:
|
||||
reduced_chi_squared = "None"
|
||||
r_squared = data.get("rsquared", 0.0)
|
||||
if isinstance(r_squared, float) or isinstance(r_squared, int):
|
||||
r_squared = f"{r_squared:.{self._deci_precision}f}"
|
||||
else:
|
||||
r_squared = "None"
|
||||
properties = [
|
||||
("Model", data.get("model", "")),
|
||||
("Method", data.get("method", "")),
|
||||
("Chi-Squared", f"{data.get('chisqr', 0.0):.{self._deci_precision}f}"),
|
||||
("Reduced Chi-Squared", f"{data.get('redchi', 0.0):.{self._deci_precision}f}"),
|
||||
("R-Squared", f"{data.get('rsquared', 0.0):.{self._deci_precision}f}"),
|
||||
("Chi-Squared", chi_squared),
|
||||
("Reduced Chi-Squared", reduced_chi_squared),
|
||||
("R-Squared", r_squared),
|
||||
("Message", data.get("message", "")),
|
||||
]
|
||||
for prop, val in properties:
|
||||
@@ -149,18 +240,62 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
Args:
|
||||
params (list): List of LMFit parameters for the fit curve.
|
||||
"""
|
||||
self._move_buttons = []
|
||||
self.ui.param_tree.clear()
|
||||
for param in params:
|
||||
param_name, param_value, param_std = (
|
||||
param[0],
|
||||
f"{param[1]:.{self._deci_precision}f}",
|
||||
f"{param[7]:.{self._deci_precision}f}",
|
||||
)
|
||||
QTreeWidgetItem(self.ui.param_tree, [param_name, param_value, param_std])
|
||||
param_name = param[0]
|
||||
param_value = param[1]
|
||||
if isinstance(param_value, float) or isinstance(param_value, int):
|
||||
param_value = f"{param_value:.{self._deci_precision}f}"
|
||||
else:
|
||||
param_value = "None"
|
||||
param_std = param[7]
|
||||
if isinstance(param_std, float) or isinstance(param_std, int):
|
||||
param_std = f"{param_std:.{self._deci_precision}f}"
|
||||
else:
|
||||
param_std = "None"
|
||||
|
||||
tree_item = QTreeWidgetItem(self.ui.param_tree, [param_name, param_value, param_std])
|
||||
if param_name in self.active_action_list: # pylint: disable=unsupported-membership-test
|
||||
# Create a push button to move the motor to a specific position
|
||||
widget = QWidget()
|
||||
button = QPushButton(f"Move to {param_name}")
|
||||
button.clicked.connect(self._create_move_action(param_name, param[1]))
|
||||
if self.enable_actions is True:
|
||||
button.setEnabled(True)
|
||||
else:
|
||||
button.setEnabled(False)
|
||||
button.setStyleSheet(
|
||||
f"""
|
||||
QPushButton:enabled {{ background-color: {self._accent_colors.success.name()};color: white; }}
|
||||
QPushButton:disabled {{ background-color: grey;color: white; }}
|
||||
"""
|
||||
)
|
||||
self.action_buttons[param_name] = button
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.action_buttons[param_name])
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
widget.setLayout(layout)
|
||||
self.ui.param_tree.setItemWidget(tree_item, 3, widget)
|
||||
|
||||
def _create_move_action(self, param_name: str, param_value: float) -> callable:
|
||||
"""Create a move action for the given parameter name and value.
|
||||
|
||||
Args:
|
||||
param_name (str): The name of the parameter.
|
||||
param_value (float): The value of the parameter.
|
||||
Returns:
|
||||
callable: The move action with the given parameter name and value.
|
||||
"""
|
||||
|
||||
def move_action():
|
||||
self.move_action.emit((param_name, param_value))
|
||||
|
||||
return move_action
|
||||
|
||||
def populate_curve_list(self):
|
||||
"""Populate the curve list with the available fit curves."""
|
||||
for curve_name in self.summary_data.keys():
|
||||
for curve_name in self.summary_data:
|
||||
self.ui.curve_list.addItem(curve_name)
|
||||
|
||||
def refresh_curve_list(self):
|
||||
@@ -183,10 +318,10 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
self.update_summary_tree(data, {"curve_id": curve_name})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
dialog = LMFitDialog()
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>274</width>
|
||||
<height>568</height>
|
||||
<width>303</width>
|
||||
<height>457</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -19,15 +19,40 @@
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,0,0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="1,2,2">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_curve_selection">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Select Curve</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="3">
|
||||
<item>
|
||||
<widget class="QListWidget" name="curve_list"/>
|
||||
<widget class="QListWidget" name="curve_list">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -35,15 +60,15 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_summary">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>200</height>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
@@ -53,7 +78,7 @@
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="summary_tree">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -88,15 +113,15 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_parameters">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>200</height>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
@@ -106,7 +131,7 @@
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="param_tree">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -117,8 +142,11 @@
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<attribute name="headerDefaultSectionSize">
|
||||
<number>80</number>
|
||||
<number>70</number>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
@@ -135,6 +163,11 @@
|
||||
<string>Std</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Action</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
@@ -2,11 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.qt_utils.toolbar import DeviceSelectionAction, MaterialIconAction, ModularToolBar
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.figure.plots.motor_map.motor_map import MotorMapConfig
|
||||
@@ -50,10 +52,10 @@ class BECMotorMapWidget(BECWidget, QWidget):
|
||||
self.toolbar = ModularToolBar(
|
||||
actions={
|
||||
"motor_x": DeviceSelectionAction(
|
||||
"Motor X:", DeviceComboBox(device_filter="Positioner")
|
||||
"Motor X:", DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
|
||||
),
|
||||
"motor_y": DeviceSelectionAction(
|
||||
"Motor Y:", DeviceComboBox(device_filter="Positioner")
|
||||
"Motor Y:", DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
|
||||
),
|
||||
"connect": MaterialIconAction(icon_name="link", tooltip="Connect Motors"),
|
||||
"history": MaterialIconAction(icon_name="history", tooltip="Reset Trace History"),
|
||||
|
||||
@@ -1,67 +1,285 @@
|
||||
from qtpy.QtCore import Qt, Slot
|
||||
from qtpy.QtGui import QPainter, QPen
|
||||
import numpy as np
|
||||
from qtpy.QtCore import Property, QSize, Qt, Slot
|
||||
from qtpy.QtGui import QBrush, QColor, QPainter, QPainterPath, QPen
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors, get_theme_palette
|
||||
|
||||
class PositionIndicator(QWidget):
|
||||
|
||||
class PositionIndicator(BECWidget, QWidget):
|
||||
USER_ACCESS = ["set_value", "set_range", "vertical", "indicator_width", "rounded_corners"]
|
||||
|
||||
ICON_NAME = "horizontal_distribute"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.position = 0.5
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.position = 50
|
||||
self.min_value = 0
|
||||
self.max_value = 100
|
||||
self.scaling_factor = 0.5
|
||||
self.setMinimumHeight(10)
|
||||
self.is_vertical = False
|
||||
self._current_indicator_position = 0
|
||||
self._draw_position = 0
|
||||
self._rounded_corners = 10
|
||||
self._indicator_width = 2
|
||||
self._indicator_color = get_accent_colors().success
|
||||
self._background_color = get_theme_palette().mid().color()
|
||||
self._use_color_palette = True
|
||||
|
||||
def set_range(self, min_value, max_value):
|
||||
def set_range(self, min_value: float, max_value: float):
|
||||
"""
|
||||
Set the range of the position indicator
|
||||
|
||||
Args:
|
||||
min_value(float): Minimum value of the range
|
||||
max_value(float): Maximum value of the range
|
||||
"""
|
||||
self.minimum = min_value
|
||||
self.maximum = max_value
|
||||
|
||||
@Property(float)
|
||||
def minimum(self):
|
||||
"""
|
||||
Property to get the minimum value of the position indicator
|
||||
"""
|
||||
return self.min_value
|
||||
|
||||
@minimum.setter
|
||||
def minimum(self, min_value: float):
|
||||
"""
|
||||
Setter for the minimum property
|
||||
|
||||
Args:
|
||||
min_value: The minimum value of the position indicator
|
||||
"""
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.update()
|
||||
|
||||
@Property(float)
|
||||
def maximum(self):
|
||||
"""
|
||||
Property to get the maximum value of the position indicator
|
||||
"""
|
||||
return self.max_value
|
||||
|
||||
@maximum.setter
|
||||
def maximum(self, max_value: float):
|
||||
"""
|
||||
Setter for the maximum property
|
||||
|
||||
Args:
|
||||
max_value: The maximum value of the position indicator
|
||||
"""
|
||||
self.max_value = max_value
|
||||
self.update()
|
||||
|
||||
@Property(bool)
|
||||
def vertical(self):
|
||||
"""
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
return self.is_vertical
|
||||
|
||||
@vertical.setter
|
||||
def vertical(self, is_vertical: bool):
|
||||
"""
|
||||
Setter for the vertical property
|
||||
|
||||
Args:
|
||||
is_vertical: True if the indicator should be vertical, False if horizontal
|
||||
"""
|
||||
|
||||
self.is_vertical = is_vertical
|
||||
self.update()
|
||||
|
||||
@Property(float)
|
||||
def value(self):
|
||||
"""
|
||||
Property to get the current value of the position indicator
|
||||
"""
|
||||
return self.position
|
||||
|
||||
@value.setter
|
||||
def value(self, position: float):
|
||||
"""
|
||||
Setter for the value property
|
||||
|
||||
Args:
|
||||
position: The new position of the indicator
|
||||
"""
|
||||
self.set_value(position)
|
||||
|
||||
@Property(int)
|
||||
def indicator_width(self):
|
||||
"""
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
return self._indicator_width
|
||||
|
||||
@indicator_width.setter
|
||||
def indicator_width(self, width: int):
|
||||
"""
|
||||
Setter for the indicator width property
|
||||
|
||||
Args:
|
||||
width: The new width of the indicator
|
||||
"""
|
||||
self._indicator_width = width
|
||||
self.update()
|
||||
|
||||
@Property(int)
|
||||
def rounded_corners(self):
|
||||
"""
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
return self._rounded_corners
|
||||
|
||||
@rounded_corners.setter
|
||||
def rounded_corners(self, value: int):
|
||||
"""
|
||||
Setter for the rounded corners property
|
||||
|
||||
Args:
|
||||
value: The new value for the rounded corners
|
||||
"""
|
||||
self._rounded_corners = value
|
||||
self.update()
|
||||
|
||||
@Property(QColor)
|
||||
def indicator_color(self):
|
||||
"""
|
||||
Property to get the color of the indicator
|
||||
"""
|
||||
return self._indicator_color
|
||||
|
||||
@indicator_color.setter
|
||||
def indicator_color(self, color: QColor):
|
||||
"""
|
||||
Setter for the indicator color property
|
||||
|
||||
Args:
|
||||
color: The new color for the indicator
|
||||
"""
|
||||
self._indicator_color = color
|
||||
self.update()
|
||||
|
||||
@Property(QColor)
|
||||
def background_color(self):
|
||||
"""
|
||||
Property to get the background color of the position indicator
|
||||
"""
|
||||
return self._background_color
|
||||
|
||||
@background_color.setter
|
||||
def background_color(self, color: QColor):
|
||||
"""
|
||||
Setter for the background color property
|
||||
|
||||
Args:
|
||||
color: The new background color
|
||||
"""
|
||||
self._background_color = color
|
||||
self.update()
|
||||
|
||||
@Property(bool)
|
||||
def use_color_palette(self):
|
||||
"""
|
||||
Property to determine if the indicator should use the color palette or the custom color.
|
||||
"""
|
||||
return self._use_color_palette
|
||||
|
||||
@use_color_palette.setter
|
||||
def use_color_palette(self, use_palette: bool):
|
||||
"""
|
||||
Setter for the use color palette property
|
||||
|
||||
Args:
|
||||
use_palette: True if the indicator should use the color palette, False if custom color
|
||||
"""
|
||||
self._use_color_palette = use_palette
|
||||
self.update()
|
||||
|
||||
# @Property(float)
|
||||
@Slot(int)
|
||||
@Slot(float)
|
||||
def on_position_update(self, position: float):
|
||||
def set_value(self, position: float):
|
||||
self.position = position
|
||||
self.update()
|
||||
|
||||
def _get_indicator_color(self):
|
||||
if self._use_color_palette:
|
||||
return get_accent_colors().success
|
||||
return self._indicator_color
|
||||
|
||||
def _get_background_brush(self):
|
||||
if self._use_color_palette:
|
||||
return get_theme_palette().mid()
|
||||
return QBrush(self._background_color)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
width = self.width()
|
||||
height = self.height()
|
||||
|
||||
# Draw horizontal line
|
||||
painter.setPen(Qt.black)
|
||||
painter.drawLine(0, height // 2, width, height // 2)
|
||||
# Set up the brush for the background
|
||||
painter.setBrush(self._get_background_brush())
|
||||
|
||||
# Draw shorter vertical line at the current position
|
||||
x_pos = int(self.position * width)
|
||||
painter.setPen(QPen(Qt.red, 2))
|
||||
short_line_height = int(height * self.scaling_factor)
|
||||
painter.drawLine(
|
||||
x_pos,
|
||||
(height // 2) - (short_line_height // 2),
|
||||
x_pos,
|
||||
(height // 2) + (short_line_height // 2),
|
||||
# Create a QPainterPath with a rounded rectangle for clipping
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(0, 0, width, height, self._rounded_corners, self._rounded_corners)
|
||||
|
||||
# Set clipping to the rounded rectangle
|
||||
painter.setClipPath(path)
|
||||
|
||||
# Draw the rounded rectangle background first
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawRoundedRect(0, 0, width, height, self._rounded_corners, self._rounded_corners)
|
||||
|
||||
# get the position scaled to the defined min and max values
|
||||
self._current_indicator_position = position = np.interp(
|
||||
self.position, [self.min_value, self.max_value], [0, 100]
|
||||
)
|
||||
|
||||
# Draw thicker vertical lines at the ends
|
||||
end_line_pen = QPen(Qt.blue, 5)
|
||||
painter.setPen(end_line_pen)
|
||||
painter.drawLine(0, 0, 0, height)
|
||||
painter.drawLine(width - 1, 0, width - 1, height)
|
||||
if self.is_vertical:
|
||||
# If vertical, rotate the coordinate system by -90 degrees
|
||||
painter.translate(width // 2, height // 2) # Move origin to center
|
||||
painter.rotate(-90) # Rotate by -90 degrees for vertical drawing
|
||||
painter.translate(-height // 2, -width // 2) # Restore the origin for drawing
|
||||
|
||||
# Switch width and height for the vertical orientation
|
||||
width, height = height, width
|
||||
|
||||
# Draw the moving vertical indicator, respecting the clip path
|
||||
self._draw_position = x_pos = round(
|
||||
position * width / 100
|
||||
) # Position for the vertical line
|
||||
|
||||
indicator_pen = QPen(self._get_indicator_color(), self._indicator_width)
|
||||
painter.setPen(indicator_pen)
|
||||
painter.drawLine(x_pos, 0, x_pos, height)
|
||||
|
||||
painter.end()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
# Set the smallest possible size
|
||||
return QSize(10, 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from bec_qthemes import setup_theme
|
||||
from qtpy.QtWidgets import QApplication, QSlider, QVBoxLayout
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
setup_theme("dark")
|
||||
# Create position indicator and slider
|
||||
position_indicator = PositionIndicator()
|
||||
# position_indicator.set_range(0, 1)
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.valueChanged.connect(lambda value: position_indicator.on_position_update(value / 100))
|
||||
|
||||
slider.valueChanged.connect(lambda value: position_indicator.set_value(value))
|
||||
position_indicator.is_vertical = False
|
||||
# position_indicator.set_value(100)
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(position_indicator)
|
||||
layout.addWidget(slider)
|
||||
|
||||
@@ -14,9 +14,11 @@ from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtGui import QDoubleValidator
|
||||
from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.utils.colors import get_accent_colors, set_theme
|
||||
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -24,7 +26,7 @@ logger = bec_logger.logger
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
class PositionerBox(BECWidget, QWidget):
|
||||
class PositionerBox(BECWidget, CompactPopupWidget):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
ui_file = "positioner_box.ui"
|
||||
@@ -33,6 +35,8 @@ class PositionerBox(BECWidget, QWidget):
|
||||
ICON_NAME = "switch_right"
|
||||
USER_ACCESS = ["set_positioner"]
|
||||
device_changed = Signal(str, str)
|
||||
# Signal emitted to inform listeners about a position update
|
||||
position_update = Signal(float)
|
||||
|
||||
def __init__(self, parent=None, device: Positioner = None, **kwargs):
|
||||
"""Initialize the PositionerBox widget.
|
||||
@@ -42,7 +46,7 @@ class PositionerBox(BECWidget, QWidget):
|
||||
device (Positioner): The device to control.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
|
||||
self.get_bec_shortcuts()
|
||||
self._device = ""
|
||||
self._limits = None
|
||||
@@ -61,8 +65,7 @@ class PositionerBox(BECWidget, QWidget):
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.ui)
|
||||
self.addWidget(self.ui)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
@@ -73,6 +76,10 @@ class PositionerBox(BECWidget, QWidget):
|
||||
|
||||
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
|
||||
self.ui.stop.clicked.connect(self.on_stop)
|
||||
self.ui.stop.setToolTip("Stop")
|
||||
self.ui.stop.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
self.ui.tweak_right.clicked.connect(self.on_tweak_right)
|
||||
self.ui.tweak_right.setToolTip("Tweak right")
|
||||
self.ui.tweak_left.clicked.connect(self.on_tweak_left)
|
||||
@@ -91,7 +98,9 @@ class PositionerBox(BECWidget, QWidget):
|
||||
self._dialog = QDialog(self)
|
||||
self._dialog.setWindowTitle("Positioner Selection")
|
||||
layout = QVBoxLayout()
|
||||
line_edit = DeviceLineEdit(self, client=self.client, device_filter="Positioner")
|
||||
line_edit = DeviceLineEdit(
|
||||
self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
|
||||
)
|
||||
line_edit.textChanged.connect(self.set_positioner)
|
||||
layout.addWidget(line_edit)
|
||||
close_button = QPushButton("Close")
|
||||
@@ -129,8 +138,12 @@ class PositionerBox(BECWidget, QWidget):
|
||||
"""Setter, checks if device is a string"""
|
||||
if not value or not isinstance(value, str):
|
||||
return
|
||||
if not self._check_device_is_valid(value):
|
||||
return
|
||||
old_device = self._device
|
||||
self._device = value
|
||||
if not self.label:
|
||||
self.label = value
|
||||
self.device_changed.emit(old_device, value)
|
||||
|
||||
@Property(bool)
|
||||
@@ -235,12 +248,15 @@ class PositionerBox(BECWidget, QWidget):
|
||||
if is_moving:
|
||||
self.ui.spinner_widget.start()
|
||||
self.ui.spinner_widget.setToolTip("Device is moving")
|
||||
self.set_global_state("warning")
|
||||
else:
|
||||
self.ui.spinner_widget.stop()
|
||||
self.ui.spinner_widget.setToolTip("Device is idle")
|
||||
self.set_global_state("success")
|
||||
|
||||
if readback_val is not None:
|
||||
self.ui.readback.setText(f"{readback_val:.{precision}f}")
|
||||
self.position_update.emit(readback_val)
|
||||
|
||||
if setpoint_val is not None:
|
||||
self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
|
||||
@@ -249,7 +265,7 @@ class PositionerBox(BECWidget, QWidget):
|
||||
self.update_limits(limits)
|
||||
if limits is not None and readback_val is not None and limits[0] != limits[1]:
|
||||
pos = (readback_val - limits[0]) / (limits[1] - limits[0])
|
||||
self.ui.position_indicator.on_position_update(pos)
|
||||
self.ui.position_indicator.set_value(pos)
|
||||
|
||||
def update_limits(self, limits: tuple):
|
||||
"""Update limits
|
||||
@@ -314,7 +330,7 @@ class PositionerBox(BECWidget, QWidget):
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("dark")
|
||||
|
||||
@@ -170,7 +170,20 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PositionIndicator" name="position_indicator"/>
|
||||
<widget class="PositionIndicator" name="position_indicator">
|
||||
<property name="maximum" stdset="0">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="value" stdset="0">
|
||||
<double>0.500000000000000</double>
|
||||
</property>
|
||||
<property name="indicator_width" stdset="0">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rounded_corners" stdset="0">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="readback">
|
||||
|
||||
@@ -24,11 +24,9 @@ class PositionerControlLine(PositionerBox):
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
import qdarktheme
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("dark")
|
||||
widget = PositionerControlLine(device="samy")
|
||||
|
||||
widget.show()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>785</width>
|
||||
<width>612</width>
|
||||
<height>91</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -59,16 +59,6 @@
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="PositionIndicator" name="position_indicator">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SpinnerWidget" name="spinner_widget">
|
||||
<property name="minimumSize">
|
||||
@@ -139,14 +129,14 @@
|
||||
<widget class="QToolButton" name="tweak_left">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -154,8 +144,8 @@
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
@@ -170,14 +160,14 @@
|
||||
<widget class="QToolButton" name="tweak_right">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -185,8 +175,8 @@
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
@@ -194,6 +184,34 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PositionIndicator" name="position_indicator">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>15</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>15</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximum" stdset="0">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="vertical" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="value" stdset="0">
|
||||
<double>0.500000000000000</double>
|
||||
</property>
|
||||
<property name="rounded_corners" stdset="0">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
0
bec_widgets/widgets/positioner_group/__init__.py
Normal file
170
bec_widgets/widgets/positioner_group/positioner_group.py
Normal file
@@ -0,0 +1,170 @@
|
||||
""" Module for a PositionerGroup widget to control a positioner device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, QSize, Signal, Slot
|
||||
from qtpy.QtWidgets import QGridLayout, QGroupBox, QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class PositionerGroupBox(QGroupBox):
|
||||
|
||||
position_update = Signal(float)
|
||||
|
||||
def __init__(self, parent, dev_name):
|
||||
super().__init__(parent)
|
||||
|
||||
self.device_name = dev_name
|
||||
|
||||
QVBoxLayout(self)
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
self.layout().setSpacing(0)
|
||||
self.widget = PositionerBox(self, dev_name)
|
||||
self.widget.compact_view = True
|
||||
self.widget.expand_popup = False
|
||||
self.layout().addWidget(self.widget)
|
||||
self.widget.position_update.connect(self._on_position_update)
|
||||
self.widget.expand.connect(self._on_expand)
|
||||
self.setTitle(self.device_name)
|
||||
self.widget.init_device() # force readback
|
||||
|
||||
def _on_expand(self, expand):
|
||||
if expand:
|
||||
self.setTitle("")
|
||||
self.setFlat(True)
|
||||
else:
|
||||
self.setTitle(self.device_name)
|
||||
self.setFlat(False)
|
||||
|
||||
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
|
||||
|
||||
def close(self):
|
||||
self.widget.close()
|
||||
super().close()
|
||||
|
||||
|
||||
class PositionerGroup(BECWidget, QWidget):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
ICON_NAME = "grid_view"
|
||||
USER_ACCESS = ["set_positioners"]
|
||||
|
||||
# Signal emitted to inform listeners about a position update of the first positioner
|
||||
position_update = Signal(float)
|
||||
# Signal emitted to inform listeners about (positioner, pos) updates
|
||||
device_position_update = Signal(str, float)
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
"""Initialize the widget.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
QWidget.__init__(self, parent)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
QGridLayout(self)
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self._device_widgets = {}
|
||||
self._grid_ncols = 2
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return QSize(300, 30)
|
||||
|
||||
@Slot(str)
|
||||
def set_positioners(self, device_names: str):
|
||||
"""Redraw grid with positioners from device_names string
|
||||
|
||||
Device names must be separated by space
|
||||
"""
|
||||
devs = device_names.split()
|
||||
for dev_name in devs:
|
||||
if not self._check_device_is_valid(dev_name):
|
||||
raise ValueError(f"{dev_name} is not a valid Positioner")
|
||||
for i, existing_widget in enumerate(self._device_widgets.values()):
|
||||
self.layout().removeWidget(existing_widget)
|
||||
existing_widget.position_update.disconnect(self._on_position_update)
|
||||
if i == 0:
|
||||
existing_widget.position_update.disconnect(self.position_update)
|
||||
for i, dev_name in enumerate(devs):
|
||||
widget = self._device_widgets.get(dev_name)
|
||||
if widget is None:
|
||||
widget = PositionerGroupBox(self, dev_name)
|
||||
self._device_widgets[dev_name] = widget
|
||||
widget.position_update.connect(self._on_position_update)
|
||||
if i == 0:
|
||||
# only emit 'position_update' for the first positioner in grid
|
||||
widget.position_update.connect(self.position_update)
|
||||
self.layout().addWidget(widget, i // self._grid_ncols, i % self._grid_ncols)
|
||||
to_remove = set(self._device_widgets) - set(devs)
|
||||
for dev_name in to_remove:
|
||||
self._device_widgets[dev_name].close()
|
||||
del self._device_widgets[dev_name]
|
||||
|
||||
def _check_device_is_valid(self, device: str):
|
||||
"""Check if the device is a positioner
|
||||
|
||||
Args:
|
||||
device (str): The device name
|
||||
"""
|
||||
if device not in self.dev:
|
||||
logger.info(f"Device {device} not found in the device list")
|
||||
return False
|
||||
if not isinstance(self.dev[device], Positioner):
|
||||
logger.info(f"Device {device} is not a positioner")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _on_position_update(self, pos: float):
|
||||
widget = self.sender()
|
||||
self.device_position_update.emit(widget.title(), pos)
|
||||
|
||||
@Property(str)
|
||||
def devices_list(self):
|
||||
"""Device names string separated by space"""
|
||||
return " ".join(self._device_widgets)
|
||||
|
||||
@devices_list.setter
|
||||
def devices_list(self, device_names: str):
|
||||
"""Set devices list from device names string separated by space"""
|
||||
devs = device_names.split()
|
||||
for dev_name in devs:
|
||||
if not self._check_device_is_valid(dev_name):
|
||||
return
|
||||
self.set_positioners(device_names)
|
||||
|
||||
@Property(int)
|
||||
def grid_max_cols(self):
|
||||
"""Max number of columns for widgets grid"""
|
||||
return self._grid_ncols
|
||||
|
||||
@grid_max_cols.setter
|
||||
def grid_max_cols(self, ncols: int):
|
||||
"""Set max number of columns for widgets grid"""
|
||||
self._grid_ncols = ncols
|
||||
self.set_positioners(self.devices_list)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = PositionerGroup()
|
||||
widget.grid_max_cols = 3
|
||||
widget.set_positioners("samx samy samz")
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['positioner_group.py']}
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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 bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.positioner_group.positioner_group import PositionerGroup
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='PositionerGroup' name='positioner_group'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = PositionerGroup(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(PositionerGroup.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "positioner_group"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "PositionerGroup"
|
||||
|
||||
def toolTip(self):
|
||||
return "Container Widget to control positioners in compact form, in a grid"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.positioner_group.positioner_group_plugin import PositionerGroupPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerGroupPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,4 +1,5 @@
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
from typing import Optional
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
@@ -7,11 +8,11 @@ from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
@@ -19,6 +20,7 @@ from qtpy.QtWidgets import (
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.widgets.scan_control.scan_group_box import ScanGroupBox
|
||||
from bec_widgets.widgets.stop_button.stop_button import StopButton
|
||||
from bec_widgets.widgets.toggle.toggle import ToggleSwitch
|
||||
@@ -42,6 +44,8 @@ class ScanControl(BECWidget, QWidget):
|
||||
|
||||
scan_started = Signal()
|
||||
scan_selected = Signal(str)
|
||||
device_selected = Signal(str)
|
||||
scan_args = Signal(list)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -60,6 +64,8 @@ class ScanControl(BECWidget, QWidget):
|
||||
super().__init__(client=client, gui_id=gui_id, config=config)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
self._hide_add_remove_buttons = False
|
||||
|
||||
# Client from BEC + shortcuts to device manager and scans
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@@ -83,12 +89,54 @@ class ScanControl(BECWidget, QWidget):
|
||||
"""
|
||||
Initializes the UI of the scan control widget. Create the top box for scan selection and populate scans to main combobox.
|
||||
"""
|
||||
palette = get_accent_colors()
|
||||
if palette is None:
|
||||
palette = SimpleNamespace(
|
||||
default=QColor("blue"),
|
||||
success=QColor("green"),
|
||||
warning=QColor("orange"),
|
||||
emergency=QColor("red"),
|
||||
)
|
||||
# Scan selection box
|
||||
self.scan_selection_group = QWidget(self)
|
||||
QVBoxLayout(self.scan_selection_group)
|
||||
scan_selection_layout = QHBoxLayout()
|
||||
self.comboBox_scan_selection_label = QLabel("Scan:", self.scan_selection_group)
|
||||
self.comboBox_scan_selection = QComboBox(self.scan_selection_group)
|
||||
scan_selection_layout.addWidget(self.comboBox_scan_selection_label, 0)
|
||||
scan_selection_layout.addWidget(self.comboBox_scan_selection, 1)
|
||||
self.scan_selection_group.layout().addLayout(scan_selection_layout)
|
||||
|
||||
# Scan selection group box
|
||||
self.scan_selection_group = self.create_scan_selection_group()
|
||||
# Label to reload the last scan parameters within scan selection group box
|
||||
self.toggle_layout = QHBoxLayout()
|
||||
self.toggle_layout.addSpacerItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
)
|
||||
self.last_scan_label = QLabel("Restore last scan parameters", self.scan_selection_group)
|
||||
self.toggle = ToggleSwitch(parent=self.scan_selection_group, checked=False)
|
||||
self.toggle.enabled.connect(self.request_last_executed_scan_parameters)
|
||||
self.toggle_layout.addWidget(self.last_scan_label)
|
||||
self.toggle_layout.addWidget(self.toggle)
|
||||
self.scan_selection_group.layout().addLayout(self.toggle_layout)
|
||||
self.scan_selection_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.layout.addWidget(self.scan_selection_group)
|
||||
|
||||
# Scan control (Run/Stop) buttons
|
||||
self.scan_control_group = QWidget(self)
|
||||
self.scan_control_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.button_layout = QHBoxLayout(self.scan_control_group)
|
||||
self.button_run_scan = QPushButton("Start", self.scan_control_group)
|
||||
self.button_run_scan.setStyleSheet(
|
||||
f"background-color: {palette.success.name()}; color: white"
|
||||
)
|
||||
self.button_stop_scan = StopButton(parent=self.scan_control_group)
|
||||
self.button_stop_scan.setStyleSheet(
|
||||
f"background-color: {palette.emergency.name()}; color: white"
|
||||
)
|
||||
self.button_layout.addWidget(self.button_run_scan)
|
||||
self.button_layout.addWidget(self.button_stop_scan)
|
||||
self.layout.addWidget(self.scan_control_group)
|
||||
|
||||
# Default scan from config
|
||||
if self.config.default_scan is not None:
|
||||
self.comboBox_scan_selection.setCurrentText(self.config.default_scan)
|
||||
@@ -98,67 +146,11 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.comboBox_scan_selection.currentIndexChanged.connect(self.on_scan_selection_changed)
|
||||
self.button_run_scan.clicked.connect(self.run_scan)
|
||||
|
||||
# Add bundle button
|
||||
self.button_add_bundle = QPushButton("Add Bundle")
|
||||
self.button_add_bundle.setVisible(False)
|
||||
# Remove bundle button
|
||||
self.button_remove_bundle = QPushButton("Remove Bundle")
|
||||
self.button_remove_bundle.setVisible(False)
|
||||
|
||||
bundle_layout = QHBoxLayout()
|
||||
bundle_layout.addWidget(self.button_add_bundle)
|
||||
bundle_layout.addWidget(self.button_remove_bundle)
|
||||
self.layout.addLayout(bundle_layout)
|
||||
|
||||
self.button_add_bundle.clicked.connect(self.add_arg_bundle)
|
||||
self.button_remove_bundle.clicked.connect(self.remove_arg_bundle)
|
||||
|
||||
self.scan_selected.connect(self.scan_select)
|
||||
|
||||
# Initialize scan selection
|
||||
self.populate_scans()
|
||||
|
||||
def create_scan_selection_group(self) -> QGroupBox:
|
||||
"""
|
||||
Creates the scan selection group box with combobox to select the scan and start/stop button.
|
||||
|
||||
Returns:
|
||||
QGroupBox: Group box containing the scan selection widgets.
|
||||
"""
|
||||
|
||||
scan_selection_group = QGroupBox("Scan Selection", self)
|
||||
self.scan_selection_layout = QVBoxLayout(scan_selection_group)
|
||||
self.comboBox_scan_selection = QComboBox(scan_selection_group)
|
||||
|
||||
# Buttons
|
||||
self.button_layout = QHBoxLayout()
|
||||
## Run button
|
||||
self.button_run_scan = QPushButton("Start", scan_selection_group)
|
||||
self.button_run_scan.setStyleSheet("background-color: #559900; color: white")
|
||||
## Stop button
|
||||
self.button_stop_scan = StopButton(parent=scan_selection_group)
|
||||
## Add buttons to layout
|
||||
self.button_layout.addWidget(self.button_run_scan)
|
||||
self.button_layout.addWidget(self.button_stop_scan)
|
||||
|
||||
# Label to reload the last scan parameters
|
||||
self.toggle_layout = QHBoxLayout()
|
||||
## Label
|
||||
self.last_scan_label = QLabel("Restore last scan parameters", scan_selection_group)
|
||||
## Switch toggle button
|
||||
self.toggle = ToggleSwitch(parent=scan_selection_group, checked=False)
|
||||
self.toggle.enabled.connect(self.request_last_executed_scan_parameters)
|
||||
## Add label and switch to layout
|
||||
self.toggle_layout.addWidget(self.last_scan_label)
|
||||
self.toggle_layout.addWidget(self.toggle)
|
||||
|
||||
# Add widgets to layout
|
||||
self.scan_selection_layout.addWidget(self.comboBox_scan_selection)
|
||||
self.scan_selection_layout.addLayout(self.button_layout)
|
||||
self.scan_selection_layout.addLayout(self.toggle_layout)
|
||||
|
||||
return scan_selection_group
|
||||
|
||||
def populate_scans(self):
|
||||
"""Populates the scan selection combo box with available scans from BEC session."""
|
||||
self.available_scans = self.client.connector.get(
|
||||
@@ -244,27 +236,42 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.current_scan = scan_name
|
||||
|
||||
@Property(bool)
|
||||
def hide_scan_remember_toggle(self):
|
||||
"""Property to hide the scan remember toggle."""
|
||||
return not self.toggle.isVisible()
|
||||
def hide_arg_box(self):
|
||||
"""Property to hide the argument box."""
|
||||
if self.arg_box is None:
|
||||
return True
|
||||
return not self.arg_box.isVisible()
|
||||
|
||||
@hide_scan_remember_toggle.setter
|
||||
def hide_scan_remember_toggle(self, hide: bool):
|
||||
"""Setter for the hide_scan_remember_toggle property.
|
||||
@hide_arg_box.setter
|
||||
def hide_arg_box(self, hide: bool):
|
||||
"""Setter for the hide_arg_box property.
|
||||
|
||||
Args:
|
||||
hide(bool): Hide or show the scan remember toggle.
|
||||
hide(bool): Hide or show the argument box.
|
||||
"""
|
||||
self.show_scan_remember_toggle(not hide)
|
||||
if self.arg_box is not None:
|
||||
self.arg_box.setVisible(not hide)
|
||||
|
||||
@Slot(bool)
|
||||
def show_scan_remember_toggle(self, show: bool):
|
||||
"""Shows or hides the scan control buttons."""
|
||||
self.toggle.setVisible(show)
|
||||
self.last_scan_label.setVisible(show)
|
||||
@Property(bool)
|
||||
def hide_kwarg_boxes(self):
|
||||
"""Property to hide the keyword argument boxes."""
|
||||
if len(self.kwarg_boxes) == 0:
|
||||
return True
|
||||
|
||||
show_group = show or self.button_run_scan.isVisible()
|
||||
self.scan_selection_group.setVisible(show_group)
|
||||
for box in self.kwarg_boxes:
|
||||
if box is not None:
|
||||
return not box.isVisible()
|
||||
|
||||
@hide_kwarg_boxes.setter
|
||||
def hide_kwarg_boxes(self, hide: bool):
|
||||
"""Setter for the hide_kwarg_boxes property.
|
||||
|
||||
Args:
|
||||
hide(bool): Hide or show the keyword argument boxes.
|
||||
"""
|
||||
if len(self.kwarg_boxes) > 0:
|
||||
for box in self.kwarg_boxes:
|
||||
box.setVisible(not hide)
|
||||
|
||||
@Property(bool)
|
||||
def hide_scan_control_buttons(self):
|
||||
@@ -283,11 +290,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
@Slot(bool)
|
||||
def show_scan_control_buttons(self, show: bool):
|
||||
"""Shows or hides the scan control buttons."""
|
||||
self.button_run_scan.setVisible(show)
|
||||
self.button_stop_scan.setVisible(show)
|
||||
|
||||
show_group = show or self.button_run_scan.isVisible()
|
||||
self.scan_selection_group.setVisible(show_group)
|
||||
self.scan_control_group.setVisible(show)
|
||||
|
||||
@Property(bool)
|
||||
def hide_scan_selection_combobox(self):
|
||||
@@ -306,10 +309,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
@Slot(bool)
|
||||
def show_scan_selection_combobox(self, show: bool):
|
||||
"""Shows or hides the scan selection combobox."""
|
||||
self.comboBox_scan_selection.setVisible(show)
|
||||
|
||||
show_group = show or self.button_run_scan.isVisible()
|
||||
self.scan_selection_group.setVisible(show_group)
|
||||
self.scan_selection_group.setVisible(show)
|
||||
|
||||
@Slot(str)
|
||||
def scan_select(self, scan_name: str):
|
||||
@@ -326,11 +326,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.arg_group = gui_config.get("arg_group", None)
|
||||
self.kwarg_groups = gui_config.get("kwarg_groups", None)
|
||||
|
||||
show_bundle_buttons = bool(self.arg_group["arg_inputs"])
|
||||
|
||||
self._show_bundle_buttons(show_bundle_buttons)
|
||||
|
||||
if show_bundle_buttons:
|
||||
if bool(self.arg_group["arg_inputs"]):
|
||||
self.add_arg_group(self.arg_group)
|
||||
if len(self.kwarg_groups) > 0:
|
||||
self.add_kwargs_boxes(self.kwarg_groups)
|
||||
@@ -338,28 +334,21 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.update()
|
||||
self.adjustSize()
|
||||
|
||||
def _show_bundle_buttons(self, show: bool):
|
||||
"""Shows or hides the bundle buttons based on the show argument.
|
||||
|
||||
Args:
|
||||
show(bool): Show or hide the bundle buttons.
|
||||
"""
|
||||
self.button_add_bundle.setVisible(show)
|
||||
self.button_remove_bundle.setVisible(show)
|
||||
|
||||
@Property(bool)
|
||||
def hide_bundle_buttons(self):
|
||||
"""Property to hide the bundle buttons."""
|
||||
return not self.button_add_bundle.isVisible()
|
||||
def hide_add_remove_buttons(self):
|
||||
"""Property to hide the add_remove buttons."""
|
||||
return self._hide_add_remove_buttons
|
||||
|
||||
@hide_bundle_buttons.setter
|
||||
def hide_bundle_buttons(self, hide: bool):
|
||||
"""Setter for the hide_bundle_buttons property.
|
||||
@hide_add_remove_buttons.setter
|
||||
def hide_add_remove_buttons(self, hide: bool):
|
||||
"""Setter for the hide_add_remove_buttons property.
|
||||
|
||||
Args:
|
||||
hide(bool): Hide or show the bundle buttons.
|
||||
hide(bool): Hide or show the add_remove buttons.
|
||||
"""
|
||||
self._show_bundle_buttons(not hide)
|
||||
self._hide_add_remove_buttons = hide
|
||||
if self.arg_box is not None:
|
||||
self.arg_box.hide_add_remove_buttons = hide
|
||||
|
||||
def add_kwargs_boxes(self, groups: list):
|
||||
"""
|
||||
@@ -381,18 +370,20 @@ class ScanControl(BECWidget, QWidget):
|
||||
Args:
|
||||
"""
|
||||
self.arg_box = ScanGroupBox(box_type="args", config=group)
|
||||
self.arg_box.device_selected.connect(self.emit_device_selected)
|
||||
self.arg_box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.arg_box.hide_add_remove_buttons = self._hide_add_remove_buttons
|
||||
self.layout.addWidget(self.arg_box)
|
||||
|
||||
@Slot()
|
||||
def add_arg_bundle(self):
|
||||
"""Adds a new argument bundle to the scan control layout."""
|
||||
self.arg_box.add_widget_bundle()
|
||||
@Slot(str)
|
||||
def emit_device_selected(self, dev_names):
|
||||
"""
|
||||
Emit the signal to inform about selected device(s)
|
||||
|
||||
@Slot()
|
||||
def remove_arg_bundle(self):
|
||||
"""Removes the last argument bundle from the scan control layout."""
|
||||
self.arg_box.remove_widget_bundle()
|
||||
"dev_names" is a string separated by space, in case of multiple devices
|
||||
"""
|
||||
self._selected_devices = dev_names
|
||||
self.device_selected.emit(dev_names)
|
||||
|
||||
def reset_layout(self):
|
||||
"""Clears the scan control layout from GuiGroups and ArgGroups boxes."""
|
||||
@@ -465,10 +456,11 @@ class ScanControl(BECWidget, QWidget):
|
||||
@SafeSlot(popup_error=True)
|
||||
def run_scan(self):
|
||||
"""Starts the selected scan with the given parameters."""
|
||||
self.scan_started.emit()
|
||||
args, kwargs = self.get_scan_parameters()
|
||||
self.scan_args.emit(args)
|
||||
scan_function = getattr(self.scans, self.comboBox_scan_selection.currentText())
|
||||
if callable(scan_function):
|
||||
self.scan_started.emit()
|
||||
scan_function(*args, **kwargs)
|
||||
|
||||
def cleanup(self):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Qt
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Property, Qt, Signal, Slot
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
@@ -11,12 +12,16 @@ from qtpy.QtWidgets import (
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -133,6 +138,8 @@ class ScanGroupBox(QGroupBox):
|
||||
ScanArgType.LITERALS: QComboBox, # TODO figure out combobox logic
|
||||
}
|
||||
|
||||
device_selected = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
@@ -144,13 +151,36 @@ class ScanGroupBox(QGroupBox):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
self.config = config
|
||||
self.box_type = box_type
|
||||
self._hide_add_remove_buttons = False
|
||||
|
||||
vbox_layout = QVBoxLayout(self)
|
||||
hbox_layout = QHBoxLayout()
|
||||
vbox_layout.addLayout(hbox_layout)
|
||||
self.layout = QGridLayout(self)
|
||||
vbox_layout.addLayout(self.layout)
|
||||
|
||||
# Add bundle button
|
||||
self.button_add_bundle = QPushButton(self)
|
||||
self.button_add_bundle.setIcon(
|
||||
material_icon(icon_name="add", size=(15, 15), convert_to_pixmap=False)
|
||||
)
|
||||
# Remove bundle button
|
||||
self.button_remove_bundle = QPushButton(self)
|
||||
self.button_remove_bundle.setIcon(
|
||||
material_icon(icon_name="remove", size=(15, 15), convert_to_pixmap=False)
|
||||
)
|
||||
hbox_layout.addWidget(self.button_add_bundle)
|
||||
hbox_layout.addWidget(self.button_remove_bundle)
|
||||
|
||||
self.labels = []
|
||||
self.widgets = []
|
||||
self.selected_devices = {}
|
||||
|
||||
self.init_box(self.config)
|
||||
|
||||
self.button_add_bundle.clicked.connect(self.add_widget_bundle)
|
||||
self.button_remove_bundle.clicked.connect(self.remove_widget_bundle)
|
||||
|
||||
def init_box(self, config: dict):
|
||||
box_name = config.get("name", "ScanGroupBox")
|
||||
self.inputs = config.get("inputs", {})
|
||||
@@ -166,6 +196,8 @@ class ScanGroupBox(QGroupBox):
|
||||
self.add_input_widgets(self.inputs, i)
|
||||
else:
|
||||
self.add_input_widgets(self.inputs, 1)
|
||||
self.button_add_bundle.setVisible(False)
|
||||
self.button_remove_bundle.setVisible(False)
|
||||
|
||||
def add_input_labels(self, group_inputs: dict, row: int) -> None:
|
||||
"""
|
||||
@@ -192,27 +224,35 @@ class ScanGroupBox(QGroupBox):
|
||||
for column_index, item in enumerate(group_inputs):
|
||||
arg_name = item.get("name", None)
|
||||
default = item.get("default", None)
|
||||
widget = self.WIDGET_HANDLER.get(item["type"], None)
|
||||
if widget is None:
|
||||
widget_class = self.WIDGET_HANDLER.get(item["type"], None)
|
||||
if widget_class is None:
|
||||
logger.error(
|
||||
f"Unsupported annotation '{item['type']}' for parameter '{item['name']}'"
|
||||
)
|
||||
continue
|
||||
if default == "_empty":
|
||||
default = None
|
||||
widget_to_add = widget(arg_name=arg_name, default=default)
|
||||
widget = widget_class(arg_name=arg_name, default=default)
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
widget.set_device_filter(BECDeviceFilter.DEVICE)
|
||||
self.selected_devices[widget] = ""
|
||||
widget.device_selected.connect(self.emit_device_selected)
|
||||
tooltip = item.get("tooltip", None)
|
||||
if tooltip is not None:
|
||||
widget_to_add.setToolTip(item["tooltip"])
|
||||
self.layout.addWidget(widget_to_add, row, column_index)
|
||||
self.widgets.append(widget_to_add)
|
||||
widget.setToolTip(item["tooltip"])
|
||||
self.layout.addWidget(widget, row, column_index)
|
||||
self.widgets.append(widget)
|
||||
|
||||
@Slot(str)
|
||||
def emit_device_selected(self, device_name):
|
||||
self.selected_devices[self.sender()] = device_name.strip()
|
||||
selected_devices_str = " ".join(self.selected_devices.values())
|
||||
self.device_selected.emit(selected_devices_str)
|
||||
|
||||
def add_widget_bundle(self):
|
||||
"""
|
||||
Adds a new row of widgets to the scan control layout. Only usable for arg_groups.
|
||||
"""
|
||||
if self.box_type != "args":
|
||||
return
|
||||
arg_max = self.config.get("max", None)
|
||||
row = self.layout.rowCount()
|
||||
if arg_max is not None and row >= arg_max:
|
||||
@@ -224,17 +264,34 @@ class ScanGroupBox(QGroupBox):
|
||||
"""
|
||||
Removes the last row of widgets from the scan control layout. Only usable for arg_groups.
|
||||
"""
|
||||
if self.box_type != "args":
|
||||
return
|
||||
arg_min = self.config.get("min", None)
|
||||
row = self.count_arg_rows()
|
||||
if arg_min is not None and row <= arg_min:
|
||||
return
|
||||
|
||||
for widget in self.widgets[-len(self.inputs) :]:
|
||||
if isinstance(widget, DeviceLineEdit):
|
||||
self.selected_devices[widget] = ""
|
||||
widget.deleteLater()
|
||||
self.widgets = self.widgets[: -len(self.inputs)]
|
||||
|
||||
selected_devices_str = " ".join(self.selected_devices.values())
|
||||
self.device_selected.emit(selected_devices_str.strip())
|
||||
|
||||
@Property(bool)
|
||||
def hide_add_remove_buttons(self):
|
||||
return self._hide_add_remove_buttons
|
||||
|
||||
@hide_add_remove_buttons.setter
|
||||
def hide_add_remove_buttons(self, hide: bool):
|
||||
self._hide_add_remove_buttons = hide
|
||||
if not hide and self.box_type == "args":
|
||||
self.button_add_bundle.show()
|
||||
self.button_remove_bundle.show()
|
||||
return
|
||||
self.button_add_bundle.hide()
|
||||
self.button_remove_bundle.hide()
|
||||
|
||||
def get_parameters(self, device_object: bool = True):
|
||||
"""
|
||||
Returns the parameters from the widgets in the scan control layout formated to run scan from BEC.
|
||||
@@ -251,7 +308,7 @@ class ScanGroupBox(QGroupBox):
|
||||
try: # In case that the bundle size changes
|
||||
widget = self.layout.itemAtPosition(i, j).widget()
|
||||
if isinstance(widget, DeviceLineEdit) and device_object:
|
||||
value = widget.get_device()
|
||||
value = widget.get_current_device()
|
||||
else:
|
||||
value = WidgetIO.get_value(widget)
|
||||
args.append(value)
|
||||
|
||||
0
bec_widgets/widgets/signal_combobox/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.signal_combobox.signal_combobox_plugin import SignalComboBoxPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(SignalComboBoxPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
115
bec_widgets/widgets/signal_combobox/signal_combobox.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from bec_lib.device import Positioner
|
||||
from qtpy.QtCore import QSize, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox, QSizePolicy
|
||||
|
||||
from bec_widgets.utils.filter_io import ComboBoxFilterHandler, FilterIO
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
from bec_widgets.widgets.base_classes.device_signal_input_base import DeviceSignalInputBase
|
||||
|
||||
|
||||
class SignalComboBox(DeviceSignalInputBase, QComboBox):
|
||||
"""
|
||||
Line edit widget for device input with autocomplete for device names.
|
||||
|
||||
Args:
|
||||
parent: Parent widget.
|
||||
client: BEC client object.
|
||||
config: Device input configuration.
|
||||
gui_id: GUI ID.
|
||||
device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
|
||||
default: Default device name.
|
||||
arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
|
||||
"""
|
||||
|
||||
ICON_NAME = "list_alt"
|
||||
|
||||
device_signal_changed = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
client=None,
|
||||
config: DeviceSignalInputBase = None,
|
||||
gui_id: str | None = None,
|
||||
device: str | None = None,
|
||||
signal_filter: str | list[str] | None = None,
|
||||
default: str | None = None,
|
||||
arg_name: str | None = None,
|
||||
):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QComboBox.__init__(self, parent=parent)
|
||||
if arg_name is not None:
|
||||
self.config.arg_name = arg_name
|
||||
self.arg_name = arg_name
|
||||
if default is not None:
|
||||
self.set_device(default)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.setMinimumSize(QSize(100, 0))
|
||||
# We do not consider the config that is passed here, this produced problems
|
||||
# with QtDesigner, since config and input arguments may differ and resolve properly
|
||||
# Implementing this logic and config recoverage is postponed.
|
||||
self.currentTextChanged.connect(self.on_text_changed)
|
||||
if signal_filter is not None:
|
||||
self.set_filter(signal_filter)
|
||||
else:
|
||||
self.set_filter([Kind.hinted, Kind.normal, Kind.config])
|
||||
if device is not None:
|
||||
self.set_device(device)
|
||||
if default is not None:
|
||||
self.set_signal(default)
|
||||
|
||||
def update_signals_from_filters(self):
|
||||
"""Update the filters for the combobox"""
|
||||
super().update_signals_from_filters()
|
||||
# pylint: disable=protected-access
|
||||
if FilterIO._find_handler(self) is ComboBoxFilterHandler:
|
||||
if len(self._config_signals) > 0:
|
||||
self.insertItem(
|
||||
len(self._hinted_signals) + len(self._normal_signals), "Config Signals"
|
||||
)
|
||||
self.model().item(len(self._hinted_signals) + len(self._normal_signals)).setEnabled(
|
||||
False
|
||||
)
|
||||
if len(self._normal_signals) > 0:
|
||||
self.insertItem(len(self._hinted_signals), "Normal Signals")
|
||||
self.model().item(len(self._hinted_signals)).setEnabled(False)
|
||||
if len(self._hinted_signals) > 0:
|
||||
self.insertItem(0, "Hinted Signals")
|
||||
self.model().item(0).setEnabled(False)
|
||||
|
||||
@Slot(str)
|
||||
def on_text_changed(self, text: str):
|
||||
"""Slot for text changed. If a device is selected and the signal is changed and valid it emits a signal.
|
||||
For a positioner, the readback value has to be renamed to the device name.
|
||||
|
||||
Args:
|
||||
text (str): Text in the combobox.
|
||||
"""
|
||||
if self.validate_device(self.device) is False:
|
||||
return
|
||||
if self.validate_signal(text) is False:
|
||||
return
|
||||
if text == "readback" and isinstance(self.get_device_object(self.device), Positioner):
|
||||
device_signal = self.device
|
||||
else:
|
||||
device_signal = f"{self.device}_{text}"
|
||||
self.device_signal_changed.emit(device_signal)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
box = SignalComboBox(device="samx")
|
||||
layout.addWidget(box)
|
||||
widget.show()
|
||||
app.exec_()
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['signal_combobox.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.signal_combobox.signal_combobox import SignalComboBox
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='SignalComboBox' name='signal_combobox'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = SignalComboBox(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Input Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(SignalComboBox.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "signal_combobox"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "SignalComboBox"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
0
bec_widgets/widgets/signal_line_edit/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.signal_line_edit.signal_line_edit_plugin import SignalLineEditPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(SignalLineEditPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
140
bec_widgets/widgets/signal_line_edit/signal_line_edit.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from bec_lib.device import Positioner
|
||||
from qtpy.QtCore import QSize, Signal, Slot
|
||||
from qtpy.QtGui import QPainter, QPaintEvent, QPen
|
||||
from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
from bec_widgets.widgets.base_classes.device_signal_input_base import DeviceSignalInputBase
|
||||
|
||||
|
||||
class SignalLineEdit(DeviceSignalInputBase, QLineEdit):
|
||||
"""
|
||||
Line edit widget for device input with autocomplete for device names.
|
||||
|
||||
Args:
|
||||
parent: Parent widget.
|
||||
client: BEC client object.
|
||||
config: Device input configuration.
|
||||
gui_id: GUI ID.
|
||||
device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
|
||||
default: Default device name.
|
||||
arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
|
||||
"""
|
||||
|
||||
device_signal_changed = Signal(str)
|
||||
|
||||
ICON_NAME = "vital_signs"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
client=None,
|
||||
config: DeviceSignalInputBase = None,
|
||||
gui_id: str | None = None,
|
||||
device: str | None = None,
|
||||
signal_filter: str | list[str] | None = None,
|
||||
default: str | None = None,
|
||||
arg_name: str | None = None,
|
||||
):
|
||||
self._is_valid_input = False
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QLineEdit.__init__(self, parent=parent)
|
||||
self._accent_colors = get_accent_colors()
|
||||
self.completer = QCompleter(self)
|
||||
self.setCompleter(self.completer)
|
||||
if arg_name is not None:
|
||||
self.config.arg_name = arg_name
|
||||
self.arg_name = arg_name
|
||||
if default is not None:
|
||||
self.set_device(default)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.setMinimumSize(QSize(100, 0))
|
||||
# We do not consider the config that is passed here, this produced problems
|
||||
# with QtDesigner, since config and input arguments may differ and resolve properly
|
||||
# Implementing this logic and config recoverage is postponed.
|
||||
if signal_filter is not None:
|
||||
self.set_filter(signal_filter)
|
||||
else:
|
||||
self.set_filter([Kind.hinted, Kind.normal, Kind.config])
|
||||
if device is not None:
|
||||
self.set_device(device)
|
||||
if default is not None:
|
||||
self.set_signal(default)
|
||||
self.textChanged.connect(self.validate_device)
|
||||
self.validate_device(self.text())
|
||||
|
||||
def get_current_device(self) -> object:
|
||||
"""
|
||||
Get the current device object based on the current value.
|
||||
|
||||
Returns:
|
||||
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
||||
"""
|
||||
dev_name = self.text()
|
||||
return self.get_device_object(dev_name)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None:
|
||||
"""Extend the paint event to set the border color based on the validity of the input.
|
||||
|
||||
Args:
|
||||
event (PySide6.QtGui.QPaintEvent) : Paint event.
|
||||
"""
|
||||
super().paintEvent(event)
|
||||
painter = QPainter(self)
|
||||
pen = QPen()
|
||||
pen.setWidth(2)
|
||||
|
||||
if self._is_valid_input is False and self.isEnabled() is True:
|
||||
pen.setColor(self._accent_colors.emergency)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
|
||||
|
||||
@Slot(str)
|
||||
def check_validity(self, input_text: str) -> None:
|
||||
"""
|
||||
Check if the current value is a valid device name.
|
||||
"""
|
||||
if self.validate_signal(input_text) is True:
|
||||
self._is_valid_input = True
|
||||
self.on_text_changed(input_text)
|
||||
else:
|
||||
self._is_valid_input = False
|
||||
self.update()
|
||||
|
||||
@Slot(str)
|
||||
def on_text_changed(self, text: str):
|
||||
"""Slot for text changed. If a device is selected and the signal is changed and valid it emits a signal.
|
||||
For a positioner, the readback value has to be renamed to the device name.
|
||||
|
||||
Args:
|
||||
text (str): Text in the combobox.
|
||||
"""
|
||||
print("test")
|
||||
if self.validate_device(self.device) is False:
|
||||
return
|
||||
if self.validate_signal(text) is False:
|
||||
return
|
||||
if text == "readback" and isinstance(self.get_device_object(self.device), Positioner):
|
||||
device_signal = self.device
|
||||
else:
|
||||
device_signal = f"{self.device}_{text}"
|
||||
self.device_signal_changed.emit(device_signal)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(SignalLineEdit(device="samx"))
|
||||
widget.show()
|
||||
app.exec_()
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['signal_line_edit.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.signal_line_edit.signal_line_edit import SignalLineEdit
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='SignalLineEdit' name='signal_line_edit'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = SignalLineEdit(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Input Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(SignalLineEdit.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "signal_line_edit"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "SignalLineEdit"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -1,6 +1,6 @@
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QToolButton, QWidget
|
||||
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QSizePolicy, QToolButton, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
@@ -28,9 +28,10 @@ class StopButton(BECWidget, QWidget):
|
||||
self.button.setToolTip("Stop the scan queue")
|
||||
else:
|
||||
self.button = QPushButton()
|
||||
self.button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||
self.button.setText("Stop")
|
||||
self.button.setStyleSheet(
|
||||
"background-color: #cc181e; color: white; font-weight: bold; font-size: 12px;"
|
||||
f"background-color: #cc181e; color: white; font-weight: bold; font-size: 12px;"
|
||||
)
|
||||
self.button.clicked.connect(self.stop_scan)
|
||||
|
||||
@@ -47,3 +48,14 @@ class StopButton(BECWidget, QWidget):
|
||||
scan_id(str|None): The scan id to stop. If None, the current scan will be stopped.
|
||||
"""
|
||||
self.queue.request_scan_halt()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = StopButton()
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -5,10 +5,19 @@ import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
from qtpy.QtCore import Signal, Slot
|
||||
|
||||
from bec_widgets.widgets.website.website import WebsiteWidget
|
||||
|
||||
|
||||
class VSCodeInstructionMessage(BaseModel):
|
||||
command: Literal["open", "write", "close", "zenMode", "save", "new", "setCursor"]
|
||||
content: str = ""
|
||||
|
||||
|
||||
def get_free_port():
|
||||
"""
|
||||
Get a free port on the local machine.
|
||||
@@ -28,6 +37,8 @@ class VSCodeEditor(WebsiteWidget):
|
||||
A widget to display the VSCode editor.
|
||||
"""
|
||||
|
||||
file_saved = Signal(str)
|
||||
|
||||
token = "bec"
|
||||
host = "127.0.0.1"
|
||||
|
||||
@@ -41,6 +52,7 @@ class VSCodeEditor(WebsiteWidget):
|
||||
self._url = f"http://{self.host}:{self.port}?tkn={self.token}"
|
||||
super().__init__(parent=parent, config=config, client=client, gui_id=gui_id)
|
||||
self.start_server()
|
||||
self.bec_dispatcher.connect_slot(self.on_vscode_event, f"vscode-events/{self.gui_id}")
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
@@ -49,11 +61,19 @@ class VSCodeEditor(WebsiteWidget):
|
||||
This method starts the server for the VSCode editor in a subprocess.
|
||||
"""
|
||||
|
||||
env = os.environ.copy()
|
||||
env["BEC_Widgets_GUIID"] = self.gui_id
|
||||
env["BEC_REDIS_HOST"] = self.client.connector.host
|
||||
cmd = shlex.split(
|
||||
f"code serve-web --port {self.port} --connection-token={self.token} --accept-server-license-terms"
|
||||
)
|
||||
self.process = subprocess.Popen(
|
||||
cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, preexec_fn=os.setsid
|
||||
cmd,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
preexec_fn=os.setsid,
|
||||
env=env,
|
||||
)
|
||||
|
||||
os.set_blocking(self.process.stdout.fileno(), False)
|
||||
@@ -66,6 +86,92 @@ class VSCodeEditor(WebsiteWidget):
|
||||
self.set_url(self._url)
|
||||
self.wait_until_loaded()
|
||||
|
||||
@Slot(str)
|
||||
def open_file(self, file_path: str):
|
||||
"""
|
||||
Open a file in the VSCode editor.
|
||||
|
||||
Args:
|
||||
file_path: The file path to open
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="open", content=f"file://{file_path}")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_vscode_event(self, content, _metadata):
|
||||
"""
|
||||
Handle the VSCode event. VSCode events are received as RawMessages.
|
||||
|
||||
Args:
|
||||
content: The content of the event
|
||||
metadata: The metadata of the event
|
||||
"""
|
||||
|
||||
# the message also contains the content but I think is fine for now to just emit the file path
|
||||
if not isinstance(content["data"], dict):
|
||||
return
|
||||
if "uri" not in content["data"]:
|
||||
return
|
||||
if not content["data"]["uri"].startswith("file://"):
|
||||
return
|
||||
file_path = content["data"]["uri"].split("file://")[1]
|
||||
self.file_saved.emit(file_path)
|
||||
|
||||
@Slot()
|
||||
def save_file(self):
|
||||
"""
|
||||
Save the file in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="save")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def new_file(self):
|
||||
"""
|
||||
Create a new file in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="new")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def close_file(self):
|
||||
"""
|
||||
Close the file in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="close")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot(str)
|
||||
def write_file(self, content: str):
|
||||
"""
|
||||
Write content to the file in the VSCode editor.
|
||||
|
||||
Args:
|
||||
content: The content to write
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="write", content=content)
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def zen_mode(self):
|
||||
"""
|
||||
Toggle the Zen mode in the VSCode editor.
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="zenMode")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
@Slot(int, int)
|
||||
def set_cursor(self, line: int, column: int):
|
||||
"""
|
||||
Set the cursor in the VSCode editor.
|
||||
|
||||
Args:
|
||||
line: The line number
|
||||
column: The column number
|
||||
"""
|
||||
msg = VSCodeInstructionMessage(command="setCursor", content=f"{line},{column}")
|
||||
self.client.connector.raw_send(f"vscode-instructions/{self.gui_id}", msg.model_dump_json())
|
||||
|
||||
def cleanup_vscode(self):
|
||||
"""
|
||||
Cleanup the VSCode editor.
|
||||
@@ -79,6 +185,7 @@ class VSCodeEditor(WebsiteWidget):
|
||||
"""
|
||||
Cleanup the widget. This method is called from the dock area when the widget is removed.
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(self.on_vscode_event, f"vscode-events/{self.gui_id}")
|
||||
self.cleanup_vscode()
|
||||
return super().cleanup()
|
||||
|
||||
@@ -89,7 +196,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = VSCodeEditor()
|
||||
widget = VSCodeEditor(gui_id="unknown")
|
||||
widget.show()
|
||||
app.exec_()
|
||||
widget.bec_dispatcher.disconnect_all()
|
||||
|
||||
@@ -53,7 +53,7 @@ class CurveSettings(SettingWidget):
|
||||
x_entry = self.target_widget.waveform._x_axis_mode["entry"]
|
||||
self._enable_ui_elements(x_name, x_entry)
|
||||
cm = self.target_widget.config.color_palette
|
||||
self.ui.color_map_selector_scan.combo.setCurrentText(cm)
|
||||
self.ui.color_map_selector_scan.colormap = cm
|
||||
|
||||
# Scan Curve Table
|
||||
for source in ["scan_segment", "async"]:
|
||||
@@ -115,10 +115,10 @@ class CurveSettings(SettingWidget):
|
||||
@Slot()
|
||||
def change_colormap(self, target: Literal["scan", "dap"]):
|
||||
if target == "scan":
|
||||
cm = self.ui.color_map_selector_scan.combo.currentText()
|
||||
cm = self.ui.color_map_selector_scan.colormap
|
||||
table = self.ui.scan_table
|
||||
if target == "dap":
|
||||
cm = self.ui.color_map_selector_dap.combo.currentText()
|
||||
cm = self.ui.color_map_selector_dap.colormap
|
||||
table = self.ui.dap_table
|
||||
rows = table.rowCount()
|
||||
colors = Colors.golden_angle_color(colormap=cm, num=max(10, rows + 1), format="HEX")
|
||||
|
||||
@@ -231,7 +231,14 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="ColormapSelector" name="color_map_selector_scan"/>
|
||||
<widget class="BECColorMapWidget" name="color_map_selector_scan">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -330,7 +337,14 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="ColormapSelector" name="color_map_selector_dap"/>
|
||||
<widget class="BECColorMapWidget" name="color_map_selector_dap">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -348,9 +362,9 @@
|
||||
<header>device_line_edit</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ColormapSelector</class>
|
||||
<class>BECColorMapWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>colormap_selector</header>
|
||||
<header>bec_color_map_widget</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
|
||||
@@ -52,6 +52,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"set_legend_label_size",
|
||||
"set_auto_range",
|
||||
"set_grid",
|
||||
"enable_fps_monitor",
|
||||
"enable_scatter",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"export_to_matplotlib",
|
||||
@@ -118,9 +120,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"fit_params": MaterialIconAction(
|
||||
icon_name="monitoring", tooltip="Open Fitting Parameters"
|
||||
),
|
||||
"axis_settings": MaterialIconAction(
|
||||
icon_name="settings", tooltip="Open Configuration Dialog"
|
||||
),
|
||||
"separator_3": SeparatorAction(),
|
||||
"crosshair": MaterialIconAction(
|
||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
||||
),
|
||||
@@ -129,6 +129,13 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
tooltip="Add ROI region for DAP",
|
||||
checkable=True,
|
||||
),
|
||||
"separator_4": SeparatorAction(),
|
||||
"fps_monitor": MaterialIconAction(
|
||||
icon_name="speed", tooltip="Show FPS Monitor", checkable=True
|
||||
),
|
||||
"axis_settings": MaterialIconAction(
|
||||
icon_name="settings", tooltip="Open Configuration Dialog"
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
)
|
||||
@@ -186,6 +193,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
|
||||
self.toolbar.widgets["crosshair"].action.triggered.connect(self.waveform.toggle_crosshair)
|
||||
self.toolbar.widgets["roi_select"].action.toggled.connect(self.waveform.toggle_roi)
|
||||
self.toolbar.widgets["fps_monitor"].action.toggled.connect(self.enable_fps_monitor)
|
||||
# self.toolbar.widgets["import"].action.triggered.connect(
|
||||
# lambda: self.load_config(path=None, gui=True)
|
||||
# )
|
||||
@@ -594,6 +602,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
checked(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
self.waveform.toggle_roi(checked)
|
||||
if self.toolbar.widgets["roi_select"].action.isChecked() != checked:
|
||||
self.toolbar.widgets["roi_select"].action.setChecked(checked)
|
||||
|
||||
def select_roi(self, region: tuple):
|
||||
"""
|
||||
@@ -604,6 +614,17 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.select_roi(region)
|
||||
|
||||
def enable_fps_monitor(self, enabled: bool):
|
||||
"""
|
||||
Enable the FPS monitor of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the FPS monitor.
|
||||
"""
|
||||
self.waveform.enable_fps_monitor(enabled)
|
||||
if self.toolbar.widgets["fps_monitor"].action.isChecked() != enabled:
|
||||
self.toolbar.widgets["fps_monitor"].action.setChecked(enabled)
|
||||
|
||||
@SafeSlot()
|
||||
def _auto_range_from_toolbar(self):
|
||||
"""
|
||||
@@ -630,6 +651,15 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_outer_axes(show)
|
||||
|
||||
def enable_scatter(self, enabled: bool):
|
||||
"""
|
||||
Enable the scatter plot of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the scatter plot.
|
||||
"""
|
||||
self.waveform.enable_scatter(enabled)
|
||||
|
||||
def lock_aspect_ratio(self, lock: bool):
|
||||
"""
|
||||
Lock the aspect ratio of the plot widget.
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
docs/assets/widget_screenshots/signal_inputs.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 109 KiB |
@@ -7,6 +7,6 @@ sphinx-copybutton
|
||||
sphinx-inline-tabs
|
||||
myst-parser
|
||||
sphinx-design
|
||||
PySide6
|
||||
PySide6~=6.7.2
|
||||
bec-widgets
|
||||
tomli
|
||||
@@ -39,6 +39,17 @@ The `Colormap Selector` is a specialized combobox that allows users to select a
|
||||
**Key Features:**
|
||||
- **Colormap Selection**: Provides a dropdown to select from all available colormaps in `pyqtgraph`.
|
||||
- **Visual Preview**: Displays a small preview of the colormap next to its name, enhancing usability.
|
||||
|
||||
## Colormap Button
|
||||
|
||||
The `Colormap Button` is a custom widget that displays the current colormap and, upon clicking, shows a nested menu for selecting a different colormap. It integrates the `ColorMapMenu` from `pyqtgraph`, providing an intuitive and interactive way for users to choose colormaps within the GUI.
|
||||
|
||||
**Key Features:**
|
||||
- **Current Colormap Display**: Shows the name and a gradient icon of the current colormap directly on the button.
|
||||
- **Nested Menu Selection**: Offers a nested menu with categorized colormaps, making it easy to find and select the desired colormap.
|
||||
- **Signal Emission**: Emits a signal when the colormap changes, providing the new colormap name as a string.
|
||||
- **Qt Designer Integration**: Exposes properties and signals to be used within Qt Designer, allowing for customization within the designer interface.
|
||||
- **Resizable and Styled**: Features adjustable size policies and styles to match the look and feel of standard `QPushButton` widgets, including rounded edges.
|
||||
`````
|
||||
|
||||
````{tab} Examples
|
||||
@@ -104,6 +115,33 @@ class MyGui(QWidget):
|
||||
my_gui = MyGui()
|
||||
my_gui.show()
|
||||
```
|
||||
|
||||
## Example 4 - Adding a Colormap Button
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QWidget, QVBoxLayout
|
||||
from bec_widgets.widgets.buttons import ColormapButton
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setLayout(QVBoxLayout(self))
|
||||
|
||||
# Create and add the ColormapButton to the layout
|
||||
self.colormap_button = ColormapButton()
|
||||
self.layout().addWidget(self.colormap_button)
|
||||
|
||||
# Connect the signal to handle colormap changes
|
||||
self.colormap_button.colormap_changed_signal.connect(self.on_colormap_changed)
|
||||
|
||||
def on_colormap_changed(self, colormap_name):
|
||||
print(f"Selected colormap: {colormap_name}")
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
my_gui = MyGui()
|
||||
my_gui.show()
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
|
||||
BIN
docs/user/widgets/device_input/QProperties_DeviceInput.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
@@ -13,9 +13,11 @@ The `DeviceLineEdit` widget provides a line edit interface with autocomplete fun
|
||||
The `DeviceComboBox` widget offers a dropdown interface for device selection, providing a more visual way to browse through available devices.
|
||||
|
||||
## Key Features:
|
||||
- **Device Filtering**: Both widgets allow users to filter devices by their class names, ensuring that only relevant devices are shown.
|
||||
- **Device Filtering**: Both widgets allow users to filter devices by device type and readout priority, ensuring that only relevant devices are shown.
|
||||
- **Default Device Setting**: Users can set a default device to be pre-selected when the widget is initialized.
|
||||
- **Set Device Selection**: Both widgets allow users to set the available devices to be displayed independent of the applied filters.
|
||||
- **Real-Time Autocomplete (LineEdit)**: The `DeviceLineEdit` widget supports real-time autocomplete, helping users find devices faster.
|
||||
- **Real-Time Input Validation (LineEdit)**: User input is validated in real-time with a red border around the `DeviceLineEdit` indicating an invalid input.
|
||||
- **Dropdown Selection (ComboBox)**: The `DeviceComboBox` widget displays devices in a dropdown list, making selection straightforward.
|
||||
- **QtDesigner Integration**: Both widgets can be added as custom widgets in `QtDesigner` or instantiated directly in code.
|
||||
|
||||
@@ -28,11 +30,15 @@ Both `DeviceLineEdit` and `DeviceComboBox` can be integrated within a GUI applic
|
||||
|
||||
## Example 1 - Creating a DeviceLineEdit in Code
|
||||
|
||||
In this example, we demonstrate how to create a `DeviceLineEdit` widget in code and customize its behavior.
|
||||
In this example, we demonstrate how to create a `DeviceLineEdit` widget in code and customize its behavior.
|
||||
We filter down to Positioners with readout_priority Baseline.
|
||||
Note, if we do not specify a device_filter or readout_filter, all enabled devices will be included.
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from bec_widgets.widgets.device_line_edit import DeviceLineEdit
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
@@ -40,7 +46,7 @@ class MyGui(QWidget):
|
||||
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
|
||||
|
||||
# Create and add the DeviceLineEdit to the layout
|
||||
self.device_line_edit = DeviceLineEdit(device_filter="Motor")
|
||||
self.device_line_edit = DeviceLineEdit(device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
|
||||
self.layout().addWidget(self.device_line_edit)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
@@ -56,7 +62,9 @@ Similarly, here is an example of creating a `DeviceComboBox` widget in code and
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from bec_widgets.widgets.device_combo_box import DeviceComboBox
|
||||
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
@@ -64,8 +72,8 @@ class MyGui(QWidget):
|
||||
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
|
||||
|
||||
# Create and add the DeviceComboBox to the layout
|
||||
self.device_combo_box = DeviceComboBox(device_filter="Motor")
|
||||
self.layout().addWidget(self.device_combo_box)
|
||||
self.device_combobox = DeviceComboBox(device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
|
||||
self.layout().addWidget(self.device_combobox)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
app = QApplication([])
|
||||
@@ -80,11 +88,24 @@ Both `DeviceLineEdit` and `DeviceComboBox` allow you to set a default device tha
|
||||
|
||||
```python
|
||||
# Set default device for DeviceLineEdit
|
||||
self.device_line_edit.set_default_device("motor1")
|
||||
self.device_line_edit.set_device("motor1")
|
||||
|
||||
# Set default device for DeviceComboBox
|
||||
self.device_combo_box.set_default_device("motor2")
|
||||
self.device_combo_box.set_device("motor2")
|
||||
|
||||
# Set the available devices to be displayed independent of the applied filters
|
||||
self.device_combo_box.set_available_devices(["motor1", "motor2", "motor3"])
|
||||
```
|
||||
````
|
||||
````{tab} BEC Designer
|
||||
Both widgets are also available as plugins for the BEC Designer. We have included Qt properties for both widgets, allowing customization of filtering and default device settings directly from the designer. In addition to the common signals and slots for `DeviceLineEdit` and `DeviceComboBox`, the following slots are available:
|
||||
- `set_device(str)` to set the default device
|
||||
- `update_devices()` to refresh the devices list
|
||||
|
||||
The following Qt properties are also included:
|
||||
```{figure} ./QProperties_DeviceInput.png
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API - ComboBox
|
||||
|
||||
@@ -4,62 +4,86 @@
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The Image Widget is a versatile tool designed for visualizing 2D image data, such as camera images, in real-time. Directly integrated with the `BEC` framework, it can display live data streams from connected cameras or other image sources within the current `BEC` session. The widget provides advanced customization options for color maps and scale bars, allowing users to tailor the visualization to their specific needs.
|
||||
The Image Widget is a versatile tool designed for visualizing both 1D and 2D data, such as camera images or waveform data, in real-time. Directly integrated with the `BEC` framework, it can display live data streams from connected detectors or other data sources within the current `BEC` session. The widget provides advanced customization options for color maps and scale bars, allowing users to tailor the visualization to their specific needs.
|
||||
|
||||
## Key Features:
|
||||
- **Flexible Integration**: The widget can be integrated into both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`.
|
||||
- **Live Data Visualization**: Real-time plotting of 2D image data from cameras or other image sources, provided that a data stream is available in the BEC session.
|
||||
- **Live Data Visualization**: Real-time plotting of both 1D and 2D data from detectors or other data sources, provided that a data stream is available in the BEC session.
|
||||
- **Support for Multiple Monitor Types**: The Image Widget supports different monitor types (`'1d'` and `'2d'`), allowing visualization of various data dimensions.
|
||||
- **Customizable Color Maps and Scale Bars**: Users can customize the appearance of images with various color maps and adjust scale bars to better interpret the visualized data.
|
||||
- **Real-time Image Processing**: Apply real-time image processing techniques directly within the widget to enhance the quality or analyze specific aspects of the images such as rotation, log scaling, and FFT.
|
||||
- **Data Export**: Export visualized image data to various formats such as PNG, TIFF, or H5 for further analysis or reporting.
|
||||
- **Real-time Image Processing**: Apply real-time image processing techniques directly within the widget to enhance the quality or analyze specific aspects of the data, such as rotation, logarithmic scaling, and Fast Fourier Transform (FFT).
|
||||
- **Data Export**: Export visualized data to various formats such as PNG, TIFF, or H5 for further analysis or reporting.
|
||||
- **Interactive Controls**: Offers interactive controls for zooming, panning, and adjusting the visual properties of the images on the fly.
|
||||
|
||||
## Monitor Types
|
||||
|
||||
The Image Widget can handle different types of data, specified by the `monitor_type` parameter:
|
||||
|
||||
- **1D Monitor (`monitor_type='1d'`)**: Used for visualizing 1D waveform data. The widget collects incoming 1D data arrays and constructs a 2D image by stacking them, adjusting for varying lengths if necessary.
|
||||
- **2D Monitor (`monitor_type='2d'`)**: Used for visualizing 2D image data directly from detectors like cameras.
|
||||
|
||||
By specifying the appropriate `monitor_type`, you can configure the Image Widget to handle data from different detectors and sources.
|
||||
|
||||

|
||||
````
|
||||
|
||||
````{tab} Examples - CLI
|
||||
|
||||
`ImageWidget` can be embedded in both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`. However, the command-line API is the same for all cases.
|
||||
`ImageWidget` can be embedded in both [`BECFigure`](user.widgets.bec_figure) and [`BECDockArea`](user.widgets.bec_dock_area), or used as an individual component in your application through `BECDesigner`. The command-line API is the same for all cases.
|
||||
|
||||
## Example 1 - Adding Image Widget to BECFigure
|
||||
## Example 1 - Visualizing 2D Image Data from a Detector
|
||||
|
||||
In this example, we demonstrate how to add an `ImageWidget` to a [`BECFigure`](user.widgets.bec_figure) to visualize live data from a connected camera.
|
||||
In this example, we demonstrate how to add an `ImageWidget` to a [`BECFigure`](user.widgets.bec_figure) to visualize live 2D image data from a connected camera detector.
|
||||
|
||||
```python
|
||||
# Add a new dock with BECFigure widget
|
||||
fig = gui.add_dock().add_widget('BECFigure')
|
||||
|
||||
# Add an ImageWidget to the BECFigure
|
||||
img_widget = fig.image(source='eiger')
|
||||
img_widget.set_title("Camera Image Eiger")
|
||||
# Add an ImageWidget to the BECFigure for a 2D detector
|
||||
img_widget = fig.image(monitor='eiger', monitor_type='2d')
|
||||
img_widget.set_title("Camera Image - Eiger Detector")
|
||||
```
|
||||
|
||||
## Example 2 - Adding Image Widget as a Dock in BECDockArea
|
||||
## Example 2 - Visualizing 1D Waveform Data from a Detector
|
||||
|
||||
Adding `ImageWidget` into a [`BECDockArea`](user.widgets.bec_dock_area) is similar to adding any other widget. The widget has the same API as the one in [`BECFigure`](user.widgets.bec_figure); however, as an independent widget outside [`BECFigure`](user.widgets.bec_figure), it has its own toolbar, allowing users to configure the widget without needing CLI commands.
|
||||
This example demonstrates how to set up the Image Widget to visualize 1D waveform data from a detector, such as a line detector or a spectrometer. The widget will stack incoming 1D data arrays to construct a 2D image.
|
||||
|
||||
```python
|
||||
# Add an ImageWidget to the BECDockArea
|
||||
img_widget = gui.add_dock().add_widget('BECImageWidget')
|
||||
# Add an ImageWidget to the BECFigure for a 1D detector
|
||||
img_widget = fig.image(monitor='line_detector', monitor_type='1d')
|
||||
img_widget.set_title("Line Detector Data")
|
||||
|
||||
# Visualize live data from a camera with range from 0 to 100
|
||||
img_widget.image(source='eiger')
|
||||
# Optional: Set the color map and value range
|
||||
img_widget.set_colormap("plasma")
|
||||
img_widget.set_vrange(vmin=0, vmax=100)
|
||||
```
|
||||
|
||||
## Example 3 - Customizing Image Display
|
||||
## Example 3 - Adding Image Widget as a Dock in BECDockArea
|
||||
|
||||
Adding an `ImageWidget` into a [`BECDockArea`](user.widgets.bec_dock_area) is similar to adding any other widget. The widget has the same API as the one in [`BECFigure`](user.widgets.bec_figure); however, as an independent widget outside of `BECFigure`, it has its own toolbar, allowing users to configure the widget without needing CLI commands.
|
||||
|
||||
```python
|
||||
# Add an ImageWidget to the BECDockArea for a 2D detector
|
||||
img_widget = gui.add_dock().add_widget('BECImageWidget')
|
||||
|
||||
# Visualize live data from a camera with a specified value range
|
||||
img_widget.image(monitor='eiger', monitor_type='2d')
|
||||
img_widget.set_vrange(vmin=0, vmax=100)
|
||||
```
|
||||
|
||||
## Example 4 - Customizing Image Display
|
||||
|
||||
This example demonstrates how to customize the color map and scale bar for an image being visualized in an `ImageWidget`.
|
||||
|
||||
```python
|
||||
# Set the color map and adjust the scale bar range
|
||||
# Set the color map and adjust the value range
|
||||
img_widget.set_colormap("viridis")
|
||||
img_widget.set_vrange(vmin=10, vmax=200)
|
||||
```
|
||||
|
||||
## Example 4 - Real-time Image Processing
|
||||
## Example 5 - Real-time Image Processing
|
||||
|
||||
The `ImageWidget` provides real-time image processing capabilities, such as rotating, scaling, and applying FFT to the displayed images. The following example demonstrates how to rotate an image by 90 degrees, transpose it, and apply FFT.
|
||||
The `ImageWidget` provides real-time image processing capabilities, such as rotating, scaling, applying logarithmic scaling, and performing FFT on the displayed images. The following example demonstrates how to apply these transformations to an image.
|
||||
|
||||
```python
|
||||
# Rotate the image by 90 degrees
|
||||
@@ -71,10 +95,35 @@ img_widget.set_transpose(enable=True)
|
||||
# Apply FFT to the image
|
||||
img_widget.set_fft(enable=True)
|
||||
|
||||
# Set the logarithmic scale for the image display
|
||||
# Set logarithmic scaling for the image display
|
||||
img_widget.set_log(enable=True)
|
||||
```
|
||||
|
||||
## Example 6 - Setting Up for Different Detectors
|
||||
|
||||
The Image Widget can be configured for different detectors by specifying the correct monitor name and monitor type. Here's how to set it up for various detectors:
|
||||
|
||||
### For a 2D Camera Detector (e.g., 'eiger')
|
||||
|
||||
```python
|
||||
# For a 2D camera detector
|
||||
img_widget = fig.image(monitor='eiger', monitor_type='2d')
|
||||
img_widget.set_title("Eiger Camera Image")
|
||||
```
|
||||
|
||||
### For a 1D Line Detector (e.g., 'waveform')
|
||||
|
||||
```python
|
||||
# For a 1D line detector
|
||||
img_widget = fig.image(monitor='waveform', monitor_type='1d')
|
||||
img_widget.set_title("Line Detector Data")
|
||||
```
|
||||
|
||||
```{note}
|
||||
Since the Image Widget does not have prior information about the shape of incoming data, it is essential to specify the correct `monitor_type` when setting up the widget. This ensures that the data is processed and displayed correctly.
|
||||
```
|
||||
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
|
||||
@@ -10,8 +10,28 @@ The [`PositionIndicator`](/api_reference/_autosummary/bec_widgets.cli.client.Pos
|
||||
- **Position Visualization**: Displays the current position of a motor on a linear scale, showing its location relative to the defined limits.
|
||||
- **Customizable Range**: The widget allows you to set the minimum and maximum range, adapting to different motor configurations.
|
||||
- **Real-Time Updates**: Responds to real-time updates, allowing the position indicator to move dynamically as the motor's position changes.
|
||||
- **Compact Design**: The widget is designed to be compact and visually appealing, making it suitable for various GUI applications.
|
||||
- **Customizable Appearance**: The appearance of the position indicator can be customized to match the overall design of your application, including colors, orientation, and size.
|
||||
- **QtDesigner Integration**: Can be added directly in code or through `QtDesigner`, making it adaptable to various use cases.
|
||||
|
||||
|
||||
## BEC Designer Customization
|
||||
Within the BECDesigner's [property editor](https://doc.qt.io/qt-6/designer-widget-mode.html#the-property-editor/), the `PositionIndicator` widget can be customized to suit your application's requirements. The widget provides the following customization options:
|
||||
- **minimum**: The minimum value of the position indicator.
|
||||
- **maximum**: The maximum value of the position indicator.
|
||||
- **value**: The current value of the position indicator.
|
||||
- **vertical**: A boolean value indicating whether the position indicator is oriented vertically or horizontally.
|
||||
- **indicator_width**: The width of the position indicator.
|
||||
- **rounded_corners**: The radius of the rounded corners of the position indicator.
|
||||
- **indicator_color**: The color of the position indicator.
|
||||
- **background_color**: The color of the background of the position indicator.
|
||||
- **use_color_palette**: A boolean value indicating whether to use the color palette for the position indicator or the custom colors.
|
||||
|
||||
**BEC Designer properties:**
|
||||
```{figure} ./position_indicator_designer_props.png
|
||||
```
|
||||
|
||||
|
||||
````
|
||||
|
||||
````{tab} Examples
|
||||
@@ -33,7 +53,7 @@ position_indicator = PositionIndicator()
|
||||
|
||||
# Create a slider to simulate position changes
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.valueChanged.connect(lambda value: position_indicator.on_position_update(value / 100))
|
||||
slider.valueChanged.connect(lambda value: position_indicator.set_value(value))
|
||||
|
||||
# Create a layout and add the widgets
|
||||
layout = QVBoxLayout()
|
||||
@@ -63,7 +83,13 @@ The `PositionIndicator` can be added to your GUI layout using `QtDesigner`. Once
|
||||
|
||||
```python
|
||||
# Example: Updating the position in a QtDesigner-based application
|
||||
self.position_indicator.on_position_update(new_position_value)
|
||||
self.position_indicator.set_value(new_position_value)
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.PositionIndicator.rst
|
||||
```
|
||||
````
|
||||
|
After Width: | Height: | Size: 26 KiB |