1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-08 09:47:52 +02:00

Compare commits

..

132 Commits

Author SHA1 Message Date
semantic-release
2f5cc3030d 0.90.0
Automatically generated by python-semantic-release
2024-07-23 16:26:53 +00:00
1cf6e32303 fix(axis_setting): fix compatibility for issue with horizontal line for PyQt6 2024-07-23 18:17:35 +02:00
7f49893d2c fix(image_widget): image_widget autorange fixed 2024-07-23 18:17:35 +02:00
ba0d1ea903 refactor(jupyter_console_example): added examples of standalone widgets 2024-07-23 18:17:35 +02:00
70fb276fdf test(image_widget): tests added 2024-07-23 18:17:35 +02:00
43711680ba feat(image_widget): plugin added 2024-07-23 18:17:35 +02:00
3d2ca4855c fix(image_widget): image widget adjusted 2024-07-23 18:17:35 +02:00
fe7e542b19 fix(image): only single monitor image is allowed 2024-07-23 18:17:35 +02:00
501eb923f1 feat(image_widget): all toolbar actions added 2024-07-23 18:17:35 +02:00
c15035b6b7 fix(image): raw data are saved in image item to always have precise processing 2024-07-23 18:17:35 +02:00
6a9317facd feat(image_widget): image_widget added 2024-07-23 18:17:35 +02:00
semantic-release
95e515114a 0.89.0
Automatically generated by python-semantic-release
2024-07-22 19:43:00 +00:00
fd6ae91993 Revert "feat(themes): moved themes to bec_qthemes"
This reverts commit 3798714369
2024-07-22 21:35:07 +02:00
3798714369 feat(themes): moved themes to bec_qthemes 2024-07-22 21:30:35 +02:00
semantic-release
067496b18c 0.88.1
Automatically generated by python-semantic-release
2024-07-22 15:56:01 +00:00
ad112d1f08 refactor(toolbar): generalizations of the ToolBarAction 2024-07-19 18:35:44 +02:00
a3dff7decc fix(plot_base): set_xy autorange moved to plotbase from waveform 2024-07-19 18:35:43 +02:00
2bcaa4256d docs: readthedocs icon path fixed 2024-07-19 16:40:45 +02:00
semantic-release
006f7c01fd 0.88.0
Automatically generated by python-semantic-release
2024-07-19 14:33:09 +00:00
2c8764a27d fix(waveform_widget): plot API unified with BECFigure 2024-07-19 16:12:36 +02:00
50135b5fe9 fix(colormap_selector): compatibility for PyQt6 when using designer fixed 2024-07-18 21:13:13 +02:00
8d764e2d46 test(waveform_widget): test added 2024-07-18 21:13:13 +02:00
6eb313fa76 fix(waveform_widget): adapted for BECWidget base class 2024-07-18 15:50:45 +02:00
1f8ef52b60 feat(waveform_widget): designer plugin added 2024-07-18 15:44:52 +02:00
2be009c647 feat(waveform_widget): switch between drag and rectangle mode 2024-07-18 15:44:52 +02:00
8df6b003e5 feat(waveform_widget): autorange button 2024-07-18 15:44:52 +02:00
7089cf356a fix(waveform_widget): temporary disabled save/load config 2024-07-18 15:44:52 +02:00
1e551d6e96 feat(waveform_widget): dap parameter window 2024-07-18 15:44:52 +02:00
8e588d79c8 fix(waveform_widget): use @SafeSlot decorator for automatic error message 2024-07-18 15:44:52 +02:00
8d93405399 feat(waveform): export to matplotlib window of current scene 2024-07-18 15:44:52 +02:00
6ff6111091 feat(figure): export dialog can be launched from CLI and from toolbar 2024-07-18 15:44:52 +02:00
a8b6ef20cc refactor(icons): icons moved to the assets directory 2024-07-18 15:44:52 +02:00
a8ff1d4cd0 feat(waveform_widget): added error handle utility 2024-07-18 15:44:52 +02:00
47fcb9ebfe refactor(waveform_widget): removed PYSIDE6 check 2024-07-18 15:44:52 +02:00
e8305652fd feat(curve_dialog): add DAP functionality 2024-07-18 15:44:52 +02:00
33495cfe03 fix(waveform): colormaps of curves can be changed and normalised
feat(waveform): colormap can be changed from curve dialog

fix(curve_dialog): default dialog parameters fixed

curve Dialog colormap WIP
2024-07-18 15:44:52 +02:00
8ac35d7280 fix(waveform_widget): adapted for changes from improved scan logic from waveform widget 2024-07-18 15:44:52 +02:00
c926a75a79 feat(curve_dialog): curves can be added 2024-07-18 15:44:52 +02:00
fa9b17191d feat(waveform_widget): BECWaveformWidget toolbar added import/export config 2024-07-18 15:44:52 +02:00
755b394c1c feat(waveform_widget): BECWaveformWidget added with toolbar 2024-07-18 15:44:52 +02:00
semantic-release
717df63d62 0.87.1
Automatically generated by python-semantic-release
2024-07-18 13:42:30 +00:00
d75c55b2b1 fix(dock): added hasattr to cleanup method for widgets 2024-07-18 15:28:45 +02:00
e52ee2604c fix: add missing close() call, ensure jupyter console client.shutdown() is called in closeEvent 2024-07-18 11:07:19 +02:00
c7feb6952d refactor: BECWidget is a mixin based on BECConnector, for each QWidget in BEC
Handles closeEvent() and RPC registering/unregistering
2024-07-18 11:07:19 +02:00
d64758f268 fix: BECWidget checks if it is a widget, and implements closeEvent and cleanup 2024-07-17 16:26:03 +02:00
6202d224fe fix: add exit handlers for BECConnection objects 2024-07-17 16:13:46 +02:00
semantic-release
4ac90e1e56 0.87.0
Automatically generated by python-semantic-release
2024-07-17 13:55:11 +00:00
8f104cf402 tests: add unit tests for error and warning message boxes 2024-07-17 13:26:39 +02:00
787f74949b feat(qt_utils): added warning utility with simple API to setup warning message 2024-07-17 13:26:38 +02:00
196ef7afe1 feat(qt_utils): added error handle utility with popup messageBoxes 2024-07-17 13:25:12 +02:00
semantic-release
18ac3ffac0 0.86.0
Automatically generated by python-semantic-release
2024-07-17 09:03:55 +00:00
ba69e7957c feat(toolbar): added separator action 2024-07-17 10:57:31 +02:00
semantic-release
5acb47532c 0.85.1
Automatically generated by python-semantic-release
2024-07-17 08:04:55 +00:00
b5b0aa4f82 fix(waveform): readout_priority dict fixed, not overwritten to 'baseline' key 2024-07-16 21:49:39 +02:00
semantic-release
9a91583ed0 0.85.0
Automatically generated by python-semantic-release
2024-07-16 10:53:17 +00:00
b98fd00ade feat(color_map_selector): added colormap selector with plugin 2024-07-15 22:26:14 +02:00
semantic-release
d5c5e12589 0.84.0
Automatically generated by python-semantic-release
2024-07-15 15:23:17 +00:00
e495fd30c4 fix(waveform): timestamp are not converted to human readable format 2024-07-15 16:51:22 +02:00
8516a1d639 fix(waveform): set_x method various bugs fixed 2024-07-14 19:32:38 +02:00
006992e43c test(waveform): tests extended 2024-07-14 19:32:36 +02:00
48911e9348 refactor(waveform): plot can be prompted without specifying kwargs 2024-07-14 19:29:32 +02:00
e4e1a905d1 fix(waveform): x axis switching logic fixed when axis are not compatible 2024-07-14 19:29:32 +02:00
fc935d9fc8 refactor(jupyter_console_window): added more examples of waveforms 2024-07-14 19:13:15 +02:00
0c6a9f2310 feat(waveform): async readback update implemented for async devices 2024-07-14 19:12:19 +02:00
d23fd8bd07 fix(waveform): dap leaked RID for all daps in current process; dap RID is now f"{scan_id}-{gui_id}" to distinguish for each plot instance 2024-07-14 19:12:19 +02:00
9d6ae87d0f fix(waveform): only one type of x axis allowed; x mode validated 2024-07-14 19:12:19 +02:00
fc5a8bdd8b fix(waveform): data for axis are taken by separate method; validation consolidated 2024-07-14 19:12:19 +02:00
b8717f1327 feat(waveform): data are taken directly from ScanItem which is defined from scan_status endpoint; scan update is triggered from scan_segment; plots can be added just specifying y_name -> best effort for setting x reported device 2024-07-14 19:12:19 +02:00
0aa317aae5 fix(bec_dispatcher): connect_slot can accept kwargs 2024-07-14 19:12:19 +02:00
semantic-release
edc19bdff8 0.83.1
Automatically generated by python-semantic-release
2024-07-14 17:09:51 +00:00
11a7204c98 test(toolbar): added reference pngs for spinner for Darwin 2024-07-14 16:49:33 +02:00
eab7883979 fix(toolbar): default transparent background 2024-07-14 16:49:04 +02:00
2d4249e73a fix: use apply_theme 2024-07-10 15:36:32 +02:00
63db1352ee fix: spinner: update reference image for widget test, use apply_theme 2024-07-10 15:36:06 +02:00
8308115f36 fix: replace pyqtdarktheme by qdarkstyle, add 'apply_theme' function (in utils/colors.py) 2024-07-10 15:36:06 +02:00
semantic-release
02fce838db 0.83.0
Automatically generated by python-semantic-release
2024-07-08 22:48:46 +00:00
360d171355 fix(terminal): added default args to avoid designer crashes on startup 2024-07-09 00:40:15 +02:00
eb26e2a11b test(vscode): fixed vscode tests for new cleanup routine 2024-07-09 00:40:15 +02:00
2b29e34b52 fix(widget): fixed widget cleanup routine 2024-07-09 00:40:15 +02:00
fd8766ed87 fix(bec_widget): added cleanup method to bec widget base class 2024-07-09 00:40:15 +02:00
5de8804da1 test(vscode): improved vscode test 2024-07-09 00:40:15 +02:00
2988fd387e feat: added reference utils to compare renderings of widgets 2024-07-09 00:40:15 +02:00
1b017edfad feat(widgets): added device box with spinner 2024-07-09 00:40:15 +02:00
903ce7d46b fix(website): fixed dummy input 2024-07-09 00:40:15 +02:00
41bcb80167 feat(designer): added option to skip the widget validation for DesignerPluginGenerator 2024-07-09 00:40:15 +02:00
semantic-release
d3f85060ca 0.82.2
Automatically generated by python-semantic-release
2024-07-08 14:24:06 +00:00
90178e2f61 fix(rpc_server): pass cli config to server 2024-07-07 23:25:45 +02:00
semantic-release
b9f9a003a2 0.82.1
Automatically generated by python-semantic-release
2024-07-07 21:12:19 +00:00
734f4c7750 tests(motor_map_widget): tests added 2024-07-07 22:59:02 +02:00
c78cd898f2 fix(motor_map): bug where motors without limits were selected 2024-07-07 22:59:02 +02:00
74a249bd06 test(setting_dialog): tests added 2024-07-07 22:59:02 +02:00
2020953b93 feat(settings_dialog):apply button 2024-07-07 22:59:02 +02:00
3826bb3d9e refactor(setting_dialog): moved to qt_utils 2024-07-07 22:59:02 +02:00
7ffc06f3c7 refactor(toolbar): toolbar moved from widgets to qt_utils 2024-07-07 22:59:02 +02:00
semantic-release
eea1a75d4a 0.82.0
Automatically generated by python-semantic-release
2024-07-07 20:58:30 +00:00
b9bff38b64 feat(toggle): added angular component-like toggle 2024-07-07 22:50:25 +02:00
f04862933f refactor(device_input): DeviceComboBox and DeviceLineEdit moved to top layer of widgets 2024-07-07 18:53:32 +02:00
f5b8375fd3 refactor(stop_button): moved to top layer, plugin added 2024-07-07 18:53:32 +02:00
db1cdf4280 refactor(motor_map_widget): removed restriction of only PySide6 for widget 2024-07-07 18:42:51 +02:00
fa1e86ff07 refactor(color_button): ColorButton moved to top level of widgets 2024-07-07 18:42:51 +02:00
semantic-release
9a95454723 0.81.2
Automatically generated by python-semantic-release
2024-07-07 16:28:57 +00:00
dd1875ea5c fix(waveform): scan_history error check for IndexError 2024-07-07 18:21:08 +02:00
semantic-release
df4fabb32a 0.81.1
Automatically generated by python-semantic-release
2024-07-07 11:45:09 +00:00
99114f14f6 fix(motor_control): temporary remove of motor control widgets 2024-07-07 11:41:07 +02:00
semantic-release
fc3a69bbb0 0.81.0
Automatically generated by python-semantic-release
2024-07-06 12:24:27 +00:00
9594be2606 feat(color_button): can get colors in RGBA or HEX 2024-07-06 12:31:29 +02:00
semantic-release
96fd239608 0.80.1
Automatically generated by python-semantic-release
2024-07-06 10:24:27 +00:00
61de7e9e22 fix(entry_validator): check for entry == "" 2024-07-06 12:16:29 +02:00
semantic-release
24c4cdc39f 0.80.0
Automatically generated by python-semantic-release
2024-07-06 10:14:47 +00:00
fadbf77866 feat(qt5): dropped support for qt5; pyside2 and pyqt5 2024-07-06 12:00:35 +02:00
03819a3d90 feat(plugins): moved plugin dict to dataclass and container 2024-07-06 12:00:35 +02:00
d6d0777113 feat(plugins): added support for pyqt6 ui files 2024-07-05 22:50:19 +02:00
1aa83e0ef1 feat(plugins): added bec widgets base class 2024-07-05 21:22:51 +02:00
semantic-release
b09c644e01 0.79.3
Automatically generated by python-semantic-release
2024-07-05 08:25:43 +00:00
e403870874 fix: changed inheritance to adress qt designer bug in rendering 2024-07-05 08:06:10 +02:00
1586ce2d6c fix: add designer plugin classes 2024-07-05 08:05:20 +02:00
576353cfe8 refactor: simplify logic in bec_status_box 2024-07-05 08:05:20 +02:00
semantic-release
cefc415c98 0.79.2
Automatically generated by python-semantic-release
2024-07-04 22:45:13 +00:00
bc0ef7893e fix: overwrite closeEvent and call super class 2024-07-04 17:27:29 +02:00
semantic-release
0e802d8194 0.79.1
Automatically generated by python-semantic-release
2024-07-03 09:34:03 +00:00
d7718d4dcb fix: use libdir env var to preload Python library, also for Linux platform 2024-07-03 11:07:30 +02:00
semantic-release
4c2e02e912 0.79.0
Automatically generated by python-semantic-release
2024-07-03 08:45:07 +00:00
b8774e0b0b fix(toolbar): change default color to black to match BECFigure theme 2024-07-03 10:34:05 +02:00
6e75642090 feat(motor_map_widget): standalone MotorMap Widget with toolbar + plugin 2024-07-03 10:34:05 +02:00
aaa0d1003d fix(motor_map): fixed bug with residual trace after changing motors 2024-07-03 10:34:05 +02:00
5960918137 feat(motor_map): method to reset history trace 2024-07-03 10:34:05 +02:00
3dc0532df0 fix(widget_io): widget handler adjusted for spinboxes and comboboxes 2024-07-03 10:34:05 +02:00
96863adf53 refactor(toolbar): cleanup and adjusted colors 2024-07-03 10:34:05 +02:00
semantic-release
08425a623e 0.78.1
Automatically generated by python-semantic-release
2024-07-02 21:06:54 +00:00
b787759f44 fix(ui_loader): ui loader is compatible with bec plugins 2024-07-02 22:07:24 +02:00
semantic-release
25ef7c05e6 0.78.0
Automatically generated by python-semantic-release
2024-07-02 20:03:45 +00:00
c36bb80d6a feat(color_button): patched ColorButton from pyqtgraph to be able to be opened in another QDialog 2024-07-02 20:54:39 +02:00
209 changed files with 8751 additions and 4859 deletions

View File

@@ -144,6 +144,9 @@ tests:
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- tests/reference_failures/
when: always
test-matrix:
parallel:
@@ -154,7 +157,6 @@ test-matrix:
- "3.12"
QT_PCKG:
- "pyside6"
- "pyqt5"
- "pyqt6"
stage: AdditionalTests

View File

@@ -3,7 +3,7 @@
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=PyQt5, pyqtgraph
extension-pkg-allow-list=PyQt6, PySide6, pyqtgraph
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may

View File

@@ -1,155 +1,147 @@
# CHANGELOG
## v0.77.0 (2024-07-02)
## v0.90.0 (2024-07-23)
### Feature
* feat(bec_connector): export config to yaml ([`a391f30`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a391f3018c50fee6a4a06884491b957df80c3cd3))
* feat(image_widget): plugin added ([`4371168`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/43711680ba253f81fb0ffe764bcaae701b02bb49))
* feat(utils): colors added convertor for rgba to hex ([`572f2fb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/572f2fb8110d5cb0e80f3ca45ce57ef405572456))
* feat(image_widget): all toolbar actions added ([`501eb92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/501eb923f12fa6aaa93f5428ca78e57694edfbc0))
* feat(image_widget): image_widget added ([`6a9317f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6a9317facda896ee784c7fc1db0cd3d68cdfcf73))
### Fix
* fix(waveform): scatter 2D brush error ([`215d59c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/215d59c8bfe7fda9aff8cec8353bef9e1ce2eca1))
* fix(axis_setting): fix compatibility for issue with horizontal line for PyQt6 ([`1cf6e32`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1cf6e32303f82bc7c3f3391d0e96a88bc31f29fc))
* fix(figure): API cleanup ([`008a33a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/008a33a9b192473cc58e90cd6d98c5bcb5f7b8c0))
* fix(image_widget): image_widget autorange fixed ([`7f49893`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7f49893d2ce3b9d02efa764f7f10442ed6ab8f3c))
* fix(figure): if/else logic corrected in subplot_factory ([`3e78723`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3e787234c7274b0698423d7bf9a4c54ec46bad5f))
* fix(image_widget): image widget adjusted ([`3d2ca48`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3d2ca4855c36fe0af59a4b540caa3c8023a81773))
* fix(image): processing of already displayed data; closes #106 ([`1173510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1173510105d2d70d7e498c2ac1e122cea3a16597))
* fix(image): only single monitor image is allowed ([`fe7e542`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fe7e542b19dc5b401523501acb74ac03edf62ad4))
* fix(bec_figure): full reconstruction with config from other bec figure ([`b6e1e20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b6e1e20b7c8549bb092e981062329e601411dda6))
* fix(motor_map): API changes updates current visualisation; motor_map can be initialised from config ([`2e2d422`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2e2d422910685a2527a3d961a468c787f771ca44))
* fix(image): image add_custom_image fixed, closes #225 ([`f0556e4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0556e44113ffee66cf735aa2dd758c62cb634f4))
* fix(figure): subplot methods consolidated; added subplot factory ([`4a97105`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4a97105e4bd2ce77d72dfe5f8307dd9ee65b21b0))
* fix(image): image can be fully reconstructed from config ([`797f73c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/797f73c39aa73e07d6311f3de4baea53f6c380e0))
* fix(image_item): vrange added int for pydantic model check ([`b8f796f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8f796fd3fcc15641e8fc6a3ca75c344ce90fc45))
* fix(bec_figure): waveforms can be initialised from the config; widgets are deleteLater after removal ([`78673ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/78673ea11a47aad878128197ae6213925228ed59))
### Unknown
* Resolve "add VT100 console executing BEC as a widget" ([`c6a14c0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c6a14c0768a90695567a83a7895247ed0c64f3ce))
## v0.76.1 (2024-06-29)
### Fix
* fix(plugins): fixes and tests for auto-gen plugins ([`c42511d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c42511dd44cc13577e108a6cef3166376e594f54))
## v0.76.0 (2024-06-28)
### Feature
* feat(designer): added support for creating designer plugins automatically ([`c1dd0ee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c1dd0ee1906dba1f2e2ae9ce40a84d55c26a1cce))
### Fix
* fix: fixed qwidget inheritance for ring progress bar ([`0610d2f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0610d2f9f027f8659e7149f2dfbb316ff30e337d))
### Unknown
* fix:parent set as first kwarg TextBox and WebsiteWidget ([`a45c407`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a45c4075684b93bfdcee03e5a416b84f61d3bc6f))
## v0.75.0 (2024-06-26)
### Feature
* feat(widgets): added simple bec queue widget ([`3faee98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3faee98ec80041a27e4c1f1156178de6f9dcdc63))
* fix(image): raw data are saved in image item to always have precise processing ([`c15035b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c15035b6b769a96780a16da9e7f75af3b823654c))
### Refactor
* refactor(dispatcher): cleanup ([`ca02132`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca02132c8d18535b37e9192e00459d2aca6ba5cf))
* refactor(jupyter_console_example): added examples of standalone widgets ([`ba0d1ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ba0d1ea9031b4ae2e2e73bf269fbfad973b924a5))
## v0.74.1 (2024-06-26)
### Test
### Build
* test(image_widget): tests added ([`70fb276`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/70fb276fdf31dffc105435d3dfe7c5caea0b10ce))
* build: added missing pytest-bec-e2e dependency; closes #219 ([`56fdae4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56fdae42757bdb9fa301c1e425a77e98b6eaf92b))
## v0.89.0 (2024-07-22)
* build: fixed dependency ranges; closes #135 ([`e6a06c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6a06c9f43e0ad6bbfcfa550a2f580d2a27aff66))
### Feature
### Chore
* feat(themes): moved themes to bec_qthemes ([`3798714`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3798714369adf4023f833b7749d2f46a0ec74eee))
* chore: sorted dependencies alphabetically ([`21c807f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/21c807f35831fdd1ef2e488ab90edae4719f0cb7))
### Unknown
* Revert "feat(themes): moved themes to bec_qthemes"
This reverts commit 3798714369adf4023f833b7749d2f46a0ec74eee ([`fd6ae91`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fd6ae91993a23a7b8dbb2cf3c4b7c3eda6d2b0f6))
## v0.88.1 (2024-07-22)
### Documentation
* docs: fixed doc string ([`f979a63`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f979a63d3d1a008f80e500510909750878ff4303))
* docs: readthedocs icon path fixed ([`2bcaa42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2bcaa4256d6daaefacb3ead8c72458d7b1498e29))
### Fix
* fix(rings): rings properties updated right after setting ([`c8b7367`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c8b7367815b095f8e4aa8b819481efb701f2e542))
* fix(plot_base): set_xy autorange moved to plotbase from waveform ([`a3dff7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3dff7decc16115c12dc6b4ef1572552368da309))
* fix(motor_map): motor map can be removed from BECFigure with .remove() ([`6b25abf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b25abff70280271e2eeb70450553c05d4b7c99c))
### Refactor
### Test
* refactor(toolbar): generalizations of the ToolBarAction ([`ad112d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad112d1f08157f6987edd48a0bacf9f669ef1997))
* test(bec_figure): tests for removing widgets with rpc e2e ([`a268caa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a268caaa30711fcc7ece542d24578d74cbf65c77))
## v0.74.0 (2024-06-25)
### Documentation
* docs(becfigure): docs added ([`a51b15d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a51b15da3f5e83e0c897a0342bdb05b9c677a179))
## v0.88.0 (2024-07-19)
### Feature
* feat(waveform1d): dap LMFit model can be added to plot ([`1866ba6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1866ba66c8e3526661beb13fff3e13af6a0ae562))
* feat(waveform_widget): designer plugin added ([`1f8ef52`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f8ef52b606283038052640849094f515a463403))
* feat(waveform_widget): switch between drag and rectangle mode ([`2be009c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2be009c6477ba26c5cfb4d827534c5d5eb428999))
* feat(waveform_widget): autorange button ([`8df6b00`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8df6b003e5c6a942fa2e875d9790e492c087bf26))
* feat(waveform_widget): dap parameter window ([`1e551d6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1e551d6e9696f79ea2e0a179d13a4fc6c2a128b2))
* feat(waveform): export to matplotlib window of current scene ([`8d93405`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d9340539967b06b1e15f21a2106a39d5c740f31))
* feat(figure): export dialog can be launched from CLI and from toolbar ([`6ff6111`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ff611109153b9412dce37c527b19e839d99bba7))
* feat(waveform_widget): added error handle utility ([`a8ff1d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8ff1d4cd09cae5eaeb4bd0ea90fdd102e32f3a3))
* feat(curve_dialog): add DAP functionality ([`e830565`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e8305652fde384da037242cf8f7e3606f22bcfb6))
* feat(curve_dialog): curves can be added ([`c926a75`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c926a75a7927d672c044ea8f68771209ae5accc6))
* feat(waveform_widget): BECWaveformWidget toolbar added import/export config ([`fa9b171`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fa9b17191ddbb4043a658dae9aa0801e1dc22b84))
* feat(waveform_widget): BECWaveformWidget added with toolbar ([`755b394`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/755b394c1c4d7c443c442d89c630d08ce5415554))
### Fix
* fix(waveform_widget): plot API unified with BECFigure ([`2c8764a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2c8764a27de89b39b717032b58465e120ec57fbc))
* fix(colormap_selector): compatibility for PyQt6 when using designer fixed ([`50135b5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/50135b5fe90a88618291e9357f180cb19251dace))
* fix(waveform_widget): adapted for BECWidget base class ([`6eb313f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6eb313fa76e559d62ecd8fa8849142b83817e47c))
* fix(waveform_widget): temporary disabled save/load config ([`7089cf3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7089cf356a43d805241d5621952e544d690e65e0))
* fix(waveform_widget): use @SafeSlot decorator for automatic error message ([`8e588d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e588d79c86e950f6915e89c08fa9415c4bd8033))
* fix(waveform): colormaps of curves can be changed and normalised
feat(waveform): colormap can be changed from curve dialog
fix(curve_dialog): default dialog parameters fixed
curve Dialog colormap WIP ([`33495cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33495cfe03b363f18db61d8af2983f49027b7a43))
* fix(waveform_widget): adapted for changes from improved scan logic from waveform widget ([`8ac35d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ac35d7280b1ff007c10612228d163cc0c5d1a99))
### Refactor
* refactor(icons): icons moved to the assets directory ([`a8b6ef2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8b6ef20cccae87515b10f054d0ed5b10e152769))
* refactor(waveform_widget): removed PYSIDE6 check ([`47fcb9e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/47fcb9ebfe35ae600cced95a1edc68f6f6e37a04))
### Test
* test(waveform1d): dap e2e test added ([`7271b42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7271b422f98ef9264970d708811c414b69a644db))
* test(waveform_widget): test added ([`8d764e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d764e2d46a1e017dadc3c4630648c1ca708afc2))
## v0.73.2 (2024-06-25)
## v0.87.1 (2024-07-18)
### Fix
* fix(vscode): only run terminate if the process is still alive ([`7120f3e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7120f3e93b054b788f15e2d5bcd688e3c140c1ce))
* fix(dock): added hasattr to cleanup method for widgets ([`d75c55b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d75c55b2b1ccf156fb789c7813f1c5bdf256f860))
* fix(rpc): trigger shutdown of server when gui is terminated ([`acc1318`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/acc13183e28030e3ca9af21bb081e1eed081622b))
* fix: add missing close() call, ensure jupyter console client.shutdown() is called in closeEvent ([`e52ee26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e52ee2604cb35096f1bd833ca9516d8a34197d35))
* fix(rpc): remove of calling "close" and waiting for gui_is_alive ([`f75fc19`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f75fc19c5b10022763252917ca473f404a25165a))
* fix: BECWidget checks if it is a widget, and implements closeEvent and cleanup ([`d64758f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d64758f268cad69e6a17bd52dc9913a6367d3cde))
## v0.73.1 (2024-06-25)
* fix: add exit handlers for BECConnection objects ([`6202d22`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6202d224fe85c103a4c33bd8c255f18cfd027303))
### Fix
### Refactor
* fix(ringprogressbar): removed hard-coded endpoint strings ([`1de3cbf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1de3cbf65a1832150917a7549a1bf3efdee6371a))
* refactor: BECWidget is a mixin based on BECConnector, for each QWidget in BEC
## v0.73.0 (2024-06-25)
Handles closeEvent() and RPC registering/unregistering ([`c7feb69`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c7feb6952d590b569f7b0cba3b019a9af0ce0c93))
## v0.87.0 (2024-07-17)
### Feature
* feat: add new default scaling of image_item ([`df812ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df812eaad5989f2930dde41d87491868505af946))
* feat(qt_utils): added warning utility with simple API to setup warning message ([`787f749`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/787f74949bac27aaa51cbb43911919071481707c))
### Test
* feat(qt_utils): added error handle utility with popup messageBoxes ([`196ef7a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/196ef7afe11a1b5dcc536f8859dc3b6044ea628e))
* test: add test for imageitem ([`88ecd05`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/88ecd05b95974938ef1efff40e81854baf004cb4))
### Unknown
## v0.72.2 (2024-06-25)
* tests: add unit tests for error and warning message boxes ([`8f104cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8f104cf4024d3a4516e6aba5daa8fb78c85e2bfd))
### Fix
* fix(designer): fixed designer for pyenv and venv; closes #237 ([`e631fc1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e631fc15d8707b73d58cb64316e115a7e43961ea))
## v0.72.1 (2024-06-24)
### Fix
* fix: renamed spiral progress bar to ring progress bar; closes #235 ([`e5c0087`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e5c0087c9aed831edbe1c172746325a772a3bafa))
### Test
* test: bugfix to prohibit leackage of mock ([`4348ed1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4348ed1bb2182da6bdecaf372d6db85279e60af8))
## v0.72.0 (2024-06-24)
### Feature
* feat(connector): added threadpool wrapper ([`4ca1efe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4ca1efeeb8955604069f7b98374c7f82e1a8da67))
## v0.86.0 (2024-07-17)

View File

@@ -17,7 +17,7 @@ cd bec_widgets
pip install -e .[dev,pyqt6]
```
BEC Widgets currently supports both PyQt5 and PyQt6, however, no default distribution is specified. As a result, users must install one of the supported
BEC Widgets currently supports both Pyside6 and PyQt6, however, no default distribution is specified. As a result, users must install one of the supported
Python Qt distributions manually.
To select a specific Python Qt distribution, install the package with an additional tag:
@@ -28,7 +28,7 @@ pip install bec_widgets[pyqt6]
or
```bash
pip install bec_widgets[pyqt5]
pip install bec_widgets[pyside6]
```
## Documentation

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M440.39-440.39H185.87v-79.22h254.52V-774.7h79.22v255.09H774.7v79.22H519.61v254.52h-79.22v-254.52Z"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M114.02-114.02v-308.13h68.13v192.02l547.72-547.72H537.85v-68.37h308.37v308.37h-68.37v-192.02L230.13-182.15h192.02v68.13H114.02Z"/>
</svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="m311.5-154.02-47.74-47.74 116.94-116.7H74.02v-68.37H380.7L263.76-503.76l47.74-47.74 198.98 198.74L311.5-154.02Zm337-254.72L449.76-607.48 648.5-806.22l47.74 47.74-116.7 116.94h306.68v68.37H579.54l116.7 116.69-47.74 47.74Z"/>
</svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z"/>
</svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M480-54 303.43-230.56 361-288.13l79.39 78.83v-231.09H209.3L283.13-366l-57.57 57.57L54-480l172.56-172.57L284.13-595l-74.83 75.39h231.09v-231.65L366-676.87l-57.57-57.57L480-906l171.57 171.56L594-676.87l-74.39-74.39v231.65h231.65L676.87-594l57.57-57.57L906-480 734.44-308.43 676.87-366l74.39-74.39H519.61v231.09L599-288.13l57.57 57.57L480-54Z"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
fill="#FFFFFF">
<g>
<rect fill="none" height="24" width="24"/>
</g>
<g>
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5-5L7,9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 100 96" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Artboard1" x="0" y="0" width="100" height="96.486" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<path d="M11.379,24.832C11.379,24.261 11.843,23.798 12.414,23.798C18.11,23.798 20.117,19.072 22.06,14.503C23.704,10.634 25.403,6.634 29.483,6.634C33.902,6.634 35.376,12.91 36.934,19.555C38.473,26.113 40.065,32.893 44.138,32.893C48.25,32.893 50.78,28.962 53.457,24.8C56.279,20.412 59.198,15.876 64.31,15.876C69.322,15.876 72.165,20.305 74.915,24.588C77.707,28.935 80.343,33.04 84.999,33.04C85.571,33.04 86.034,33.503 86.034,34.075C86.034,34.647 85.571,35.11 84.999,35.11C79.212,35.11 76.004,30.115 73.175,25.708C70.612,21.716 68.192,17.946 64.31,17.946C60.328,17.946 57.835,21.819 55.197,25.92C52.338,30.366 49.381,34.963 44.138,34.963C38.425,34.963 36.643,27.371 34.92,20.028C33.613,14.46 32.262,8.703 29.482,8.703C26.953,8.703 25.71,11.2 23.963,15.311C21.964,20.014 19.479,25.867 12.413,25.867C11.843,25.867 11.379,25.403 11.379,24.832M44.361,44.584C43.504,44.584 42.557,44.882 42.557,45.739L42.586,50.703L39.522,50.703L43.922,61.878L48.807,50.703L45.691,50.703L45.604,46.255C45.602,45.398 45.218,44.584 44.361,44.584ZM6.034,37.487L6.034,6.674L5,6.674L5,38.522L95,38.522L95,37.487L6.034,37.487M77.414,91.881L77.414,63.849C77.414,63.277 76.951,62.814 76.379,62.814C75.808,62.814 75.345,63.277 75.345,63.849L75.345,91.881C75.345,92.045 75.391,92.194 75.458,92.332L61.955,92.332C62.022,92.194 62.068,92.045 62.068,91.881L62.068,82.718C62.068,82.146 61.605,81.683 61.034,81.683C60.462,81.683 59.999,82.146 59.999,82.718L59.999,91.881C59.999,92.045 60.045,92.194 60.112,92.332L45.059,92.332C45.126,92.194 45.172,92.045 45.172,91.881L45.172,75.943C45.172,75.372 44.709,74.909 44.138,74.909C43.567,74.909 43.104,75.372 43.104,75.943L43.104,91.881C43.104,92.045 43.15,92.194 43.217,92.332L23.852,92.332C23.92,92.194 23.966,92.045 23.966,91.881L23.966,63.849C23.966,63.277 23.502,62.814 22.931,62.814C22.36,62.814 21.897,63.277 21.897,63.849L21.897,91.881C21.897,92.045 21.943,92.194 22.011,92.332L6.034,92.332L6.034,62.881L5,62.881L5,93.366L95,93.366L95,92.332L77.301,92.332C77.368,92.194 77.414,92.045 77.414,91.881"
style="fill:white;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="m78.89-112.59 263.02-367.17h202l303.5-354.22v721.39H78.89Zm62.24-262.74-54.7-39.78 166.59-233.02h201L640.46-864.8l51.45 44.26-205.82 240.78H287.33l-146.2 204.43Zm70.54 194.37h567.37v-468.78L574.98-411.63H376.22L211.67-180.96Zm567.37 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M283.85-289.91h67.41l42.32-112.66h172.59l43.33 112.66h68.85l-164-428h-67.5l-163 428Zm127.74-165.72 67.34-182.87H481l68.41 182.87H411.59Zm68.53 381.61q-86.32 0-160.51-31t-128.89-85.7q-54.7-54.7-85.7-128.89-31-74.19-31-160.51 0-85.31 30.94-159.4t85.7-128.9q54.76-54.8 128.95-86.3t160.51-31.5q85.31 0 159.42 31.47 74.1 31.47 128.91 86.27 54.82 54.8 86.29 128.88 31.48 74.08 31.48 159.6 0 86.2-31.5 160.39-31.5 74.19-86.3 128.95-54.81 54.76-128.9 85.7-74.09 30.94-159.4 30.94ZM480-480Zm-.04 337.85q144.08 0 240.99-96.74 96.9-96.74 96.9-241.07 0-144.32-96.86-241.11-96.86-96.78-240.95-96.78-144.08 0-240.99 96.74-96.9 96.74-96.9 241.07 0 144.32 96.86 241.11 96.86 96.78 240.95 96.78Z"/>
</svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
fill="#FFFFFF">
<g>
<rect fill="none" height="24" width="24"/>
</g>
<g>
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M110.39-110.39v-89.57l77.52-77.52v167.09h-77.52Zm165.57 0v-250.13l77.52-77.52v327.65h-77.52Zm165.56 0v-327.65l77.52 77.95v249.7h-77.52Zm165.57 0v-250.83l77.52-76.96v327.79h-77.52Zm165.56 0v-411.83l76.96-76.96v488.79h-76.96ZM110.39-335.65v-112.7L400-735.96l160 160 289.61-290.61v112.14L560-463.26l-160-160-289.61 287.61Z"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M725.93-155.93q0-118.18-45-222.09t-122-180.91q-77-77-180.91-122t-222.09-45v-68.14q132.68 0 248.61 50.23 115.92 50.23 202.5 136.75 86.57 86.53 136.8 202.53 50.23 116.01 50.23 248.63h-68.14Z"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M571.91-279.09h191v-194h-60v134h-131v60ZM198.09-486.91h60v-134h131v-60h-191v194Zm-53 341.04q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-509.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h669.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v509.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H145.09Zm0-79.22h669.82v-509.82H145.09v509.82Zm0 0v-509.82 509.82Z"/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M354.61-386.61h391l-127-171-103 135-68-87-93 123ZM274.7-195.48q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-549.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h549.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v549.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H274.7Zm0-79.22h549.82v-549.82H274.7v549.82ZM135.48-55.69q-32.74 0-56.26-23.53-23.53-23.52-23.53-56.26v-629.04h79.79v629.04h629.04v79.79H135.48ZM274.7-824.52v549.82-549.82Z"/>
</svg>

After

Width:  |  Height:  |  Size: 564 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M444.19-421.3q39.72 0 67.4-27.78 27.67-27.78 27.67-66.91 0-39.14-27.71-66.57Q483.83-610 444.4-610q-39.44 0-66.92 27.28Q350-555.44 350-516.3q0 39.13 27.36 67.06 27.35 27.94 66.83 27.94ZM634.52-274 532.78-375.74q-22.56 12.31-44.41 19.02-21.84 6.72-43.28 6.72-69.57 0-117.7-48.62-48.13-48.62-48.13-117.88 0-67.98 48.29-116.39t117.08-48.41q68.79 0 117.08 48.41Q610-584.48 610-515.75q0 21.72-6.93 44.13-6.94 22.4-20.37 44.97l102.73 102.74L634.52-274ZM185.09-105.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87V-352h79.22v166.91H352v79.22H185.09Zm422.91 0v-79.22h166.91V-352h79.79v166.91q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H608ZM105.87-608v-166.91q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53H352v79.79H185.09V-608h-79.22Zm669.04 0v-166.91H608v-79.79h166.91q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26V-608h-79.79Z"/>
</svg>

After

Width:  |  Height:  |  Size: 946 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
fill="#8B1A10">
<rect fill="none" height="24" width="24"/>
<path d="M19,19H5V5h14V19z M3,3v18h18V3H3z M17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41L8.41,7L12,10.59L15.59,7 L17,8.41L13.41,12L17,15.59z"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M473.54-344.48v-63.59h187.18v63.59H473.54Zm81.92 230.46v-56.15h-81.92v-63.59h81.92v-56.39h63.58v176.13h-63.58Zm103.58-56.15v-63.59h187.18v63.59H659.04Zm41.68-116.92v-177.13h63.58v56.15h81.92v63.59H764.3v57.39h-63.58ZM841.22-540h-68.46q-22.98-101.89-103.94-170.83-80.95-68.93-188.89-68.93-125.29 0-212.37 87.18T180.48-480q0 78.61 36.59 143.32 36.58 64.7 96.95 104.46v-108.26h66.46v226.46H154.02v-66.46h117.41q-71.56-48.72-114.48-127.36-42.93-78.64-42.93-172.16 0-76.22 28.86-142.78 28.86-66.57 78.29-116.04 49.42-49.47 116.01-78.43 66.58-28.97 142.82-28.97 136.52 0 237.87 87.8Q819.22-670.63 841.22-540Z"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M435-72.11q-49.67-7-95.99-25.36-46.31-18.36-86.55-49.55l49.21-49.74q31.76 24 65.29 37.26 33.52 13.26 68.04 19.02v68.37Zm90 0v-68.37q109.28-21.24 181.07-102.9 71.78-81.66 71.78-197.71 0-124.37-84.11-210.87t-208.72-86.5h-18.8L540.67-664l-49.74 49.74L332.2-773l158.73-158.74 49.74 49.26-75.41 75.65h19.28q75.48 0 141.34 28.72t114.98 78.56q49.12 49.83 77.24 116.29 28.12 66.46 28.12 142.17 0 142.39-90.8 245.07Q664.63-93.35 525-72.11ZM189.46-209.78q-28.96-38.72-47.82-86.3-18.86-47.57-25.62-100.01h68.89q5 37.76 18.38 72.17 13.38 34.4 36.14 64.16l-49.97 49.98Zm-73.44-276.31q6.52-50.71 25-97.29 18.48-46.58 48.44-87.77l50.21 48.26q-22.76 33-36.14 67.52-13.38 34.52-18.62 69.28h-68.89Z"/>
</svg>

After

Width:  |  Height:  |  Size: 811 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M527-72.11v-68.37q34.52-5.76 68.04-19.02 33.53-13.26 65.29-37.26l49.21 49.74q-40.24 31.19-86.55 49.55Q576.67-79.11 527-72.11Zm-90 0Q297.37-93.35 206.7-196.02q-90.68-102.68-90.68-245.07 0-75.71 28-142.17t77.12-116.29q49.12-49.84 114.98-78.56 65.86-28.72 141.34-28.72h19.28l-75.41-75.65 49.74-49.26L629.8-773 471.07-614.26 421.33-664l74.69-74.46h-19.04q-124.61 0-208.72 86.5t-84.11 210.87q0 116.05 71.78 197.71 71.79 81.66 181.07 102.9v68.37Zm335.54-137.67-49.97-49.98q22.76-29.76 36.14-64.16 13.38-34.41 18.38-72.17h69.13q-7 52.44-25.86 100.01-18.86 47.58-47.82 86.3Zm73.68-276.31h-69.13q-5.24-34.76-18.62-69.28t-36.14-67.52l50.21-48.26q29.96 41.19 48.44 87.77 18.48 46.58 25.24 97.29Z"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M854.7-689.22v504.13q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H185.09q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-589.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h504.13L854.7-689.22Zm-79.79 35.48L653.74-774.91H185.09v589.82h589.82v-468.65ZM479.76-250.09q43.24 0 73.74-30.26 30.5-30.27 30.5-73.5 0-43.24-30.26-73.74-30.27-30.5-73.5-30.5-43.24 0-73.74 30.27-30.5 30.26-30.5 73.5 0 43.23 30.26 73.73 30.27 30.5 73.5 30.5ZM238.09-578.91h358v-143h-358v143Zm-53-74.83v468.65-589.82 121.17Z"/>
</svg>

After

Width:  |  Height:  |  Size: 621 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.09-.16-.26-.25-.44-.25-.06 0-.12.01-.17.03l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.06-.02-.12-.03-.18-.03-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.09.16.26.25.44.25.06 0 .12-.01.17-.03l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M636.17-32.59 480.35-188.41l45.82-46.07 77.13 77.37v-137.32H356.93q-28.1 0-46.8-18.55-18.7-18.54-18.7-46.95V-600.3H74.02v-65.27h217.41v-137.32l-77.36 77.37-45.59-45.83 155.59-155.82 156.06 155.82-46.06 45.83-77.14-77.37v442.96h529.29v65.5H669.04v137.32l77.13-77.37L792-188.41 636.17-32.59ZM603.3-419.93V-600.3H416.93v-65.27H603.3q28.37 0 47.06 18.56 18.68 18.55 18.68 46.71v180.37H603.3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -13,13 +13,20 @@ class Widgets(str, enum.Enum):
Enum for the available widgets.
"""
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
BECDock = "BECDock"
BECDockArea = "BECDockArea"
BECFigure = "BECFigure"
BECImageWidget = "BECImageWidget"
BECMotorMapWidget = "BECMotorMapWidget"
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
BECWaveformWidget = "BECWaveformWidget"
DeviceBox = "DeviceBox"
DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
StopButton = "StopButton"
TextBox = "TextBox"
VSCodeEditor = "VSCodeEditor"
WebsiteWidget = "WebsiteWidget"
@@ -179,7 +186,7 @@ class BECDock(RPCBase):
@property
@rpc_call
def widget_list(self) -> "list[BECConnector]":
def widget_list(self) -> "list[BECWidget]":
"""
Get the widgets in the dock.
@@ -220,13 +227,13 @@ class BECDock(RPCBase):
@rpc_call
def add_widget(
self,
widget: "BECConnector | str",
widget: "BECWidget | str",
row=None,
col=0,
rowspan=1,
colspan=1,
shift: "Literal['down', 'up', 'left', 'right']" = "down",
) -> "BECConnector":
) -> "BECWidget":
"""
Add a widget to the dock.
@@ -463,8 +470,9 @@ class BECFigure(RPCBase):
@rpc_call
def plot(
self,
x: "list | np.ndarray | None" = None,
arg1: "list | np.ndarray | str | None" = None,
y: "list | np.ndarray | None" = None,
x: "list | np.ndarray | None" = None,
x_name: "str | None" = None,
y_name: "str | None" = None,
z_name: "str | None" = None,
@@ -486,8 +494,9 @@ class BECFigure(RPCBase):
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
Args:
x(list | np.ndarray): Custom x data to plot.
arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
y(list | np.ndarray): Custom y data to plot.
x(list | np.ndarray): Custom x data to plot.
x_name(str): The name of the device for the x-axis.
y_name(str): The name of the device for the y-axis.
z_name(str): The name of the device for the z-axis.
@@ -607,6 +616,12 @@ class BECFigure(RPCBase):
theme(Literal["dark","light"]): The theme to set for the figure widget.
"""
@rpc_call
def export(self):
"""
Export the plot widget.
"""
@rpc_call
def clear_all(self):
"""
@@ -808,7 +823,7 @@ class BECImageShow(RPCBase):
"""
@rpc_call
def add_monitor_image(
def image(
self,
monitor: "str",
color_map: "Optional[str]" = "magma",
@@ -819,7 +834,17 @@ class BECImageShow(RPCBase):
**kwargs,
) -> "BECImageItem":
"""
None
Add an image to the figure. Always access the first image widget in the figure.
Args:
monitor(str): The name of the 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.
vrange(tuple[float, float]): The range of values to display.
Returns:
BECImageItem: The image item.
"""
@rpc_call
@@ -1089,6 +1114,12 @@ class BECImageShow(RPCBase):
lock(bool): True to lock, False to unlock.
"""
@rpc_call
def export(self):
"""
Show the Export Dialog of the plot widget.
"""
@rpc_call
def remove(self):
"""
@@ -1105,6 +1136,180 @@ class BECImageShow(RPCBase):
"""
class BECImageWidget(RPCBase):
@rpc_call
def image(
self,
monitor: "str",
color_map: "Optional[str]" = "magma",
color_bar: "Optional[Literal['simple', 'full']]" = "full",
downsample: "Optional[bool]" = True,
opacity: "Optional[float]" = 1.0,
vrange: "Optional[tuple[int, int]]" = None,
**kwargs,
) -> "BECImageItem":
"""
None
"""
@rpc_call
def set(self, **kwargs):
"""
Set the properties of the plot widget.
Args:
**kwargs: Keyword arguments for the properties to be set.
Possible properties:
- title: str
- x_label: str
- y_label: str
- x_scale: Literal["linear", "log"]
- y_scale: Literal["linear", "log"]
- x_lim: tuple
- y_lim: tuple
- legend_label_size: int
"""
@rpc_call
def set_title(self, title: "str"):
"""
Set the title of the plot widget.
Args:
title(str): Title of the plot.
"""
@rpc_call
def set_x_label(self, x_label: "str"):
"""
Set the x-axis label of the plot widget.
Args:
x_label(str): Label of the x-axis.
"""
@rpc_call
def set_y_label(self, y_label: "str"):
"""
Set the y-axis label of the plot widget.
Args:
y_label(str): Label of the y-axis.
"""
@rpc_call
def set_x_scale(self, x_scale: "Literal['linear', 'log']"):
"""
Set the scale of the x-axis of the plot widget.
Args:
x_scale(Literal["linear", "log"]): Scale of the x-axis.
"""
@rpc_call
def set_y_scale(self, y_scale: "Literal['linear', 'log']"):
"""
Set the scale of the y-axis of the plot widget.
Args:
y_scale(Literal["linear", "log"]): Scale of the y-axis.
"""
@rpc_call
def set_x_lim(self, x_lim: "tuple"):
"""
Set the limits of the x-axis of the plot widget.
Args:
x_lim(tuple): Limits of the x-axis.
"""
@rpc_call
def set_y_lim(self, y_lim: "tuple"):
"""
Set the limits of the y-axis of the plot widget.
Args:
y_lim(tuple): Limits of the y-axis.
"""
@rpc_call
def set_vrange(self, vmin: "float", vmax: "float", name: "str" = None):
"""
Set the range of the color bar.
If name is not specified, then set vrange for all images.
Args:
vmin(float): Minimum value of the color bar.
vmax(float): Maximum value of the color bar.
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
def set_fft(self, enable: "bool" = False, name: "str" = None):
"""
Set the FFT of the image.
If name is not specified, then set FFT for all images.
Args:
enable(bool): Whether to perform FFT on the monitor data.
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
def set_transpose(self, enable: "bool" = False, name: "str" = None):
"""
Set the transpose of the image.
If name is not specified, then set transpose for all images.
Args:
enable(bool): Whether to transpose the monitor data before displaying.
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
def set_rotation(self, deg_90: "int" = 0, name: "str" = None):
"""
Set the rotation of the image.
If name is not specified, then set rotation for all images.
Args:
deg_90(int): The rotation angle of the monitor data before displaying.
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
def set_log(self, enable: "bool" = False, name: "str" = None):
"""
Set the log of the image.
If name is not specified, then set log for all images.
Args:
enable(bool): Whether to perform log on the monitor data.
name(str): The name of the image. If None, apply to all images.
"""
@rpc_call
def set_grid(self, x_grid: "bool", y_grid: "bool"):
"""
Set the grid visibility of the plot widget.
Args:
x_grid(bool): Visibility of the x-axis grid.
y_grid(bool): Visibility of the y-axis grid.
"""
@rpc_call
def lock_aspect_ratio(self, lock: "bool"):
"""
Lock the aspect ratio of the plot widget.
Args:
lock(bool): Lock the aspect ratio.
"""
class BECMotorMap(RPCBase):
@property
@rpc_call
@@ -1192,16 +1397,117 @@ class BECMotorMap(RPCBase):
def get_data(self) -> "dict":
"""
Get the data of the motor map.
Returns:
dict: Data of the motor map.
"""
@rpc_call
def export(self):
"""
Show the Export Dialog of the plot widget.
"""
@rpc_call
def remove(self):
"""
Remove the plot widget from the figure.
"""
@rpc_call
def reset_history(self):
"""
Reset the history of the motor map.
"""
class BECMotorMapWidget(RPCBase):
@rpc_call
def change_motors(
self,
motor_x: "str",
motor_y: "str",
motor_x_entry: "str" = None,
motor_y_entry: "str" = None,
validate_bec: "bool" = True,
) -> "None":
"""
Change the active motors for the plot.
Args:
motor_x(str): Motor name for the X axis.
motor_y(str): Motor name for the Y axis.
motor_x_entry(str): Motor entry for the X axis.
motor_y_entry(str): Motor entry for the Y axis.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
"""
@rpc_call
def set_max_points(self, max_points: "int") -> "None":
"""
Set the maximum number of points to display on the motor map.
Args:
max_points(int): Maximum number of points to display.
"""
@rpc_call
def set_precision(self, precision: "int") -> "None":
"""
Set the precision of the motor map.
Args:
precision(int): Precision to set.
"""
@rpc_call
def set_num_dim_points(self, num_dim_points: "int") -> "None":
"""
Set the number of points to display on the motor map.
Args:
num_dim_points(int): Number of points to display.
"""
@rpc_call
def set_background_value(self, background_value: "int") -> "None":
"""
Set the background value of the motor map.
Args:
background_value(int): Background value of the motor map.
"""
@rpc_call
def set_scatter_size(self, scatter_size: "int") -> "None":
"""
Set the scatter size of the motor map.
Args:
scatter_size(int): Scatter size of the motor map.
"""
@rpc_call
def get_data(self) -> "dict":
"""
Get the data of the motor map.
Returns:
dict: Data of the motor map.
"""
@rpc_call
def reset_history(self) -> "None":
"""
Reset the history of the motor map.
"""
@rpc_call
def export(self):
"""
Show the export dialog for the motor map.
"""
class BECPlotBase(RPCBase):
@property
@@ -1330,6 +1636,12 @@ class BECPlotBase(RPCBase):
lock(bool): True to lock, False to unlock.
"""
@rpc_call
def export(self):
"""
Show the Export Dialog of the plot widget.
"""
@rpc_call
def remove(self):
"""
@@ -1403,8 +1715,9 @@ class BECWaveform(RPCBase):
@rpc_call
def plot(
self,
x: "list | np.ndarray | None" = None,
arg1: "list | np.ndarray | str | None" = None,
y: "list | np.ndarray | None" = None,
x: "list | np.ndarray | None" = None,
x_name: "str | None" = None,
y_name: "str | None" = None,
z_name: "str | None" = None,
@@ -1416,13 +1729,20 @@ class BECWaveform(RPCBase):
label: "str | None" = None,
validate: "bool" = True,
dap: "str | None" = None,
**kwargs,
) -> "BECCurve":
"""
Plot a curve to the plot widget.
Args:
x(list | np.ndarray): Custom x data to plot.
arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
y(list | np.ndarray): Custom y data to plot.
x_name(str): The name of the device for the x-axis.
x(list | np.ndarray): Custom y data to plot.
x_name(str): Name of the x signal.
- "best_effort": Use the best effort signal.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- Custom signal name of device from BEC.
y_name(str): The name of the device for the y-axis.
z_name(str): The name of the device for the z-axis.
x_entry(str): The name of the entry for the x-axis.
@@ -1432,7 +1752,7 @@ class BECWaveform(RPCBase):
color_map_z(str): The color map to use for the z-axis.
label(str): The label of the curve.
validate(bool): If True, validate the device names and entries.
dap(str): The dap model to use for the curve. If not specified, none will be added.
dap(str): The dap model to use for the curve, only available for sync devices. If not specified, none will be added.
Returns:
BECCurve: The curve object.
@@ -1441,12 +1761,13 @@ class BECWaveform(RPCBase):
@rpc_call
def add_dap(
self,
x_name: "str",
y_name: "str",
x_name: "str | None" = None,
y_name: "str | None" = None,
x_entry: "Optional[str]" = None,
y_entry: "Optional[str]" = None,
color: "Optional[str]" = None,
dap: "str" = "GaussianModel",
validate_bec: "bool" = True,
**kwargs,
) -> "BECCurve":
"""
@@ -1458,15 +1779,28 @@ class BECWaveform(RPCBase):
y_name(str): Name of the y signal.
y_entry(str): Entry of the y signal.
color(str, optional): Color of the curve. Defaults to None.
color_map_z(str): The color map to use for the z-axis.
label(str, optional): Label of the curve. Defaults to None.
dap(str): The dap model to use for the curve.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
**kwargs: Additional keyword arguments for the curve configuration.
Returns:
BECCurve: The curve object.
"""
@rpc_call
def set_x(self, x_name: "str", x_entry: "str | None" = None):
"""
Change the x axis of the plot widget.
Args:
x_name(str): Name of the x signal.
- "best_effort": Use the best effort signal.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- Custom signal name of device from BEC.
x_entry(str): Entry of the x signal.
"""
@rpc_call
def get_dap_params(self) -> "dict":
"""
@@ -1636,6 +1970,15 @@ class BECWaveform(RPCBase):
y(bool): Show grid on the y-axis.
"""
@rpc_call
def set_colormap(self, colormap: "str | None" = None):
"""
Set the colormap of the plot widget.
Args:
colormap(str, optional): Scale the colors of curves to colormap. If None, use the default color palette.
"""
@rpc_call
def lock_aspect_ratio(self, lock):
"""
@@ -1645,12 +1988,24 @@ class BECWaveform(RPCBase):
lock(bool): True to lock, False to unlock.
"""
@rpc_call
def export(self):
"""
Show the Export Dialog of the plot widget.
"""
@rpc_call
def remove(self):
"""
Remove the plot widget from the figure.
"""
@rpc_call
def clear_all(self):
"""
None
"""
@rpc_call
def set_legend_label_size(self, size: "int" = None):
"""
@@ -1661,6 +2016,295 @@ class BECWaveform(RPCBase):
"""
class BECWaveformWidget(RPCBase):
@property
@rpc_call
def curves(self) -> "list[BECCurve]":
"""
Get the curves of the plot widget as a list
Returns:
list: List of curves.
"""
@rpc_call
def plot(
self,
arg1: "list | np.ndarray | str | None" = None,
x: "list | np.ndarray | None" = None,
y: "list | np.ndarray | None" = None,
x_name: "str | None" = None,
y_name: "str | None" = None,
z_name: "str | None" = None,
x_entry: "str | None" = None,
y_entry: "str | None" = None,
z_entry: "str | None" = None,
color: "str | None" = None,
color_map_z: "str | None" = "plasma",
label: "str | None" = None,
validate: "bool" = True,
dap: "str | None" = None,
**kwargs,
) -> "BECCurve":
"""
Plot a curve to the plot widget.
Args:
arg1(list | np.ndarray | str | None): First argument which can be x data(list | np.ndarray), y data(list | np.ndarray), or y_name(str).
x(list | np.ndarray): Custom x data to plot.
y(list | np.ndarray): Custom y data to plot.
x_name(str): The name of the device for the x-axis.
y_name(str): The name of the device for the y-axis.
z_name(str): The name of the device for the z-axis.
x_entry(str): The name of the entry for the x-axis.
y_entry(str): The name of the entry for the y-axis.
z_entry(str): The name of the entry for the z-axis.
color(str): The color of the curve.
color_map_z(str): The color map to use for the z-axis.
label(str): The label of the curve.
validate(bool): If True, validate the device names and entries.
dap(str): The dap model to use for the curve. If not specified, none will be added.
Returns:
BECCurve: The curve object.
"""
@rpc_call
def add_dap(
self,
x_name: "str",
y_name: "str",
x_entry: "str | None" = None,
y_entry: "str | None" = None,
color: "str | None" = None,
dap: "str" = "GaussianModel",
validate_bec: "bool" = True,
**kwargs,
) -> "BECCurve":
"""
Add LMFIT dap model curve to the plot widget.
Args:
x_name(str): Name of the x signal.
x_entry(str): Entry of the x signal.
y_name(str): Name of the y signal.
y_entry(str): Entry of the y signal.
color(str, optional): Color of the curve. Defaults to None.
dap(str): The dap model to use for the curve.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
**kwargs: Additional keyword arguments for the curve configuration.
Returns:
BECCurve: The curve object.
"""
@rpc_call
def get_dap_params(self) -> "dict":
"""
Get the DAP parameters of all DAP curves.
Returns:
dict: DAP parameters of all DAP curves.
"""
@rpc_call
def remove_curve(self, *identifiers):
"""
Remove a curve from the plot widget.
Args:
*identifiers: Identifier of the curve to be removed. Can be either an integer (index) or a string (curve_id).
"""
@rpc_call
def scan_history(self, scan_index: "int" = None, scan_id: "str" = None):
"""
Update the scan curves with the data from the scan storage.
Provide only one of scan_id or scan_index.
Args:
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
scan_index(int, optional): Index of the scan to be updated. Defaults to None.
"""
@rpc_call
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict | pd.DataFrame":
"""
Extract all curve data into a dictionary or a pandas DataFrame.
Args:
output (Literal["dict", "pandas"]): Format of the output data.
Returns:
dict | pd.DataFrame: Data of all curves in the specified format.
"""
@rpc_call
def set(self, **kwargs):
"""
Set the properties of the plot widget.
Args:
**kwargs: Keyword arguments for the properties to be set.
Possible properties:
- title: str
- x_label: str
- y_label: str
- x_scale: Literal["linear", "log"]
- y_scale: Literal["linear", "log"]
- x_lim: tuple
- y_lim: tuple
- legend_label_size: int
"""
@rpc_call
def set_x(self, x_name: "str", x_entry: "str | None" = None):
"""
Change the x axis of the plot widget.
Args:
x_name(str): Name of the x signal.
- "best_effort": Use the best effort signal.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- Custom signal name of device from BEC.
x_entry(str): Entry of the x signal.
"""
@rpc_call
def set_title(self, title: "str"):
"""
Set the title of the plot widget.
Args:
title(str): Title of the plot.
"""
@rpc_call
def set_x_label(self, x_label: "str"):
"""
Set the x-axis label of the plot widget.
Args:
x_label(str): Label of the x-axis.
"""
@rpc_call
def set_y_label(self, y_label: "str"):
"""
Set the y-axis label of the plot widget.
Args:
y_label(str): Label of the y-axis.
"""
@rpc_call
def set_x_scale(self, x_scale: "Literal['linear', 'log']"):
"""
Set the scale of the x-axis of the plot widget.
Args:
x_scale(Literal["linear", "log"]): Scale of the x-axis.
"""
@rpc_call
def set_y_scale(self, y_scale: "Literal['linear', 'log']"):
"""
Set the scale of the y-axis of the plot widget.
Args:
y_scale(Literal["linear", "log"]): Scale of the y-axis.
"""
@rpc_call
def set_x_lim(self, x_lim: "tuple"):
"""
Set the limits of the x-axis of the plot widget.
Args:
x_lim(tuple): Limits of the x-axis.
"""
@rpc_call
def set_y_lim(self, y_lim: "tuple"):
"""
Set the limits of the y-axis of the plot widget.
Args:
y_lim(tuple): Limits of the y-axis.
"""
@rpc_call
def set_legend_label_size(self, legend_label_size: "int"):
"""
Set the size of the legend labels of the plot widget.
Args:
legend_label_size(int): Size of the legend labels.
"""
@rpc_call
def set_auto_range(self, enabled: "bool", axis: "str" = "xy"):
"""
Set the auto range of the plot widget.
Args:
enabled(bool): If True, enable the auto range.
axis(str, optional): The axis to enable the auto range.
- "xy": Enable auto range for both x and y axis.
- "x": Enable auto range for x axis.
- "y": Enable auto range for y axis.
"""
@rpc_call
def set_grid(self, x_grid: "bool", y_grid: "bool"):
"""
Set the grid visibility of the plot widget.
Args:
x_grid(bool): Visibility of the x-axis grid.
y_grid(bool): Visibility of the y-axis grid.
"""
@rpc_call
def lock_aspect_ratio(self, lock: "bool"):
"""
Lock the aspect ratio of the plot widget.
Args:
lock(bool): Lock the aspect ratio.
"""
@rpc_call
def export(self):
"""
Show the export dialog for the plot widget.
"""
@rpc_call
def export_to_matplotlib(self):
"""
Export the plot widget to Matplotlib.
"""
class DeviceBox(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 DeviceComboBox(RPCBase):
@property
@rpc_call

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import importlib
import importlib.metadata as imd
import json
import os
import select
import subprocess
@@ -87,7 +88,7 @@ def _get_output(process, logger) -> None:
print(f"Error reading process output: {str(e)}")
def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger=None) -> None:
"""
Start the plot in a new process.
@@ -98,6 +99,8 @@ def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
# pylint: disable=subprocess-run-check
command = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__]
if config:
if isinstance(config, dict):
config = json.dumps(config)
command.extend(["--config", config])
env_dict = os.environ.copy()
@@ -190,7 +193,7 @@ class BECGuiClientMixin:
if self._process is None or self._process.poll() is not None:
self._start_update_script()
self._process, self._process_output_processing_thread = _start_plot_process(
self._gui_id, self.__class__, self._client._service_config.config_path
self._gui_id, self.__class__, self._client._service_config.config
)
while not self.gui_is_alive():
print("Waiting for GUI to start...")

View File

@@ -5,13 +5,12 @@ import argparse
import inspect
import os
import sys
from typing import Literal
import black
import isort
from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator
from bec_widgets.utils.plugin_utils import get_rpc_classes
from bec_widgets.utils.plugin_utils import BECClassContainer, get_rpc_classes
if sys.version_info >= (3, 11):
from typing import get_overloads
@@ -40,17 +39,20 @@ from bec_widgets.cli.client_utils import RPCBase, rpc_call, BECGuiClientMixin
self.content = ""
def generate_client(
self, published_classes: dict[Literal["connector_classes", "top_level_classes"], list[type]]
):
def generate_client(self, class_container: BECClassContainer):
"""
Generate the client for the published classes.
Args:
published_classes(dict): A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
class_container: The class container with the classes to generate the client for.
"""
self.write_client_enum(published_classes["top_level_classes"])
for cls in published_classes["connector_classes"]:
rpc_top_level_classes = class_container.rpc_top_level_classes
rpc_top_level_classes.sort(key=lambda x: x.__name__)
connector_classes = class_container.connector_classes
connector_classes.sort(key=lambda x: x.__name__)
self.write_client_enum(rpc_top_level_classes)
for cls in connector_classes:
self.content += "\n\n"
self.generate_content_for_class(cls)
@@ -156,13 +158,12 @@ def main():
client_path = os.path.join(current_path, "client.py")
rpc_classes = get_rpc_classes("bec_widgets")
rpc_classes["connector_classes"].sort(key=lambda x: x.__name__)
generator = ClientGenerator()
generator.generate_client(rpc_classes)
generator.write(client_path)
for cls in rpc_classes["top_level_classes"]:
for cls in rpc_classes.plugins:
plugin = DesignerPluginGenerator(cls)
if not hasattr(plugin, "info"):
continue

View File

@@ -29,7 +29,7 @@ class RPCWidgetHandler:
from bec_widgets.utils.plugin_utils import get_rpc_classes
clss = get_rpc_classes("bec_widgets")
self._widget_classes = {cls.__name__: cls for cls in clss["top_level_classes"]}
self._widget_classes = {cls.__name__: cls for cls in clss.top_level_classes}
def create_widget(self, widget_type, **kwargs) -> BECConnector:
"""

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import inspect
import json
import signal
import sys
from contextlib import redirect_stderr, redirect_stdout
@@ -141,10 +142,30 @@ class SimpleFileLikeFromLogOutputFunc:
return
def _start_server(gui_id: str, gui_class: Union[BECFigure, BECDockArea], config: str | None = None):
if config:
try:
config = json.loads(config)
service_config = ServiceConfig(config=config)
except (json.JSONDecodeError, TypeError):
service_config = ServiceConfig(config_path=config)
else:
# if no config is provided, use the default config
service_config = ServiceConfig()
bec_logger.configure(
service_config.redis,
QtRedisConnector,
service_name="BECWidgetsCLIServer",
service_config=service_config.service_config,
)
server = BECWidgetsCLIServer(gui_id=gui_id, config=service_config, gui_class=gui_class)
return server
def main():
import argparse
import os
import sys
from qtpy.QtCore import QSize
from qtpy.QtGui import QIcon
@@ -159,7 +180,7 @@ def main():
type=str,
help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
)
parser.add_argument("--config", type=str, help="Config file")
parser.add_argument("--config", type=str, help="Config file or config string.")
args = parser.parse_args()
@@ -181,21 +202,15 @@ def main():
module_path = os.path.dirname(bec_widgets.__file__)
icon = QIcon()
icon.addFile(
os.path.join(module_path, "assets", "bec_widgets_icon.png"), size=QSize(48, 48)
os.path.join(module_path, "assets", "app_icons", "bec_widgets_icon.png"),
size=QSize(48, 48),
)
app.setWindowIcon(icon)
win = QMainWindow()
win.setWindowTitle("BEC Widgets")
service_config = ServiceConfig(args.config)
bec_logger.configure(
service_config.redis,
QtRedisConnector,
service_name="BECWidgetsCLIServer",
service_config=service_config.service_config,
)
server = BECWidgetsCLIServer(gui_id=args.id, config=service_config, gui_class=gui_class)
server = _start_server(args.id, gui_class, args.config)
gui = server.gui
win.setCentralWidget(gui)

View File

@@ -1,9 +0,0 @@
from .motor_movement import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,
MotorControlPanelAbsolute,
MotorControlPanelRelative,
MotorCoordinateTable,
MotorThread,
)

View File

@@ -2,14 +2,20 @@ import os
import numpy as np
import pyqtgraph as pg
import qdarktheme
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtpy.QtCore import QSize
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from qtpy.QtWidgets import (
QApplication,
QGroupBox,
QHBoxLayout,
QSplitter,
QTabWidget,
QVBoxLayout,
QWidget,
)
from bec_widgets.utils import BECDispatcher, UILoader
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.colors import apply_theme
from bec_widgets.widgets.dock.dock_area import BECDockArea
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
@@ -21,14 +27,8 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
def __init__(self, parent=None):
super().__init__(parent)
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "jupyter_console_window.ui"), self)
self._init_ui()
self.ui.splitter.setSizes([200, 100])
self.safe_close = False
# console push
if self.console.inprocess is True:
self.console.kernel_manager.kernel.shell.push(
@@ -40,27 +40,48 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
"w1_c": self.w1_c,
"w2_c": self.w2_c,
"w3_c": self.w3_c,
"w4": self.w4,
"w5": self.w5,
"w6": self.w6,
"w7": self.w7,
"w8": self.w8,
"w9": self.w9,
"d0": self.d0,
"d1": self.d1,
"d2": self.d2,
"plt": self.plt,
"bar": self.bar,
"wave": self.wf,
# "bar": self.bar,
# "cm": self.colormap,
"im": self.im,
"mm": self.mm,
}
)
def _init_ui(self):
# Plotting window
self.glw_1_layout = QVBoxLayout(self.ui.glw) # Create a new QVBoxLayout
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
self.layout = QHBoxLayout(self)
self.dock_layout = QVBoxLayout(self.ui.dock_placeholder)
self.dock = BECDockArea(gui_id="remote")
self.dock_layout.addWidget(self.dock)
# Horizontal splitter
splitter = QSplitter(self)
self.layout.addWidget(splitter)
tab_widget = QTabWidget(splitter)
first_tab = QWidget()
first_tab_layout = QVBoxLayout(first_tab)
self.dock = BECDockArea(gui_id="dock")
first_tab_layout.addWidget(self.dock)
tab_widget.addTab(first_tab, "Dock Area")
second_tab = QWidget()
second_tab_layout = QVBoxLayout(second_tab)
self.figure = BECFigure(parent=self, gui_id="figure")
second_tab_layout.addWidget(self.figure)
tab_widget.addTab(second_tab, "BEC Figure")
group_box = QGroupBox("Jupyter Console", splitter)
group_box_layout = QVBoxLayout(group_box)
self.console = BECJupyterConsole(inprocess=True)
group_box_layout.addWidget(self.console)
# add stuff to figure
self._init_figure()
@@ -68,72 +89,102 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
# init dock for testing
self._init_dock()
self.console_layout = QVBoxLayout(self.ui.widget_console)
self.console = BECJupyterConsole(inprocess=True)
self.console_layout.addWidget(self.console)
self.setWindowTitle("Jupyter Console Window")
def _init_figure(self):
self.figure.plot(x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="cividis")
self.figure.motor_map("samx", "samy")
self.figure.image("eiger", color_map="viridis", vrange=(0, 100))
self.figure.plot(
x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="magma", new=True
self.w1 = self.figure.plot(
x_name="samx",
y_name="bpm4i",
# title="Standard Plot with sync device, custom labels - w1",
# x_label="Motor Position",
# y_label="Intensity (A.U.)",
row=0,
col=0,
)
self.w1.set(
title="Standard Plot with sync device, custom labels - w1",
x_label="Motor Position",
y_label="Intensity (A.U.)",
)
self.w2 = self.figure.motor_map("samx", "samy", row=0, col=1)
self.w3 = self.figure.image(
"eiger", color_map="viridis", vrange=(0, 100), title="Eiger Image - w3", row=0, col=2
)
self.w4 = self.figure.plot(
x_name="samx",
y_name="samy",
z_name="bpm4i",
color_map_z="magma",
new=True,
title="2D scatter plot - w4",
row=0,
col=3,
)
self.w5 = self.figure.plot(
y_name="bpm4i",
new=True,
title="Best Effort Plot - w5",
dap="GaussianModel",
row=1,
col=0,
)
self.w6 = self.figure.plot(
x_name="timestamp", y_name="bpm4i", new=True, title="Timestamp Plot - w6", row=1, col=1
)
self.w7 = self.figure.plot(
x_name="index", y_name="bpm4i", new=True, title="Index Plot - w7", row=1, col=2
)
self.w8 = self.figure.plot(
y_name="monitor_async", new=True, title="Async Plot - Best Effort - w8", row=2, col=0
)
self.w9 = self.figure.plot(
x_name="timestamp",
y_name="monitor_async",
new=True,
title="Async Plot - timestamp - w9",
row=2,
col=1,
)
self.w10 = self.figure.plot(
x_name="index",
y_name="monitor_async",
new=True,
title="Async Plot - index - w10",
row=2,
col=2,
)
self.figure.change_layout(2, 2)
self.w1 = self.figure[0, 0]
self.w2 = self.figure[0, 1]
self.w3 = self.figure[1, 0]
self.w4 = self.figure[1, 1]
# Plot Customisation
self.w1.set_title("Waveform 1")
self.w1.set_x_label("Motor Position (samx)")
self.w1.set_y_label("Intensity A.U.")
# Image Customisation
self.w3.set_title("Eiger Image")
self.w3.set_x_label("X")
self.w3.set_y_label("Y")
# Configs to try to pass
self.w1_c = self.w1._config_dict
self.w2_c = self.w2._config_dict
self.w3_c = self.w3._config_dict
# curves for w1
self.c1 = self.w1.get_config()
self.fig_c = self.figure._config_dict
def _init_dock(self):
self.d0 = self.dock.add_dock(name="dock_0")
self.fig0 = self.d0.add_widget("BECFigure")
data = np.random.rand(10, 2)
self.fig0.plot(data, label="2d Data")
self.fig0.image("eiger", vrange=(0, 100))
self.mm = self.d0.add_widget("BECMotorMapWidget")
self.mm.change_motors("samx", "samy")
self.d1 = self.dock.add_dock(name="dock_1", position="right")
self.fig1 = self.d1.add_widget("BECFigure")
self.fig1.plot(x_name="samx", y_name="bpm4i")
self.fig1.plot(x_name="samx", y_name="bpm3a")
self.im = self.d1.add_widget("BECImageWidget")
self.im.image("eiger")
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
self.fig2 = self.d2.add_widget("BECFigure", row=0, col=0)
self.plt = self.fig2.plot(x_name="samx", y_name="bpm3a")
self.plt.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
self.bar = self.d2.add_widget("RingProgressBar", row=0, col=1)
self.bar.set_diameter(200)
self.wf = self.d2.add_widget("BECWaveformWidget", row=0, col=0)
self.wf.plot(x_name="samx", y_name="bpm3a")
self.wf.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
# self.bar = self.d2.add_widget("RingProgressBar", row=0, col=1)
# self.bar.set_diameter(200)
# self.d3 = self.dock.add_dock(name="dock_3", position="bottom")
# self.colormap = pg.GradientWidget()
# self.d3.add_widget(self.colormap, row=0, col=0)
self.dock.save_state()
def closeEvent(self, event):
"""Override to handle things when main window is closed."""
self.dock.cleanup()
self.figure.clear_all()
self.figure.client.shutdown()
self.dock.close()
self.figure.cleanup()
self.figure.close()
self.console.close()
super().closeEvent(event)
@@ -147,9 +198,11 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console")
app.setApplicationDisplayName("Jupyter Console")
qdarktheme.setup_theme("auto")
apply_theme("dark")
icon = QIcon()
icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
icon.addFile(
os.path.join(module_path, "assets", "app_icons", "terminal_icon.png"), size=QSize(48, 48)
)
app.setWindowIcon(icon)
bec_dispatcher = BECDispatcher()

View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>2104</width>
<height>966</height>
</rect>
</property>
<property name="windowTitle">
<string>Plotting Console</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
<string>BECDock</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="dock_placeholder" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>BECFigure</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QWidget" name="glw" native="true"/>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="widget_console" native="true"/>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,9 +0,0 @@
from .motor_control_compilations import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,
MotorControlPanelAbsolute,
MotorControlPanelRelative,
MotorCoordinateTable,
MotorThread,
)

View File

@@ -1,250 +0,0 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
import qdarktheme
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication, QSplitter, QVBoxLayout, QWidget
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets.motor_control.motor_control import MotorThread
from bec_widgets.widgets.motor_control.motor_table.motor_table import MotorCoordinateTable
from bec_widgets.widgets.motor_control.movement_absolute.movement_absolute import (
MotorControlAbsolute,
)
from bec_widgets.widgets.motor_control.movement_relative.movement_relative import (
MotorControlRelative,
)
from bec_widgets.widgets.motor_control.selection.selection import MotorControlSelection
CONFIG_DEFAULT = {
"motor_control": {
"motor_x": "samx",
"motor_y": "samy",
"step_size_x": 3,
"step_size_y": 3,
"precision": 4,
"step_x_y_same": False,
"move_with_arrows": False,
},
"plot_settings": {
"colormap": "Greys",
"scatter_size": 5,
"max_points": 1000,
"num_dim_points": 100,
"precision": 2,
"num_columns": 1,
"background_value": 25,
},
"motors": [
{
"plot_name": "Motor Map",
"x_label": "Motor X",
"y_label": "Motor Y",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "samy", "entry": "samy"}],
},
}
],
}
class MotorControlApp(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
# Widgets
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
# Create MotorMap
# self.motion_map = MotorMap(client=self.client, config=self.config)
# Create MotorCoordinateTable
self.motor_table = MotorCoordinateTable(client=self.client, config=self.config)
# Create the splitter and add MotorMap and MotorControlPanel
splitter = QSplitter(Qt.Horizontal)
# splitter.addWidget(self.motion_map)
splitter.addWidget(self.motor_control_panel)
splitter.addWidget(self.motor_table)
# Set the main layout
layout = QVBoxLayout(self)
layout.addWidget(splitter)
self.setLayout(layout)
# Connecting signals and slots
# self.motor_control_panel.selection_widget.selected_motors_signal.connect(
# lambda x, y: self.motion_map.change_motors(x, y, 0)
# )
self.motor_control_panel.absolute_widget.coordinates_signal.connect(
self.motor_table.add_coordinate
)
self.motor_control_panel.relative_widget.precision_signal.connect(
self.motor_table.set_precision
)
self.motor_control_panel.relative_widget.precision_signal.connect(
self.motor_control_panel.absolute_widget.set_precision
)
# self.motor_table.plot_coordinates_signal.connect(self.motion_map.plot_saved_coordinates)
class MotorControlMap(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
# Widgets
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
# Create MotorMap
# self.motion_map = MotorMap(client=self.client, config=self.config)
# Create the splitter and add MotorMap and MotorControlPanel
splitter = QSplitter(Qt.Horizontal)
# splitter.addWidget(self.motion_map)
splitter.addWidget(self.motor_control_panel)
# Set the main layout
layout = QVBoxLayout(self)
layout.addWidget(splitter)
self.setLayout(layout)
# Connecting signals and slots
# self.motor_control_panel.selection_widget.selected_motors_signal.connect(
# lambda x, y: self.motion_map.change_motors(x, y, 0)
# )
class MotorControlPanel(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
self.motor_thread = MotorThread(client=self.client)
self.selection_widget = MotorControlSelection(
client=self.client, config=self.config, motor_thread=self.motor_thread
)
self.relative_widget = MotorControlRelative(
client=self.client, config=self.config, motor_thread=self.motor_thread
)
self.absolute_widget = MotorControlAbsolute(
client=self.client, config=self.config, motor_thread=self.motor_thread
)
layout = QVBoxLayout(self)
layout.addWidget(self.selection_widget)
layout.addWidget(self.relative_widget)
layout.addWidget(self.absolute_widget)
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
# Set the window to a fixed size based on its contents
# self.layout().setSizeConstraint(layout.SetFixedSize)
class MotorControlPanelAbsolute(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
self.motor_thread = MotorThread(client=self.client)
self.selection_widget = MotorControlSelection(
client=client, config=config, motor_thread=self.motor_thread
)
self.absolute_widget = MotorControlAbsolute(
client=client, config=config, motor_thread=self.motor_thread
)
layout = QVBoxLayout(self)
layout.addWidget(self.selection_widget)
layout.addWidget(self.absolute_widget)
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
class MotorControlPanelRelative(QWidget):
def __init__(self, parent=None, client=None, config=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.config = config
self.motor_thread = MotorThread(client=self.client)
self.selection_widget = MotorControlSelection(
client=client, config=config, motor_thread=self.motor_thread
)
self.relative_widget = MotorControlRelative(
client=client, config=config, motor_thread=self.motor_thread
)
layout = QVBoxLayout(self)
layout.addWidget(self.selection_widget)
layout.addWidget(self.relative_widget)
# Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
if __name__ == "__main__": # pragma: no cover
import argparse
import sys
parser = argparse.ArgumentParser(description="Run various Motor Control Widgets compositions.")
parser.add_argument(
"-v",
"--variant",
type=str,
choices=["app", "map", "panel", "panel_abs", "panel_rel"],
help="Select the variant of the motor control to run. "
"'app' for the full application, "
"'map' for MotorMap, "
"'panel' for the MotorControlPanel, "
"'panel_abs' for MotorControlPanel with absolute control, "
"'panel_rel' for MotorControlPanel with relative control.",
)
args = parser.parse_args()
bec_dispatcher = BECDispatcher()
client = bec_dispatcher.client
client.start()
app = QApplication([])
qdarktheme.setup_theme("auto")
if args.variant == "app":
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "map":
window = MotorControlMap(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel":
window = MotorControlPanel(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel_abs":
window = MotorControlPanelAbsolute(client=client) # , config=CONFIG_DEFAULT)
elif args.variant == "panel_rel":
window = MotorControlPanelRelative(client=client) # , config=CONFIG_DEFAULT)
else:
print("Please specify a valid variant to run. Use -h for help.")
print("Running the full application by default.")
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
window.show()
sys.exit(app.exec())

View File

@@ -1,926 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1561</width>
<height>748</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1409</width>
<height>748</height>
</size>
</property>
<property name="windowTitle">
<string>Motor Controller</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="8,5,8">
<item>
<widget class="GraphicsLayoutWidget" name="glw">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="Controls">
<property name="minimumSize">
<size>
<width>221</width>
<height>471</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6" stretch="1,1,1,0,1">
<property name="spacing">
<number>1</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QGroupBox" name="motorSelection">
<property name="minimumSize">
<size>
<width>261</width>
<height>145</height>
</size>
</property>
<property name="title">
<string>Motor Selection</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Motor Y</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_motor_x"/>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_motor_y"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Motor X</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="pushButton_connecMotors">
<property name="text">
<string>Connect Motors</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>18</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="motorControl">
<property name="minimumSize">
<size>
<width>261</width>
<height>339</height>
</size>
</property>
<property name="title">
<string>Motor Relative</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="checkBox_enableArrows">
<property name="text">
<string>Move with arrow keys</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_same_xy">
<property name="text">
<string>Step [X] = Step [Y]</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="step_grid">
<item row="2" column="0">
<widget class="QLabel" name="label_step_y">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [Y]</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Decimal</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_x">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_step_x">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [X]</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_y">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_precision">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="direction_grid">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_up">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
<item row="2" column="4">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_down">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::DownArrow</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="toolButton_left">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::LeftArrow</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="toolButton_right">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::RightArrow</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>18</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="motorControl_absolute">
<property name="minimumSize">
<size>
<width>261</width>
<height>195</height>
</size>
</property>
<property name="title">
<string>Move Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox_save_with_go">
<property name="text">
<string>Save position with Go</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="pushButton_save">
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_set">
<property name="text">
<string>Set</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_go_absolute">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>Stop Movement</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget_tables">
<property name="enabled">
<bool>true</bool>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_coordinates">
<attribute name="title">
<string>Coordinates</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Entries Mode:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<item>
<property name="text">
<string>Individual</string>
</property>
</item>
<item>
<property name="text">
<string>Start/Stop</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_coordinates">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Show</string>
</property>
</column>
<column>
<property name="text">
<string>Move</string>
</property>
</column>
<column>
<property name="text">
<string>Tag</string>
</property>
</column>
<column>
<property name="text">
<string>X</string>
</property>
</column>
<column>
<property name="text">
<string>Y</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_resize_table">
<property name="text">
<string>Resize Table</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_resize_auto">
<property name="text">
<string>Resize Auto</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="pushButton_importCSV">
<property name="text">
<string>Import CSV</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="pushButton_exportCSV">
<property name="text">
<string>Export CSV</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="pushButton_help">
<property name="text">
<string>Help</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_duplicate">
<property name="text">
<string>Duplicate Last Entry</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_settings">
<attribute name="title">
<string>Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="motorLimits">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Motor Limits</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_updateLimits">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_Y_max">
<property name="text">
<string>+ Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="label_Y_min">
<property name="text">
<string>- Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_X_min">
<property name="text">
<string>- X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_X_max">
<property name="text">
<string>+ X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_y_max">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="spinBox_y_min">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QDoubleSpinBox" name="spinBox_x_min">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QDoubleSpinBox" name="spinBox_x_max">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-1000.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Plotting Options</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_max_points">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>5000</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Max Points</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_scatter_size">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>15</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Scatter Size</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QPushButton" name="pushButton_update_config">
<property name="text">
<string>Update Settings</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox_num_dim_points">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>N dim</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QPushButton" name="pushButton_enableGUI">
<property name="text">
<string>Enable Control GUI</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_queue">
<attribute name="title">
<string>Queue</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Work in progress</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_5">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Reset Queue</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_2">
<property name="enabled">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>queueID</string>
</property>
</column>
<column>
<property name="text">
<string>scan_id</string>
</property>
</column>
<column>
<property name="text">
<string>is_scan</string>
</property>
</column>
<column>
<property name="text">
<string>type</string>
</property>
</column>
<column>
<property name="text">
<string>scan_number</string>
</property>
</column>
<column>
<property name="text">
<string>IQ status</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -16,6 +16,7 @@ class TicTacToe(QWidget): # pragma: no cover
super().__init__(parent)
self._state = DEFAULT_STATE
self._turn_number = 0
print("TicTac HERE !!!!!!")
def minimumSizeHint(self):
return QSize(200, 200)

View File

@@ -0,0 +1,225 @@
import functools
import sys
import traceback
from qtpy.QtCore import QObject, Qt, Signal, Slot
from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget
def SafeSlot(*slot_args, **slot_kwargs):
"""Function with args, acting like a decorator, applying "error_managed" decorator + Qt Slot
to the passed function, to display errors instead of potentially raising an exception
'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
otherwise error display is left to the original exception hook
"""
popup_error = bool(slot_kwargs.pop("popup_error", False))
def error_managed(method):
@Slot(*slot_args, **slot_kwargs)
@functools.wraps(method)
def wrapper(*args, **kwargs):
try:
return method(*args, **kwargs)
except Exception:
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=popup_error)
return wrapper
return error_managed
class WarningPopupUtility(QObject):
"""
Utility class to show warning popups in the application.
"""
def __init__(self, parent=None):
super().__init__(parent)
@Slot(str, str, str, QWidget)
def show_warning_message(self, title, message, detailed_text, widget):
msg = QMessageBox(widget)
msg.setIcon(QMessageBox.Warning)
msg.setWindowTitle(title)
msg.setText(message)
msg.setStandardButtons(QMessageBox.Ok)
msg.setDetailedText(detailed_text)
msg.exec_()
def show_warning(self, title: str, message: str, detailed_text: str, widget: QWidget = None):
"""
Show a warning message with the given title, message, and detailed text.
Args:
title (str): The title of the warning message.
message (str): The main text of the warning message.
detailed_text (str): The detailed text to show when the user expands the message.
widget (QWidget): The parent widget for the message box.
"""
self.show_warning_message(title, message, detailed_text, widget)
class ErrorPopupUtility(QObject):
"""
Utility class to manage error popups in the application to show error messages to the users.
This class is singleton and the error popup can be enabled or disabled globally or attach to widget methods with decorator @error_managed.
"""
error_occurred = Signal(str, str, QWidget)
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(ErrorPopupUtility, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, parent=None):
if not self._initialized:
super().__init__(parent=parent)
self.error_occurred.connect(self.show_error_message)
self.enable_error_popup = False
self._initialized = True
sys.excepthook = self.custom_exception_hook
@Slot(str, str, QWidget)
def show_error_message(self, title, message, widget):
detailed_text = self.format_traceback(message)
error_message = self.parse_error_message(detailed_text)
msg = QMessageBox(widget)
msg.setIcon(QMessageBox.Critical)
msg.setWindowTitle(title)
msg.setText(error_message)
msg.setStandardButtons(QMessageBox.Ok)
msg.setDetailedText(detailed_text)
msg.setTextInteractionFlags(Qt.TextSelectableByMouse)
msg.setMinimumWidth(600)
msg.setMinimumHeight(400)
msg.exec_()
def format_traceback(self, traceback_message: str) -> str:
"""
Format the traceback message to be displayed in the error popup by adding indentation to each line.
Args:
traceback_message(str): The traceback message to be formatted.
Returns:
str: The formatted traceback message.
"""
formatted_lines = []
lines = traceback_message.split("\n")
for line in lines:
formatted_lines.append(" " + line) # Add indentation to each line
return "\n".join(formatted_lines)
def parse_error_message(self, traceback_message):
lines = traceback_message.split("\n")
error_message = "Error occurred. See details."
capture = False
captured_message = []
for line in lines:
if "raise" in line:
capture = True
continue
if capture:
if line.strip() and not line.startswith(" File "):
captured_message.append(line.strip())
else:
break
if captured_message:
error_message = " ".join(captured_message)
return error_message
def custom_exception_hook(self, exctype, value, tb, popup_error=False):
if popup_error or self.enable_error_popup:
error_message = traceback.format_exception(exctype, value, tb)
self.error_occurred.emit(
"Method error" if popup_error else "Application Error",
"".join(error_message),
self.parent(),
)
else:
sys.__excepthook__(exctype, value, tb) # Call the original excepthook
def enable_global_error_popups(self, state: bool):
"""
Enable or disable global error popups for all applications.
Args:
state(bool): True to enable error popups, False to disable error popups.
"""
self.enable_error_popup = bool(state)
@classmethod
def reset_singleton(cls):
"""
Reset the singleton instance.
"""
cls._instance = None
cls._initialized = False
class ExampleWidget(QWidget): # pragma: no cover
"""
Example widget to demonstrate error handling with the ErrorPopupUtility.
Warnings -> This example works properly only with PySide6, PyQt6 has a bug with the error handling.
"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.init_ui()
self.warning_utility = WarningPopupUtility(self)
def init_ui(self):
self.layout = QVBoxLayout(self)
# Button to trigger method with error handling
self.error_button = QPushButton("Trigger Handled Error", self)
self.error_button.clicked.connect(self.method_with_error_handling)
self.layout.addWidget(self.error_button)
# Button to trigger method without error handling
self.normal_button = QPushButton("Trigger Normal Error", self)
self.normal_button.clicked.connect(self.method_without_error_handling)
self.layout.addWidget(self.normal_button)
# Button to trigger warning popup
self.warning_button = QPushButton("Trigger Warning", self)
self.warning_button.clicked.connect(self.trigger_warning)
self.layout.addWidget(self.warning_button)
@SafeSlot(popup_error=True)
def method_with_error_handling(self):
"""This method raises an error and the exception is handled by the decorator."""
raise ValueError("This is a handled error.")
@SafeSlot()
def method_without_error_handling(self):
"""This method raises an error and the exception is not handled here."""
raise ValueError("This is an unhandled error.")
@SafeSlot()
def trigger_warning(self):
"""Trigger a warning using the WarningPopupUtility."""
self.warning_utility.show_warning(
title="Warning",
message="This is a warning message.",
detailed_text="This is the detailed text of the warning message.",
widget=self,
)
if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
widget = ExampleWidget()
widget.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1,107 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
class SettingWidget(QWidget):
"""
Abstract class for a settings widget to enforce the implementation of the accept_changes and display_current_settings.
Can be used for toolbar actions to display the settings of a widget.
Args:
target_widget (QWidget): The widget that the settings will be taken from and applied to.
"""
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.target_widget = None
def set_target_widget(self, target_widget: QWidget):
self.target_widget = target_widget
@Slot()
def accept_changes(self):
"""
Accepts the changes made in the settings widget and applies them to the target widget.
"""
pass
@Slot(dict)
def display_current_settings(self, config_dict: dict):
"""
Displays the current settings of the target widget in the settings widget.
Args:
config_dict(dict): The current settings of the target widget.
"""
pass
class SettingsDialog(QDialog):
"""
Dialog to display and edit the settings of a widget with accept and cancel buttons.
Args:
parent (QWidget): The parent widget of the dialog.
target_widget (QWidget): The widget that the settings will be taken from and applied to.
settings_widget (SettingWidget): The widget that will display the settings.
"""
def __init__(
self,
parent=None,
settings_widget: SettingWidget = None,
window_title: str = "Settings",
config: dict = None,
*args,
**kwargs,
):
super().__init__(parent, *args, **kwargs)
self.setModal(False)
self.setWindowTitle(window_title)
self.widget = settings_widget
self.widget.set_target_widget(parent)
if config is None:
config = parent.get_config()
self.widget.display_current_settings(config)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.apply_button = QPushButton("Apply")
button_layout = QHBoxLayout()
button_layout.addWidget(self.button_box.button(QDialogButtonBox.Cancel))
button_layout.addWidget(self.apply_button)
button_layout.addWidget(self.button_box.button(QDialogButtonBox.Ok))
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.apply_button.clicked.connect(self.apply_changes)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(5, 5, 5, 5)
self.layout.addWidget(self.widget)
self.layout.addLayout(button_layout)
ok_button = self.button_box.button(QDialogButtonBox.Ok)
ok_button.setDefault(True)
ok_button.setAutoDefault(True)
@Slot()
def accept(self):
"""
Accept the changes made in the settings widget and close the dialog.
"""
self.widget.accept_changes()
super().accept()
@Slot()
def apply_changes(self):
"""
Apply the changes made in the settings widget without closing the dialog.
"""
self.widget.accept_changes()

View File

@@ -0,0 +1,138 @@
# pylint: disable=no-name-in-module
import os
from abc import ABC, abstractmethod
from collections import defaultdict
from qtpy.QtCore import QSize
from qtpy.QtGui import QAction, QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolBar, QToolButton, QWidget
import bec_widgets
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class ToolBarAction(ABC):
"""
Abstract base class for toolbar actions.
Args:
icon_path (str, optional): The name of the icon file from `assets/toolbar_icons`. Defaults to None.
tooltip (bool, optional): The tooltip for the action. Defaults to None.
checkable (bool, optional): Whether the action is checkable. Defaults to False.
"""
def __init__(self, icon_path: str = None, tooltip: str = None, checkable: bool = False):
self.icon_path = (
os.path.join(MODULE_PATH, "assets", "toolbar_icons", icon_path) if icon_path else None
)
self.tooltip = tooltip
self.checkable = checkable
self.action = None
@abstractmethod
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
"""Adds an action or widget to a toolbar.
Args:
toolbar (QToolBar): The toolbar to add the action or widget to.
target (QWidget): The target widget for the action.
"""
class SeparatorAction(ToolBarAction):
"""Separator action for the toolbar."""
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
self.separator = QToolButton()
self.separator.setFixedSize(2, 22)
toolbar.addWidget(self.separator)
class IconAction(ToolBarAction):
"""
Action with an icon for the toolbar.
Args:
icon_path (str): The path to the icon file.
tooltip (str): The tooltip for the action.
checkable (bool, optional): Whether the action is checkable. Defaults to False.
"""
def __init__(self, icon_path: str = None, tooltip: str = None, checkable: bool = False):
super().__init__(icon_path, tooltip, checkable)
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
icon = QIcon()
icon.addFile(self.icon_path, size=QSize(20, 20))
self.action = QAction(icon, self.tooltip, target)
self.action.setCheckable(self.checkable)
toolbar.addAction(self.action)
class DeviceSelectionAction(ToolBarAction):
"""
Action for selecting a device in a combobox.
Args:
label (str): The label for the combobox.
device_combobox (DeviceComboBox): The combobox for selecting the device.
"""
def __init__(self, label: str, device_combobox):
super().__init__()
self.label = label
self.device_combobox = device_combobox
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
def add_to_toolbar(self, toolbar, target):
widget = QWidget()
layout = QHBoxLayout(widget)
label = QLabel(f"{self.label}")
layout.addWidget(label)
layout.addWidget(self.device_combobox)
toolbar.addWidget(widget)
def set_combobox_style(self, color: str):
self.device_combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
class ModularToolBar(QToolBar):
"""Modular toolbar with optional automatic initialization.
Args:
parent (QWidget, optional): The parent widget of the toolbar. Defaults to None.
actions (list[ToolBarAction], optional): A list of action creators to populate the toolbar. Defaults to None.
target_widget (QWidget, optional): The widget that the actions will target. Defaults to None.
color (str, optional): The background color of the toolbar. Defaults to "black".
"""
def __init__(
self, parent=None, actions=None, target_widget=None, color: str = "rgba(255, 255, 255, 0)"
):
super().__init__(parent)
self.widgets = defaultdict(dict)
self.set_background_color(color)
if actions is not None and target_widget is not None:
self.populate_toolbar(actions, target_widget)
def populate_toolbar(self, actions: dict, target_widget):
"""Populates the toolbar with a set of actions.
Args:
actions (list[ToolBarAction]): A list of action creators to populate the toolbar.
target_widget (QWidget): The widget that the actions will target.
"""
self.clear()
for action_id, action in actions.items():
action.add_to_toolbar(self, target_widget)
self.widgets[action_id] = action
def set_background_color(self, color: str):
self.setStyleSheet(f"QToolBar {{ background: {color}; }}")
self.setIconSize(QSize(20, 20))
self.setMovable(False)
self.setFloatable(False)
self.setContentsMargins(0, 0, 0, 0)

View File

@@ -11,8 +11,10 @@ from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QApplication
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
@@ -64,15 +66,29 @@ class Worker(QRunnable):
class BECConnector:
"""Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
"""Connection mixin class to handle BEC client and device manager"""
USER_ACCESS = ["_config_dict", "_get_all_rpc"]
EXIT_HANDLERS = {}
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
# BEC related connections
self.bec_dispatcher = BECDispatcher(client=client)
self.client = self.bec_dispatcher.client if client is None else client
if not self.client in BECConnector.EXIT_HANDLERS:
# register function to clean connections at exit;
# the function depends on BECClient, and BECDispatcher
@pyqtSlot()
def terminate(client=self.client, dispatcher=self.bec_dispatcher):
print("Disconnecting", repr(dispatcher))
dispatcher.disconnect_all()
print("Shutting down BEC Client", repr(client))
client.shutdown()
BECConnector.EXIT_HANDLERS[self.client] = terminate
QApplication.instance().aboutToQuit.connect(terminate)
if config:
self.config = config
self.config.widget_class = self.__class__.__name__
@@ -90,9 +106,14 @@ class BECConnector:
self.gui_id = self.config.gui_id
# register widget to rpc register
# be careful: when registering, and the object is not a BECWidget,
# cleanup has to called manually since there is no 'closeEvent'
self.rpc_register = RPCRegister()
self.rpc_register.add_rpc(self)
# Error popups
self.error_utility = ErrorPopupUtility()
self._thread_pool = QThreadPool.globalInstance()
def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker:
@@ -279,16 +300,3 @@ class BECConnector:
return self.config.model_dump()
else:
return self.config
def cleanup(self):
"""Cleanup the widget."""
self.rpc_register.remove_rpc(self)
all_connections = self.rpc_register.list_all_connections()
if len(all_connections) == 0:
print("No more connections. Shutting down GUI BEC client.")
self.bec_dispatcher.disconnect_all()
self.client.shutdown()
# def closeEvent(self, event):
# self.cleanup()
# super().closeEvent(event)

View File

@@ -73,26 +73,28 @@ def patch_designer(): # pragma: no cover
os.environ["PY_MAJOR_VERSION"] = str(major_version)
os.environ["PY_MINOR_VERSION"] = str(minor_version)
if sys.platform == "linux":
if sys.platform == "win32":
if is_virtual_env():
_extend_path_var("PATH", os.fspath(Path(sys._base_executable).parent), True)
else:
if sys.platform == "linux":
suffix = f"{sys.abiflags}.so"
env_var = "LD_PRELOAD"
elif sys.platform == "darwin":
suffix = ".dylib"
env_var = "DYLD_INSERT_LIBRARIES"
else:
raise RuntimeError(f"Unsupported platform: {sys.platform}")
version = f"{major_version}.{minor_version}"
library_name = f"libpython{version}{sys.abiflags}.so"
if is_pyenv_python():
library_name = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
os.environ["LD_PRELOAD"] = library_name
elif sys.platform == "darwin":
library_name = f"libpython{major_version}.{minor_version}.dylib"
library_name = f"libpython{version}{suffix}"
lib_path = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
os.environ["DYLD_INSERT_LIBRARIES"] = lib_path
os.environ[env_var] = lib_path
if is_pyenv_python() or is_virtual_env():
# append all editable packages to the PYTHONPATH
editable_packages = list_editable_packages()
for pckg in editable_packages:
_extend_path_var("PYTHONPATH", pckg, True)
elif sys.platform == "win32":
if is_virtual_env():
_extend_path_var("PATH", os.fspath(Path(sys._base_executable).parent), True)
qt_tool_wrapper(ui_tool_binary("designer"), sys.argv[1:])

View File

@@ -8,7 +8,7 @@ import redis
from bec_lib.client import BECClient
from bec_lib.redis_connector import MessageObject, RedisConnector
from bec_lib.service_config import ServiceConfig
from qtpy.QtCore import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QCoreApplication, QObject
from qtpy.QtCore import PYQT6, PYSIDE6, QCoreApplication, QObject
from qtpy.QtCore import Signal as pyqtSignal
if TYPE_CHECKING:
@@ -127,14 +127,17 @@ class BECDispatcher:
return
# shutdown QCoreApp if it exists
if PYQT5 or PYQT6:
if PYQT6:
cls.qapp.exit()
elif PYSIDE2 or PYSIDE6:
elif PYSIDE6:
cls.qapp.shutdown()
cls.qapp = None
def connect_slot(
self, slot: Callable, topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]]
self,
slot: Callable,
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
**kwargs,
) -> None:
"""Connect widget's pyqt slot, so that it is called on new pub/sub topic message.
@@ -144,7 +147,7 @@ class BECDispatcher:
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
"""
slot = QtThreadSafeCallback(slot)
self.client.connector.register(topics, cb=slot)
self.client.connector.register(topics, cb=slot, **kwargs)
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
self._slots[slot].update(set(topics_str))

View File

@@ -0,0 +1,23 @@
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
class BECWidget(BECConnector):
"""Mixin class for all BEC widgets, to handle cleanup"""
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
if not isinstance(self, QWidget):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
super().__init__(client, config, gui_id)
def cleanup(self):
"""Cleanup the widget."""
pass
def closeEvent(self, event):
self.rpc_register.remove_rpc(self)
try:
self.cleanup()
finally:
super().closeEvent(event)

View File

@@ -1,10 +1,37 @@
import itertools
import re
from typing import Literal
import numpy as np
import pyqtgraph as pg
import qdarkstyle
from pydantic_core import PydanticCustomError
from qdarkstyle import DarkPalette, LightPalette
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QApplication
CURRENT_THEME = "dark"
def get_theme_palette():
return DarkPalette if CURRENT_THEME == "dark" else LightPalette
def apply_theme(theme: Literal["dark", "light"]):
global CURRENT_THEME
CURRENT_THEME = theme
app = QApplication.instance()
# go through all pyqtgraph widgets and set background
children = itertools.chain.from_iterable(
top.findChildren(pg.GraphicsLayoutWidget) for top in app.topLevelWidgets()
)
for pg_widget in children:
pg_widget.setBackground("k" if theme == "dark" else "w")
# now define stylesheet according to theme and apply it
style = qdarkstyle.load_stylesheet(palette=get_theme_palette())
app.setStyleSheet(style)
class Colors:

View File

@@ -19,7 +19,7 @@ class EntryValidator:
device = self.devices[name]
description = device.describe()
if entry is None:
if entry is None or entry == "":
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
if entry not in description:
raise ValueError(f"Entry '{entry}' not found in device '{name}' signals")

View File

@@ -58,11 +58,12 @@ class DesignerPluginGenerator:
os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
)
def run(self):
def run(self, validate=True):
if self._excluded:
print(f"Plugin {self.widget.__name__} is excluded from generation.")
return
self._check_class_validity()
if validate:
self._check_class_validity()
self._load_templates()
self._write_templates()
@@ -142,7 +143,7 @@ class DesignerPluginGenerator:
if __name__ == "__main__": # pragma: no cover
# from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
from bec_widgets.widgets.dock import BECDockArea
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
generator = DesignerPluginGenerator(BECDockArea)
generator.run()
generator = DesignerPluginGenerator(SpinnerWidget)
generator.run(validate=False)

View File

@@ -1,12 +1,13 @@
import importlib
import inspect
import os
from typing import Literal
from dataclasses import dataclass
from bec_lib.plugin_helper import _get_available_plugins
from qtpy.QtWidgets import QGraphicsWidget, QWidget
from bec_widgets.utils import BECConnector
from bec_widgets.utils.bec_widget import BECWidget
def get_plugin_widgets() -> dict[str, BECConnector]:
@@ -44,9 +45,74 @@ def _filter_plugins(obj):
return inspect.isclass(obj) and issubclass(obj, BECConnector)
def get_rpc_classes(
repo_name: str,
) -> dict[Literal["connector_classes", "top_level_classes"], list[type]]:
@dataclass
class BECClassInfo:
name: str
module: str
file: str
obj: type
is_connector: bool = False
is_widget: bool = False
is_top_level: bool = False
class BECClassContainer:
def __init__(self):
self._collection = []
def add_class(self, class_info: BECClassInfo):
"""
Add a class to the collection.
Args:
class_info(BECClassInfo): The class information
"""
self.collection.append(class_info)
@property
def collection(self):
"""
Get the collection of classes.
"""
return self._collection
@property
def connector_classes(self):
"""
Get all connector classes.
"""
return [info.obj for info in self.collection if info.is_connector]
@property
def top_level_classes(self):
"""
Get all top-level classes.
"""
return [info.obj for info in self.collection if info.is_top_level]
@property
def plugins(self):
"""
Get all plugins. These are all classes that are on the top level and are widgets.
"""
return [info.obj for info in self.collection if info.is_widget and info.is_top_level]
@property
def widgets(self):
"""
Get all widgets. These are all classes inheriting from BECWidget.
"""
return [info.obj for info in self.collection if info.is_widget]
@property
def rpc_top_level_classes(self):
"""
Get all top-level classes that are RPC-enabled. These are all classes that users can choose from.
"""
return [info.obj for info in self.collection if info.is_top_level and info.is_connector]
def get_rpc_classes(repo_name: str) -> BECClassContainer:
"""
Get all RPC-enabled classes in the specified repository.
@@ -56,8 +122,7 @@ def get_rpc_classes(
Returns:
dict: A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
"""
connector_classes = []
top_level_classes = []
collection = BECClassContainer()
anchor_module = importlib.import_module(f"{repo_name}.widgets")
directory = os.path.dirname(anchor_module.__file__)
for root, _, files in sorted(os.walk(directory)):
@@ -78,11 +143,16 @@ def get_rpc_classes(
obj = getattr(module, name)
if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
continue
if isinstance(obj, type) and issubclass(obj, BECConnector):
connector_classes.append(obj)
if isinstance(obj, type):
class_info = BECClassInfo(name=name, module=module_name, file=path, obj=obj)
if issubclass(obj, BECConnector):
class_info.is_connector = True
if issubclass(obj, BECWidget):
class_info.is_widget = True
if len(subs) == 1 and (
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
):
top_level_classes.append(obj)
class_info.is_top_level = True
collection.add_class(class_info)
return {"connector_classes": connector_classes, "top_level_classes": top_level_classes}
return collection

View File

@@ -0,0 +1,92 @@
import os
import sys
from PIL import Image, ImageChops
from qtpy.QtGui import QPixmap
import bec_widgets
REFERENCE_DIR = os.path.join(
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/references"
)
REFERENCE_DIR_FAILURES = os.path.join(
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/reference_failures"
)
def compare_images(image1_path: str, reference_image_path: str):
"""
Load two images and compare them pixel by pixel
Args:
image1_path(str): The path to the first image
reference_image_path(str): The path to the reference image
Raises:
ValueError: If the images are different
"""
image1 = Image.open(image1_path)
image2 = Image.open(reference_image_path)
if image1.size != image2.size:
raise ValueError("Image size has changed")
diff = ImageChops.difference(image1, image2)
if diff.getbbox():
# copy image1 to the reference directory to upload as artifact
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
image_name = os.path.join(REFERENCE_DIR_FAILURES, os.path.basename(image1_path))
image1.save(image_name)
print(f"Image saved to {image_name}")
raise ValueError("Images are different")
def snap_and_compare(widget: any, output_directory: str, suffix: str = ""):
"""
Save a rendering of a widget and compare it to a reference image
Args:
widget(any): The widget to render
output_directory(str): The directory to save the image to
suffix(str): A suffix to append to the image name
Raises:
ValueError: If the images are different
Examples:
snap_and_compare(widget, tmpdir, suffix="started")
"""
if not isinstance(output_directory, str):
output_directory = str(output_directory)
os_suffix = sys.platform
name = (
f"{widget.__class__.__name__}_{suffix}_{os_suffix}.png"
if suffix
else f"{widget.__class__.__name__}_{os_suffix}.png"
)
# Save the widget to a pixmap
test_image_path = os.path.join(output_directory, name)
pixmap = QPixmap(widget.size())
widget.render(pixmap)
pixmap.save(test_image_path)
try:
reference_path = os.path.join(REFERENCE_DIR, f"{widget.__class__.__name__}")
reference_image_path = os.path.join(reference_path, name)
if not os.path.exists(reference_image_path):
raise ValueError(f"Reference image not found: {reference_image_path}")
compare_images(test_image_path, reference_image_path)
except ValueError:
image = Image.open(test_image_path)
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
image_name = os.path.join(REFERENCE_DIR_FAILURES, name)
image.save(image_name)
print(f"Image saved to {image_name}")
raise

View File

@@ -1,27 +1,45 @@
from qtpy import QT_VERSION
import os
from qtpy import PYQT6, PYSIDE6, QT_VERSION
from qtpy.QtCore import QFile, QIODevice
from bec_widgets.utils.generate_designer_plugin import DesignerPluginInfo
from bec_widgets.utils.plugin_utils import get_rpc_classes
if PYSIDE6:
from PySide6.QtUiTools import QUiLoader
class CustomUiLoader(QUiLoader):
def __init__(self, baseinstance, custom_widgets: dict = None):
super().__init__(baseinstance)
self.custom_widgets = custom_widgets or {}
self.baseinstance = baseinstance
def createWidget(self, class_name, parent=None, name=""):
if class_name in self.custom_widgets:
widget = self.custom_widgets[class_name](parent)
widget.setObjectName(name)
return widget
return super().createWidget(class_name, parent, name)
class UILoader:
"""Universal UI loader for PyQt5, PyQt6, PySide2, and PySide6."""
"""Universal UI loader for PyQt6 and PySide6."""
def __init__(self, parent=None):
self.parent = parent
if QT_VERSION.startswith("5"):
# PyQt5 or PySide2
from qtpy import uic
self.loader = uic.loadUi
elif QT_VERSION.startswith("6"):
# PyQt6 or PySide6
try:
from PySide6.QtUiTools import QUiLoader
widgets = get_rpc_classes("bec_widgets").top_level_classes
self.loader = self.load_ui_pyside6
except ImportError:
from PyQt6.uic import loadUi
self.custom_widgets = {widget.__name__: widget for widget in widgets}
self.loader = loadUi
if PYSIDE6:
self.loader = self.load_ui_pyside6
elif PYQT6:
self.loader = self.load_ui_pyqt6
else:
raise ImportError("No compatible Qt bindings found.")
def load_ui_pyside6(self, ui_file, parent=None):
"""
@@ -33,9 +51,8 @@ class UILoader:
Returns:
QWidget: The loaded widget.
"""
from PySide6.QtUiTools import QUiLoader
loader = QUiLoader(parent)
loader = CustomUiLoader(parent, self.custom_widgets)
file = QFile(ui_file)
if not file.open(QIODevice.ReadOnly):
raise IOError(f"Cannot open file: {ui_file}")
@@ -43,6 +60,71 @@ class UILoader:
file.close()
return widget
def load_ui_pyqt6(self, ui_file, parent=None):
"""
Specific loader for PyQt6 using loadUi.
Args:
ui_file(str): Path to the .ui file.
parent(QWidget): Parent widget.
Returns:
QWidget: The loaded widget.
"""
from PyQt6.uic.Loader.loader import DynamicUILoader
class CustomDynamicUILoader(DynamicUILoader):
def __init__(self, package, custom_widgets: dict = None):
super().__init__(package)
self.custom_widgets = custom_widgets or {}
def _handle_custom_widgets(self, el):
"""Handle the <customwidgets> element."""
def header2module(header):
"""header2module(header) -> string
Convert paths to C++ header files to according Python modules
>>> header2module("foo/bar/baz.h")
'foo.bar.baz'
"""
if header.endswith(".h"):
header = header[:-2]
mpath = []
for part in header.split("/"):
# Ignore any empty parts or those that refer to the current
# directory.
if part not in ("", "."):
if part == "..":
# We should allow this for Python3.
raise SyntaxError(
"custom widget header file name may not contain '..'."
)
mpath.append(part)
return ".".join(mpath)
for custom_widget in el:
classname = custom_widget.findtext("class")
header = custom_widget.findtext("header")
if header:
header = self._translate_bec_widgets_header(header)
self.factory.addCustomWidget(
classname,
custom_widget.findtext("extends") or "QWidget",
header2module(header),
)
def _translate_bec_widgets_header(self, header):
for name, value in self.custom_widgets.items():
if header == DesignerPluginInfo.pascal_to_snake(name):
return value.__module__
return header
return CustomDynamicUILoader("", self.custom_widgets).loadUi(ui_file, parent)
def load_ui(self, ui_file, parent=None):
"""
Universal UI loader method.

View File

@@ -44,8 +44,11 @@ class ComboBoxHandler(WidgetHandler):
def get_value(self, widget: QComboBox) -> int:
return widget.currentIndex()
def set_value(self, widget: QComboBox, value: int) -> None:
widget.setCurrentIndex(value)
def set_value(self, widget: QComboBox, value: int | str) -> None:
if isinstance(value, str):
value = widget.findText(value)
if isinstance(value, int):
widget.setCurrentIndex(value)
class TableWidgetHandler(WidgetHandler):
@@ -142,6 +145,26 @@ class WidgetIO:
elif not ignore_errors:
raise ValueError(f"No handler for widget type: {type(widget)}")
@staticmethod
def check_and_adjust_limits(spin_box: QDoubleSpinBox, number: float):
"""
Check if the new limits are within the current limits, if not adjust the limits.
Args:
number(float): The new value to check against the limits.
"""
min_value = spin_box.minimum()
max_value = spin_box.maximum()
# Calculate the new limits
new_limit = number + 5 * number
if number < min_value:
spin_box.setMinimum(new_limit)
elif number > max_value:
spin_box.setMaximum(new_limit)
@staticmethod
def _find_handler(widget):
"""

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from bec_widgets.utils import BECConnector, ConnectionConfig
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
class DeviceInputConfig(ConnectionConfig):
@@ -9,7 +10,7 @@ class DeviceInputConfig(ConnectionConfig):
arg_name: str | None = None
class DeviceInputBase(BECConnector):
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.
@@ -25,6 +26,7 @@ class DeviceInputBase(BECConnector):
super().__init__(client=client, config=config, gui_id=gui_id)
self.get_bec_shortcuts()
self._device_filter = None
self._devices = []
@property
@@ -56,6 +58,7 @@ class DeviceInputBase(BECConnector):
"""
self.validate_device_filter(device_filter)
self.config.device_filter = device_filter
self._device_filter = device_filter
def set_default_device(self, default_device: str):
"""
@@ -118,6 +121,3 @@ class DeviceInputBase(BECConnector):
"""
if device not in self.get_device_list(self.config.device_filter):
raise ValueError(f"Device {device} is not valid.")
def cleanup(self):
super().cleanup()

View File

@@ -2,10 +2,11 @@ from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import Qt, Slot
from qtpy.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem, QWidget
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
class BECQueue(BECConnector, QTableWidget):
class BECQueue(BECWidget, QTableWidget):
"""
Widget to display the BEC queue.
"""

View File

@@ -5,15 +5,16 @@ The widget automatically updates the status of all running BEC services, and dis
from __future__ import annotations
import sys
from collections import defaultdict
from dataclasses import dataclass
from typing import TYPE_CHECKING
import qdarktheme
from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QObject, QTimer, Signal, Slot
from qtpy.QtWidgets import QTreeWidget, QTreeWidgetItem
from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
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:
@@ -23,45 +24,18 @@ if TYPE_CHECKING:
BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
class BECStatusBoxConfig(ConnectionConfig):
pass
class BECServiceInfoContainer(BaseModel):
@dataclass
class BECServiceInfoContainer:
"""Container to store information about the BEC services."""
service_name: str
status: BECStatus | str = Field(
default="NOTCONNECTED",
description="The status of the service. Can be any of the BECStatus names, or NOTCONNECTED.",
)
status: str
info: dict
metrics: dict | None
model_config: dict = {"validate_assignment": True}
@field_validator("status")
@classmethod
def validate_status(cls, v):
"""Validate input for status. Accept BECStatus and NOTCONNECTED.
Args:
v (BECStatus | str): The input value.
Returns:
str: The validated status.
"""
if v in list(BECStatus.__members__.values()):
return v.name
if v in list(BECStatus.__members__.keys()) or v == "NOTCONNECTED":
return v
raise ValueError(
f"Status must be one of {BECStatus.__members__.values()} or 'NOTCONNECTED'. Input {v}"
)
class BECServiceStatusMixin(QObject):
"""A mixin class to update the service status, and metrics.
It emits a signal 'services_update' when the service status is updated.
"""Mixin to receive the latest service status from the BEC server and emit it via services_update signal.
Args:
client (BECClient): The client object to connect to the BEC server.
@@ -69,29 +43,26 @@ class BECServiceStatusMixin(QObject):
services_update = Signal(dict, dict)
def __init__(self, client: BECClient):
super().__init__()
def __init__(self, parent, client: BECClient):
super().__init__(parent)
self.client = client
self._service_update_timer = QTimer()
self._service_update_timer.timeout.connect(self._get_service_status)
self._service_update_timer.start(1000)
def _get_service_status(self):
"""Pull latest service and metrics updates from REDIS for all services, and emit both via 'services_update' signal."""
"""Get the latest service status from the BEC server."""
# pylint: disable=protected-access
self.client._update_existing_services()
self.services_update.emit(self.client._services_info, self.client._services_metric)
class BECStatusBox(BECConnector, QTreeWidget):
"""A widget to display the status of different BEC services.
This widget automatically updates the status of all running BEC services, and displays their status.
Information about the individual services is collapsible, and double clicking on
the individual service will display the metrics about the service.
class BECStatusBox(BECWidget, QWidget):
"""An autonomous widget to display the status of BEC services.
Args:
parent Optional : The parent widget for the BECStatusBox. Defaults to None.
service_name Optional(str): The name of the top service label. Defaults to "BEC Server".
box_name Optional(str): The name of the top service label. Defaults to "BEC Server".
client Optional(BECClient): The client object to connect to the BEC server. Defaults to None
config Optional(BECStatusBoxConfig | dict): The configuration for the status box. Defaults to None.
gui_id Optional(str): The unique id for the widget. Defaults to None.
@@ -99,58 +70,67 @@ class BECStatusBox(BECConnector, QTreeWidget):
CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"]
service_update = Signal(dict)
service_update = Signal(BECServiceInfoContainer)
bec_core_state = Signal(str)
def __init__(
self,
parent=None,
service_name: str = "BEC Server",
box_name: str = "BEC Server",
client: BECClient = None,
config: BECStatusBoxConfig | dict = None,
bec_service_status_mixin: BECServiceStatusMixin = None,
gui_id: str = None,
):
if config is None:
config = BECStatusBoxConfig(widget_class=self.__class__.__name__)
else:
if isinstance(config, dict):
config = BECStatusBoxConfig(**config)
super().__init__(client=client, config=config, gui_id=gui_id)
QTreeWidget.__init__(self, parent=parent)
super().__init__(client=client, gui_id=gui_id)
QWidget.__init__(self, parent=parent)
self.tree = QTreeWidget(self)
self.layout = QHBoxLayout(self)
self.service_name = service_name
self.config = config
self.bec_service_info_container = {}
self.tree_items = {}
self.tree_top_item = None
self.box_name = box_name
self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None})
if not bec_service_status_mixin:
bec_service_status_mixin = BECServiceStatusMixin(client=self.client)
bec_service_status_mixin = BECServiceStatusMixin(self, client=self.client)
self.bec_service_status = bec_service_status_mixin
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.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
self.tree.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
self.layout.addWidget(self.tree)
def init_ui(self) -> None:
"""Initialize the UI for the status box, and add QTreeWidget as the basis for the status box."""
"""Init the UI for the BECStatusBox widget, should only take place once."""
self.init_ui_tree_widget()
top_label = self._create_status_widget(self.service_name, status=BECStatus.IDLE)
self.tree_top_item = QTreeWidgetItem()
self.tree_top_item.setExpanded(True)
self.tree_top_item.setDisabled(True)
self.addTopLevelItem(self.tree_top_item)
self.setItemWidget(self.tree_top_item, 0, top_label)
top_label = self._create_status_widget(self.box_name, status=BECStatus.IDLE)
tree_item = QTreeWidgetItem()
tree_item.setExpanded(True)
tree_item.setDisabled(True)
self.status_container[self.box_name].update({"item": tree_item, "widget": top_label})
self.tree.addTopLevelItem(tree_item)
self.tree.setItemWidget(tree_item, 0, top_label)
self.service_update.connect(top_label.update_config)
self._initialized = True
def init_ui_tree_widget(self) -> None:
"""Initialise the tree widget for the status box."""
self.tree.setHeaderHidden(True)
# TODO probably here is a problem still with setting the stylesheet
self.tree.setStyleSheet(
"QTreeWidget::item:!selected "
"{ "
"border: 1px solid gainsboro; "
"border-left: none; "
"border-top: none; "
"}"
"QTreeWidget::item:selected {}"
)
def _create_status_widget(
self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
) -> StatusItem:
"""Creates a StatusItem (QWidget) for the given service, and stores all relevant
information about the service in the bec_service_info_container.
information about the service in the status_container.
Args:
service_name (str): The name of the service.
@@ -163,16 +143,8 @@ class BECStatusBox(BECConnector, QTreeWidget):
"""
if info is None:
info = {}
self._update_bec_service_container(service_name, status, info, metrics)
item = StatusItem(
parent=self,
config={
"service_name": service_name,
"status": status.name,
"info": info,
"metrics": metrics,
},
)
self._update_status_container(service_name, status, info, metrics)
item = StatusItem(parent=self, config=self.status_container[service_name]["info"])
return item
@Slot(str)
@@ -183,30 +155,35 @@ class BECStatusBox(BECConnector, QTreeWidget):
Args:
status (BECStatus): The state of the core services.
"""
self.bec_service_info_container[self.service_name].status = status
self.service_update.emit(self.bec_service_info_container[self.service_name].model_dump())
self.status_container[self.box_name]["info"].status = status
self.service_update.emit(self.status_container[self.box_name]["info"])
def _update_bec_service_container(
def _update_status_container(
self, service_name: str, status: BECStatus, info: dict, metrics: dict = None
) -> None:
"""Update the bec_service_info_container with the newest status and metrics for the BEC service.
"""Update the status_container with the newest status and metrics for the BEC service.
If information about the service already exists, it will create a new entry.
Args:
service_name (str): The name of the service.
service_info (StatusMessage): A class containing the service status.
service_metric (ServiceMetricMessage): A class containing the service metrics.
status (BECStatus): The status of the service.
info (dict): The information about the service.
metrics (dict): The metrics of the service.
"""
container = self.bec_service_info_container.get(service_name, None)
container = self.status_container[service_name].get("info", None)
if container:
container.status = status
container.status = status.name
container.info = info
container.metrics = metrics
return
service_info_item = BECServiceInfoContainer(
service_name=service_name, status=status, info=info, metrics=metrics
service_name=service_name,
status=status.name if isinstance(status, BECStatus) else status,
info=info,
metrics=metrics,
)
self.bec_service_info_container.update({service_name: service_info_item})
self.status_container[service_name].update({"info": service_info_item})
@Slot(dict, dict)
def update_service_status(self, services_info: dict, services_metric: dict) -> None:
@@ -217,7 +194,7 @@ class BECStatusBox(BECConnector, QTreeWidget):
services_info (dict): A dictionary containing the service status for all running BEC services.
services_metric (dict): A dictionary containing the service metrics for all running BEC services.
"""
checked = []
checked = [self.box_name]
services_info = self.update_core_services(services_info, services_metric)
checked.extend(self.CORE_SERVICES)
@@ -225,28 +202,19 @@ class BECStatusBox(BECConnector, QTreeWidget):
checked.append(service_name)
metric_msg = services_metric.get(service_name, None)
metrics = metric_msg.metrics if metric_msg else None
if service_name in self.tree_items:
self._update_bec_service_container(
service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
)
self.service_update.emit(self.bec_service_info_container[service_name].model_dump())
if service_name in self.status_container:
if not msg:
self.add_tree_item(service_name, "NOTCONNECTED", {}, metrics)
continue
self._update_status_container(service_name, msg.status, msg.info, metrics)
self.service_update.emit(self.status_container[service_name]["info"])
continue
item_widget = self._create_status_widget(
service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
)
item = QTreeWidgetItem()
item.setDisabled(True)
self.service_update.connect(item_widget.update_config)
self.tree_top_item.addChild(item)
self.setItemWidget(item, 0, item_widget)
self.tree_items.update({service_name: (item, item_widget)})
self.add_tree_item(service_name, msg.status, msg.info, metrics)
self.check_redundant_tree_items(checked)
def update_core_services(self, services_info: dict, services_metric: dict) -> dict:
"""Method to process status and metrics updates of core services (stored in CORE_SERVICES).
If a core services is not connected, it should not be removed from the status widget
"""Update the core services of BEC, and emit the updated status to the BECStatusBox.
Args:
services_info (dict): A dictionary containing the service status of different services.
@@ -255,28 +223,28 @@ class BECStatusBox(BECConnector, QTreeWidget):
Returns:
dict: The services_info dictionary after removing the info updates related to the CORE_SERVICES
"""
bec_core_state = "RUNNING"
core_state = BECStatus.RUNNING
for service_name in sorted(self.CORE_SERVICES):
metric_msg = services_metric.get(service_name, None)
metrics = metric_msg.metrics if metric_msg else None
if service_name not in services_info:
self.bec_service_info_container[service_name].status = "NOTCONNECTED"
bec_core_state = "ERROR"
else:
msg = services_info.pop(service_name)
self._update_bec_service_container(
service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
)
bec_core_state = (
"RUNNING" if (msg.status.value > 1 and bec_core_state == "RUNNING") else "ERROR"
)
if service_name in self.tree_items:
self.service_update.emit(self.bec_service_info_container[service_name].model_dump())
msg = services_info.pop(service_name, None)
if service_name not in self.status_container:
if not msg:
self.add_tree_item(service_name, "NOTCONNECTED", {}, metrics)
continue
self.add_tree_item(service_name, msg.status, msg.info, metrics)
continue
self.add_tree_item(service_name, msg.status, msg.info, metrics)
if not msg:
self.status_container[service_name]["info"].status = "NOTCONNECTED"
core_state = None
else:
self._update_status_container(service_name, msg.status, msg.info, metrics)
if core_state:
core_state = msg.status if msg.status.value < core_state.value else core_state
self.bec_core_state.emit(bec_core_state)
self.service_update.emit(self.status_container[service_name]["info"])
self.bec_core_state.emit(core_state.name if core_state else "NOTCONNECTED")
return services_info
def check_redundant_tree_items(self, checked: list) -> None:
@@ -285,11 +253,12 @@ class BECStatusBox(BECConnector, QTreeWidget):
Args:
checked (list): A list of services that are currently running.
"""
to_be_deleted = [key for key in self.tree_items if key not in checked]
to_be_deleted = [key for key in self.status_container if key not in checked]
for key in to_be_deleted:
item, _ = self.tree_items.pop(key)
self.tree_top_item.removeChild(item)
obj = self.status_container.pop(key)
item = obj["item"]
self.status_container[self.box_name]["item"].removeChild(item)
def add_tree_item(
self, service_name: str, status: BECStatus, info: dict = None, metrics: dict = None
@@ -298,30 +267,16 @@ class BECStatusBox(BECConnector, QTreeWidget):
Args:
service_name (str): The name of the service.
service_status_msg (StatusMessage): The status of the service.
status (BECStatus): The status of the service.
info (dict): The information about the service.
metrics (dict): The metrics of the service.
"""
item_widget = self._create_status_widget(
service_name=service_name, status=status, info=info, metrics=metrics
)
item_widget = self._create_status_widget(service_name, status, info, metrics)
item = QTreeWidgetItem()
self.service_update.connect(item_widget.update_config)
self.tree_top_item.addChild(item)
self.setItemWidget(item, 0, item_widget)
self.tree_items.update({service_name: (item, item_widget)})
def init_ui_tree_widget(self) -> None:
"""Initialise the tree widget for the status box."""
self.setHeaderHidden(True)
self.setStyleSheet(
"QTreeWidget::item:!selected "
"{ "
"border: 1px solid gainsboro; "
"border-left: none; "
"border-top: none; "
"}"
"QTreeWidget::item:selected {}"
)
self.status_container[self.box_name]["item"].addChild(item)
self.tree.setItemWidget(item, 0, item_widget)
self.status_container[service_name].update({"item": item, "widget": item_widget})
@Slot(QTreeWidgetItem, int)
def on_tree_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None:
@@ -331,13 +286,9 @@ class BECStatusBox(BECConnector, QTreeWidget):
item (QTreeWidgetItem): The item that was double clicked.
column (int): The column that was double clicked.
"""
for _, (tree_item, status_widget) in self.tree_items.items():
if tree_item == item:
status_widget.show_popup()
def closeEvent(self, event):
super().cleanup()
QTreeWidget().closeEvent(event)
for _, objects in self.status_container.items():
if objects["item"] == item:
objects["widget"].show_popup()
def main():
@@ -346,7 +297,7 @@ def main():
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
qdarktheme.setup_theme("auto")
apply_theme("dark")
main_window = BECStatusBox()
main_window.show()
sys.exit(app.exec())

View File

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

View File

@@ -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 qtpy.QtGui import QIcon
from bec_widgets.widgets.bec_status_box.bec_status_box import BECStatusBox
DOM_XML = """
<ui language='c++'>
<widget class='BECStatusBox' name='bec_status_box'>
</widget>
</ui>
"""
class BECStatusBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = BECStatusBox(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "bec_status_box"
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 "BECStatusBox"
def toolTip(self):
return "An autonomous widget to display the status of BEC services."
def whatsThis(self):
return self.toolTip()

View 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.bec_status_box.bec_status_box_plugin import BECStatusBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(BECStatusBoxPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -2,17 +2,12 @@
The widget is bound to be used with the BECStatusBox widget."""
import enum
import sys
from datetime import datetime
import qdarktheme
from bec_lib.utils.import_utils import lazy_import_from
from pydantic import Field
from qtpy.QtCore import Qt, Slot
from qtpy.QtWidgets import QDialog, QHBoxLayout, QLabel, QStyle, QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
# TODO : Put normal imports back when Pydantic gets faster
BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
@@ -27,39 +22,29 @@ class IconsEnum(enum.Enum):
NOTCONNECTED = "SP_TitleBarContextHelpButton"
class StatusWidgetConfig(ConnectionConfig):
"""Configuration class for the status item widget."""
service_name: str
status: str
info: dict
metrics: dict | None
icon_size: tuple = Field(default=(24, 24), description="The size of the icon in the widget.")
font_size: int = Field(16, description="The font size of the text in the widget.")
class StatusItem(QWidget):
"""A widget to display the status of a service.
Args:
parent: The parent widget.
config (dict): The configuration for the service.
config (dict): The configuration for the service, must be a BECServiceInfoContainer.
"""
def __init__(self, parent=None, config: dict = None):
if config is None:
config = StatusWidgetConfig(widget_class=self.__class__.__name__)
else:
if isinstance(config, dict):
config = StatusWidgetConfig(**config)
self.config = config
def __init__(self, parent: QWidget = None, config=None):
QWidget.__init__(self, parent=parent)
if config is None:
# needed because we need parent to be the first argument for QT Designer
raise ValueError(
"Please initialize the StatusItem with a BECServiceInfoContainer for config, received None."
)
self.config = config
self.parent = parent
self.layout = None
self.config = config
self._popup_label_ref = {}
self._label = None
self._icon = None
self.icon_size = (24, 24)
self._popup_label_ref = {}
self.init_ui()
def init_ui(self) -> None:
@@ -74,23 +59,21 @@ class StatusItem(QWidget):
self.update_ui()
@Slot(dict)
def update_config(self, config: dict) -> None:
"""Update the configuration of the status item widget.
This method is invoked from the parent widget.
The UI values are later updated based on the new configuration.
def update_config(self, config) -> None:
"""Update the config of the status item widget.
Args:
config (dict): Config updates from parent widget.
config (dict): Config updates from parent widget, must be a BECServiceInfoContainer.
"""
if config["service_name"] != self.config.service_name:
if self.config is None or config.service_name != self.config.service_name:
return
self.config.status = config["status"]
self.config.info = config["info"]
self.config.metrics = config["metrics"]
self.config = config
self.update_ui()
def update_ui(self) -> None:
"""Update the UI of the labels, and popup dialog."""
if self.config is None:
return
self.set_text()
self.set_status()
self._set_popup_text()
@@ -99,8 +82,8 @@ class StatusItem(QWidget):
"""Set the text of the QLabel basae on the config."""
service = self.config.service_name
status = self.config.status
if "BECClient" in service.split("/"):
service = service.split("/")[0] + "/..." + service.split("/")[1][-4:]
if len(service.split("/")) > 1 and service.split("/")[0].startswith("BEC"):
service = service.split("/", maxsplit=1)[0] + "/..." + service.split("/")[1][-4:]
if status == "NOTCONNECTED":
status = "NOT CONNECTED"
text = f"{service} is {status}"
@@ -110,7 +93,7 @@ class StatusItem(QWidget):
"""Set the status icon for the status item widget."""
icon_name = IconsEnum[self.config.status].value
icon = self.style().standardIcon(getattr(QStyle.StandardPixmap, icon_name))
self._icon.setPixmap(icon.pixmap(*self.config.icon_size))
self._icon.setPixmap(icon.pixmap(*self.icon_size))
self._icon.setAlignment(Qt.AlignmentFlag.AlignRight)
def show_popup(self) -> None:
@@ -153,19 +136,3 @@ class StatusItem(QWidget):
def _cleanup_popup_label(self) -> None:
"""Cleanup the popup label."""
self._popup_label_ref.clear()
def main():
"""Run the status item widget."""
# pylint: disable=import-outside-toplevel
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
qdarktheme.setup_theme("auto")
main_window = StatusItem()
main_window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

View File

@@ -1 +0,0 @@
from .stop_button.stop_button import StopButton

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,36 @@
from __future__ import annotations
from typing import Literal
import pyqtgraph as pg
class ColorButton(pg.ColorButton):
"""
A ColorButton that opens a dialog to select a color. Inherits from pyqtgraph.ColorButton.
Patches event loop of the ColorDialog, if opened in another QDialog.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def selectColor(self):
self.origColor = self.color()
self.colorDialog.setCurrentColor(self.color())
self.colorDialog.open()
self.colorDialog.exec()
def get_color(self, format: Literal["RGBA", "HEX"] = "RGBA") -> tuple | str:
"""
Get the color of the button in the specified format.
Args:
format(Literal["RGBA", "HEX"]): The format of the returned color.
Returns:
tuple|str: The color in the specified format.
"""
if format == "RGBA":
return self.color().getRgb()
if format == "HEX":
return self.color().name()

View File

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

View File

@@ -0,0 +1,55 @@
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.color_button.color_button import ColorButton
DOM_XML = """
<ui language='c++'>
<widget class='ColorButton' name='color_button'>
</widget>
</ui>
"""
class ColorButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = ColorButton(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Buttons"
def icon(self):
current_path = os.path.dirname(__file__)
icon_path = os.path.join(current_path, "assets", "color_button.png")
return QIcon(icon_path)
def includeFile(self):
return "color_button"
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 "ColorButton"
def toolTip(self):
return "ColorButton which opens a color dialog."
def whatsThis(self):
return self.toolTip()

View 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.color_button.color_button_plugin import ColorButtonPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(ColorButtonPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
import pyqtgraph as pg
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtGui import QColor, QFontMetrics, QImage
from qtpy.QtWidgets import QApplication, QComboBox, QStyledItemDelegate, QVBoxLayout, QWidget
class ColormapDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super(ColormapDelegate, self).__init__(parent)
self.image_width = 25
self.image_height = 10
self.gap = 10
def paint(self, painter, option, index):
text = index.data()
colormap = pg.colormap.get(text)
colors = colormap.getLookupTable(start=0.0, stop=1.0, alpha=False)
font_metrics = QFontMetrics(painter.font())
text_width = font_metrics.width(text)
text_height = font_metrics.height()
total_height = max(text_height, self.image_height)
image = QImage(self.image_width, self.image_height, QImage.Format_RGB32)
for i in range(self.image_width):
color = QColor(*colors[int(i * (len(colors) - 1) / (self.image_width - 1))])
for j in range(self.image_height):
image.setPixel(i, j, color.rgb())
painter.drawImage(
option.rect.x(), option.rect.y() + (total_height - self.image_height) // 2, image
)
painter.drawText(
option.rect.x() + self.image_width + self.gap,
option.rect.y() + (total_height - text_height) // 2 + font_metrics.ascent(),
text,
)
class ColormapSelector(QWidget):
"""
Simple colormap combobox widget. By default it loads all the available colormaps in pyqtgraph.
"""
colormap_changed_signal = Signal(str)
def __init__(self, parent=None, default_colormaps=None):
super().__init__(parent=parent)
self._colormaps = []
self.initUI(default_colormaps)
def initUI(self, default_colormaps=None):
self.layout = QVBoxLayout(self)
self.combo = QComboBox()
self.combo.setItemDelegate(ColormapDelegate())
self.combo.currentTextChanged.connect(self.colormap_changed)
self.available_colormaps = pg.colormap.listMaps()
if default_colormaps is None:
default_colormaps = self.available_colormaps
self.add_color_maps(default_colormaps)
self.layout.addWidget(self.combo)
@Slot()
def colormap_changed(self):
"""
Emit the colormap changed signal with the current colormap selected in the combobox.
"""
self.colormap_changed_signal.emit(self.combo.currentText())
def add_color_maps(self, colormaps=None):
"""
Add colormaps to the combobox.
Args:
colormaps(list): List of colormaps to add to the combobox. If None, all available colormaps are added.
"""
self.combo.clear()
if colormaps is not None:
for name in colormaps:
if name in self.available_colormaps:
self.combo.addItem(name)
else:
for name in self.available_colormaps:
self.combo.addItem(name)
self._colormaps = colormaps if colormaps is not None else self.available_colormaps
@Property("QStringList")
def colormaps(self):
"""
Property to get and set the colormaps in the combobox.
"""
return self._colormaps
@colormaps.setter
def colormaps(self, value):
"""
Set the colormaps in the combobox.
"""
if self._colormaps != value:
self._colormaps = value
self.add_color_maps(value)
if __name__ == "__main__": # pragma: no cover
import sys
app = QApplication(sys.argv)
ex = ColormapSelector()
ex.show()
sys.exit(app.exec_())

View File

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

View File

@@ -0,0 +1,59 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
import bec_widgets
from bec_widgets.widgets.colormap_selector.colormap_selector import ColormapSelector
DOM_XML = """
<ui language='c++'>
<widget class='ColormapSelector' name='colormap_selector'>
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class ColormapSelectorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = ColormapSelector(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Buttons"
def icon(self):
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "colormap_selector.png")
return QIcon(icon_path)
def includeFile(self):
return "colormap_selector"
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 "ColormapSelector"
def toolTip(self):
return ""
def whatsThis(self):
return self.toolTip()

View File

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

View File

@@ -222,7 +222,7 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
Start ``Backend`` process and render Pyte output as text.
"""
def __init__(self, parent, numColumns, numLines, **kwargs):
def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
super().__init__(parent)
# file descriptor to communicate with the subprocess

View File

@@ -0,0 +1,197 @@
import os
import uuid
from bec_lib.endpoints import MessageEndpoints
from bec_lib.messages import ScanQueueMessage
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDoubleSpinBox, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
class DeviceBox(BECWidget, QWidget):
device_changed = Signal(str, str)
def __init__(self, parent=None, device=None, *args, **kwargs):
super().__init__(*args, **kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts()
self._device = ""
self._limits = None
self.init_ui()
if device is not None:
self.device = device
self.init_device()
def init_ui(self):
self.device_changed.connect(self.on_device_change)
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, "device_box.ui"))
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
# fix the size of the device box
db = self.ui.device_box
db.setFixedHeight(234)
db.setFixedWidth(224)
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
self.ui.stop.clicked.connect(self.on_stop)
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)
self.ui.tweak_left.setToolTip("Tweak left")
self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
self.setpoint_validator = QDoubleValidator()
self.ui.setpoint.setValidator(self.setpoint_validator)
self.ui.spinner_widget.start()
def init_device(self):
if self.device in self.dev:
data = self.dev[self.device].read()
self.on_device_readback({"signals": data}, {})
@Property(str)
def device(self):
return self._device
@device.setter
def device(self, value):
if not value or not isinstance(value, str):
return
old_device = self._device
self._device = value
self.device_changed.emit(old_device, value)
@Slot(str, str)
def on_device_change(self, old_device: str, new_device: str):
if new_device not in self.dev:
print(f"Device {new_device} not found in the device list")
return
print(f"Device changed from {old_device} to {new_device}")
self.init_device()
self.bec_dispatcher.disconnect_slot(
self.on_device_readback, MessageEndpoints.device_readback(old_device)
)
self.bec_dispatcher.connect_slot(
self.on_device_readback, MessageEndpoints.device_readback(new_device)
)
self.ui.device_box.setTitle(new_device)
self.ui.readback.setToolTip(f"{self.device} readback")
self.ui.setpoint.setToolTip(f"{self.device} setpoint")
self.ui.step_size.setToolTip(f"Step size for {new_device}")
precision = self.dev[new_device].precision
if precision is not None:
self.ui.step_size.setDecimals(precision)
self.ui.step_size.setValue(10**-precision * 10)
@Slot(dict, dict)
def on_device_readback(self, msg_content: dict, metadata: dict):
signals = msg_content.get("signals", {})
# pylint: disable=protected-access
hinted_signals = self.dev[self.device]._hints
precision = self.dev[self.device].precision
readback_val = None
setpoint_val = None
if len(hinted_signals) == 1:
signal = hinted_signals[0]
readback_val = signals.get(signal, {}).get("value")
if f"{self.device}_setpoint" in signals:
setpoint_val = signals.get(f"{self.device}_setpoint", {}).get("value")
if f"{self.device}_motor_is_moving" in signals:
is_moving = signals.get(f"{self.device}_motor_is_moving", {}).get("value")
if is_moving:
self.ui.spinner_widget.start()
self.ui.spinner_widget.setToolTip("Device is moving")
else:
self.ui.spinner_widget.stop()
self.ui.spinner_widget.setToolTip("Device is idle")
if readback_val is not None:
self.ui.readback.setText(f"{readback_val:.{precision}f}")
if setpoint_val is not None:
self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
limits = self.dev[self.device].limits
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)
def update_limits(self, limits):
if limits == self._limits:
return
self._limits = limits
if limits is not None and limits[0] != limits[1]:
self.ui.position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
self.setpoint_validator.setRange(limits[0], limits[1])
else:
self.ui.position_indicator.setToolTip("No limits set")
self.setpoint_validator.setRange(float("-inf"), float("inf"))
@Slot()
def on_stop(self):
request_id = str(uuid.uuid4())
params = {
"device": self.device,
"rpc_id": request_id,
"func": "stop",
"args": [],
"kwargs": {},
}
msg = ScanQueueMessage(
scan_type="device_rpc",
parameter=params,
queue="emergency",
metadata={"RID": request_id, "response": False},
)
self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
@property
def step_size(self):
return self.ui.step_size.value()
@Slot()
def on_tweak_right(self):
self.dev[self.device].move(self.step_size, relative=True)
@Slot()
def on_tweak_left(self):
self.dev[self.device].move(-self.step_size, relative=True)
@Slot()
def on_setpoint_change(self):
self.ui.setpoint.clearFocus()
setpoint = self.ui.setpoint.text()
self.dev[self.device].move(float(setpoint), relative=False)
self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
apply_theme("light")
widget = DeviceBox(device="samx")
widget.show()
sys.exit(app.exec_())

View File

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

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>251</width>
<height>289</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>192</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="device_box">
<property name="title">
<string>Device Name</string>
</property>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0">
<property name="topMargin">
<number>0</number>
</property>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="step_size"/>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="tweak_right">
<property name="minimumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::RightArrow</enum>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QLineEdit" name="setpoint"/>
</item>
<item row="3" column="0">
<widget class="QToolButton" name="tweak_left">
<property name="minimumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>50</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::LeftArrow</enum>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QPushButton" name="stop">
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="SpinnerWidget" name="spinner_widget">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="PositionIndicator" name="position_indicator"/>
</item>
<item>
<widget class="QLabel" name="readback">
<property name="text">
<string>Position</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SpinnerWidget</class>
<extends>QWidget</extends>
<header>spinner_widget</header>
</customwidget>
<customwidget>
<class>PositionIndicator</class>
<extends>QWidget</extends>
<header>position_indicator</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -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 qtpy.QtGui import QIcon
from bec_widgets.widgets.device_box.device_box import DeviceBox
DOM_XML = """
<ui language='c++'>
<widget class='DeviceBox' name='device_box'>
</widget>
</ui>
"""
class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = DeviceBox(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "Device Control"
def icon(self):
return QIcon()
def includeFile(self):
return "device_box"
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 "DeviceBox"
def toolTip(self):
return "A widget for controlling a single positioner. "
def whatsThis(self):
return self.toolTip()

View 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.device_box.device_box_plugin import DeviceBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceBoxPlugin())
if __name__ == "__main__": # pragma: no cover
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -2,10 +2,11 @@ from typing import TYPE_CHECKING
from qtpy.QtWidgets import QComboBox
from bec_widgets.widgets.device_inputs.device_input_base import 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.device_inputs.device_input_base import DeviceInputConfig
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig
class DeviceComboBox(DeviceInputBase, QComboBox):
@@ -82,11 +83,3 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
if device_obj is None:
raise ValueError(f"Device {device_name} is not found.")
return device_obj
def cleanup(self):
"""Cleanup the widget."""
super().cleanup()
def closeEvent(self, event):
super().cleanup()
QComboBox().closeEvent(event)

View File

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

View File

@@ -1,10 +1,11 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.device_inputs import DeviceComboBox
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
DOM_XML = """
<ui language='c++'>
@@ -27,10 +28,12 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML
def group(self):
return ""
return "BEC Device Inputs"
def icon(self):
return QIcon()
current_path = os.path.dirname(__file__)
icon_path = os.path.join(current_path, "assets", "device_combobox_icon.png")
return QIcon(icon_path)
def includeFile(self):
return "device_combobox"

View File

@@ -6,9 +6,7 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.device_inputs.device_combobox.device_combobox_plugin import (
DeviceComboBoxPlugin,
)
from bec_widgets.widgets.device_combobox.device_combobox_plugin import DeviceComboBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceComboBoxPlugin())

View File

@@ -1,2 +0,0 @@
from .device_combobox.device_combobox import DeviceComboBox
from .device_line_edit.device_line_edit import DeviceLineEdit

View File

@@ -1,4 +0,0 @@
{
"files": ["device_combobox.py", "launch_device_combobox.py",
]
}

View File

@@ -1,11 +0,0 @@
from bec_widgets.widgets.device_inputs import DeviceComboBox
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
w = DeviceComboBox()
w.show()
sys.exit(app.exec_())

View File

@@ -1,4 +0,0 @@
{
"files": ["device_line_edit.py", "launch_device_line_edit.py",
]
}

View File

@@ -1,11 +0,0 @@
from bec_widgets.widgets.device_inputs import DeviceLineEdit
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
w = DeviceLineEdit()
w.show()
sys.exit(app.exec_())

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