mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-08-09 19:50:34 +02:00
Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d363e58d6 | ||
|
|
a9b808d86a | ||
|
|
7018506872 | ||
|
|
b5e2c83492 | ||
|
|
08a39bf41c | ||
|
|
b9f2abcb21 | ||
|
|
88996e8cde | ||
|
|
b1d5fbbfad | ||
|
|
be0915c054 | ||
|
|
767a492613 | ||
|
|
b9f9a003a2 | ||
|
|
734f4c7750 | ||
|
|
c78cd898f2 | ||
|
|
74a249bd06 | ||
|
|
2020953b93 | ||
|
|
3826bb3d9e | ||
|
|
7ffc06f3c7 | ||
|
|
eea1a75d4a | ||
|
|
b9bff38b64 | ||
|
|
f04862933f | ||
|
|
f5b8375fd3 | ||
|
|
db1cdf4280 | ||
|
|
fa1e86ff07 | ||
|
|
9a95454723 | ||
|
|
dd1875ea5c | ||
|
|
df4fabb32a | ||
|
|
99114f14f6 | ||
|
|
fc3a69bbb0 | ||
|
|
9594be2606 | ||
|
|
96fd239608 | ||
|
|
61de7e9e22 | ||
|
|
24c4cdc39f | ||
|
|
fadbf77866 | ||
|
|
03819a3d90 | ||
|
|
d6d0777113 | ||
|
|
1aa83e0ef1 | ||
|
|
b09c644e01 | ||
|
|
e403870874 | ||
|
|
1586ce2d6c | ||
|
|
576353cfe8 | ||
|
|
cefc415c98 | ||
|
|
bc0ef7893e | ||
|
|
0e802d8194 | ||
|
|
d7718d4dcb | ||
|
|
4c2e02e912 | ||
|
|
b8774e0b0b | ||
|
|
6e75642090 | ||
|
|
aaa0d1003d | ||
|
|
5960918137 | ||
|
|
3dc0532df0 | ||
|
|
96863adf53 | ||
|
|
08425a623e | ||
|
|
b787759f44 | ||
|
|
25ef7c05e6 | ||
|
|
c36bb80d6a | ||
|
|
c069f3e1b3 | ||
|
|
215d59c8bf | ||
|
|
008a33a9b1 | ||
|
|
3e787234c7 | ||
|
|
1173510105 | ||
|
|
a391f3018c | ||
|
|
b6e1e20b7c | ||
|
|
572f2fb811 | ||
|
|
2e2d422910 | ||
|
|
f0556e4411 | ||
|
|
4a97105e4b | ||
|
|
797f73c39a | ||
|
|
b8f796fd3f | ||
|
|
78673ea11a | ||
|
|
c6a14c0768 | ||
|
|
70a966d8dc | ||
|
|
c42511dd44 | ||
|
|
db62f9e998 | ||
|
|
0610d2f9f0 | ||
|
|
c1dd0ee190 | ||
|
|
a45c407568 | ||
|
|
813f57861c | ||
|
|
3faee98ec8 | ||
|
|
ca02132c8d | ||
|
|
cb4ef25b73 | ||
|
|
c8b7367815 | ||
|
|
a268caaa30 | ||
|
|
6b25abff70 | ||
|
|
21c807f358 | ||
|
|
56fdae4275 | ||
|
|
e6a06c9f43 | ||
|
|
f979a63d3d | ||
|
|
327bc54e22 | ||
|
|
a51b15da3f | ||
|
|
7271b422f9 | ||
|
|
1866ba66c8 | ||
|
|
6175a04a90 | ||
|
|
7120f3e93b | ||
|
|
acc13183e2 | ||
|
|
f75fc19c5b | ||
|
|
2650c8b8cf | ||
|
|
1de3cbf65a | ||
|
|
4a9d0c9e44 | ||
|
|
88ecd05b95 | ||
|
|
df812eaad5 | ||
|
|
d62da494c8 | ||
|
|
e631fc15d8 |
+3
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+114
-122
@@ -1,155 +1,147 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.72.1 (2024-06-24)
|
||||
## v0.82.1 (2024-07-07)
|
||||
|
||||
### 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))
|
||||
|
||||
### Unknown
|
||||
|
||||
* tests(status_box_test): temporary disabled tests for status_box due to high rate of failures ([`aa7ce2e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aa7ce2ea27bb9564d4f5104bbff30725b8656453))
|
||||
|
||||
## v0.71.1 (2024-06-23)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: don't print exception if the auto-update module cannot be found in plugins ([`860517a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/860517a3211075d1f6e2af7fa6a567b9e0cd77f3))
|
||||
|
||||
## v0.71.0 (2024-06-23)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(scan_group_box): scan box for args and kwargs separated from ScanControlGUI code ([`d8cf441`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d8cf44134c30063e586771f9068947fef7a306d1))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(cleanup): cleanup added to device_input widgets and scan_control ([`8badb6a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8badb6adc1d003dbf0b2b1a800c34821f3fc9aa3))
|
||||
|
||||
* fix(scan_group_box): added row counter based on widgets ([`37682e7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37682e7b8a6ede38308880d285e41a948d6fe831))
|
||||
|
||||
* fix(scan_control): added default min limit for args bundle if specified ([`ec4574e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ec4574ed5c2c85ea6fbbe2b98f162a8e1220653b))
|
||||
|
||||
* fix(scan_control): argbox delete later added to prevent overlapping gui if scan changed ([`7ce3a83`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7ce3a83c58cb69c2bf7cb7f4eaba7e6a2ca6c546))
|
||||
|
||||
* fix(scan_control): only scans with defined gui_config are allowed ([`6dff187`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6dff1879c4178df0f8ebfd35101acdebb028d572))
|
||||
|
||||
* fix(WidgetIO): find handlers within base classes ([`ca85638`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca856384f380dabf28d43f1cd48511af784c035b))
|
||||
|
||||
* fix(scan_control): adapted widget to scan BEC gui config ([`8b822e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8b822e0fa8e28f080b9a4bf81948a7280a4c07bf))
|
||||
|
||||
* fix(scan_control): scan_control.py combatible with the newest BEC versions, test disabled ([`67d398c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/67d398caf74e08ab25a70cc5d85a5f0c2de8212d))
|
||||
* fix(motor_map): bug where motors without limits were selected ([`c78cd89`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c78cd898f203f950d7cb589eb5609feaa88062cf))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(device_line_edit): renamed default_device to default ([`4e2c9df`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4e2c9df6a4979d935285fd7eba17fd7fd455a35c))
|
||||
* refactor(setting_dialog): moved to qt_utils ([`3826bb3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3826bb3d9e870e85709b5b20ef09a4d22641280c))
|
||||
|
||||
* refactor(toolbar): toolbar moved from widgets to qt_utils ([`7ffc06f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7ffc06f3c7ddd86a1681408a75221b9bbadb236b))
|
||||
|
||||
### Test
|
||||
|
||||
* test(scan_control): tests added ([`56e74a0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56e74a0e7da72d18e89bc30d1896dbf9ef97cd6b))
|
||||
* test(setting_dialog): tests added ([`74a249b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/74a249bd065d01006cb532bfff2a9bfedb34b592))
|
||||
|
||||
### Unknown
|
||||
|
||||
* test(scan_control):e2e tests added ([`83001a0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/83001a0d8267e1320549b07032857dcf46ecd293))
|
||||
* tests(motor_map_widget): tests added ([`734f4c7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/734f4c77507a1edafd477d81b5f7401d8e759be2))
|
||||
|
||||
* doc(scan_control): docs added ([`1b7921a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b7921a7f2e3bcc846219a2a7aa0de0fd27bb8fe))
|
||||
* feat(settings_dialog):apply button ([`2020953`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2020953b933b6fcad61ecc770588d39518c26fdd))
|
||||
|
||||
* fix(device_line_edit):SizePolicy fixed for 100 horizontal ([`21d20e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/21d20e0fc78e9a3853abe802733388cce119ce20))
|
||||
|
||||
* tests WIP ([`c09644b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c09644b29ddb291c91dc58bcd6ebf02ff45cab36))
|
||||
|
||||
## v0.70.0 (2024-06-21)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: fix typo in link ([`fdf11d8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fdf11d8147750e379af9b17792761a267b49ae53))
|
||||
## v0.82.0 (2024-07-07)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(bec-designer): automatic plugin discovery ([`4639eee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4639eee0b975ebd7a946e0e290449f5b88c372eb))
|
||||
|
||||
* feat(device_line_edit): plugin added to bec-designer ([`b4b27ae`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b4b27aea3d8c08fa3d5d5514c69dbde32721d1dc))
|
||||
|
||||
* feat(device_combobox): plugin added to bec-designer ([`e483b28`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e483b282db20a81182b87938ea172654092419b5))
|
||||
|
||||
* feat: added entry point for bec-designer ([`36391db`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/36391db60735d57b371211791ddf8d3d00cebcf1))
|
||||
|
||||
* feat(utils/bec-designer): added startup script to launched QtDesigner compatible with conda environments ([`5362334`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5362334ff3b07fc83653323a084a4b6946bade96))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(bec-desiger+plugins): imports fixed, PYSIDE6 check to not enable run plugins with pyqt6 ([`50b3422`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/50b3422528d46d74317e8c903b6286e868ab7fe0))
|
||||
|
||||
## v0.69.0 (2024-06-21)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(widgets): added vscode widget ([`48ae950`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/48ae950d57b454307ce409e2511f7b7adf3cfc6b))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(generate_cli): fixed rpc generate for classes without user access; closes #226 ([`925c893`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/925c893f3ff4337fc8b4d237c8ffc19a597b0996))
|
||||
|
||||
## v0.68.0 (2024-06-21)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: properly handle SIGINT (ctrl-c) in BEC GUI server -> calls qapplication.quit() ([`3644f34`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3644f344da2df674bc0d5740c376a86b9d0dfe95))
|
||||
|
||||
* feat: bec-gui-server: redirect stdout and stderr (if any) as proper debug and error log entries ([`d1266a1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d1266a1ce148ff89557a039e3a182a87a3948f49))
|
||||
|
||||
* feat: add logger for BEC GUI server ([`630616e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/630616ec729f60aa0b4d17a9e0379f9c6198eb96))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: ignore GUI server output (any output will go to log file)
|
||||
|
||||
If a logger is given to log `_start_log_process`, the server stdout and
|
||||
stderr streams will be redirected as log entries with levels DEBUG or ERROR
|
||||
in their parent process ([`ce37416`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ce374163cab87a92847409051739777bc505a77b))
|
||||
|
||||
* fix: do not create 'BECClient' logger when instantiating BECDispatcher ([`f7d0b07`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f7d0b0768ace42a33e2556bb33611d4f02e5a6d9))
|
||||
|
||||
## v0.67.0 (2024-06-21)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: add widget to documentation ([`6fa1c06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6fa1c06053131dabd084bb3cf13c853b5d3ce833))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: introduce BECStatusBox Widget ([`443b6c1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/443b6c1d7b02c772fda02e2d1eefd5bd40249e0c))
|
||||
* feat(toggle): added angular component-like toggle ([`b9bff38`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b9bff38b64b86f06b3bc047922ef9df0c7d32e71))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: Change inheritance to QTreeWidget from QWidget ([`d2f2b20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2f2b206bb0eab60b8a9b0d0ac60a6b7887fa6fb))
|
||||
* refactor(device_input): DeviceComboBox and DeviceLineEdit moved to top layer of widgets ([`f048629`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f04862933f049030554086adef3ec9e1aebd3eda))
|
||||
|
||||
### Test
|
||||
* refactor(stop_button): moved to top layer, plugin added ([`f5b8375`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f5b8375fd36e3bb681de571da86a6c0bdb3cb6f0))
|
||||
|
||||
* test: add test suite for bec_status_box and status_item ([`5d4ca81`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d4ca816cdedec4c88aba9eb326f85392504ea1c))
|
||||
* refactor(motor_map_widget): removed restriction of only PySide6 for widget ([`db1cdf4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/db1cdf42806fef6d7c6d2db83528f32df3f9751d))
|
||||
|
||||
### Unknown
|
||||
* refactor(color_button): ColorButton moved to top level of widgets ([`fa1e86f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fa1e86ff07b25d2c47c73117b00765b8e2f25da4))
|
||||
|
||||
* Update file requirements.txt ([`505a5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/505a5ec8334ff4422913b3a7b79d39bcb42ad535))
|
||||
|
||||
## v0.66.1 (2024-06-20)
|
||||
## v0.81.2 (2024-07-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: fixed shutdown for pyside ([`2718bc6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2718bc624731301756df524d0d5beef6cb1c1430))
|
||||
* fix(waveform): scan_history error check for IndexError ([`dd1875e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dd1875ea5cc18bcef9aad743347a8accf144c08d))
|
||||
|
||||
## v0.66.0 (2024-06-20)
|
||||
## v0.81.1 (2024-07-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(motor_control): temporary remove of motor control widgets ([`99114f1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99114f14f62202e1fd8bf145616fa8c69937ada4))
|
||||
|
||||
## v0.81.0 (2024-07-06)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(rpc): discover widgets automatically ([`ef25f56`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ef25f5638032f931ceb292540ada618508bb2aed))
|
||||
* feat(color_button): can get colors in RGBA or HEX ([`9594be2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9594be260680d11c8550ff74ffb8d679e5a5b8f6))
|
||||
|
||||
## v0.80.1 (2024-07-06)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(entry_validator): check for entry == "" ([`61de7e9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61de7e9e221c766b9fb3ec23246da6a11c96a986))
|
||||
|
||||
## v0.80.0 (2024-07-06)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(qt5): dropped support for qt5; pyside2 and pyqt5 ([`fadbf77`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fadbf77866903beff6580802bc203d53367fc7e7))
|
||||
|
||||
* feat(plugins): moved plugin dict to dataclass and container ([`03819a3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/03819a3d902b4a51f3e882d52aedd971b2a8e127))
|
||||
|
||||
* feat(plugins): added support for pyqt6 ui files ([`d6d0777`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d6d07771135335cb78dc648508ce573b8970261a))
|
||||
|
||||
* feat(plugins): added bec widgets base class ([`1aa83e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1aa83e0ef1ffe45b01677b0b4590535cb0ca1cff))
|
||||
|
||||
## v0.79.3 (2024-07-05)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: changed inheritance to adress qt designer bug in rendering ([`e403870`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e403870874bd5e45840a034d6f1b3dd576d9c846))
|
||||
|
||||
* fix: add designer plugin classes ([`1586ce2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1586ce2d6cba2bb086b2ef596e724bb9e40ab4f2))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: simplify logic in bec_status_box ([`576353c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/576353cfe8c6fd64db561f0b6e2bc951300643d3))
|
||||
|
||||
## v0.79.2 (2024-07-04)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: overwrite closeEvent and call super class ([`bc0ef78`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bc0ef7893ef100b71b62101c459655509b534a56))
|
||||
|
||||
## v0.79.1 (2024-07-03)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: use libdir env var to preload Python library, also for Linux platform ([`d7718d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d7718d4dcb9728c050b6421388af4d484f3741f2))
|
||||
|
||||
## v0.79.0 (2024-07-03)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(motor_map_widget): standalone MotorMap Widget with toolbar + plugin ([`6e75642`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6e756420907d7093557e945bc92bc4cfc0138d07))
|
||||
|
||||
* feat(motor_map): method to reset history trace ([`5960918`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5960918137dd41cdeb94e50f8abc4f169cf45c11))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(toolbar): change default color to black to match BECFigure theme ([`b8774e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8774e0b0bc43dcd00f94f42539a778e507ca27d))
|
||||
|
||||
* fix(motor_map): fixed bug with residual trace after changing motors ([`aaa0d10`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aaa0d1003d2e94b45bafe4f700852c2c05288aea))
|
||||
|
||||
* fix(widget_io): widget handler adjusted for spinboxes and comboboxes ([`3dc0532`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3dc0532df05b6ec0a2522107fa0b1e210ce7d91b))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(toolbar): cleanup and adjusted colors ([`96863ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96863adf53c15112645d20eb6200733617801c6d))
|
||||
|
||||
## v0.78.1 (2024-07-02)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(ui_loader): ui loader is compatible with bec plugins ([`b787759`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b787759f44486dc7af2c03811efb156041e4b6cb))
|
||||
|
||||
## v0.78.0 (2024-07-02)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(color_button): patched ColorButton from pyqtgraph to be able to be opened in another QDialog ([`c36bb80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c36bb80d6a4939802a4a1c8e5452c7b94bac185e))
|
||||
|
||||
## v0.77.0 (2024-07-02)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(bec_connector): export config to yaml ([`a391f30`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a391f3018c50fee6a4a06884491b957df80c3cd3))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(waveform): scatter 2D brush error ([`215d59c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/215d59c8bfe7fda9aff8cec8353bef9e1ce2eca1))
|
||||
|
||||
* fix(figure): API cleanup ([`008a33a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/008a33a9b192473cc58e90cd6d98c5bcb5f7b8c0))
|
||||
|
||||
* fix(figure): if/else logic corrected in subplot_factory ([`3e78723`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3e787234c7274b0698423d7bf9a4c54ec46bad5f))
|
||||
|
||||
* fix(image): processing of already displayed data; closes #106 ([`1173510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1173510105d2d70d7e498c2ac1e122cea3a16597))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+277
-195
@@ -13,12 +13,15 @@ class Widgets(str, enum.Enum):
|
||||
Enum for the available widgets.
|
||||
"""
|
||||
|
||||
BECStatusBox = "BECStatusBox"
|
||||
BECDock = "BECDock"
|
||||
BECDockArea = "BECDockArea"
|
||||
BECFigure = "BECFigure"
|
||||
BECMotorMapWidget = "BECMotorMapWidget"
|
||||
BECQueue = "BECQueue"
|
||||
BECStatusBox = "BECStatusBox"
|
||||
RingProgressBar = "RingProgressBar"
|
||||
ScanControl = "ScanControl"
|
||||
StopButton = "StopButton"
|
||||
TextBox = "TextBox"
|
||||
VSCodeEditor = "VSCodeEditor"
|
||||
WebsiteWidget = "WebsiteWidget"
|
||||
@@ -33,14 +36,21 @@ class BECCurve(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def dap_params(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -143,11 +153,18 @@ class BECCurve(RPCBase):
|
||||
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def dap_params(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
class BECDock(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -157,7 +174,7 @@ class BECDock(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
@@ -275,7 +292,7 @@ class BECDock(RPCBase):
|
||||
class BECDockArea(RPCBase, BECGuiClientMixin):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -383,7 +400,7 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
@@ -402,14 +419,14 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
|
||||
class BECFigure(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -417,6 +434,12 @@ class BECFigure(RPCBase):
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def axes(self, row: "int", col: "int") -> "BECPlotBase":
|
||||
"""
|
||||
@@ -439,102 +462,6 @@ class BECFigure(RPCBase):
|
||||
dict: All widgets within the figure.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_plot(
|
||||
self,
|
||||
x: "list | np.ndarray" = None,
|
||||
y: "list | np.ndarray" = None,
|
||||
x_name: "str" = None,
|
||||
y_name: "str" = None,
|
||||
z_name: "str" = None,
|
||||
x_entry: "str" = None,
|
||||
y_entry: "str" = None,
|
||||
z_entry: "str" = None,
|
||||
color: "Optional[str]" = None,
|
||||
color_map_z: "Optional[str]" = "plasma",
|
||||
label: "Optional[str]" = None,
|
||||
validate: "bool" = True,
|
||||
row: "int" = None,
|
||||
col: "int" = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> "BECWaveform":
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
|
||||
Args:
|
||||
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.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_image(
|
||||
self,
|
||||
monitor: "str" = None,
|
||||
color_bar: "Literal['simple', 'full']" = "full",
|
||||
color_map: "str" = "magma",
|
||||
data: "np.ndarray" = None,
|
||||
vrange: "tuple[float, float]" = None,
|
||||
row: "int" = None,
|
||||
col: "int" = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> "BECImageShow":
|
||||
"""
|
||||
Add an image to the figure at the specified position.
|
||||
|
||||
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.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECImageShow: The image widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_motor_map(
|
||||
self,
|
||||
motor_x: "str" = None,
|
||||
motor_y: "str" = None,
|
||||
row: "int" = None,
|
||||
col: "int" = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> "BECMotorMap":
|
||||
"""
|
||||
Args:
|
||||
motor_x(str): The name of the motor for the X axis.
|
||||
motor_y(str): The name of the motor for the Y axis.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs:
|
||||
|
||||
Returns:
|
||||
BECMotorMap: The motor map widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def plot(
|
||||
self,
|
||||
@@ -550,6 +477,11 @@ class BECFigure(RPCBase):
|
||||
color_map_z: "str | None" = "plasma",
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
new: "bool" = False,
|
||||
row: "int | None" = None,
|
||||
col: "int | None" = None,
|
||||
dap: "str | None" = None,
|
||||
config: "dict | None" = None,
|
||||
**axis_kwargs,
|
||||
) -> "BECWaveform":
|
||||
"""
|
||||
@@ -568,6 +500,11 @@ class BECFigure(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.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
dap(str): The DAP model to use for the curve.
|
||||
config(dict): Recreates the whole BECWaveform widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
@@ -582,6 +519,10 @@ class BECFigure(RPCBase):
|
||||
color_map: "str" = "magma",
|
||||
data: "np.ndarray" = None,
|
||||
vrange: "tuple[float, float]" = None,
|
||||
new: "bool" = False,
|
||||
row: "int | None" = None,
|
||||
col: "int | None" = None,
|
||||
config: "dict | None" = None,
|
||||
**axis_kwargs,
|
||||
) -> "BECImageShow":
|
||||
"""
|
||||
@@ -593,6 +534,10 @@ class BECFigure(RPCBase):
|
||||
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.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Recreates the whole BECImageShow widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
@@ -601,7 +546,14 @@ class BECFigure(RPCBase):
|
||||
|
||||
@rpc_call
|
||||
def motor_map(
|
||||
self, motor_x: "str" = None, motor_y: "str" = None, **axis_kwargs
|
||||
self,
|
||||
motor_x: "str" = None,
|
||||
motor_y: "str" = None,
|
||||
new: "bool" = False,
|
||||
row: "int | None" = None,
|
||||
col: "int | None" = None,
|
||||
config: "dict | None" = None,
|
||||
**axis_kwargs,
|
||||
) -> "BECMotorMap":
|
||||
"""
|
||||
Add a motor map to the figure. Always access the first motor map widget in the figure.
|
||||
@@ -609,6 +561,10 @@ class BECFigure(RPCBase):
|
||||
Args:
|
||||
motor_x(str): The name of the motor for the X axis.
|
||||
motor_y(str): The name of the motor for the Y axis.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Recreates the whole BECImageShow widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
@@ -659,12 +615,6 @@ class BECFigure(RPCBase):
|
||||
Clear all widgets from the figure and reset to default state
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def widget_list(self) -> "list[BECPlotBase]":
|
||||
@@ -678,14 +628,14 @@ class BECFigure(RPCBase):
|
||||
class BECImageItem(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -711,6 +661,7 @@ class BECImageItem(RPCBase):
|
||||
- log
|
||||
- rot
|
||||
- transpose
|
||||
- autorange_mode
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -767,6 +718,15 @@ class BECImageItem(RPCBase):
|
||||
autorange(bool): Whether to autorange the color bar.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_autorange_mode(self, mode: "Literal['max', 'mean']" = "mean"):
|
||||
"""
|
||||
Set the autorange mode to scale the vrange of the color bar. Choose between min/max or mean +/- std.
|
||||
|
||||
Args:
|
||||
mode(Literal["max","mean"]): Max for min/max or mean for mean +/- std.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_color_map(self, cmap: "str" = "magma"):
|
||||
"""
|
||||
@@ -796,7 +756,11 @@ class BECImageItem(RPCBase):
|
||||
|
||||
@rpc_call
|
||||
def set_vrange(
|
||||
self, vmin: "float" = None, vmax: "float" = None, vrange: "tuple[int, int]" = None
|
||||
self,
|
||||
vmin: "float" = None,
|
||||
vmax: "float" = None,
|
||||
vrange: "tuple[float, float]" = None,
|
||||
change_autorange: "bool" = True,
|
||||
):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
@@ -818,14 +782,14 @@ class BECImageItem(RPCBase):
|
||||
class BECImageShow(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -845,34 +809,12 @@ class BECImageShow(RPCBase):
|
||||
BECImageItem: The image object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_image_config(self, image_id, dict_output: "bool" = True) -> "ImageItemConfig | dict":
|
||||
"""
|
||||
Get the configuration of the image.
|
||||
|
||||
Args:
|
||||
image_id(str): The ID of the image.
|
||||
dict_output(bool): Whether to return the configuration as a dictionary. Defaults to True.
|
||||
|
||||
Returns:
|
||||
ImageItemConfig|dict: The configuration of the image.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_image_dict(self) -> "dict[str, dict[str, BECImageItem]]":
|
||||
"""
|
||||
Get all images.
|
||||
|
||||
Returns:
|
||||
dict[str, dict[str, BECImageItem]]: The dictionary of images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_monitor_image(
|
||||
self,
|
||||
monitor: "str",
|
||||
color_map: "Optional[str]" = "magma",
|
||||
color_bar: "Optional[Literal['simple', 'full']]" = "simple",
|
||||
color_bar: "Optional[Literal['simple', 'full']]" = "full",
|
||||
downsample: "Optional[bool]" = True,
|
||||
opacity: "Optional[float]" = 1.0,
|
||||
vrange: "Optional[tuple[int, int]]" = None,
|
||||
@@ -888,7 +830,7 @@ class BECImageShow(RPCBase):
|
||||
name: "str",
|
||||
data: "Optional[np.ndarray]" = None,
|
||||
color_map: "Optional[str]" = "magma",
|
||||
color_bar: "Optional[Literal['simple', 'full']]" = "simple",
|
||||
color_bar: "Optional[Literal['simple', 'full']]" = "full",
|
||||
downsample: "Optional[bool]" = True,
|
||||
opacity: "Optional[float]" = 1.0,
|
||||
vrange: "Optional[tuple[int, int]]" = None,
|
||||
@@ -931,6 +873,17 @@ class BECImageShow(RPCBase):
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_autorange_mode(self, mode: "Literal['max', 'mean']", name: "str" = None):
|
||||
"""
|
||||
Set the autoscale mode of the image, that decides how the vrange of the color bar is scaled.
|
||||
Choose betwen 'max' -> min/max of the data, 'mean' -> mean +/- fudge_factor*std of the data (fudge_factor~2).
|
||||
|
||||
Args:
|
||||
mode(str): The autoscale mode of the image.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_monitor(self, monitor: "str", name: "str" = None):
|
||||
"""
|
||||
@@ -1022,15 +975,6 @@ class BECImageShow(RPCBase):
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def toggle_threading(self, use_threading: "bool"):
|
||||
"""
|
||||
Toggle threading for the widgets postprocessing and updating.
|
||||
|
||||
Args:
|
||||
use_threading(bool): Whether to use threading.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs) -> "None":
|
||||
"""
|
||||
@@ -1166,7 +1110,14 @@ class BECImageShow(RPCBase):
|
||||
class BECMotorMap(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1243,15 +1194,110 @@ 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 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.
|
||||
"""
|
||||
|
||||
|
||||
class BECPlotBase(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1391,10 +1437,10 @@ class BECPlotBase(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECStatusBox(RPCBase):
|
||||
class BECQueue(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1403,7 +1449,25 @@ class BECStatusBox(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
|
||||
class BECStatusBox(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.
|
||||
"""
|
||||
@@ -1412,14 +1476,14 @@ class BECStatusBox(RPCBase):
|
||||
class BECWaveform(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1442,6 +1506,7 @@ class BECWaveform(RPCBase):
|
||||
color_map_z: "str | None" = "plasma",
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
dap: "str | None" = None,
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
@@ -1458,11 +1523,50 @@ 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.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_dap(
|
||||
self,
|
||||
x_name: "str",
|
||||
y_name: "str",
|
||||
x_entry: "Optional[str]" = None,
|
||||
y_entry: "Optional[str]" = None,
|
||||
color: "Optional[str]" = None,
|
||||
dap: "str" = "GaussianModel",
|
||||
**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.
|
||||
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.
|
||||
**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):
|
||||
"""
|
||||
@@ -1504,28 +1608,6 @@ class BECWaveform(RPCBase):
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_curve_config(self, curve_id: "str", dict_output: "bool" = True) -> "CurveConfig | dict":
|
||||
"""
|
||||
Get the configuration of a curve by its ID.
|
||||
|
||||
Args:
|
||||
curve_id(str): ID of the curve.
|
||||
|
||||
Returns:
|
||||
CurveConfig|dict: Configuration of the curve.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def apply_config(self, config: "dict | SubplotConfig", replot_last_scan: "bool" = False):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
|
||||
Args:
|
||||
config(dict|SubplotConfig): Configuration settings.
|
||||
replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict | pd.DataFrame":
|
||||
"""
|
||||
@@ -1673,7 +1755,7 @@ class BECWaveform(RPCBase):
|
||||
class DeviceComboBox(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1682,7 +1764,7 @@ class DeviceComboBox(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
@@ -1691,7 +1773,7 @@ class DeviceComboBox(RPCBase):
|
||||
class DeviceInputBase(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1700,7 +1782,7 @@ class DeviceInputBase(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
@@ -1709,7 +1791,7 @@ class DeviceInputBase(RPCBase):
|
||||
class DeviceLineEdit(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1718,7 +1800,7 @@ class DeviceLineEdit(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
@@ -1726,21 +1808,21 @@ class DeviceLineEdit(RPCBase):
|
||||
|
||||
class Ring(RPCBase):
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -1826,21 +1908,21 @@ class Ring(RPCBase):
|
||||
|
||||
class RingProgressBar(RPCBase):
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rpc_id(self) -> "str":
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -2006,7 +2088,7 @@ class RingProgressBar(RPCBase):
|
||||
class ScanControl(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -2015,7 +2097,7 @@ class ScanControl(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
@@ -2024,7 +2106,7 @@ class ScanControl(RPCBase):
|
||||
class StopButton(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -2033,7 +2115,7 @@ class StopButton(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_rpc(self) -> "dict":
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@@ -203,15 +203,12 @@ class BECGuiClientMixin:
|
||||
if self._process is None:
|
||||
return
|
||||
|
||||
self._run_rpc("close", (), wait_for_rpc_response=False)
|
||||
while self.gui_is_alive():
|
||||
time.sleep(0.2)
|
||||
|
||||
self._client.shutdown()
|
||||
if self._process:
|
||||
self._process.terminate()
|
||||
if self._process_output_processing_thread:
|
||||
self._process_output_processing_thread.join()
|
||||
self._process.wait()
|
||||
self._process = None
|
||||
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import argparse
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
import black
|
||||
import isort
|
||||
|
||||
from bec_widgets.utils.plugin_utils import get_rpc_classes
|
||||
from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator
|
||||
from bec_widgets.utils.plugin_utils import BECClassContainer, get_rpc_classes
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import get_overloads
|
||||
@@ -39,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)
|
||||
|
||||
@@ -155,12 +158,31 @@ 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.plugins:
|
||||
plugin = DesignerPluginGenerator(cls)
|
||||
if not hasattr(plugin, "info"):
|
||||
continue
|
||||
|
||||
# if the class directory already has a register, plugin and pyproject file, skip
|
||||
if os.path.exists(
|
||||
os.path.join(plugin.info.base_path, f"register_{plugin.info.plugin_name_snake}.py")
|
||||
):
|
||||
continue
|
||||
if os.path.exists(
|
||||
os.path.join(plugin.info.base_path, f"{plugin.info.plugin_name_snake}_plugin.py")
|
||||
):
|
||||
continue
|
||||
if os.path.exists(
|
||||
os.path.join(plugin.info.base_path, f"{plugin.info.plugin_name_snake}.pyproject")
|
||||
):
|
||||
continue
|
||||
plugin.run()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.argv = ["generate_cli.py", "--core"]
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import signal
|
||||
import sys
|
||||
@@ -29,7 +31,7 @@ class BECWidgetsCLIServer:
|
||||
dispatcher: BECDispatcher = None,
|
||||
client=None,
|
||||
config=None,
|
||||
gui_class: Union["BECFigure", "BECDockArea"] = BECFigure,
|
||||
gui_class: Union[BECFigure, BECDockArea] = BECFigure,
|
||||
) -> None:
|
||||
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
|
||||
self.client = self.dispatcher.client if client is None else client
|
||||
@@ -118,6 +120,7 @@ class BECWidgetsCLIServer:
|
||||
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
||||
self._shutdown_event = True
|
||||
self._heartbeat_timer.stop()
|
||||
self.gui.close()
|
||||
self.client.shutdown()
|
||||
|
||||
|
||||
@@ -207,6 +210,7 @@ def main():
|
||||
app.quit()
|
||||
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from .motor_movement import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorControlPanelRelative,
|
||||
MotorCoordinateTable,
|
||||
MotorThread,
|
||||
)
|
||||
|
||||
@@ -14,21 +14,6 @@ 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
|
||||
|
||||
# class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
# def __init__(self):
|
||||
# super().__init__()
|
||||
#
|
||||
# self.kernel_manager = QtInProcessKernelManager()
|
||||
# self.kernel_manager.start_kernel(show_banner=False)
|
||||
# self.kernel_client = self.kernel_manager.client()
|
||||
# self.kernel_client.start_channels()
|
||||
#
|
||||
# self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
#
|
||||
# def shutdown_kernel(self):
|
||||
# self.kernel_client.stop_channels()
|
||||
# self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"""A widget that contains a Jupyter console linked to BEC Widgets with full API access (contains Qt and pyqtgraph API)."""
|
||||
@@ -55,12 +40,14 @@ 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,
|
||||
"d0": self.d0,
|
||||
"d1": self.d1,
|
||||
"d2": self.d2,
|
||||
"fig0": self.fig0,
|
||||
"fig1": self.fig1,
|
||||
"fig2": self.fig2,
|
||||
"plt": self.plt,
|
||||
"bar": self.bar,
|
||||
}
|
||||
)
|
||||
@@ -89,17 +76,37 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
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.add_plot(x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="magma")
|
||||
self.figure.plot(
|
||||
x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="magma", new=True
|
||||
)
|
||||
|
||||
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")
|
||||
@@ -115,7 +122,8 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.fig2 = self.d2.add_widget("BECFigure", row=0, col=0)
|
||||
self.fig2.plot(x_name="samx", y_name="bpm4i")
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from .motor_control_compilations import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorControlPanelRelative,
|
||||
MotorCoordinateTable,
|
||||
MotorThread,
|
||||
)
|
||||
@@ -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())
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -0,0 +1,55 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtWidgets import QToolBar, QWidget
|
||||
|
||||
|
||||
class ToolBarAction(ABC):
|
||||
@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 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 = "black"):
|
||||
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)
|
||||
@@ -1,15 +1,20 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
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 bec_widgets.cli.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
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",))
|
||||
|
||||
@@ -59,10 +64,10 @@ class Worker(QRunnable):
|
||||
self.signals.completed.emit()
|
||||
|
||||
|
||||
class BECConnector:
|
||||
class BECConnector(BECWidget):
|
||||
"""Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
|
||||
|
||||
USER_ACCESS = ["config_dict", "get_all_rpc"]
|
||||
USER_ACCESS = ["_config_dict", "_get_all_rpc"]
|
||||
|
||||
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
|
||||
# BEC related connections
|
||||
@@ -126,23 +131,23 @@ class BECConnector:
|
||||
self._thread_pool.start(worker)
|
||||
return worker
|
||||
|
||||
def get_all_rpc(self) -> dict:
|
||||
def _get_all_rpc(self) -> dict:
|
||||
"""Get all registered RPC objects."""
|
||||
all_connections = self.rpc_register.list_all_connections()
|
||||
return dict(all_connections)
|
||||
|
||||
@property
|
||||
def rpc_id(self) -> str:
|
||||
def _rpc_id(self) -> str:
|
||||
"""Get the RPC ID of the widget."""
|
||||
return self.gui_id
|
||||
|
||||
@rpc_id.setter
|
||||
def rpc_id(self, rpc_id: str) -> None:
|
||||
@_rpc_id.setter
|
||||
def _rpc_id(self, rpc_id: str) -> None:
|
||||
"""Set the RPC ID of the widget."""
|
||||
self.gui_id = rpc_id
|
||||
|
||||
@property
|
||||
def config_dict(self) -> dict:
|
||||
def _config_dict(self) -> dict:
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -151,8 +156,8 @@ class BECConnector:
|
||||
"""
|
||||
return self.config.model_dump()
|
||||
|
||||
@config_dict.setter
|
||||
def config_dict(self, config: BaseModel) -> None:
|
||||
@_config_dict.setter
|
||||
def _config_dict(self, config: BaseModel) -> None:
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
@@ -161,6 +166,60 @@ class BECConnector:
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
def apply_config(self, config: dict, generate_new_id: bool = True) -> None:
|
||||
"""
|
||||
Apply the configuration to the widget.
|
||||
|
||||
Args:
|
||||
config(dict): Configuration settings.
|
||||
generate_new_id(bool): If True, generate a new GUI ID for the widget.
|
||||
"""
|
||||
self.config = ConnectionConfig(**config)
|
||||
if generate_new_id is True:
|
||||
gui_id = str(uuid.uuid4())
|
||||
self.rpc_register.remove_rpc(self)
|
||||
self.set_gui_id(gui_id)
|
||||
self.rpc_register.add_rpc(self)
|
||||
else:
|
||||
self.gui_id = self.config.gui_id
|
||||
|
||||
def load_config(self, path: str | None = None, gui: bool = False):
|
||||
"""
|
||||
Load the configuration of the widget from YAML.
|
||||
|
||||
Args:
|
||||
path(str): Path to the configuration file for non-GUI dialog mode.
|
||||
gui(bool): If True, use the GUI dialog to load the configuration file.
|
||||
"""
|
||||
if gui is True:
|
||||
config = load_yaml_gui(self)
|
||||
else:
|
||||
config = load_yaml(path)
|
||||
|
||||
if config is not None:
|
||||
if config.get("widget_class") != self.__class__.__name__:
|
||||
raise ValueError(
|
||||
f"Configuration file is not for {self.__class__.__name__}. Got configuration for {config.get('widget_class')}."
|
||||
)
|
||||
self.apply_config(config)
|
||||
|
||||
def save_config(self, path: str | None = None, gui: bool = False):
|
||||
"""
|
||||
Save the configuration of the widget to YAML.
|
||||
|
||||
Args:
|
||||
path(str): Path to save the configuration file for non-GUI dialog mode.
|
||||
gui(bool): If True, use the GUI dialog to save the configuration file.
|
||||
"""
|
||||
if gui is True:
|
||||
save_yaml_gui(self, self._config_dict)
|
||||
else:
|
||||
if path is None:
|
||||
path = os.getcwd()
|
||||
file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
|
||||
|
||||
save_yaml(file_path, self._config_dict)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_gui_id(self, gui_id: str) -> None:
|
||||
"""
|
||||
@@ -228,7 +287,10 @@ class BECConnector:
|
||||
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()
|
||||
if hasattr(super(), "cleanup"):
|
||||
super().cleanup()
|
||||
|
||||
# def closeEvent(self, event):
|
||||
# self.cleanup()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import site
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
@@ -9,15 +12,55 @@ if PYSIDE6:
|
||||
from PySide6.scripts.pyside_tool import (
|
||||
_extend_path_var,
|
||||
init_virtual_env,
|
||||
qt_tool_wrapper,
|
||||
is_pyenv_python,
|
||||
is_virtual_env,
|
||||
qt_tool_wrapper,
|
||||
ui_tool_binary,
|
||||
)
|
||||
|
||||
import bec_widgets
|
||||
|
||||
|
||||
def list_editable_packages() -> set[str]:
|
||||
"""
|
||||
List all editable packages in the environment.
|
||||
|
||||
Returns:
|
||||
set: A set of paths to editable packages.
|
||||
"""
|
||||
|
||||
editable_packages = set()
|
||||
|
||||
# Get site-packages directories
|
||||
site_packages = site.getsitepackages()
|
||||
if hasattr(site, "getusersitepackages"):
|
||||
site_packages.append(site.getusersitepackages())
|
||||
|
||||
for dist in importlib.metadata.distributions():
|
||||
location = dist.locate_file("").resolve()
|
||||
is_editable = all(not str(location).startswith(site_pkg) for site_pkg in site_packages)
|
||||
|
||||
if is_editable:
|
||||
editable_packages.add(str(location))
|
||||
|
||||
for packages in site_packages:
|
||||
# all dist-info directories in site-packages that contain a direct_url.json file
|
||||
dist_info_dirs = Path(packages).rglob("*.dist-info")
|
||||
for dist_info_dir in dist_info_dirs:
|
||||
direct_url = dist_info_dir / "direct_url.json"
|
||||
if not direct_url.exists():
|
||||
continue
|
||||
# load the json file and get the path to the package
|
||||
with open(direct_url, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
path = data.get("url", "")
|
||||
if path.startswith("file://"):
|
||||
path = path[7:]
|
||||
editable_packages.add(path)
|
||||
|
||||
return editable_packages
|
||||
|
||||
|
||||
def patch_designer(): # pragma: no cover
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
@@ -30,20 +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":
|
||||
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"
|
||||
lib_path = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
|
||||
os.environ["DYLD_INSERT_LIBRARIES"] = lib_path
|
||||
elif sys.platform == "win32":
|
||||
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}{suffix}"
|
||||
lib_path = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
|
||||
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)
|
||||
qt_tool_wrapper(ui_tool_binary("designer"), sys.argv[1:])
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Union
|
||||
@@ -9,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:
|
||||
@@ -128,9 +127,9 @@ 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
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
class BECWidget:
|
||||
"""Base class for all BEC widgets."""
|
||||
|
||||
def closeEvent(self, event):
|
||||
if hasattr(self, "cleanup"):
|
||||
self.cleanup()
|
||||
if hasattr(super(), "closeEvent"):
|
||||
super().closeEvent(event)
|
||||
@@ -67,6 +67,44 @@ class Colors:
|
||||
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
||||
return colors
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgba(hex_color: str, alpha=255) -> tuple:
|
||||
"""
|
||||
Convert HEX color to RGBA.
|
||||
|
||||
Args:
|
||||
hex_color(str): HEX color string.
|
||||
alpha(int): Alpha value (0-255). Default is 255 (opaque).
|
||||
|
||||
Returns:
|
||||
tuple: RGBA color tuple (r, g, b, a).
|
||||
"""
|
||||
hex_color = hex_color.lstrip("#")
|
||||
if len(hex_color) == 6:
|
||||
r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
||||
elif len(hex_color) == 8:
|
||||
r, g, b, a = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
|
||||
return (r, g, b, a)
|
||||
else:
|
||||
raise ValueError("HEX color must be 6 or 8 characters long.")
|
||||
return (r, g, b, alpha)
|
||||
|
||||
@staticmethod
|
||||
def rgba_to_hex(r: int, g: int, b: int, a: int = 255) -> str:
|
||||
"""
|
||||
Convert RGBA color to HEX.
|
||||
|
||||
Args:
|
||||
r(int): Red value (0-255).
|
||||
g(int): Green value (0-255).
|
||||
b(int): Blue value (0-255).
|
||||
a(int): Alpha value (0-255). Default is 255 (opaque).
|
||||
|
||||
Returns:
|
||||
hec_color(str): HEX color string.
|
||||
"""
|
||||
return "#{:02X}{:02X}{:02X}{:02X}".format(r, g, b, a)
|
||||
|
||||
@staticmethod
|
||||
def validate_color(color: tuple | str) -> tuple | str:
|
||||
"""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock"]
|
||||
|
||||
|
||||
class DesignerPluginInfo:
|
||||
def __init__(self, plugin_class):
|
||||
self.plugin_class = plugin_class
|
||||
self.plugin_name_pascal = plugin_class.__name__
|
||||
self.plugin_name_snake = self.pascal_to_snake(self.plugin_name_pascal)
|
||||
self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}"
|
||||
plugin_module = (
|
||||
".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin"
|
||||
)
|
||||
self.plugin_import = f"from {plugin_module} import {self.plugin_name_pascal}Plugin"
|
||||
|
||||
# first sentence / line of the docstring is used as tooltip
|
||||
self.plugin_tooltip = (
|
||||
plugin_class.__doc__.split("\n")[0].strip().replace('"', "'")
|
||||
if plugin_class.__doc__
|
||||
else self.plugin_name_pascal
|
||||
)
|
||||
|
||||
self.base_path = os.path.dirname(inspect.getfile(plugin_class))
|
||||
|
||||
@staticmethod
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""
|
||||
Convert PascalCase to snake_case.
|
||||
|
||||
Args:
|
||||
name (str): The name to be converted.
|
||||
|
||||
Returns:
|
||||
str: The converted name.
|
||||
"""
|
||||
s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
||||
s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1)
|
||||
return s2.lower()
|
||||
|
||||
|
||||
class DesignerPluginGenerator:
|
||||
def __init__(self, widget: type):
|
||||
self._excluded = False
|
||||
self.widget = widget
|
||||
self.info = DesignerPluginInfo(widget)
|
||||
if widget.__name__ in EXCLUDED_PLUGINS:
|
||||
|
||||
self._excluded = True
|
||||
return
|
||||
|
||||
self.templates = {}
|
||||
self.template_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
|
||||
)
|
||||
|
||||
def run(self, validate=True):
|
||||
if self._excluded:
|
||||
print(f"Plugin {self.widget.__name__} is excluded from generation.")
|
||||
return
|
||||
if validate:
|
||||
self._check_class_validity()
|
||||
self._load_templates()
|
||||
self._write_templates()
|
||||
|
||||
def _check_class_validity(self):
|
||||
|
||||
# Check if the widget is a QWidget subclass
|
||||
if not issubclass(self.widget, QObject):
|
||||
return
|
||||
|
||||
# Check if the widget class has parent as the first argument. This is a strict requirement of Qt!
|
||||
signature = list(inspect.signature(self.widget.__init__).parameters.values())
|
||||
if len(signature) == 1 or signature[1].name != "parent":
|
||||
raise ValueError(
|
||||
f"Widget class {self.widget.__name__} must have parent as the first argument."
|
||||
)
|
||||
|
||||
base_cls = [val for val in self.widget.__bases__ if issubclass(val, QObject)]
|
||||
if not base_cls:
|
||||
raise ValueError(
|
||||
f"Widget class {self.widget.__name__} must inherit from a QObject subclass."
|
||||
)
|
||||
|
||||
# Check if the widget class calls the super constructor with parent argument
|
||||
init_source = inspect.getsource(self.widget.__init__)
|
||||
cls_init_found = (
|
||||
bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent=parent") > 0)
|
||||
or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent)") > 0)
|
||||
or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent,") > 0)
|
||||
)
|
||||
super_init_found = (
|
||||
bool(
|
||||
init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent=parent") > 0
|
||||
)
|
||||
or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent,") > 0)
|
||||
or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent)") > 0)
|
||||
)
|
||||
if issubclass(self.widget.__bases__[0], QObject) and not super_init_found:
|
||||
super_init_found = (
|
||||
bool(init_source.find("super().__init__(parent=parent") > 0)
|
||||
or bool(init_source.find("super().__init__(parent,") > 0)
|
||||
or bool(init_source.find("super().__init__(parent)") > 0)
|
||||
)
|
||||
|
||||
if not cls_init_found and not super_init_found:
|
||||
raise ValueError(
|
||||
f"Widget class {self.widget.__name__} must call the super constructor with parent."
|
||||
)
|
||||
|
||||
def _write_templates(self):
|
||||
self._write_register()
|
||||
self._write_plugin()
|
||||
self._write_pyproject()
|
||||
|
||||
def _write_register(self):
|
||||
file_path = os.path.join(self.info.base_path, f"register_{self.info.plugin_name_snake}.py")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(self.templates["register"].format(**self.info.__dict__))
|
||||
|
||||
def _write_plugin(self):
|
||||
file_path = os.path.join(self.info.base_path, f"{self.info.plugin_name_snake}_plugin.py")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(self.templates["plugin"].format(**self.info.__dict__))
|
||||
|
||||
def _write_pyproject(self):
|
||||
file_path = os.path.join(self.info.base_path, f"{self.info.plugin_name_snake}.pyproject")
|
||||
out = {"files": [f"{self.info.plugin_class.__module__.split('.')[-1]}.py"]}
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(str(out))
|
||||
|
||||
def _load_templates(self):
|
||||
for file in os.listdir(self.template_path):
|
||||
if not file.endswith(".template"):
|
||||
continue
|
||||
with open(os.path.join(self.template_path, file), "r", encoding="utf-8") as f:
|
||||
self.templates[file.split(".")[0]] = f.read()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
||||
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
|
||||
|
||||
generator = DesignerPluginGenerator(SpinnerWidget)
|
||||
generator.run(validate=False)
|
||||
@@ -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
|
||||
|
||||
{widget_import}
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='{plugin_name_pascal}' name='{plugin_name_snake}'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class {plugin_name_pascal}Plugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = {plugin_name_pascal}(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return QIcon()
|
||||
|
||||
def includeFile(self):
|
||||
return "{plugin_name_snake}"
|
||||
|
||||
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 "{plugin_name_pascal}"
|
||||
|
||||
def toolTip(self):
|
||||
return "{plugin_tooltip}"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
{plugin_import}
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget({plugin_name_pascal}Plugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,7 @@ import yaml
|
||||
from qtpy.QtWidgets import QFileDialog
|
||||
|
||||
|
||||
def load_yaml(instance) -> Union[dict, None]:
|
||||
def load_yaml_gui(instance) -> Union[dict, None]:
|
||||
"""
|
||||
Load YAML file from disk.
|
||||
|
||||
@@ -20,12 +20,25 @@ def load_yaml(instance) -> Union[dict, None]:
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
instance, "Load Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
|
||||
)
|
||||
config = load_yaml(file_path)
|
||||
return config
|
||||
|
||||
|
||||
def load_yaml(file_path: str) -> Union[dict, None]:
|
||||
"""
|
||||
Load YAML file from disk.
|
||||
|
||||
Args:
|
||||
file_path(str): Path to the YAML file.
|
||||
|
||||
Returns:
|
||||
dict: Configuration data loaded from the YAML file.
|
||||
"""
|
||||
if not file_path:
|
||||
return None
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
config = yaml.load(file, Loader=yaml.FullLoader)
|
||||
return config
|
||||
|
||||
except FileNotFoundError:
|
||||
@@ -38,7 +51,7 @@ def load_yaml(instance) -> Union[dict, None]:
|
||||
print(f"An error occurred while loading the settings from {file_path}: {e}")
|
||||
|
||||
|
||||
def save_yaml(instance, config: dict) -> None:
|
||||
def save_yaml_gui(instance, config: dict) -> None:
|
||||
"""
|
||||
Save YAML file to disk.
|
||||
|
||||
@@ -51,6 +64,17 @@ def save_yaml(instance, config: dict) -> None:
|
||||
instance, "Save Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
|
||||
)
|
||||
|
||||
save_yaml(file_path, config)
|
||||
|
||||
|
||||
def save_yaml(file_path: str, config: dict) -> None:
|
||||
"""
|
||||
Save YAML file to disk.
|
||||
|
||||
Args:
|
||||
file_path(str): Path to the YAML file.
|
||||
config(dict): Configuration data to be saved.
|
||||
"""
|
||||
if not file_path:
|
||||
return
|
||||
try:
|
||||
|
||||
+2
@@ -25,6 +25,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 +57,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):
|
||||
"""
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
|
||||
|
||||
class BECQueue(BECConnector, QTableWidget):
|
||||
"""
|
||||
Widget to display the BEC queue.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
client=None,
|
||||
config: ConnectionConfig = None,
|
||||
gui_id: str = None,
|
||||
):
|
||||
super().__init__(client, config, gui_id)
|
||||
QTableWidget.__init__(self, parent=parent)
|
||||
self.setColumnCount(3)
|
||||
self.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
|
||||
header = self.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
|
||||
self.reset_content()
|
||||
|
||||
@Slot(dict, dict)
|
||||
def update_queue(self, content, _metadata):
|
||||
"""
|
||||
Update the queue table with the latest queue information.
|
||||
|
||||
Args:
|
||||
content (dict): The queue content.
|
||||
_metadata (dict): The metadata.
|
||||
"""
|
||||
# only show the primary queue for now
|
||||
queue_info = content.get("queue", {}).get("primary", {}).get("info", [])
|
||||
self.setRowCount(len(queue_info))
|
||||
self.clearContents()
|
||||
|
||||
if not queue_info:
|
||||
self.reset_content()
|
||||
return
|
||||
|
||||
for index, item in enumerate(queue_info):
|
||||
blocks = item.get("request_blocks", [])
|
||||
scan_types = []
|
||||
scan_numbers = []
|
||||
status = item.get("status", "")
|
||||
for request_block in blocks:
|
||||
scan_type = request_block.get("content", {}).get("scan_type", "")
|
||||
if scan_type:
|
||||
scan_types.append(scan_type)
|
||||
scan_number = request_block.get("scan_number", "")
|
||||
if scan_number:
|
||||
scan_numbers.append(str(scan_number))
|
||||
if scan_types:
|
||||
scan_types = ", ".join(scan_types)
|
||||
if scan_numbers:
|
||||
scan_numbers = ", ".join(scan_numbers)
|
||||
self.set_row(index, scan_numbers, scan_types, status)
|
||||
|
||||
def format_item(self, content: str) -> QTableWidgetItem:
|
||||
"""
|
||||
Format the content of the table item.
|
||||
|
||||
Args:
|
||||
content (str): The content to be formatted.
|
||||
|
||||
Returns:
|
||||
QTableWidgetItem: The formatted item.
|
||||
"""
|
||||
item = QTableWidgetItem(content)
|
||||
item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
||||
return item
|
||||
|
||||
def set_row(self, index: int, scan_number: str, scan_type: str, status: str):
|
||||
"""
|
||||
Set the row of the table.
|
||||
|
||||
Args:
|
||||
index (int): The index of the row.
|
||||
scan_number (str): The scan number.
|
||||
scan_type (str): The scan type.
|
||||
status (str): The status.
|
||||
"""
|
||||
|
||||
self.setItem(index, 0, self.format_item(scan_number))
|
||||
self.setItem(index, 1, self.format_item(scan_type))
|
||||
self.setItem(index, 2, self.format_item(status))
|
||||
|
||||
def reset_content(self):
|
||||
"""
|
||||
Reset the content of the table.
|
||||
"""
|
||||
|
||||
self.setRowCount(1)
|
||||
self.set_row(0, "", "", "")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = BECQueue()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['bec_queue.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECQueue' name='bec_queue'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = BECQueue(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Core Widgets"
|
||||
|
||||
def icon(self):
|
||||
return QIcon()
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_queue"
|
||||
|
||||
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 "BECQueue"
|
||||
|
||||
def toolTip(self):
|
||||
return "Widget to display the BEC queue."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.bec_queue.bec_queue_plugin import BECQueuePlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECQueuePlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -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_connector import BECConnector
|
||||
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(BECConnector, 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,18 @@ 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()
|
||||
for _, objects in self.status_container.items():
|
||||
if objects["item"] == item:
|
||||
objects["widget"].show_popup()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Upon closing the widget, clean up the BECStatusBox and the QWidget.
|
||||
|
||||
Args:
|
||||
event: The close event.
|
||||
"""
|
||||
super().cleanup()
|
||||
QTreeWidget().closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['bec_status_box.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from 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 "BEC Core Widgets"
|
||||
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .stop_button.stop_button import StopButton
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['color_button.py']}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,496 @@
|
||||
"""
|
||||
BECConsole is a Qt widget that runs a Bash shell. The widget can be used and
|
||||
embedded like any other Qt widget.
|
||||
|
||||
BECConsole is powered by Pyte, a Python based terminal emulator
|
||||
(https://github.com/selectel/pyte).
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import html
|
||||
import os
|
||||
import pty
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import pyte
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
from qtpy.QtCore import QSize, QSocketNotifier, Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtGui import QClipboard, QTextCursor
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
|
||||
|
||||
ansi_colors = {
|
||||
"black": "#000000",
|
||||
"red": "#CD0000",
|
||||
"green": "#00CD00",
|
||||
"brown": "#996633", # Brown, replacing the yellow
|
||||
"blue": "#0000EE",
|
||||
"magenta": "#CD00CD",
|
||||
"cyan": "#00CDCD",
|
||||
"white": "#E5E5E5",
|
||||
"brightblack": "#7F7F7F",
|
||||
"brightred": "#FF0000",
|
||||
"brightgreen": "#00FF00",
|
||||
"brightyellow": "#FFFF00",
|
||||
"brightblue": "#5C5CFF",
|
||||
"brightmagenta": "#FF00FF",
|
||||
"brightcyan": "#00FFFF",
|
||||
"brightwhite": "#FFFFFF",
|
||||
}
|
||||
|
||||
control_keys_mapping = {
|
||||
QtCore.Qt.Key_A: b"\x01", # Ctrl-A
|
||||
QtCore.Qt.Key_B: b"\x02", # Ctrl-B
|
||||
QtCore.Qt.Key_C: b"\x03", # Ctrl-C
|
||||
QtCore.Qt.Key_D: b"\x04", # Ctrl-D
|
||||
QtCore.Qt.Key_E: b"\x05", # Ctrl-E
|
||||
QtCore.Qt.Key_F: b"\x06", # Ctrl-F
|
||||
QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell)
|
||||
QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace)
|
||||
QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab)
|
||||
QtCore.Qt.Key_J: b"\x0A", # Ctrl-J (Line Feed)
|
||||
QtCore.Qt.Key_K: b"\x0B", # Ctrl-K (Vertical Tab)
|
||||
QtCore.Qt.Key_L: b"\x0C", # Ctrl-L (Form Feed)
|
||||
QtCore.Qt.Key_M: b"\x0D", # Ctrl-M (Carriage Return)
|
||||
QtCore.Qt.Key_N: b"\x0E", # Ctrl-N
|
||||
QtCore.Qt.Key_O: b"\x0F", # Ctrl-O
|
||||
QtCore.Qt.Key_P: b"\x10", # Ctrl-P
|
||||
QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q
|
||||
QtCore.Qt.Key_R: b"\x12", # Ctrl-R
|
||||
QtCore.Qt.Key_S: b"\x13", # Ctrl-S
|
||||
QtCore.Qt.Key_T: b"\x14", # Ctrl-T
|
||||
QtCore.Qt.Key_U: b"\x15", # Ctrl-U
|
||||
QtCore.Qt.Key_V: b"\x16", # Ctrl-V
|
||||
QtCore.Qt.Key_W: b"\x17", # Ctrl-W
|
||||
QtCore.Qt.Key_X: b"\x18", # Ctrl-X
|
||||
QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y
|
||||
QtCore.Qt.Key_Z: b"\x1A", # Ctrl-Z
|
||||
QtCore.Qt.Key_Escape: b"\x1B", # Ctrl-Escape
|
||||
QtCore.Qt.Key_Backslash: b"\x1C", # Ctrl-\
|
||||
QtCore.Qt.Key_Underscore: b"\x1F", # Ctrl-_
|
||||
}
|
||||
|
||||
normal_keys_mapping = {
|
||||
QtCore.Qt.Key_Return: b"\n",
|
||||
QtCore.Qt.Key_Space: b" ",
|
||||
QtCore.Qt.Key_Enter: b"\n",
|
||||
QtCore.Qt.Key_Tab: b"\t",
|
||||
QtCore.Qt.Key_Backspace: b"\x08",
|
||||
QtCore.Qt.Key_Home: b"\x47",
|
||||
QtCore.Qt.Key_End: b"\x4f",
|
||||
QtCore.Qt.Key_Left: b"\x02",
|
||||
QtCore.Qt.Key_Up: b"\x10",
|
||||
QtCore.Qt.Key_Right: b"\x06",
|
||||
QtCore.Qt.Key_Down: b"\x0E",
|
||||
QtCore.Qt.Key_PageUp: b"\x49",
|
||||
QtCore.Qt.Key_PageDown: b"\x51",
|
||||
QtCore.Qt.Key_F1: b"\x1b\x31",
|
||||
QtCore.Qt.Key_F2: b"\x1b\x32",
|
||||
QtCore.Qt.Key_F3: b"\x1b\x33",
|
||||
QtCore.Qt.Key_F4: b"\x1b\x34",
|
||||
QtCore.Qt.Key_F5: b"\x1b\x35",
|
||||
QtCore.Qt.Key_F6: b"\x1b\x36",
|
||||
QtCore.Qt.Key_F7: b"\x1b\x37",
|
||||
QtCore.Qt.Key_F8: b"\x1b\x38",
|
||||
QtCore.Qt.Key_F9: b"\x1b\x39",
|
||||
QtCore.Qt.Key_F10: b"\x1b\x30",
|
||||
QtCore.Qt.Key_F11: b"\x45",
|
||||
QtCore.Qt.Key_F12: b"\x46",
|
||||
}
|
||||
|
||||
|
||||
def QtKeyToAscii(event):
|
||||
"""
|
||||
Convert the Qt key event to the corresponding ASCII sequence for
|
||||
the terminal. This works fine for standard alphanumerical characters, but
|
||||
most other characters require terminal specific control sequences.
|
||||
|
||||
The conversion below works for TERM="linux" terminals.
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
# special case for MacOS
|
||||
# /!\ Qt maps ControlModifier to CMD
|
||||
# CMD-C, CMD-V for copy/paste
|
||||
# CTRL-C and other modifiers -> key mapping
|
||||
if event.modifiers() == QtCore.Qt.MetaModifier:
|
||||
if event.key() == Qt.Key_Backspace:
|
||||
return control_keys_mapping.get(Qt.Key_W)
|
||||
return control_keys_mapping.get(event.key())
|
||||
elif event.modifiers() == QtCore.Qt.ControlModifier:
|
||||
if event.key() == Qt.Key_C:
|
||||
# copy
|
||||
return "copy"
|
||||
elif event.key() == Qt.Key_V:
|
||||
# paste
|
||||
return "paste"
|
||||
return None
|
||||
else:
|
||||
return normal_keys_mapping.get(event.key(), event.text().encode("utf8"))
|
||||
if event.modifiers() == QtCore.Qt.ControlModifier:
|
||||
return control_keys_mapping.get(event.key())
|
||||
else:
|
||||
return normal_keys_mapping.get(event.key(), event.text().encode("utf8"))
|
||||
|
||||
|
||||
class Screen(pyte.HistoryScreen):
|
||||
def __init__(self, stdin_fd, numColumns, numLines, historyLength):
|
||||
super().__init__(numColumns, numLines, historyLength, ratio=1 / numLines)
|
||||
self._fd = stdin_fd
|
||||
|
||||
def write_process_input(self, data):
|
||||
"""Response to CPR request for example"""
|
||||
os.write(self._fd, data.encode("utf-8"))
|
||||
|
||||
|
||||
class Backend(QtCore.QObject):
|
||||
"""
|
||||
Poll Bash.
|
||||
|
||||
This class will run as a qsocketnotifier (started in ``_TerminalWidget``) and poll the
|
||||
file descriptor of the Bash terminal.
|
||||
"""
|
||||
|
||||
# Signals to communicate with ``_TerminalWidget``.
|
||||
startWork = pyqtSignal()
|
||||
dataReady = pyqtSignal(object)
|
||||
|
||||
def __init__(self, fd, numColumns, numLines):
|
||||
super().__init__()
|
||||
|
||||
# File descriptor that connects to Bash process.
|
||||
self.fd = fd
|
||||
|
||||
# Setup Pyte (hard coded display size for now).
|
||||
self.screen = Screen(self.fd, numColumns, numLines, 10000)
|
||||
self.stream = pyte.ByteStream()
|
||||
self.stream.attach(self.screen)
|
||||
|
||||
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
|
||||
self.notifier.activated.connect(self._fd_readable)
|
||||
|
||||
def _fd_readable(self):
|
||||
"""
|
||||
Poll the Bash output, run it through Pyte, and notify the main applet.
|
||||
"""
|
||||
# Read the shell output until the file descriptor is closed.
|
||||
try:
|
||||
out = os.read(self.fd, 2**16)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
# Feed output into Pyte's state machine and send the new screen
|
||||
# output to the GUI
|
||||
self.stream.feed(out)
|
||||
self.dataReady.emit(self.screen)
|
||||
|
||||
|
||||
class BECConsole(QtWidgets.QScrollArea):
|
||||
"""Container widget for the terminal text area"""
|
||||
|
||||
def __init__(self, parent=None, numLines=50, numColumns=125):
|
||||
super().__init__(parent)
|
||||
|
||||
self.innerWidget = QtWidgets.QWidget(self)
|
||||
QHBoxLayout(self.innerWidget)
|
||||
self.innerWidget.layout().setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.term = _TerminalWidget(self.innerWidget, numLines, numColumns)
|
||||
self.term.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.innerWidget.layout().addWidget(self.term)
|
||||
|
||||
self.scroll_bar = QScrollBar(Qt.Vertical, self.term)
|
||||
self.innerWidget.layout().addWidget(self.scroll_bar)
|
||||
|
||||
self.term.set_scroll(self.scroll_bar)
|
||||
|
||||
self.setWidget(self.innerWidget)
|
||||
|
||||
def start(self, cmd=["bec", "--nogui"], deactivate_ctrl_d=True):
|
||||
self.term._cmd = cmd
|
||||
self.term.start(deactivate_ctrl_d=deactivate_ctrl_d)
|
||||
|
||||
def push(self, text):
|
||||
"""Push some text to the terminal"""
|
||||
return self.term.push(text)
|
||||
|
||||
|
||||
class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
"""
|
||||
Start ``Backend`` process and render Pyte output as text.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
|
||||
super().__init__(parent)
|
||||
|
||||
# file descriptor to communicate with the subprocess
|
||||
self.fd = None
|
||||
self.backend = None
|
||||
self.lock = threading.Lock()
|
||||
# command to execute
|
||||
self._cmd = None
|
||||
# should ctrl-d be deactivated ? (prevent Python exit)
|
||||
self._deactivate_ctrl_d = False
|
||||
|
||||
# Specify the terminal size in terms of lines and columns.
|
||||
self.numLines = numLines
|
||||
self.numColumns = numColumns
|
||||
self.output = [""] * numLines
|
||||
|
||||
# Use Monospace fonts and disable line wrapping.
|
||||
self.setFont(QtGui.QFont("Courier", 9))
|
||||
self.setFont(QtGui.QFont("Monospace"))
|
||||
self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
|
||||
|
||||
# Disable vertical scrollbar (we use our own, to be set via .set_scroll())
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
|
||||
fmt = QtGui.QFontMetrics(self.font())
|
||||
self._char_width = fmt.width("w")
|
||||
self._char_height = fmt.height()
|
||||
self.setCursorWidth(self._char_width)
|
||||
# self.setStyleSheet("QPlainTextEdit { color: #ffff00; background-color: #303030; } ");
|
||||
|
||||
def start(self, deactivate_ctrl_d=False):
|
||||
self._deactivate_ctrl_d = deactivate_ctrl_d
|
||||
|
||||
# Start the Bash process
|
||||
self.fd = self.forkShell()
|
||||
|
||||
# Create the ``Backend`` object
|
||||
self.backend = Backend(self.fd, self.numColumns, self.numLines)
|
||||
self.backend.dataReady.connect(self.dataReady)
|
||||
|
||||
def minimumSizeHint(self):
|
||||
width = self._char_width * self.numColumns
|
||||
height = self._char_height * self.numLines
|
||||
return QSize(width, height + 20)
|
||||
|
||||
def set_scroll(self, scroll):
|
||||
self.scroll = scroll
|
||||
self.scroll.setMinimum(0)
|
||||
self.scroll.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
def scroll_value_change(self, value, old={"value": 0}):
|
||||
if value <= old["value"]:
|
||||
# scroll up
|
||||
# value is number of lines from the start
|
||||
nlines = old["value"] - value
|
||||
# history ratio gives prev_page == 1 line
|
||||
for i in range(nlines):
|
||||
self.backend.screen.prev_page()
|
||||
else:
|
||||
# scroll down
|
||||
nlines = value - old["value"]
|
||||
for i in range(nlines):
|
||||
self.backend.screen.next_page()
|
||||
old["value"] = value
|
||||
self.dataReady(self.backend.screen, reset_scroll=False)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Redirect all keystrokes to the terminal process.
|
||||
"""
|
||||
# Convert the Qt key to the correct ASCII code.
|
||||
if (
|
||||
self._deactivate_ctrl_d
|
||||
and event.modifiers() == QtCore.Qt.ControlModifier
|
||||
and event.key() == QtCore.Qt.Key_D
|
||||
):
|
||||
return None
|
||||
|
||||
code = QtKeyToAscii(event)
|
||||
if code == "copy":
|
||||
# MacOS only: CMD-C handling
|
||||
self.copy()
|
||||
elif code == "paste":
|
||||
# MacOS only: CMD-V handling
|
||||
self._push_clipboard()
|
||||
elif code is not None:
|
||||
os.write(self.fd, code)
|
||||
|
||||
def push(self, text):
|
||||
"""
|
||||
Write 'text' to terminal
|
||||
"""
|
||||
os.write(self.fd, text.encode("utf-8"))
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
menu = self.createStandardContextMenu()
|
||||
for action in menu.actions():
|
||||
# remove all actions except copy and paste
|
||||
if "opy" in action.text():
|
||||
# redefine text without shortcut
|
||||
# since it probably clashes with control codes (like CTRL-C etc)
|
||||
action.setText("Copy")
|
||||
continue
|
||||
if "aste" in action.text():
|
||||
# redefine text without shortcut
|
||||
action.setText("Paste")
|
||||
# paste -> have to insert with self.push
|
||||
action.triggered.connect(self._push_clipboard)
|
||||
continue
|
||||
menu.removeAction(action)
|
||||
menu.exec_(event.globalPos())
|
||||
|
||||
def _push_clipboard(self):
|
||||
clipboard = QApplication.instance().clipboard()
|
||||
self.push(clipboard.text())
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MiddleButton:
|
||||
# push primary selection buffer ("mouse clipboard") to terminal
|
||||
clipboard = QApplication.instance().clipboard()
|
||||
if clipboard.supportsSelection():
|
||||
self.push(clipboard.text(QClipboard.Selection))
|
||||
return None
|
||||
elif event.button() == Qt.LeftButton:
|
||||
# left button click
|
||||
textCursor = self.textCursor()
|
||||
if textCursor.selectedText():
|
||||
# mouse was used to select text -> nothing to do
|
||||
pass
|
||||
else:
|
||||
# a simple 'click', make cursor going to end
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
||||
)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
||||
)
|
||||
self.setTextCursor(textCursor)
|
||||
self.ensureCursorVisible()
|
||||
return None
|
||||
return super().mouseReleaseEvent(event)
|
||||
|
||||
def dataReady(self, screenData, reset_scroll=True):
|
||||
"""
|
||||
Render the new screen as text into the widget.
|
||||
|
||||
This method is triggered via a signal from ``Backend``.
|
||||
"""
|
||||
with self.lock:
|
||||
# Clear the widget
|
||||
self.clear()
|
||||
|
||||
# Prepare the HTML output
|
||||
for line_no in screenData.dirty:
|
||||
line = text = ""
|
||||
style = old_style = ""
|
||||
for ch in screenData.buffer[line_no].values():
|
||||
style = f"{'background-color:%s;' % ansi_colors.get(ch.bg, ansi_colors['black']) if ch.bg!='default' else ''}{'color:%s;' % ansi_colors.get(ch.fg, ansi_colors['white']) if ch.fg!='default' else ''}{'font-weight:bold;' if ch.bold else ''}{'font-style:italic;' if ch.italics else ''}"
|
||||
if style != old_style:
|
||||
if old_style:
|
||||
line += f"<span style={repr(old_style)}>{html.escape(text, quote=True)}</span>"
|
||||
else:
|
||||
line += html.escape(text, quote=True)
|
||||
text = ""
|
||||
old_style = style
|
||||
text += ch.data
|
||||
if style:
|
||||
line += f"<span style={repr(style)}>{html.escape(text, quote=True)}</span>"
|
||||
else:
|
||||
line += html.escape(text, quote=True)
|
||||
self.output[line_no] = line
|
||||
# fill the text area with HTML contents in one go
|
||||
self.appendHtml(f"<pre>{chr(10).join(self.output)}</pre>")
|
||||
# done updates, all clean
|
||||
screenData.dirty.clear()
|
||||
|
||||
# Activate cursor
|
||||
textCursor = self.textCursor()
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(QTextCursor.Down, QTextCursor.MoveAnchor, screenData.cursor.y)
|
||||
textCursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, screenData.cursor.x)
|
||||
self.setTextCursor(textCursor)
|
||||
self.ensureCursorVisible()
|
||||
|
||||
# manage scroll
|
||||
if reset_scroll:
|
||||
self.scroll.valueChanged.disconnect(self.scroll_value_change)
|
||||
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
||||
self.scroll.setMaximum(tmp if tmp > 0 else 0)
|
||||
self.scroll.setSliderPosition(len(self.backend.screen.history.top))
|
||||
self.scroll.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
# def resizeEvent(self, event):
|
||||
# with self.lock:
|
||||
# self.numColumns = int(self.width() / self._char_width)
|
||||
# self.numLines = int(self.height() / self._char_height)
|
||||
# self.output = [""] * self.numLines
|
||||
# print("RESIZING TO", self.numColumns, "x", self.numLines)
|
||||
# self.backend.screen.resize(self.numLines, self.numColumns)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
y = event.angleDelta().y()
|
||||
if y > 0:
|
||||
self.backend.screen.prev_page()
|
||||
else:
|
||||
self.backend.screen.next_page()
|
||||
self.dataReady(self.backend.screen, reset_scroll=False)
|
||||
|
||||
def forkShell(self):
|
||||
"""
|
||||
Fork the current process and execute bec in shell.
|
||||
"""
|
||||
try:
|
||||
pid, fd = pty.fork()
|
||||
except (IOError, OSError):
|
||||
return False
|
||||
if pid == 0:
|
||||
# Safe way to make it work under BSD and Linux
|
||||
try:
|
||||
ls = os.environ["LANG"].split(".")
|
||||
except KeyError:
|
||||
ls = []
|
||||
if len(ls) < 2:
|
||||
ls = ["en_US", "UTF-8"]
|
||||
try:
|
||||
os.putenv("COLUMNS", str(self.numColumns))
|
||||
os.putenv("LINES", str(self.numLines))
|
||||
os.putenv("TERM", "linux")
|
||||
os.putenv("LANG", ls[0] + ".UTF-8")
|
||||
if isinstance(self._cmd, str):
|
||||
os.execvp(self._cmd, self._cmd)
|
||||
else:
|
||||
os.execvp(self._cmd[0], self._cmd)
|
||||
# print "child_pid", child_pid, sts
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
# self.proc_finish(sid)
|
||||
os._exit(0)
|
||||
else:
|
||||
# We are in the parent process.
|
||||
# Set file control
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
print("Spawned Bash shell (PID {})".format(pid))
|
||||
return fd
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import sys
|
||||
|
||||
from qtpy import QtGui, QtWidgets
|
||||
|
||||
# Terminal size in characters.
|
||||
numLines = 25
|
||||
numColumns = 100
|
||||
|
||||
# Create the Qt application and QBash instance.
|
||||
app = QtWidgets.QApplication([])
|
||||
mainwin = QtWidgets.QMainWindow()
|
||||
title = "BECConsole ({}x{})".format(numColumns, numLines)
|
||||
mainwin.setWindowTitle(title)
|
||||
|
||||
console = BECConsole(mainwin, numColumns, numLines)
|
||||
mainwin.setCentralWidget(console)
|
||||
console.start()
|
||||
|
||||
# Show widget and launch Qt's event loop.
|
||||
mainwin.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1,201 @@
|
||||
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_connector import BECConnector
|
||||
|
||||
|
||||
class DeviceBox(BECConnector, QWidget):
|
||||
"""A widget that controls a single device."""
|
||||
|
||||
ui_file = "device_box.ui"
|
||||
dimensions = (234, 224)
|
||||
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, self.ui_file))
|
||||
|
||||
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(self.dimensions[0])
|
||||
db.setFixedWidth(self.dimensions[1])
|
||||
|
||||
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
|
||||
|
||||
import qdarktheme
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("light")
|
||||
widget = DeviceBox(device="samx")
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['device_box.py']}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -0,0 +1,22 @@
|
||||
from bec_widgets.widgets.device_box.device_box import DeviceBox
|
||||
|
||||
|
||||
class DeviceControlLine(DeviceBox):
|
||||
"""A widget that controls a single device."""
|
||||
|
||||
ui_file = "device_control_line.ui"
|
||||
dimensions = (70, 800) # height, width
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
import qdarktheme
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("light")
|
||||
widget = DeviceControlLine(device="samy")
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1,181 @@
|
||||
<?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>785</width>
|
||||
<height>91</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</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="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="PositionIndicator" name="position_indicator"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="readback">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Position</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="setpoint">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>24</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="stop">
|
||||
<property name="text">
|
||||
<string>Stop</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="tweak_left">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</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>
|
||||
<widget class="QDoubleSpinBox" name="step_size"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="tweak_right">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</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>
|
||||
<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>
|
||||
</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>
|
||||
@@ -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 |
+3
-3
@@ -2,10 +2,10 @@ 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.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):
|
||||
@@ -89,4 +89,4 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
|
||||
def closeEvent(self, event):
|
||||
super().cleanup()
|
||||
QComboBox().closeEvent(event)
|
||||
return QComboBox.closeEvent(self, event)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"files": ["device_combobox.py"]
|
||||
}
|
||||
+6
-3
@@ -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"
|
||||
+1
-3
@@ -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())
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from .device_combobox.device_combobox import DeviceComboBox
|
||||
from .device_line_edit.device_line_edit import DeviceLineEdit
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"files": ["device_combobox.py", "launch_device_combobox.py",
|
||||
]
|
||||
}
|
||||
@@ -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_())
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"files": ["device_line_edit.py", "launch_device_line_edit.py",
|
||||
]
|
||||
}
|
||||
@@ -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_())
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
+3
-3
@@ -3,10 +3,10 @@ from typing import TYPE_CHECKING
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
|
||||
|
||||
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputBase, DeviceInputConfig
|
||||
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 DeviceLineEdit(DeviceInputBase, QLineEdit):
|
||||
@@ -101,4 +101,4 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
|
||||
|
||||
def closeEvent(self, event):
|
||||
super().cleanup()
|
||||
QLineEdit().closeEvent(event)
|
||||
return QLineEdit.closeEvent(self, event)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"files": ["device_line_edit.py"]
|
||||
}
|
||||
+6
-3
@@ -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 DeviceLineEdit
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
@@ -27,10 +28,12 @@ class DeviceLineEditPlugin(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", "line_edit_icon.png")
|
||||
return QIcon(icon_path)
|
||||
|
||||
def includeFile(self):
|
||||
return "device_line_edit"
|
||||
+1
-3
@@ -6,9 +6,7 @@ def main(): # pragma: no cover
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.device_inputs.device_line_edit.device_line_edit_plugin import (
|
||||
DeviceLineEditPlugin,
|
||||
)
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit_plugin import DeviceLineEditPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceLineEditPlugin())
|
||||
|
||||
@@ -26,8 +26,8 @@ class DockConfig(ConnectionConfig):
|
||||
|
||||
class BECDock(BECConnector, Dock):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"rpc_id",
|
||||
"_config_dict",
|
||||
"_rpc_id",
|
||||
"widget_list",
|
||||
"show_title_bar",
|
||||
"hide_title_bar",
|
||||
|
||||
@@ -23,7 +23,7 @@ class DockAreaConfig(ConnectionConfig):
|
||||
|
||||
class BECDockArea(BECConnector, DockArea):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"_config_dict",
|
||||
"panels",
|
||||
"save_state",
|
||||
"remove_dock",
|
||||
@@ -32,7 +32,7 @@ class BECDockArea(BECConnector, DockArea):
|
||||
"clear_all",
|
||||
"detach_dock",
|
||||
"attach_all",
|
||||
"get_all_rpc",
|
||||
"_get_all_rpc",
|
||||
"temp_areas",
|
||||
]
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Literal, Optional
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import qdarktheme
|
||||
from pydantic import Field
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from typeguard import typechecked
|
||||
@@ -30,16 +30,36 @@ class FigureConfig(ConnectionConfig):
|
||||
{}, description="The list of widgets to be added to the figure widget."
|
||||
)
|
||||
|
||||
@field_validator("widgets", mode="before")
|
||||
@classmethod
|
||||
def validate_widgets(cls, v):
|
||||
"""Validate the widgets configuration."""
|
||||
widget_class_map = {
|
||||
"BECWaveform": Waveform1DConfig,
|
||||
"BECImageShow": ImageConfig,
|
||||
"BECMotorMap": MotorMapConfig,
|
||||
}
|
||||
validated_widgets = {}
|
||||
for key, widget_config in v.items():
|
||||
if "widget_class" not in widget_config:
|
||||
raise ValueError(f"Widget config for {key} does not contain 'widget_class'.")
|
||||
widget_class = widget_config["widget_class"]
|
||||
if widget_class not in widget_class_map:
|
||||
raise ValueError(f"Unknown widget_class '{widget_class}' for widget '{key}'.")
|
||||
config_class = widget_class_map[widget_class]
|
||||
validated_widgets[key] = config_class(**widget_config)
|
||||
return validated_widgets
|
||||
|
||||
|
||||
class WidgetHandler:
|
||||
"""Factory for creating and configuring BEC widgets for BECFigure."""
|
||||
|
||||
def __init__(self):
|
||||
self.widget_factory = {
|
||||
"PlotBase": (BECPlotBase, SubplotConfig),
|
||||
"Waveform1D": (BECWaveform, Waveform1DConfig),
|
||||
"ImShow": (BECImageShow, ImageConfig),
|
||||
"MotorMap": (BECMotorMap, MotorMapConfig),
|
||||
"BECPlotBase": (BECPlotBase, SubplotConfig),
|
||||
"BECWaveform": (BECWaveform, Waveform1DConfig),
|
||||
"BECImageShow": (BECImageShow, ImageConfig),
|
||||
"BECMotorMap": (BECMotorMap, MotorMapConfig),
|
||||
}
|
||||
|
||||
def create_widget(
|
||||
@@ -90,13 +110,11 @@ class WidgetHandler:
|
||||
|
||||
class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"_get_all_rpc",
|
||||
"axes",
|
||||
"widgets",
|
||||
"add_plot",
|
||||
"add_image",
|
||||
"add_motor_map",
|
||||
"plot",
|
||||
"image",
|
||||
"motor_map",
|
||||
@@ -104,9 +122,15 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
"change_layout",
|
||||
"change_theme",
|
||||
"clear_all",
|
||||
"get_all_rpc",
|
||||
"widget_list",
|
||||
]
|
||||
subplot_map = {
|
||||
"PlotBase": BECPlotBase,
|
||||
"BECWaveform": BECWaveform,
|
||||
"BECImageShow": BECImageShow,
|
||||
"BECMotorMap": BECMotorMap,
|
||||
}
|
||||
widget_method_map = {"BECWaveform": "plot", "BECImageShow": "image", "BECMotorMap": "motor_map"}
|
||||
|
||||
clean_signal = pyqtSignal()
|
||||
|
||||
@@ -122,8 +146,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = FigureConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
super().__init__(client=client, gui_id=gui_id)
|
||||
pg.GraphicsLayoutWidget.__init__(self, parent)
|
||||
|
||||
self.widget_handler = WidgetHandler()
|
||||
@@ -133,6 +156,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
|
||||
# Container to keep track of the grid
|
||||
self.grid = []
|
||||
# Create config and apply it
|
||||
self.apply_config(config)
|
||||
|
||||
def __getitem__(self, key: tuple | str):
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
@@ -147,6 +172,24 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
"Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
|
||||
)
|
||||
|
||||
def apply_config(self, config: dict | FigureConfig): # ,generate_new_id: bool = False):
|
||||
if isinstance(config, dict):
|
||||
try:
|
||||
config = FigureConfig(**config)
|
||||
except ValidationError as e:
|
||||
print(f"Error in applying config: {e}")
|
||||
return
|
||||
self.config = config
|
||||
self.change_theme(self.config.theme)
|
||||
|
||||
# widget_config has to be reset for not have each widget config twice when added to the figure
|
||||
widget_configs = [config for config in self.config.widgets.values()]
|
||||
self.config.widgets = {}
|
||||
for widget_config in widget_configs:
|
||||
getattr(self, self.widget_method_map[widget_config.widget_class])(
|
||||
config=widget_config.model_dump(), row=widget_config.row, col=widget_config.col
|
||||
)
|
||||
|
||||
@property
|
||||
def widget_list(self) -> list[BECPlotBase]:
|
||||
"""
|
||||
@@ -195,11 +238,12 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
z_entry: str = None,
|
||||
x: list | np.ndarray = None,
|
||||
y: list | np.ndarray = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
):
|
||||
dap: str | None = None,
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Configure the waveform based on the provided parameters.
|
||||
|
||||
@@ -217,6 +261,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
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 x is not None and y is None:
|
||||
if isinstance(x, np.ndarray):
|
||||
@@ -240,7 +285,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
return waveform
|
||||
# User wants to add scan curve -> 1D Waveform
|
||||
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
|
||||
waveform.add_curve_scan(
|
||||
waveform.plot(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
x_entry=x_entry,
|
||||
@@ -248,6 +293,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
validate=validate,
|
||||
color=color,
|
||||
label=label,
|
||||
dap=dap,
|
||||
)
|
||||
# User wants to add scan curve -> 2D Waveform Scatter
|
||||
if (
|
||||
@@ -257,7 +303,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
and x is None
|
||||
and y is None
|
||||
):
|
||||
waveform.add_curve_scan(
|
||||
waveform.plot(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
@@ -268,6 +314,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
dap=dap,
|
||||
)
|
||||
# User wants to add custom curve
|
||||
elif x is not None and y is not None and x_name is None and y_name is None:
|
||||
@@ -275,73 +322,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
|
||||
return waveform
|
||||
|
||||
def add_plot(
|
||||
self,
|
||||
x: list | np.ndarray = None,
|
||||
y: list | np.ndarray = None,
|
||||
x_name: str = None,
|
||||
y_name: str = None,
|
||||
z_name: str = None,
|
||||
x_entry: str = None,
|
||||
y_entry: str = None,
|
||||
z_entry: str = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
|
||||
Args:
|
||||
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.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
|
||||
"""
|
||||
widget_id = str(uuid.uuid4())
|
||||
waveform = self.add_widget(
|
||||
widget_type="Waveform1D",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
waveform = self._init_waveform(
|
||||
waveform=waveform,
|
||||
x=x,
|
||||
y=y,
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
return waveform
|
||||
|
||||
@typechecked
|
||||
def plot(
|
||||
self,
|
||||
@@ -357,6 +337,11 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
color_map_z: str | None = "plasma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
new: bool = False,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
dap: str | None = None,
|
||||
config: dict | None = None, # TODO make logic more transparent
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
@@ -375,20 +360,23 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
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.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
dap(str): The DAP model to use for the curve.
|
||||
config(dict): Recreates the whole BECWaveform widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECWaveform: The waveform plot widget.
|
||||
"""
|
||||
waveform = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, BECWaveform, can_fail=True
|
||||
waveform = self.subplot_factory(
|
||||
widget_type="BECWaveform", config=config, row=row, col=col, new=new, **axis_kwargs
|
||||
)
|
||||
if waveform is not None:
|
||||
if axis_kwargs:
|
||||
waveform.set(**axis_kwargs)
|
||||
else:
|
||||
waveform = self.add_plot(**axis_kwargs)
|
||||
if config is not None:
|
||||
return waveform
|
||||
|
||||
# Passing args to init_waveform
|
||||
waveform = self._init_waveform(
|
||||
waveform=waveform,
|
||||
x=x,
|
||||
@@ -403,8 +391,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
dap=dap,
|
||||
)
|
||||
# TODO remove repetition from .plot method
|
||||
return waveform
|
||||
|
||||
def _init_image(
|
||||
@@ -451,6 +439,10 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
new: bool = False,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
config: dict | None = None,
|
||||
**axis_kwargs,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
@@ -462,78 +454,22 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
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.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECImageShow: The image widget.
|
||||
"""
|
||||
image = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, BECImageShow, can_fail=True
|
||||
)
|
||||
if image is not None:
|
||||
if axis_kwargs:
|
||||
image.set(**axis_kwargs)
|
||||
else:
|
||||
image = self.add_image(color_bar=color_bar, **axis_kwargs)
|
||||
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
vrange=vrange,
|
||||
)
|
||||
return image
|
||||
|
||||
def add_image(
|
||||
self,
|
||||
monitor: str = None,
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
Add an image to the figure at the specified position.
|
||||
|
||||
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.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
config(dict): Recreates the whole BECImageShow widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECImageShow: The image widget.
|
||||
"""
|
||||
|
||||
widget_id = str(uuid.uuid4())
|
||||
if config is None:
|
||||
config = ImageConfig(
|
||||
widget_class="BECImageShow",
|
||||
gui_id=widget_id,
|
||||
parent_id=self.gui_id,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
vrange=vrange,
|
||||
)
|
||||
image = self.add_widget(
|
||||
widget_type="ImShow",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
image = self.subplot_factory(
|
||||
widget_type="BECImageShow", config=config, row=row, col=col, new=new, **axis_kwargs
|
||||
)
|
||||
if config is not None:
|
||||
return image
|
||||
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
@@ -544,76 +480,99 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
)
|
||||
return image
|
||||
|
||||
def motor_map(self, motor_x: str = None, motor_y: str = None, **axis_kwargs) -> BECMotorMap:
|
||||
def motor_map(
|
||||
self,
|
||||
motor_x: str = None,
|
||||
motor_y: str = None,
|
||||
new: bool = False,
|
||||
row: int | None = None,
|
||||
col: int | None = None,
|
||||
config: dict | None = None,
|
||||
**axis_kwargs,
|
||||
) -> BECMotorMap:
|
||||
"""
|
||||
Add a motor map to the figure. Always access the first motor map widget in the figure.
|
||||
|
||||
Args:
|
||||
motor_x(str): The name of the motor for the X axis.
|
||||
motor_y(str): The name of the motor for the Y axis.
|
||||
new(bool): If True, create a new plot instead of using the first plot.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Recreates the whole BECImageShow widget from provided configuration.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECMotorMap: The motor map widget.
|
||||
"""
|
||||
motor_map = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, BECMotorMap, can_fail=True
|
||||
motor_map = self.subplot_factory(
|
||||
widget_type="BECMotorMap", config=config, row=row, col=col, new=new, **axis_kwargs
|
||||
)
|
||||
if motor_map is not None:
|
||||
if axis_kwargs:
|
||||
motor_map.set(**axis_kwargs)
|
||||
else:
|
||||
motor_map = self.add_motor_map(**axis_kwargs)
|
||||
if config is not None:
|
||||
return motor_map
|
||||
|
||||
if motor_x is not None and motor_y is not None:
|
||||
motor_map.change_motors(motor_x, motor_y)
|
||||
|
||||
return motor_map
|
||||
|
||||
def add_motor_map(
|
||||
def subplot_factory(
|
||||
self,
|
||||
motor_x: str = None,
|
||||
motor_y: str = None,
|
||||
widget_type: Literal[
|
||||
"BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap"
|
||||
] = "BECPlotBase",
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
new: bool = False,
|
||||
**axis_kwargs,
|
||||
) -> BECMotorMap:
|
||||
"""
|
||||
|
||||
Args:
|
||||
motor_x(str): The name of the motor for the X axis.
|
||||
motor_y(str): The name of the motor for the Y axis.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs:
|
||||
|
||||
Returns:
|
||||
BECMotorMap: The motor map widget.
|
||||
"""
|
||||
widget_id = str(uuid.uuid4())
|
||||
if config is None:
|
||||
config = MotorMapConfig(
|
||||
widget_class="BECMotorMap", gui_id=widget_id, parent_id=self.gui_id
|
||||
) -> BECPlotBase:
|
||||
# Case 1 - config provided, new plot, possible to define coordinates
|
||||
if config is not None:
|
||||
widget_cls = config["widget_class"]
|
||||
if widget_cls != widget_type:
|
||||
raise ValueError(
|
||||
f"Widget type '{widget_type}' does not match the provided configuration ({widget_cls})."
|
||||
)
|
||||
widget = self.add_widget(
|
||||
widget_type=widget_type, config=config, row=row, col=col, **axis_kwargs
|
||||
)
|
||||
motor_map = self.add_widget(
|
||||
widget_type="MotorMap",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
return widget
|
||||
|
||||
if motor_x is not None and motor_y is not None:
|
||||
motor_map.change_motors(motor_x, motor_y)
|
||||
# Case 2 - find first plot or create first plot if no plot available, no config provided, no coordinates
|
||||
if new is False and (row is None or col is None):
|
||||
widget = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, self.subplot_map[widget_type], can_fail=True
|
||||
)
|
||||
if widget is not None:
|
||||
if axis_kwargs:
|
||||
widget.set(**axis_kwargs)
|
||||
else:
|
||||
widget = self.add_widget(widget_type=widget_type, **axis_kwargs)
|
||||
return widget
|
||||
|
||||
return motor_map
|
||||
# Case 3 - modifying existing plot wit coordinates provided
|
||||
if new is False and (row is not None and col is not None):
|
||||
try:
|
||||
widget = self.axes(row, col)
|
||||
except ValueError:
|
||||
widget = None
|
||||
if widget is not None:
|
||||
if axis_kwargs:
|
||||
widget.set(**axis_kwargs)
|
||||
else:
|
||||
widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
|
||||
return widget
|
||||
|
||||
# Case 4 - no previous plot or new plot, no config provided, possible to define coordinates
|
||||
widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
|
||||
return widget
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
widget_type: Literal["PlotBase", "Waveform1D", "ImShow"] = "PlotBase",
|
||||
widget_type: Literal[
|
||||
"BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap"
|
||||
] = "BECPlotBase",
|
||||
widget_id: str = None,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
@@ -644,6 +603,9 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
# has to be changed manually to ensure unique id, if config is copied from existing widget, the id could be
|
||||
# used otherwise multiple times
|
||||
widget.set_gui_id(widget_id)
|
||||
|
||||
# Check if position is occupied
|
||||
if row is not None and col is not None:
|
||||
@@ -747,6 +709,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
self._reindex_grid()
|
||||
if widget_id in self.config.widgets:
|
||||
self.config.widgets.pop(widget_id)
|
||||
widget.deleteLater()
|
||||
else:
|
||||
raise ValueError(f"Widget with ID '{widget_id}' does not exist.")
|
||||
|
||||
@@ -867,6 +830,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
self.clear_all()
|
||||
super().cleanup()
|
||||
# def cleanup(self):
|
||||
# self.clear_all()
|
||||
# super().cleanup()
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import os
|
||||
|
||||
import qdarktheme
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class AxisSettings(QWidget):
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "axis_settings.ui"), self)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.ui)
|
||||
|
||||
# Hardcoded values for best appearance
|
||||
self.setMinimumHeight(280)
|
||||
self.setMaximumHeight(280)
|
||||
self.resize(380, 280)
|
||||
|
||||
@Slot(dict)
|
||||
def display_current_settings(self, axis_config: dict):
|
||||
# Top Box
|
||||
WidgetIO.set_value(self.ui.plot_title, axis_config["title"])
|
||||
|
||||
# X Axis Box
|
||||
WidgetIO.set_value(self.ui.x_label, axis_config["x_label"])
|
||||
WidgetIO.set_value(self.ui.x_scale, axis_config["x_scale"])
|
||||
WidgetIO.set_value(self.ui.x_grid, axis_config["x_grid"])
|
||||
if axis_config["x_lim"] is not None:
|
||||
WidgetIO.check_and_adjust_limits(self.ui.x_min, axis_config["x_lim"][0])
|
||||
WidgetIO.check_and_adjust_limits(self.ui.x_max, axis_config["x_lim"][1])
|
||||
WidgetIO.set_value(self.ui.x_min, axis_config["x_lim"][0])
|
||||
WidgetIO.set_value(self.ui.x_max, axis_config["x_lim"][1])
|
||||
|
||||
# Y Axis Box
|
||||
WidgetIO.set_value(self.ui.y_label, axis_config["y_label"])
|
||||
WidgetIO.set_value(self.ui.y_scale, axis_config["y_scale"])
|
||||
WidgetIO.set_value(self.ui.y_grid, axis_config["y_grid"])
|
||||
if axis_config["y_lim"] is not None:
|
||||
WidgetIO.check_and_adjust_limits(self.ui.y_min, axis_config["y_lim"][0])
|
||||
WidgetIO.check_and_adjust_limits(self.ui.y_max, axis_config["y_lim"][1])
|
||||
WidgetIO.set_value(self.ui.y_min, axis_config["y_lim"][0])
|
||||
WidgetIO.set_value(self.ui.y_max, axis_config["y_lim"][1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("dark")
|
||||
window = AxisSettings()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1,249 @@
|
||||
<?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>417</width>
|
||||
<height>250</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>250</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>278</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="3">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="plot_title"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="3">
|
||||
<widget class="Line" name="line_H">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="x_axis_box">
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="x_scale_label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="x_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="x_scale">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>log</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="x_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="x_label"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="x_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="x_grid">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="x_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="Line" name="line_V">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QGroupBox" name="y_axis_box">
|
||||
<property name="title">
|
||||
<string>Y Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="y_scale">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>log</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="y_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="y_label"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="y_scale_label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="y_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="y_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="y_grid">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="y_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -5,14 +5,18 @@ from typing import Any, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import Field, ValidationError
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from qtpy.QtCore import QThread
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ImageProcessor, ProcessorWorker
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import (
|
||||
ImageProcessor,
|
||||
ImageStats,
|
||||
ProcessorWorker,
|
||||
)
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
|
||||
|
||||
@@ -25,16 +29,15 @@ class ImageConfig(SubplotConfig):
|
||||
|
||||
class BECImageShow(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"add_image_by_config",
|
||||
"get_image_config",
|
||||
"get_image_dict",
|
||||
"add_monitor_image",
|
||||
"add_custom_image",
|
||||
"set_vrange",
|
||||
"set_color_map",
|
||||
"set_autorange",
|
||||
"set_autorange_mode",
|
||||
"set_monitor",
|
||||
"set_processing",
|
||||
"set_image_properties",
|
||||
@@ -42,7 +45,6 @@ class BECImageShow(BECPlotBase):
|
||||
"set_log",
|
||||
"set_rotation",
|
||||
"set_transpose",
|
||||
"toggle_threading",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
@@ -86,6 +88,7 @@ class BECImageShow(BECPlotBase):
|
||||
# Connect signals and slots
|
||||
thread.started.connect(lambda: worker.process_image(device, image))
|
||||
worker.processed.connect(self.update_image)
|
||||
worker.stats.connect(self.update_vrange)
|
||||
worker.finished.connect(thread.quit)
|
||||
worker.finished.connect(thread.wait)
|
||||
worker.finished.connect(worker.deleteLater)
|
||||
@@ -132,7 +135,8 @@ class BECImageShow(BECPlotBase):
|
||||
self.apply_axis_config()
|
||||
self._images = defaultdict(dict)
|
||||
|
||||
# TODO extend by adding image by config
|
||||
for image_id, image_config in config.images.items():
|
||||
self.add_image_by_config(image_config)
|
||||
|
||||
def change_gui_id(self, new_gui_id: str):
|
||||
"""
|
||||
@@ -220,7 +224,7 @@ class BECImageShow(BECPlotBase):
|
||||
self,
|
||||
monitor: str,
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "simple",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
opacity: Optional[float] = 1.0,
|
||||
vrange: Optional[tuple[int, int]] = None,
|
||||
@@ -235,7 +239,7 @@ class BECImageShow(BECPlotBase):
|
||||
f"Monitor with ID '{monitor}' already exists in widget '{self.gui_id}'."
|
||||
)
|
||||
|
||||
monitor = self.entry_validator.validate_monitor(monitor)
|
||||
# monitor = self.entry_validator.validate_monitor(monitor)
|
||||
|
||||
image_config = ImageItemConfig(
|
||||
widget_class="BECImageItem",
|
||||
@@ -245,12 +249,13 @@ class BECImageShow(BECPlotBase):
|
||||
downsample=downsample,
|
||||
opacity=opacity,
|
||||
vrange=vrange,
|
||||
source=image_source,
|
||||
monitor=monitor,
|
||||
# post_processing=post_processing,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
image = self._add_image_object(source=image_source, name=monitor, config=image_config)
|
||||
self._connect_device_monitor(monitor)
|
||||
return image
|
||||
|
||||
def add_custom_image(
|
||||
@@ -258,16 +263,17 @@ class BECImageShow(BECPlotBase):
|
||||
name: str,
|
||||
data: Optional[np.ndarray] = None,
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "simple",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "full",
|
||||
downsample: Optional[bool] = True,
|
||||
opacity: Optional[float] = 1.0,
|
||||
vrange: Optional[tuple[int, int]] = None,
|
||||
# post_processing: Optional[PostProcessingConfig] = None,
|
||||
**kwargs,
|
||||
):
|
||||
image_source = "device_monitor"
|
||||
image_source = "custom"
|
||||
# image_source = "device_monitor"
|
||||
|
||||
image_exits = self._check_curve_id(name, self._images)
|
||||
image_exits = self._check_image_id(name, self._images)
|
||||
if image_exits:
|
||||
raise ValueError(f"Monitor with ID '{name}' already exists in widget '{self.gui_id}'.")
|
||||
|
||||
@@ -284,7 +290,9 @@ class BECImageShow(BECPlotBase):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
image = self._add_image_object(source=image_source, config=image_config, data=data)
|
||||
image = self._add_image_object(
|
||||
source=image_source, name=name, config=image_config, data=data
|
||||
)
|
||||
return image
|
||||
|
||||
def apply_setting_to_images(
|
||||
@@ -307,6 +315,7 @@ class BECImageShow(BECPlotBase):
|
||||
for source, images in self._images.items():
|
||||
for _, image in images.items():
|
||||
getattr(image, setting_method_name)(*args, **kwargs)
|
||||
self.refresh_image()
|
||||
|
||||
def set_vrange(self, vmin: float, vmax: float, name: str = None):
|
||||
"""
|
||||
@@ -341,6 +350,17 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
self.apply_setting_to_images("set_autorange", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_autorange_mode(self, mode: Literal["max", "mean"], name: str = None):
|
||||
"""
|
||||
Set the autoscale mode of the image, that decides how the vrange of the color bar is scaled.
|
||||
Choose betwen 'max' -> min/max of the data, 'mean' -> mean +/- fudge_factor*std of the data (fudge_factor~2).
|
||||
|
||||
Args:
|
||||
mode(str): The autoscale mode of the image.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_autorange_mode", args=[mode], kwargs={}, image_id=name)
|
||||
|
||||
def set_monitor(self, monitor: str, name: str = None):
|
||||
"""
|
||||
Set the monitor of the image.
|
||||
@@ -443,6 +463,27 @@ class BECImageShow(BECPlotBase):
|
||||
if self.use_threading is False and self.thread.isRunning():
|
||||
self.cleanup()
|
||||
|
||||
def process_image(self, device: str, image: BECImageItem, data: np.ndarray):
|
||||
"""
|
||||
Process the image data.
|
||||
|
||||
Args:
|
||||
device(str): The name of the device - image_id of image.
|
||||
image(np.ndarray): The image data to be processed.
|
||||
data(np.ndarray): The image data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed image data.
|
||||
"""
|
||||
processing_config = image.config.processing
|
||||
self.processor.set_config(processing_config)
|
||||
if self.use_threading:
|
||||
self._create_thread_worker(device, data)
|
||||
else:
|
||||
data = self.processor.process_image(data)
|
||||
self.update_image(device, data)
|
||||
self.update_vrange(device, self.processor.config.stats)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_image_update(self, msg: dict):
|
||||
"""
|
||||
@@ -453,14 +494,8 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
data = msg["data"]
|
||||
device = msg["device"]
|
||||
image_to_update = self._images["device_monitor"][device]
|
||||
processing_config = image_to_update.config.processing
|
||||
self.processor.set_config(processing_config)
|
||||
if self.use_threading:
|
||||
self._create_thread_worker(device, data)
|
||||
else:
|
||||
data = self.processor.process_image(data)
|
||||
self.update_image(device, data)
|
||||
image = self._images["device_monitor"][device]
|
||||
self.process_image(device, image, data)
|
||||
|
||||
@pyqtSlot(str, np.ndarray)
|
||||
def update_image(self, device: str, data: np.ndarray):
|
||||
@@ -474,6 +509,27 @@ class BECImageShow(BECPlotBase):
|
||||
image_to_update = self._images["device_monitor"][device]
|
||||
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
|
||||
|
||||
@pyqtSlot(str, ImageStats)
|
||||
def update_vrange(self, device: str, stats: ImageStats):
|
||||
"""
|
||||
Update the scaling of the image.
|
||||
|
||||
Args:
|
||||
stats(ImageStats): The statistics of the image.
|
||||
"""
|
||||
image_to_update = self._images["device_monitor"][device]
|
||||
if image_to_update.config.autorange:
|
||||
image_to_update.auto_update_vrange(stats)
|
||||
|
||||
def refresh_image(self):
|
||||
"""
|
||||
Refresh the image.
|
||||
"""
|
||||
for source, images in self._images.items():
|
||||
for image_id, image in images.items():
|
||||
data = image.get_data()
|
||||
self.process_image(image_id, image, data)
|
||||
|
||||
def _connect_device_monitor(self, monitor: str):
|
||||
"""
|
||||
Connect to the device monitor.
|
||||
@@ -486,16 +542,18 @@ class BECImageShow(BECPlotBase):
|
||||
previous_monitor = image_item.config.monitor
|
||||
except AttributeError:
|
||||
previous_monitor = None
|
||||
if previous_monitor != monitor:
|
||||
if previous_monitor:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor(previous_monitor)
|
||||
)
|
||||
if monitor:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor(monitor)
|
||||
)
|
||||
image_item.set_monitor(monitor)
|
||||
if previous_monitor and image_item.connected is True:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor(previous_monitor)
|
||||
)
|
||||
image_item.connected = False
|
||||
if monitor and image_item.connected is False:
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor(monitor)
|
||||
)
|
||||
image_item.set_monitor(monitor)
|
||||
image_item.connected = True
|
||||
|
||||
def _add_image_object(
|
||||
self, source: str, name: str, config: ImageItemConfig, data=None
|
||||
@@ -504,6 +562,8 @@ class BECImageShow(BECPlotBase):
|
||||
image = BECImageItem(config=config, parent_image=self)
|
||||
self.plot_item.addItem(image)
|
||||
self._images[source][name] = image
|
||||
if source == "device_monitor":
|
||||
self._connect_device_monitor(config.monitor)
|
||||
self.config.images[name] = config
|
||||
if data is not None:
|
||||
image.setImage(data)
|
||||
@@ -528,23 +588,6 @@ class BECImageShow(BECPlotBase):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _validate_monitor(self, monitor: str, validate_bec: bool = True):
|
||||
"""
|
||||
Validate the monitor name.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
validate_bec(bool): Whether to validate the monitor name with BEC.
|
||||
|
||||
Returns:
|
||||
bool: True if the monitor name is valid, False otherwise.
|
||||
"""
|
||||
if not monitor or monitor == "":
|
||||
return False
|
||||
if validate_bec:
|
||||
return monitor in self.dev
|
||||
return True
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up the widget.
|
||||
|
||||
@@ -7,7 +7,7 @@ import pyqtgraph as pg
|
||||
from pydantic import Field
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ProcessingConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ImageStats, ProcessingConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
|
||||
@@ -20,13 +20,16 @@ class ImageItemConfig(ConnectionConfig):
|
||||
color_map: Optional[str] = Field("magma", description="The color map of the image.")
|
||||
downsample: Optional[bool] = Field(True, description="Whether to downsample the image.")
|
||||
opacity: Optional[float] = Field(1.0, description="The opacity of the image.")
|
||||
vrange: Optional[tuple[int, int]] = Field(
|
||||
vrange: Optional[tuple[float | int, float | int]] = Field(
|
||||
None, description="The range of the color bar. If None, the range is automatically set."
|
||||
)
|
||||
color_bar: Optional[Literal["simple", "full"]] = Field(
|
||||
"simple", description="The type of the color bar."
|
||||
)
|
||||
autorange: Optional[bool] = Field(True, description="Whether to autorange the color bar.")
|
||||
autorange_mode: Optional[Literal["max", "mean"]] = Field(
|
||||
"mean", description="Whether to use the mean of the image for autoscaling."
|
||||
)
|
||||
processing: ProcessingConfig = Field(
|
||||
default_factory=ProcessingConfig, description="The post processing of the image."
|
||||
)
|
||||
@@ -34,8 +37,8 @@ class ImageItemConfig(ConnectionConfig):
|
||||
|
||||
class BECImageItem(BECConnector, pg.ImageItem):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_fft",
|
||||
"set_log",
|
||||
@@ -43,6 +46,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
"set_transpose",
|
||||
"set_opacity",
|
||||
"set_autorange",
|
||||
"set_autorange_mode",
|
||||
"set_color_map",
|
||||
"set_auto_downsample",
|
||||
"set_monitor",
|
||||
@@ -74,6 +78,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
self.apply_config()
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
self.connected = False
|
||||
|
||||
def apply_config(self):
|
||||
"""
|
||||
@@ -101,6 +106,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
- log
|
||||
- rot
|
||||
- transpose
|
||||
- autorange_mode
|
||||
"""
|
||||
method_map = {
|
||||
"downsample": self.set_auto_downsample,
|
||||
@@ -112,6 +118,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
"log": self.set_log,
|
||||
"rot": self.set_rotation,
|
||||
"transpose": self.set_transpose,
|
||||
"autorange_mode": self.set_autorange_mode,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
@@ -175,9 +182,18 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
autorange(bool): Whether to autorange the color bar.
|
||||
"""
|
||||
self.config.autorange = autorange
|
||||
if self.color_bar is not None:
|
||||
if self.color_bar and autorange:
|
||||
self.color_bar.autoHistogramRange()
|
||||
|
||||
def set_autorange_mode(self, mode: Literal["max", "mean"] = "mean"):
|
||||
"""
|
||||
Set the autorange mode to scale the vrange of the color bar. Choose between min/max or mean +/- std.
|
||||
|
||||
Args:
|
||||
mode(Literal["max","mean"]): Max for min/max or mean for mean +/- std.
|
||||
"""
|
||||
self.config.autorange_mode = mode
|
||||
|
||||
def set_color_map(self, cmap: str = "magma"):
|
||||
"""
|
||||
Set the color map of the image.
|
||||
@@ -212,7 +228,29 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
"""
|
||||
self.config.monitor = monitor
|
||||
|
||||
def set_vrange(self, vmin: float = None, vmax: float = None, vrange: tuple[int, int] = None):
|
||||
def auto_update_vrange(self, stats: ImageStats) -> None:
|
||||
"""Auto update of the vrange base on the stats of the image.
|
||||
|
||||
Args:
|
||||
stats(ImageStats): The stats of the image.
|
||||
"""
|
||||
fumble_factor = 2
|
||||
if self.config.autorange_mode == "mean":
|
||||
vmin = max(stats.mean - fumble_factor * stats.std, 0)
|
||||
vmax = stats.mean + fumble_factor * stats.std
|
||||
self.set_vrange(vmin, vmax, change_autorange=False)
|
||||
return
|
||||
if self.config.autorange_mode == "max":
|
||||
self.set_vrange(max(stats.minimum, 0), stats.maximum, change_autorange=False)
|
||||
return
|
||||
|
||||
def set_vrange(
|
||||
self,
|
||||
vmin: float = None,
|
||||
vmax: float = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
change_autorange: bool = True,
|
||||
):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
|
||||
@@ -224,11 +262,13 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
vmin, vmax = vrange
|
||||
self.setLevels([vmin, vmax])
|
||||
self.config.vrange = (vmin, vmax)
|
||||
self.config.autorange = False
|
||||
if change_autorange:
|
||||
self.config.autorange = False
|
||||
if self.color_bar is not None:
|
||||
if self.config.color_bar == "simple":
|
||||
self.color_bar.setLevels(low=vmin, high=vmax)
|
||||
elif self.config.color_bar == "full":
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
self.color_bar.setLevels(min=vmin, max=vmax)
|
||||
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
@@ -7,6 +8,16 @@ from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageStats:
|
||||
"""Container to store stats of an image."""
|
||||
|
||||
maximum: float
|
||||
minimum: float
|
||||
mean: float
|
||||
std: float
|
||||
|
||||
|
||||
class ProcessingConfig(BaseModel):
|
||||
fft: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
|
||||
log: Optional[bool] = Field(False, description="Whether to perform log on the monitor data.")
|
||||
@@ -20,6 +31,10 @@ class ProcessingConfig(BaseModel):
|
||||
None, description="The rotation angle of the monitor data before displaying."
|
||||
)
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
stats: ImageStats = Field(
|
||||
ImageStats(maximum=0, minimum=0, mean=0, std=0),
|
||||
description="The statistics of the image data.",
|
||||
)
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
@@ -97,6 +112,18 @@ class ImageProcessor:
|
||||
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
|
||||
# return np.unravel_index(np.argmax(data), data.shape)
|
||||
|
||||
def update_image_stats(self, data: np.ndarray) -> None:
|
||||
"""Get the statistics of the image data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The image data.
|
||||
|
||||
"""
|
||||
self.config.stats.maximum = np.max(data)
|
||||
self.config.stats.minimum = np.min(data)
|
||||
self.config.stats.mean = np.mean(data)
|
||||
self.config.stats.std = np.std(data)
|
||||
|
||||
def process_image(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Process the data according to the configuration.
|
||||
@@ -115,6 +142,7 @@ class ImageProcessor:
|
||||
data = self.transpose(data)
|
||||
if self.config.log:
|
||||
data = self.log(data)
|
||||
self.update_image_stats(data)
|
||||
return data
|
||||
|
||||
|
||||
@@ -124,6 +152,7 @@ class ProcessorWorker(QObject):
|
||||
"""
|
||||
|
||||
processed = Signal(str, np.ndarray)
|
||||
stats = Signal(str, ImageStats)
|
||||
stopRequested = Signal()
|
||||
finished = Signal()
|
||||
|
||||
@@ -147,6 +176,7 @@ class ProcessorWorker(QObject):
|
||||
self._isRunning = False
|
||||
if not self._isRunning:
|
||||
self.processed.emit(device, processed_image)
|
||||
self.stats.emit(self.processor.config.stats)
|
||||
self.finished.emit()
|
||||
|
||||
def stop(self):
|
||||
|
||||
@@ -6,37 +6,51 @@ from typing import Optional, Union
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import Field
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.utils import Colors, EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform import Signal, SignalData
|
||||
|
||||
|
||||
class MotorMapConfig(SubplotConfig):
|
||||
signals: Optional[Signal] = Field(None, description="Signals of the motor map")
|
||||
color_map: Optional[str] = Field(
|
||||
"Greys", description="Color scheme of the motor position gradient."
|
||||
) # TODO decide if useful for anything, or just keep GREYS always
|
||||
color: Optional[str | tuple] = Field(
|
||||
(255, 255, 255, 255), description="The color of the last point of current position."
|
||||
)
|
||||
scatter_size: Optional[int] = Field(5, description="Size of the scatter points.")
|
||||
max_points: Optional[int] = Field(1000, description="Maximum number of points to display.")
|
||||
max_points: Optional[int] = Field(5000, description="Maximum number of points to display.")
|
||||
num_dim_points: Optional[int] = Field(
|
||||
100,
|
||||
description="Number of points to dim before the color remains same for older recorded position.",
|
||||
)
|
||||
precision: Optional[int] = Field(2, description="Decimal precision of the motor position.")
|
||||
background_value: Optional[int] = Field(
|
||||
25, description="Background value of the motor map."
|
||||
) # TODO can be percentage from 255 calculated
|
||||
25, description="Background value of the motor map. Has to be between 0 and 255."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
|
||||
@field_validator("background_value")
|
||||
def validate_background_value(cls, value):
|
||||
if not 0 <= value <= 255:
|
||||
raise PydanticCustomError(
|
||||
"wrong_value", f"'{value}' hs to be between 0 and 255.", {"wrong_value": value}
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class BECMotorMap(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"change_motors",
|
||||
"set_max_points",
|
||||
"set_precision",
|
||||
@@ -44,6 +58,8 @@ class BECMotorMap(BECPlotBase):
|
||||
"set_background_value",
|
||||
"set_scatter_size",
|
||||
"get_data",
|
||||
"remove",
|
||||
"reset_history",
|
||||
]
|
||||
|
||||
# QT Signals
|
||||
@@ -67,31 +83,45 @@ class BECMotorMap(BECPlotBase):
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
# connect update signal to update plot
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.update_signal, rateLimit=25, slot=self._update_plot
|
||||
)
|
||||
self.apply_config(self.config)
|
||||
|
||||
def apply_config(self, config: dict | MotorMapConfig):
|
||||
"""
|
||||
Apply the config to the motor map.
|
||||
|
||||
Args:
|
||||
config(dict|MotorMapConfig): Config to be applied.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
try:
|
||||
config = MotorMapConfig(**config)
|
||||
except ValidationError as e:
|
||||
print(f"Error in applying config: {e}")
|
||||
return
|
||||
|
||||
self.config = config
|
||||
self.plot_item.clear()
|
||||
|
||||
self.motor_x = None
|
||||
self.motor_y = None
|
||||
self.database_buffer = {"x": [], "y": []}
|
||||
self.plot_components = defaultdict(dict) # container for plot components
|
||||
|
||||
# connect update signal to update plot
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.update_signal, rateLimit=25, slot=self._update_plot
|
||||
)
|
||||
self.apply_axis_config()
|
||||
|
||||
# TODO decide if needed to implement, maybe there will be no children widgets for motormap for now...
|
||||
# def find_widget_by_id(self, item_id: str) -> BECCurve:
|
||||
# """
|
||||
# Find the curve by its ID.
|
||||
# Args:
|
||||
# item_id(str): ID of the curve.
|
||||
#
|
||||
# Returns:
|
||||
# BECCurve: The curve object.
|
||||
# """
|
||||
# for curve in self.plot_item.curves:
|
||||
# if curve.gui_id == item_id:
|
||||
# return curve
|
||||
if self.config.signals is not None:
|
||||
self.change_motors(
|
||||
motor_x=self.config.signals.x.name,
|
||||
motor_y=self.config.signals.y.name,
|
||||
motor_x_entry=self.config.signals.x.entry,
|
||||
motor_y_entry=self.config.signals.y.entry,
|
||||
)
|
||||
|
||||
@pyqtSlot(str, str, str, str, bool)
|
||||
@Slot(str, str, str, str, bool)
|
||||
def change_motors(
|
||||
self,
|
||||
motor_x: str,
|
||||
@@ -110,6 +140,8 @@ class BECMotorMap(BECPlotBase):
|
||||
motor_y_entry(str): Motor entry for the Y axis.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
"""
|
||||
self.plot_item.clear()
|
||||
|
||||
motor_x_entry, motor_y_entry = self._validate_signal_entries(
|
||||
motor_x, motor_y, motor_x_entry, motor_y_entry, validate_bec
|
||||
)
|
||||
@@ -127,19 +159,42 @@ class BECMotorMap(BECPlotBase):
|
||||
# reconnect the signals
|
||||
self._connect_motor_to_slots()
|
||||
|
||||
self.database_buffer = {"x": [], "y": []}
|
||||
|
||||
# Redraw the motor map
|
||||
self._make_motor_map()
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
data = {"x": self.database_buffer["x"], "y": self.database_buffer["y"]}
|
||||
return data
|
||||
|
||||
# TODO setup all visual properties
|
||||
def reset_history(self):
|
||||
"""
|
||||
Reset the history of the motor map.
|
||||
"""
|
||||
self.database_buffer["x"] = [self.database_buffer["x"][-1]]
|
||||
self.database_buffer["y"] = [self.database_buffer["y"][-1]]
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_color(self, color: str | tuple):
|
||||
"""
|
||||
Set color of the motor trace.
|
||||
|
||||
Args:
|
||||
color(str|tuple): Color of the motor trace. Can be HEX(str) or RGBA(tuple).
|
||||
"""
|
||||
if isinstance(color, str):
|
||||
color = Colors.validate_color(color)
|
||||
color = Colors.hex_to_rgba(color, 255)
|
||||
self.config.color = color
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_max_points(self, max_points: int) -> None:
|
||||
"""
|
||||
Set the maximum number of points to display.
|
||||
@@ -148,6 +203,7 @@ class BECMotorMap(BECPlotBase):
|
||||
max_points(int): Maximum number of points to display.
|
||||
"""
|
||||
self.config.max_points = max_points
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_precision(self, precision: int) -> None:
|
||||
"""
|
||||
@@ -157,6 +213,7 @@ class BECMotorMap(BECPlotBase):
|
||||
precision(int): Decimal precision of the motor position.
|
||||
"""
|
||||
self.config.precision = precision
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_num_dim_points(self, num_dim_points: int) -> None:
|
||||
"""
|
||||
@@ -166,6 +223,7 @@ class BECMotorMap(BECPlotBase):
|
||||
num_dim_points(int): Number of dim points.
|
||||
"""
|
||||
self.config.num_dim_points = num_dim_points
|
||||
self.update_signal.emit()
|
||||
|
||||
def set_background_value(self, background_value: int) -> None:
|
||||
"""
|
||||
@@ -175,6 +233,7 @@ class BECMotorMap(BECPlotBase):
|
||||
background_value(int): Background value of the motor map.
|
||||
"""
|
||||
self.config.background_value = background_value
|
||||
self._swap_limit_map()
|
||||
|
||||
def set_scatter_size(self, scatter_size: int) -> None:
|
||||
"""
|
||||
@@ -184,6 +243,7 @@ class BECMotorMap(BECPlotBase):
|
||||
scatter_size(int): Size of the scatter points.
|
||||
"""
|
||||
self.config.scatter_size = scatter_size
|
||||
self.update_signal.emit()
|
||||
|
||||
def _disconnect_current_motors(self):
|
||||
"""Disconnect the current motors from the slots."""
|
||||
@@ -208,6 +268,16 @@ class BECMotorMap(BECPlotBase):
|
||||
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoints)
|
||||
|
||||
def _swap_limit_map(self):
|
||||
"""Swap the limit map."""
|
||||
self.plot_item.removeItem(self.plot_components["limit_map"])
|
||||
if self.config.signals.x.limits is not None and self.config.signals.y.limits is not None:
|
||||
self.plot_components["limit_map"] = self._make_limit_map(
|
||||
self.config.signals.x.limits, self.config.signals.y.limits
|
||||
)
|
||||
self.plot_components["limit_map"].setZValue(-1)
|
||||
self.plot_item.addItem(self.plot_components["limit_map"])
|
||||
|
||||
def _make_motor_map(self):
|
||||
"""
|
||||
Create the motor map plot.
|
||||
@@ -215,9 +285,10 @@ class BECMotorMap(BECPlotBase):
|
||||
# Create limit map
|
||||
motor_x_limit = self.config.signals.x.limits
|
||||
motor_y_limit = self.config.signals.y.limits
|
||||
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
|
||||
self.plot_item.addItem(self.plot_components["limit_map"])
|
||||
self.plot_components["limit_map"].setZValue(-1)
|
||||
if motor_x_limit is not None or motor_y_limit is not None:
|
||||
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
|
||||
self.plot_item.addItem(self.plot_components["limit_map"])
|
||||
self.plot_components["limit_map"].setZValue(-1)
|
||||
|
||||
# Create scatter plot
|
||||
scatter_size = self.config.scatter_size
|
||||
@@ -247,6 +318,8 @@ class BECMotorMap(BECPlotBase):
|
||||
# Set default labels for the plot
|
||||
self.set(x_label=f"Motor X ({self.motor_x})", y_label=f"Motor Y ({self.motor_y})")
|
||||
|
||||
self.update_signal.emit()
|
||||
|
||||
def _add_coordinantes_crosshair(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Add crosshair to the plot to highlight the current position.
|
||||
@@ -369,21 +442,34 @@ class BECMotorMap(BECPlotBase):
|
||||
print(f"The device '{motor}' does not have defined limits.")
|
||||
return None
|
||||
|
||||
@Slot()
|
||||
def _update_plot(self):
|
||||
"""Update the motor map plot."""
|
||||
# If the number of points exceeds max_points, delete the oldest points
|
||||
if len(self.database_buffer["x"]) > self.config.max_points:
|
||||
self.database_buffer["x"] = self.database_buffer["x"][-self.config.max_points :]
|
||||
self.database_buffer["y"] = self.database_buffer["y"][-self.config.max_points :]
|
||||
|
||||
x = self.database_buffer["x"]
|
||||
y = self.database_buffer["y"]
|
||||
|
||||
# Setup gradient brush for history
|
||||
brushes = [pg.mkBrush(50, 50, 50, 255)] * len(x)
|
||||
|
||||
# RGB color
|
||||
r, g, b, a = self.config.color
|
||||
|
||||
# Calculate the decrement step based on self.num_dim_points
|
||||
num_dim_points = self.config.num_dim_points
|
||||
decrement_step = (255 - 50) / num_dim_points
|
||||
|
||||
for i in range(1, min(num_dim_points + 1, len(x) + 1)):
|
||||
brightness = max(60, 255 - decrement_step * (i - 1))
|
||||
brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255)
|
||||
brushes[-1] = pg.mkBrush(255, 255, 255, 255) # Newest point is always full brightness
|
||||
dim_r = int(r * (brightness / 255))
|
||||
dim_g = int(g * (brightness / 255))
|
||||
dim_b = int(b * (brightness / 255))
|
||||
brushes[-i] = pg.mkBrush(dim_r, dim_g, dim_b, a)
|
||||
brushes[-1] = pg.mkBrush(r, g, b, a) # Newest point is always full brightness
|
||||
scatter_size = self.config.scatter_size
|
||||
|
||||
# Update the scatter plot
|
||||
@@ -406,7 +492,7 @@ class BECMotorMap(BECPlotBase):
|
||||
f"Motor position: ({round(float(current_x),precision)}, {round(float(current_y),precision)})"
|
||||
)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
@Slot(dict)
|
||||
def on_device_readback(self, msg: dict) -> None:
|
||||
"""
|
||||
Update the motor map plot with the new motor position.
|
||||
|
||||
@@ -46,7 +46,7 @@ class SubplotConfig(ConnectionConfig):
|
||||
|
||||
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.scan_data import ScanData
|
||||
from pydantic import Field, ValidationError
|
||||
@@ -33,15 +35,15 @@ class Waveform1DConfig(SubplotConfig):
|
||||
|
||||
class BECWaveform(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"plot",
|
||||
"add_dap",
|
||||
"get_dap_params",
|
||||
"remove_curve",
|
||||
"scan_history",
|
||||
"curves",
|
||||
"get_curve",
|
||||
"get_curve_config",
|
||||
"apply_config",
|
||||
"get_all_data",
|
||||
"set",
|
||||
"set_title",
|
||||
@@ -57,6 +59,7 @@ class BECWaveform(BECPlotBase):
|
||||
"set_legend_label_size",
|
||||
]
|
||||
scan_signal_update = pyqtSignal()
|
||||
dap_params_update = pyqtSignal(dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -73,6 +76,7 @@ class BECWaveform(BECPlotBase):
|
||||
)
|
||||
|
||||
self._curves_data = defaultdict(dict)
|
||||
self.old_scan_id = None
|
||||
self.scan_id = None
|
||||
|
||||
# Scan segment update proxy
|
||||
@@ -80,6 +84,9 @@ class BECWaveform(BECPlotBase):
|
||||
self.scan_signal_update, rateLimit=25, slot=self._update_scan_segment_plot
|
||||
)
|
||||
|
||||
self.proxy_update_dap = pg.SignalProxy(
|
||||
self.scan_signal_update, rateLimit=25, slot=self.refresh_dap
|
||||
)
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@@ -213,6 +220,8 @@ class BECWaveform(BECPlotBase):
|
||||
color_map_z: str | None = "plasma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
dap: str | None = None, # TODO add dap custom curve wrapper
|
||||
**kwargs,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
@@ -229,14 +238,17 @@ class BECWaveform(BECPlotBase):
|
||||
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.
|
||||
"""
|
||||
|
||||
if x is not None and y is not None:
|
||||
return self.add_curve_custom(x=x, y=y, label=label, color=color)
|
||||
return self.add_curve_custom(x=x, y=y, label=label, color=color, **kwargs)
|
||||
else:
|
||||
if dap:
|
||||
self.add_dap(x_name=x_name, y_name=y_name, dap=dap)
|
||||
return self.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
@@ -248,6 +260,7 @@ class BECWaveform(BECPlotBase):
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate_bec=validate,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def add_curve_custom(
|
||||
@@ -256,6 +269,7 @@ class BECWaveform(BECPlotBase):
|
||||
y: list | np.ndarray,
|
||||
label: str = None,
|
||||
color: str = None,
|
||||
curve_source: str = "custom",
|
||||
**kwargs,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
@@ -266,12 +280,13 @@ class BECWaveform(BECPlotBase):
|
||||
y(list|np.ndarray): Y data of the curve.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
color(str, optional): Color of the curve. Defaults to None.
|
||||
curve_source(str, optional): Tag for source of the curve. Defaults to "custom".
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
curve_source = "custom"
|
||||
curve_source = curve_source
|
||||
curve_id = label or f"Curve {len(self.plot_item.curves) + 1}"
|
||||
|
||||
curve_exits = self._check_curve_id(curve_id, self._curves_data)
|
||||
@@ -314,10 +329,12 @@ class BECWaveform(BECPlotBase):
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate_bec: bool = True,
|
||||
source: str = "scan_segment",
|
||||
dap: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Add a curve to the plot widget from the scan segment.
|
||||
Add a curve to the plot widget from the scan segment. #TODO adapt docs to DAP
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
@@ -335,7 +352,7 @@ class BECWaveform(BECPlotBase):
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
# Check if curve already exists
|
||||
curve_source = "scan_segment"
|
||||
curve_source = source
|
||||
|
||||
# Get entry if not provided and validate
|
||||
x_entry, y_entry, z_entry = self._validate_signal_entries(
|
||||
@@ -371,12 +388,74 @@ class BECWaveform(BECPlotBase):
|
||||
x=SignalData(name=x_name, entry=x_entry),
|
||||
y=SignalData(name=y_name, entry=y_entry),
|
||||
z=SignalData(name=z_name, entry=z_entry) if z_name else None,
|
||||
dap=dap,
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
curve = self._add_curve_object(name=label, source=curve_source, config=curve_config)
|
||||
return curve
|
||||
|
||||
def add_dap(
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
x_entry: Optional[str] = None,
|
||||
y_entry: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
dap: str = "GaussianModel",
|
||||
**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.
|
||||
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.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
x_entry, y_entry, _ = self._validate_signal_entries(
|
||||
x_name, y_name, None, x_entry, y_entry, None
|
||||
)
|
||||
label = f"{y_name}-{y_entry}-{dap}"
|
||||
curve = self.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
color=color,
|
||||
label=label,
|
||||
source="DAP",
|
||||
dap=dap,
|
||||
pen_style="dash",
|
||||
symbol="star",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.setup_dap(self.old_scan_id, self.scan_id)
|
||||
self.refresh_dap()
|
||||
return curve
|
||||
|
||||
def get_dap_params(self) -> dict:
|
||||
"""
|
||||
Get the DAP parameters of all DAP curves.
|
||||
|
||||
Returns:
|
||||
dict: DAP parameters of all DAP curves.
|
||||
"""
|
||||
params = {}
|
||||
for curve_id, curve in self._curves_data["DAP"].items():
|
||||
params[curve_id] = curve.dap_params
|
||||
return params
|
||||
|
||||
def _add_curve_object(
|
||||
self,
|
||||
name: str,
|
||||
@@ -528,13 +607,75 @@ class BECWaveform(BECPlotBase):
|
||||
return
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self.old_scan_id = self.scan_id
|
||||
self.scan_id = current_scan_id
|
||||
self.scan_segment_data = self.queue.scan_storage.find_scan_by_ID(
|
||||
self.scan_id
|
||||
) # TODO do scan access through BECFigure
|
||||
self.setup_dap(self.old_scan_id, self.scan_id)
|
||||
|
||||
self.scan_signal_update.emit()
|
||||
|
||||
def setup_dap(self, old_scan_id, new_scan_id):
|
||||
"""
|
||||
Setup DAP for the new scan.
|
||||
|
||||
Args:
|
||||
old_scan_id(str): old_scan_id, used to disconnect the previous dispatcher connection.
|
||||
new_scan_id(str): new_scan_id, used to connect the new dispatcher connection.
|
||||
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.update_dap, MessageEndpoints.dap_response(old_scan_id)
|
||||
)
|
||||
if len(self._curves_data["DAP"]) > 0:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.update_dap, MessageEndpoints.dap_response(new_scan_id)
|
||||
)
|
||||
|
||||
def refresh_dap(self):
|
||||
"""
|
||||
Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response().
|
||||
"""
|
||||
for curve_id, curve in self._curves_data["DAP"].items():
|
||||
x_name = curve.config.signals.x.name
|
||||
y_name = curve.config.signals.y.name
|
||||
x_entry = curve.config.signals.x.entry
|
||||
y_entry = curve.config.signals.y.entry
|
||||
model_name = curve.config.signals.dap
|
||||
model = getattr(self.dap, model_name)
|
||||
|
||||
msg = messages.DAPRequestMessage(
|
||||
dap_cls="LmfitService1D",
|
||||
dap_type="on_demand",
|
||||
config={
|
||||
"args": [self.scan_id, x_name, x_entry, y_name, y_entry],
|
||||
"kwargs": {},
|
||||
"class_args": model._plugin_info["class_args"],
|
||||
"class_kwargs": model._plugin_info["class_kwargs"],
|
||||
},
|
||||
metadata={"RID": self.scan_id},
|
||||
)
|
||||
self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg)
|
||||
|
||||
@pyqtSlot(dict, dict)
|
||||
def update_dap(self, msg, metadata):
|
||||
self.msg = msg
|
||||
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
|
||||
model = msg["dap_request"].content["config"]["class_kwargs"]["model"]
|
||||
|
||||
curve_id_request = f"{y_name}-{y_entry}-{model}"
|
||||
|
||||
for curve_id, curve in self._curves_data["DAP"].items():
|
||||
if curve_id == curve_id_request:
|
||||
if msg["data"] is not None:
|
||||
x = msg["data"][0]["x"]
|
||||
y = msg["data"][0]["y"]
|
||||
curve.setData(x, y)
|
||||
curve.dap_params = msg["data"][1]["fit_parameters"]
|
||||
self.dap_params_update.emit(curve.dap_params)
|
||||
break
|
||||
|
||||
def _update_scan_segment_plot(self):
|
||||
"""Update the plot with the data from the scan segment."""
|
||||
data = self.scan_segment_data.data
|
||||
@@ -564,14 +705,15 @@ class BECWaveform(BECPlotBase):
|
||||
data_y = data[y_name][y_entry].val
|
||||
if curve.config.signals.z:
|
||||
data_z = data[z_name][z_entry].val
|
||||
color_z = self._make_z_gradient(
|
||||
data_z, curve.config.color_map_z
|
||||
) # TODO decide how to implement custom gradient
|
||||
color_z = self._make_z_gradient(data_z, curve.config.color_map_z)
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
if data_z is not None and color_z is not None:
|
||||
curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
|
||||
try:
|
||||
curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
|
||||
except:
|
||||
return
|
||||
else:
|
||||
curve.setData(data_x, data_y)
|
||||
|
||||
@@ -609,13 +751,21 @@ class BECWaveform(BECPlotBase):
|
||||
if scan_index is not None and scan_id is not None:
|
||||
raise ValueError("Only one of scan_id or scan_index can be provided.")
|
||||
|
||||
# Reset DAP connector
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.update_dap, MessageEndpoints.dap_response(self.scan_id)
|
||||
)
|
||||
if scan_index is not None:
|
||||
self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id
|
||||
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
|
||||
try:
|
||||
self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id
|
||||
except IndexError:
|
||||
print(f"Scan index {scan_index} out of range.")
|
||||
return
|
||||
elif scan_id is not None:
|
||||
self.scan_id = scan_id
|
||||
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
|
||||
|
||||
self.setup_dap(self.old_scan_id, self.scan_id)
|
||||
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
|
||||
self._update_scan_curves(data)
|
||||
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict | pd.DataFrame:
|
||||
@@ -661,6 +811,9 @@ class BECWaveform(BECPlotBase):
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget connection from BECDispatcher."""
|
||||
self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.update_dap, MessageEndpoints.dap_response(self.scan_id)
|
||||
)
|
||||
for curve in self.curves:
|
||||
curve.cleanup()
|
||||
super().cleanup()
|
||||
|
||||
@@ -31,6 +31,7 @@ class Signal(BaseModel):
|
||||
x: SignalData # TODO maybe add metadata for config gui later
|
||||
y: SignalData
|
||||
z: Optional[SignalData] = None
|
||||
dap: Optional[str] = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
@@ -63,8 +64,9 @@ class CurveConfig(ConnectionConfig):
|
||||
class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
USER_ACCESS = [
|
||||
"remove",
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"dap_params",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
@@ -75,6 +77,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
"set_pen_width",
|
||||
"set_pen_style",
|
||||
"get_data",
|
||||
"dap_params",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -96,6 +99,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
|
||||
self.parent_item = parent_item
|
||||
self.apply_config()
|
||||
self.dap_params = None
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
|
||||
@@ -119,6 +123,14 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
self.setSymbolSize(self.config.symbol_size)
|
||||
self.setSymbol(self.config.symbol)
|
||||
|
||||
@property
|
||||
def dap_params(self):
|
||||
return self._dap_params
|
||||
|
||||
@dap_params.setter
|
||||
def dap_params(self, value):
|
||||
self._dap_params = value
|
||||
|
||||
def set_data(self, x, y):
|
||||
if self.config.source == "custom":
|
||||
self.setData(x, y)
|
||||
@@ -241,5 +253,6 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
|
||||
def remove(self):
|
||||
"""Remove the curve from the plot."""
|
||||
self.parent_item.removeItem(self)
|
||||
# self.parent_item.removeItem(self)
|
||||
self.parent_item.remove_curve(self.name())
|
||||
self.cleanup()
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from enum import Enum
|
||||
|
||||
from bec_lib.alarm_handler import AlarmBase
|
||||
from bec_lib.device import Positioner
|
||||
from qtpy.QtCore import QThread
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QMessageBox, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"motor_control": {
|
||||
"motor_x": "samx",
|
||||
"motor_y": "samy",
|
||||
"step_size_x": 3,
|
||||
"step_size_y": 50,
|
||||
"precision": 4,
|
||||
"step_x_y_same": False,
|
||||
"move_with_arrows": False,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MotorControlWidget(QWidget):
|
||||
"""Base class for motor control widgets."""
|
||||
|
||||
def __init__(self, parent=None, client=None, motor_thread=None, config=None):
|
||||
super().__init__(parent)
|
||||
self.client = client
|
||||
self.motor_thread = motor_thread
|
||||
self.config = config
|
||||
|
||||
self.motor_x = None
|
||||
self.motor_y = None
|
||||
|
||||
if not self.client:
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client
|
||||
|
||||
if not self.motor_thread:
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self._load_ui()
|
||||
|
||||
if self.config is None:
|
||||
print(f"No initial config found for {self.__class__.__name__}")
|
||||
self._init_ui()
|
||||
else:
|
||||
self.on_config_update(self.config)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI components specific to the widget."""
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config):
|
||||
"""Handle configuration updates."""
|
||||
self.config = config
|
||||
self._init_ui()
|
||||
|
||||
|
||||
class MotorControlErrors:
|
||||
"""Class for displaying formatted error messages."""
|
||||
|
||||
@staticmethod
|
||||
def display_error_message(error_message: str) -> None:
|
||||
"""
|
||||
Display a critical error message.
|
||||
Args:
|
||||
error_message(str): Error message to display.
|
||||
"""
|
||||
# Create a QMessageBox
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
msg.setWindowTitle("Critical Error")
|
||||
|
||||
# Format the message
|
||||
formatted_message = MotorControlErrors._format_error_message(error_message)
|
||||
msg.setText(formatted_message)
|
||||
|
||||
# Display the message box
|
||||
msg.exec_()
|
||||
|
||||
@staticmethod
|
||||
def _format_error_message(error_message: str) -> str:
|
||||
"""
|
||||
Format the error message.
|
||||
Args:
|
||||
error_message(str): Error message to format.
|
||||
|
||||
Returns:
|
||||
str: Formatted error message.
|
||||
"""
|
||||
# Split the message into lines
|
||||
lines = error_message.split("\n")
|
||||
formatted_lines = [
|
||||
f"<b>{line.strip()}</b>" if i == 0 else line.strip()
|
||||
for i, line in enumerate(lines)
|
||||
if line.strip()
|
||||
]
|
||||
|
||||
# Join the lines with double breaks for empty lines in between
|
||||
formatted_message = "<br><br>".join(formatted_lines)
|
||||
|
||||
return formatted_message
|
||||
|
||||
|
||||
class MotorActions(Enum):
|
||||
"""Enum for motor actions."""
|
||||
|
||||
MOVE_ABSOLUTE = "move_absolute"
|
||||
MOVE_RELATIVE = "move_relative"
|
||||
|
||||
|
||||
class MotorThread(QThread):
|
||||
"""
|
||||
QThread subclass for controlling motor actions asynchronously.
|
||||
|
||||
Signals:
|
||||
coordinates_updated (pyqtSignal): Signal to emit current coordinates.
|
||||
motor_error (pyqtSignal): Signal to emit when there is an error with the motors.
|
||||
lock_gui (pyqtSignal): Signal to lock/unlock the GUI.
|
||||
"""
|
||||
|
||||
coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates
|
||||
motor_error = pyqtSignal(str) # Signal to emit when there is an error with the motors
|
||||
lock_gui = pyqtSignal(bool) # Signal to lock/unlock the GUI
|
||||
|
||||
def __init__(self, parent=None, client=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.scans = self.client.scans
|
||||
self.queue = self.client.queue
|
||||
self.action = None
|
||||
|
||||
self.motor = None
|
||||
self.motor_x = None
|
||||
self.motor_y = None
|
||||
self.target_coordinates = None
|
||||
self.value = None
|
||||
|
||||
def get_all_motors_names(self) -> list:
|
||||
"""
|
||||
Get all the motors names.
|
||||
Returns:
|
||||
list: List of all the motors names.
|
||||
"""
|
||||
all_devices = self.client.device_manager.devices.enabled_devices
|
||||
all_motors_names = [motor.name for motor in all_devices if isinstance(motor, Positioner)]
|
||||
return all_motors_names
|
||||
|
||||
def get_coordinates(self, motor_x: str, motor_y: str) -> tuple:
|
||||
"""
|
||||
Get the current coordinates of the motors.
|
||||
Args:
|
||||
motor_x(str): Motor X to get positions from.
|
||||
motor_y(str): Motor Y to get positions from.
|
||||
|
||||
Returns:
|
||||
tuple: Current coordinates of the motors.
|
||||
"""
|
||||
x = self.dev[motor_x].readback.get()
|
||||
y = self.dev[motor_y].readback.get()
|
||||
return x, y
|
||||
|
||||
def move_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
|
||||
"""
|
||||
Wrapper for moving the motor to the target coordinates.
|
||||
Args:
|
||||
motor_x(str): Motor X to move.
|
||||
motor_y(str): Motor Y to move.
|
||||
target_coordinates(tuple): Target coordinates.
|
||||
"""
|
||||
self.action = MotorActions.MOVE_ABSOLUTE
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.target_coordinates = target_coordinates
|
||||
self.start()
|
||||
|
||||
def move_relative(self, motor: str, value: float) -> None:
|
||||
"""
|
||||
Wrapper for moving the motor relative to the current position.
|
||||
Args:
|
||||
motor(str): Motor to move.
|
||||
value(float): Value to move.
|
||||
"""
|
||||
self.action = MotorActions.MOVE_RELATIVE
|
||||
self.motor = motor
|
||||
self.value = value
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the thread.
|
||||
Possible actions:
|
||||
- Move to coordinates
|
||||
- Move relative
|
||||
"""
|
||||
if self.action == MotorActions.MOVE_ABSOLUTE:
|
||||
self._move_motor_absolute(self.motor_x, self.motor_y, self.target_coordinates)
|
||||
elif self.action == MotorActions.MOVE_RELATIVE:
|
||||
self._move_motor_relative(self.motor, self.value)
|
||||
|
||||
def _move_motor_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
|
||||
"""
|
||||
Move the motor to the target coordinates.
|
||||
Args:
|
||||
motor_x(str): Motor X to move.
|
||||
motor_y(str): Motor Y to move.
|
||||
target_coordinates(tuple): Target coordinates.
|
||||
"""
|
||||
self.lock_gui.emit(False)
|
||||
try:
|
||||
status = self.scans.mv(
|
||||
self.dev[motor_x],
|
||||
target_coordinates[0],
|
||||
self.dev[motor_y],
|
||||
target_coordinates[1],
|
||||
relative=False,
|
||||
)
|
||||
status.wait()
|
||||
except AlarmBase as e:
|
||||
self.motor_error.emit(str(e))
|
||||
finally:
|
||||
self.lock_gui.emit(True)
|
||||
|
||||
def _move_motor_relative(self, motor, value: float) -> None:
|
||||
"""
|
||||
Move the motor relative to the current position.
|
||||
Args:
|
||||
motor(str): Motor to move.
|
||||
value(float): Value to move.
|
||||
"""
|
||||
self.lock_gui.emit(False)
|
||||
try:
|
||||
status = self.scans.mv(self.dev[motor], value, relative=True)
|
||||
status.wait()
|
||||
except AlarmBase as e:
|
||||
self.motor_error.emit(str(e))
|
||||
finally:
|
||||
self.lock_gui.emit(True)
|
||||
|
||||
def stop_movement(self):
|
||||
self.queue.request_scan_abortion()
|
||||
self.queue.request_queue_reset()
|
||||
@@ -1,484 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtGui import QDoubleValidator, QKeySequence
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QShortcut,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
)
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
|
||||
|
||||
|
||||
class MotorCoordinateTable(MotorControlWidget):
|
||||
"""
|
||||
Widget to save coordinates from motor, display them in the table and move back to them.
|
||||
There are two modes of operation:
|
||||
- Individual: Each row is a single coordinate.
|
||||
- Start/Stop: Each pair of rows is a start and end coordinate.
|
||||
Signals:
|
||||
plot_coordinates_signal (pyqtSignal(list, str, str)): Signal to plot the coordinates in the MotorMap.
|
||||
Slots:
|
||||
add_coordinate (pyqtSlot(tuple)): Slot to add a coordinate to the table.
|
||||
mode_switch (pyqtSlot): Slot to switch between individual and start/stop mode.
|
||||
"""
|
||||
|
||||
plot_coordinates_signal = pyqtSignal(list, str, str)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI for the coordinate table."""
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "motor_table.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI"""
|
||||
# Setup table behaviour
|
||||
self._setup_table()
|
||||
self.ui.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
|
||||
# for tag columns default tag
|
||||
self.tag_counter = 1
|
||||
|
||||
# Connect signals and slots
|
||||
self.ui.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
|
||||
self.ui.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
|
||||
|
||||
# Keyboard shortcuts for deleting a row
|
||||
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.ui.table)
|
||||
self.delete_shortcut.activated.connect(self.delete_selected_row)
|
||||
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.ui.table)
|
||||
self.backspace_shortcut.activated.connect(self.delete_selected_row)
|
||||
|
||||
# Warning message for mode switch enable/disable
|
||||
self.warning_message = True
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""
|
||||
Update config dict
|
||||
Args:
|
||||
config(dict): New config dict
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
# Decimal precision of the table coordinates
|
||||
self.precision = self.config["motor_control"].get("precision", 3)
|
||||
|
||||
# Mode switch default option
|
||||
self.mode = self.config["motor_control"].get("mode", "Individual")
|
||||
|
||||
# Set combobox to default mode
|
||||
self.ui.comboBox_mode.setCurrentText(self.mode)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _setup_table(self):
|
||||
"""Setup the table with appropriate headers and configurations."""
|
||||
mode = self.ui.comboBox_mode.currentText()
|
||||
|
||||
if mode == "Individual":
|
||||
self._setup_individual_mode()
|
||||
elif mode == "Start/Stop":
|
||||
self._setup_start_stop_mode()
|
||||
self.start_stop_counter = 0 # TODO: remove this??
|
||||
|
||||
self.wipe_motor_map_coordinates()
|
||||
|
||||
def _setup_individual_mode(self):
|
||||
"""Setup the table for individual mode."""
|
||||
self.ui.table.setColumnCount(5)
|
||||
self.ui.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
|
||||
self.ui.table.verticalHeader().setVisible(False)
|
||||
|
||||
def _setup_start_stop_mode(self):
|
||||
"""Setup the table for start/stop mode."""
|
||||
self.ui.table.setColumnCount(8)
|
||||
self.ui.table.setHorizontalHeaderLabels(
|
||||
[
|
||||
"Show",
|
||||
"Move [start]",
|
||||
"Move [end]",
|
||||
"Tag",
|
||||
"X [start]",
|
||||
"Y [start]",
|
||||
"X [end]",
|
||||
"Y [end]",
|
||||
]
|
||||
)
|
||||
self.ui.table.verticalHeader().setVisible(False)
|
||||
# Set flag to track if the coordinate is stat or the end of the entry
|
||||
self.is_next_entry_end = False
|
||||
|
||||
def mode_switch(self):
|
||||
"""Switch between individual and start/stop mode."""
|
||||
last_selected_index = self.ui.comboBox_mode.currentIndex()
|
||||
|
||||
if self.ui.table.rowCount() > 0 and self.warning_message is True:
|
||||
msgBox = QMessageBox()
|
||||
msgBox.setIcon(QMessageBox.Critical)
|
||||
msgBox.setText(
|
||||
"Switching modes will delete all table entries. Do you want to continue?"
|
||||
)
|
||||
msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
|
||||
returnValue = msgBox.exec()
|
||||
|
||||
if returnValue is QMessageBox.Cancel:
|
||||
self.ui.comboBox_mode.blockSignals(True) # Block signals
|
||||
self.ui.comboBox_mode.setCurrentIndex(last_selected_index)
|
||||
self.ui.comboBox_mode.blockSignals(False) # Unblock signals
|
||||
return
|
||||
|
||||
# Wipe table
|
||||
self.wipe_motor_map_coordinates()
|
||||
|
||||
# Initiate new table with new mode
|
||||
self._setup_table()
|
||||
|
||||
@pyqtSlot(tuple)
|
||||
def add_coordinate(self, coordinates: tuple):
|
||||
"""
|
||||
Add a coordinate to the table.
|
||||
Args:
|
||||
coordinates(tuple): Coordinates (x,y) to add to the table.
|
||||
"""
|
||||
tag = f"Pos {self.tag_counter}"
|
||||
self.tag_counter += 1
|
||||
x, y = coordinates
|
||||
self._add_row(tag, x, y)
|
||||
|
||||
def _add_row(self, tag: str, x: float, y: float) -> None:
|
||||
"""
|
||||
Add a row to the table.
|
||||
Args:
|
||||
tag(str): Tag of the coordinate.
|
||||
x(float): X coordinate.
|
||||
y(float): Y coordinate.
|
||||
"""
|
||||
|
||||
mode = self.ui.comboBox_mode.currentText()
|
||||
if mode == "Individual":
|
||||
checkbox_pos = 0
|
||||
button_pos = 1
|
||||
tag_pos = 2
|
||||
x_pos = 3
|
||||
y_pos = 4
|
||||
coordinate_reference = "Individual"
|
||||
color = "green"
|
||||
|
||||
# Add new row -> new entry
|
||||
row_count = self.ui.table.rowCount()
|
||||
self.ui.table.insertRow(row_count)
|
||||
|
||||
# Add Widgets
|
||||
self._add_widgets(
|
||||
tag,
|
||||
x,
|
||||
y,
|
||||
row_count,
|
||||
checkbox_pos,
|
||||
tag_pos,
|
||||
button_pos,
|
||||
x_pos,
|
||||
y_pos,
|
||||
coordinate_reference,
|
||||
color,
|
||||
)
|
||||
|
||||
if mode == "Start/Stop":
|
||||
# These positions are always fixed
|
||||
checkbox_pos = 0
|
||||
tag_pos = 3
|
||||
|
||||
if self.is_next_entry_end is False: # It is the start position of the entry
|
||||
print("Start position")
|
||||
button_pos = 1
|
||||
x_pos = 4
|
||||
y_pos = 5
|
||||
coordinate_reference = "Start"
|
||||
color = "blue"
|
||||
|
||||
# Add new row -> new entry
|
||||
row_count = self.ui.table.rowCount()
|
||||
self.ui.table.insertRow(row_count)
|
||||
|
||||
# Add Widgets
|
||||
self._add_widgets(
|
||||
tag,
|
||||
x,
|
||||
y,
|
||||
row_count,
|
||||
checkbox_pos,
|
||||
tag_pos,
|
||||
button_pos,
|
||||
x_pos,
|
||||
y_pos,
|
||||
coordinate_reference,
|
||||
color,
|
||||
)
|
||||
|
||||
# Next entry will be the end of the current entry
|
||||
self.is_next_entry_end = True
|
||||
|
||||
elif self.is_next_entry_end is True: # It is the end position of the entry
|
||||
print("End position")
|
||||
row_count = self.ui.table.rowCount() - 1 # Current row
|
||||
button_pos = 2
|
||||
x_pos = 6
|
||||
y_pos = 7
|
||||
coordinate_reference = "Stop"
|
||||
color = "red"
|
||||
|
||||
# Add Widgets
|
||||
self._add_widgets(
|
||||
tag,
|
||||
x,
|
||||
y,
|
||||
row_count,
|
||||
checkbox_pos,
|
||||
tag_pos,
|
||||
button_pos,
|
||||
x_pos,
|
||||
y_pos,
|
||||
coordinate_reference,
|
||||
color,
|
||||
)
|
||||
self.is_next_entry_end = False # Next entry will be the start of the new entry
|
||||
|
||||
# Auto table resize
|
||||
self.resize_table_auto()
|
||||
|
||||
def _add_widgets(
|
||||
self,
|
||||
tag: str,
|
||||
x: float,
|
||||
y: float,
|
||||
row: int,
|
||||
checkBox_pos: int,
|
||||
tag_pos: int,
|
||||
button_pos: int,
|
||||
x_pos: int,
|
||||
y_pos: int,
|
||||
coordinate_reference: str,
|
||||
color: str,
|
||||
) -> None:
|
||||
"""
|
||||
Add widgets to the table.
|
||||
Args:
|
||||
tag(str): Tag of the coordinate.
|
||||
x(float): X coordinate.
|
||||
y(float): Y coordinate.
|
||||
row(int): Row of the QTableWidget where to add the widgets.
|
||||
checkBox_pos(int): Column where to put CheckBox.
|
||||
tag_pos(int): Column where to put Tag.
|
||||
button_pos(int): Column where to put Move button.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
coordinate_reference(str): Reference to the coordinate for MotorMap.
|
||||
color(str): Color of the coordinate for MotorMap.
|
||||
"""
|
||||
# Add widgets
|
||||
self._add_checkbox(row, checkBox_pos, x_pos, y_pos)
|
||||
self._add_move_button(row, button_pos, x_pos, y_pos)
|
||||
self.ui.table.setItem(row, tag_pos, QTableWidgetItem(tag))
|
||||
self._add_line_edit(x, row, x_pos, x_pos, y_pos, coordinate_reference, color)
|
||||
self._add_line_edit(y, row, y_pos, x_pos, y_pos, coordinate_reference, color)
|
||||
|
||||
# # Emit the coordinates to be plotted
|
||||
self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
|
||||
|
||||
# Connect item edit to emit coordinates
|
||||
self.ui.table.itemChanged.connect(
|
||||
lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}")
|
||||
)
|
||||
self.ui.table.itemChanged.connect(
|
||||
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
|
||||
)
|
||||
|
||||
def _add_checkbox(self, row: int, checkBox_pos: int, x_pos: int, y_pos: int):
|
||||
"""
|
||||
Add a checkbox to the table.
|
||||
Args:
|
||||
row(int): Row of QTableWidget where to add the checkbox.
|
||||
checkBox_pos(int): Column where to put CheckBox.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
"""
|
||||
show_checkbox = QCheckBox()
|
||||
show_checkbox.setChecked(True)
|
||||
show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos))
|
||||
self.ui.table.setCellWidget(row, checkBox_pos, show_checkbox)
|
||||
|
||||
def _add_move_button(self, row: int, button_pos: int, x_pos: int, y_pos: int) -> None:
|
||||
"""
|
||||
Add a move button to the table.
|
||||
Args:
|
||||
row(int): Row of QTableWidget where to add the move button.
|
||||
button_pos(int): Column where to put move button.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
"""
|
||||
move_button = QPushButton("Move")
|
||||
move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos))
|
||||
self.ui.table.setCellWidget(row, button_pos, move_button)
|
||||
|
||||
def _add_line_edit(
|
||||
self,
|
||||
value: float,
|
||||
row: int,
|
||||
line_pos: int,
|
||||
x_pos: int,
|
||||
y_pos: int,
|
||||
coordinate_reference: str,
|
||||
color: str,
|
||||
) -> None:
|
||||
"""
|
||||
Add a QLineEdit to the table.
|
||||
Args:
|
||||
value(float): Initial value of the QLineEdit.
|
||||
row(int): Row of QTableWidget where to add the QLineEdit.
|
||||
line_pos(int): Column where to put QLineEdit.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
coordinate_reference(str): Reference to the coordinate for MotorMap.
|
||||
color(str): Color of the coordinate for MotorMap.
|
||||
"""
|
||||
# Adding validator
|
||||
validator = QDoubleValidator()
|
||||
validator.setDecimals(self.precision)
|
||||
|
||||
# Create line edit
|
||||
edit = QLineEdit(str(f"{value:.{self.precision}f}"))
|
||||
edit.setValidator(validator)
|
||||
edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# Add line edit to the table
|
||||
self.ui.table.setCellWidget(row, line_pos, edit)
|
||||
edit.textChanged.connect(
|
||||
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
|
||||
)
|
||||
|
||||
def wipe_motor_map_coordinates(self):
|
||||
"""Wipe the motor map coordinates."""
|
||||
try:
|
||||
self.ui.table.itemChanged.disconnect() # Disconnect all previous connections
|
||||
except TypeError:
|
||||
print("No previous connections to disconnect")
|
||||
self.ui.table.setRowCount(0)
|
||||
reference_tags = ["Individual", "Start", "Stop"]
|
||||
for reference_tag in reference_tags:
|
||||
self.plot_coordinates_signal.emit([], reference_tag, "green")
|
||||
|
||||
def handle_move_button_click(self, x_pos: int, y_pos: int) -> None:
|
||||
"""
|
||||
Handle the move button click.
|
||||
Args:
|
||||
x_pos(int): X position of the coordinate.
|
||||
y_pos(int): Y position of the coordinate.
|
||||
"""
|
||||
button = self.sender()
|
||||
row = self.ui.table.indexAt(button.pos()).row()
|
||||
|
||||
x = self.get_coordinate(row, x_pos)
|
||||
y = self.get_coordinate(row, y_pos)
|
||||
self.move_motor(x, y)
|
||||
|
||||
def emit_plot_coordinates(self, x_pos: float, y_pos: float, reference_tag: str, color: str):
|
||||
"""
|
||||
Emit the coordinates to be plotted.
|
||||
Args:
|
||||
x_pos(float): X position of the coordinate.
|
||||
y_pos(float): Y position of the coordinate.
|
||||
reference_tag(str): Reference tag of the coordinate.
|
||||
color(str): Color of the coordinate.
|
||||
"""
|
||||
print(
|
||||
f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}"
|
||||
)
|
||||
coordinates = []
|
||||
for row in range(self.ui.table.rowCount()):
|
||||
show = self.ui.table.cellWidget(row, 0).isChecked()
|
||||
x = self.get_coordinate(row, x_pos)
|
||||
y = self.get_coordinate(row, y_pos)
|
||||
|
||||
coordinates.append((x, y, show)) # (x, y, show_flag)
|
||||
self.plot_coordinates_signal.emit(coordinates, reference_tag, color)
|
||||
|
||||
def get_coordinate(self, row: int, column: int) -> float:
|
||||
"""
|
||||
Helper function to get the coordinate from the table QLineEdit cells.
|
||||
Args:
|
||||
row(int): Row of the table.
|
||||
column(int): Column of the table.
|
||||
Returns:
|
||||
float: Value of the coordinate.
|
||||
"""
|
||||
edit = self.ui.table.cellWidget(row, column)
|
||||
value = float(edit.text()) if edit and edit.text() != "" else None
|
||||
if value:
|
||||
return value
|
||||
|
||||
def delete_selected_row(self):
|
||||
"""Delete the selected row from the table."""
|
||||
selected_rows = self.ui.table.selectionModel().selectedRows()
|
||||
for row in selected_rows:
|
||||
self.ui.table.removeRow(row.row())
|
||||
if self.ui.comboBox_mode.currentText() == "Start/Stop":
|
||||
self.emit_plot_coordinates(x_pos=4, y_pos=5, reference_tag="Start", color="blue")
|
||||
self.emit_plot_coordinates(x_pos=6, y_pos=7, reference_tag="Stop", color="red")
|
||||
self.is_next_entry_end = False
|
||||
elif self.ui.comboBox_mode.currentText() == "Individual":
|
||||
self.emit_plot_coordinates(x_pos=3, y_pos=4, reference_tag="Individual", color="green")
|
||||
|
||||
def resize_table_auto(self):
|
||||
"""Resize the table to fit the contents."""
|
||||
if self.ui.checkBox_resize_auto.isChecked():
|
||||
self.ui.table.resizeColumnsToContents()
|
||||
|
||||
def move_motor(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Move the motor to the target coordinates.
|
||||
Args:
|
||||
x(float): Target x coordinate.
|
||||
y(float): Target y coordinate.
|
||||
"""
|
||||
self.motor_thread.move_absolute(self.motor_x, self.motor_y, (x, y))
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def change_motors(self, motor_x: str, motor_y: str) -> None:
|
||||
"""
|
||||
Change the active motors and update config.
|
||||
Can be connected to the selected_motors_signal from MotorControlSelection.
|
||||
Args:
|
||||
motor_x(str): New motor X to be controlled.
|
||||
motor_y(str): New motor Y to be controlled.
|
||||
"""
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.config["motor_control"]["motor_x"] = motor_x
|
||||
self.config["motor_control"]["motor_y"] = motor_y
|
||||
|
||||
@pyqtSlot(int)
|
||||
def set_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Set the precision of the coordinates.
|
||||
Args:
|
||||
precision(int): Precision of the coordinates.
|
||||
"""
|
||||
self.precision = precision
|
||||
self.config["motor_control"]["precision"] = precision
|
||||
@@ -1,113 +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>676</width>
|
||||
<height>667</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Coordinates Table</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<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="QCheckBox" name="checkBox_resize_auto">
|
||||
<property name="text">
|
||||
<string>Resize Auto</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_editColumns">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Edit Custom Column</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Entries Mode:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_mode">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<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="table">
|
||||
<property name="gridStyle">
|
||||
<enum>Qt::SolidLine</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_importCSV">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_exportCSV">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Export CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,159 +0,0 @@
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlErrors, MotorControlWidget
|
||||
|
||||
|
||||
class MotorControlAbsolute(MotorControlWidget):
|
||||
"""
|
||||
Widget for controlling the motors to absolute coordinates.
|
||||
|
||||
Signals:
|
||||
coordinates_signal (pyqtSignal(tuple)): Signal to emit the coordinates.
|
||||
Slots:
|
||||
change_motors (pyqtSlot): Slot to change the active motors.
|
||||
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls.
|
||||
"""
|
||||
|
||||
coordinates_signal = pyqtSignal(tuple)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_absolute.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI."""
|
||||
|
||||
# Check if there are any motors connected
|
||||
if self.motor_x is None or self.motor_y is None:
|
||||
self.ui.motorControl_absolute.setEnabled(False)
|
||||
return
|
||||
|
||||
# Move to absolute coordinates
|
||||
self.ui.pushButton_go_absolute.clicked.connect(
|
||||
lambda: self.move_motor_absolute(
|
||||
self.ui.spinBox_absolute_x.value(), self.ui.spinBox_absolute_y.value()
|
||||
)
|
||||
)
|
||||
|
||||
self.ui.pushButton_set.clicked.connect(self.save_absolute_coordinates)
|
||||
self.ui.pushButton_save.clicked.connect(self.save_current_coordinates)
|
||||
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
|
||||
|
||||
# Enable/Disable GUI
|
||||
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
||||
|
||||
# Error messages
|
||||
self.motor_thread.motor_error.connect(
|
||||
lambda error: MotorControlErrors.display_error_message(error)
|
||||
)
|
||||
|
||||
# Keyboard shortcuts
|
||||
self._init_keyboard_shortcuts()
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""Update config dict"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
# Update step precision
|
||||
self.precision = self.config["motor_control"]["precision"]
|
||||
|
||||
self._init_ui()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def enable_motor_controls(self, enable: bool) -> None:
|
||||
"""
|
||||
Enable or disable the motor controls.
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
|
||||
# Disable or enable all controls within the motorControl_absolute group box
|
||||
for widget in self.ui.motorControl_absolute.findChildren(QWidget):
|
||||
widget.setEnabled(enable)
|
||||
|
||||
# Enable the pushButton_stop if the motor is moving
|
||||
self.ui.pushButton_stop.setEnabled(True)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def change_motors(self, motor_x: str, motor_y: str):
|
||||
"""
|
||||
Change the active motors and update config.
|
||||
Can be connected to the selected_motors_signal from MotorControlSelection.
|
||||
Args:
|
||||
motor_x(str): New motor X to be controlled.
|
||||
motor_y(str): New motor Y to be controlled.
|
||||
"""
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.config["motor_control"]["motor_x"] = motor_x
|
||||
self.config["motor_control"]["motor_y"] = motor_y
|
||||
|
||||
@pyqtSlot(int)
|
||||
def set_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Set the precision of the coordinates.
|
||||
Args:
|
||||
precision(int): Precision of the coordinates.
|
||||
"""
|
||||
self.precision = precision
|
||||
self.config["motor_control"]["precision"] = precision
|
||||
self.ui.spinBox_absolute_x.setDecimals(precision)
|
||||
self.ui.spinBox_absolute_y.setDecimals(precision)
|
||||
|
||||
def move_motor_absolute(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Move the motor to the target coordinates.
|
||||
Args:
|
||||
x(float): Target x coordinate.
|
||||
y(float): Target y coordinate.
|
||||
"""
|
||||
# self._enable_motor_controls(False)
|
||||
target_coordinates = (x, y)
|
||||
self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
|
||||
if self.ui.checkBox_save_with_go.isChecked():
|
||||
self.save_absolute_coordinates()
|
||||
|
||||
def _init_keyboard_shortcuts(self):
|
||||
"""Initialize the keyboard shortcuts."""
|
||||
# Go absolute button
|
||||
self.ui.pushButton_go_absolute.setShortcut("Ctrl+G")
|
||||
self.ui.pushButton_go_absolute.setToolTip("Ctrl+G")
|
||||
|
||||
# Set absolute coordinates
|
||||
self.ui.pushButton_set.setShortcut("Ctrl+D")
|
||||
self.ui.pushButton_set.setToolTip("Ctrl+D")
|
||||
|
||||
# Save Current coordinates
|
||||
self.ui.pushButton_save.setShortcut("Ctrl+S")
|
||||
self.ui.pushButton_save.setToolTip("Ctrl+S")
|
||||
|
||||
# Stop Button
|
||||
self.ui.pushButton_stop.setShortcut("Ctrl+X")
|
||||
self.ui.pushButton_stop.setToolTip("Ctrl+X")
|
||||
|
||||
def save_absolute_coordinates(self):
|
||||
"""Emit the setup coordinates from the spinboxes"""
|
||||
|
||||
x, y = round(self.ui.spinBox_absolute_x.value(), self.precision), round(
|
||||
self.ui.spinBox_absolute_y.value(), self.precision
|
||||
)
|
||||
self.coordinates_signal.emit((x, y))
|
||||
|
||||
def save_current_coordinates(self):
|
||||
"""Emit the current coordinates from the motor thread"""
|
||||
x, y = self.motor_thread.get_coordinates(self.motor_x, self.motor_y)
|
||||
self.coordinates_signal.emit((round(x, self.precision), round(y, self.precision)))
|
||||
@@ -1,149 +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>285</width>
|
||||
<height>220</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Move Movement Absolute</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl_absolute">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>195</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>195</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Move Movement 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>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,230 +0,0 @@
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtGui import QKeySequence
|
||||
from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
|
||||
|
||||
|
||||
class MotorControlRelative(MotorControlWidget):
|
||||
"""
|
||||
Widget for controlling the motors to relative coordinates.
|
||||
|
||||
Signals:
|
||||
precision_signal (pyqtSignal): Signal to emit the precision of the coordinates.
|
||||
Slots:
|
||||
change_motors (pyqtSlot(str,str)): Slot to change the active motors.
|
||||
enable_motor_controls (pyqtSlot): Slot to enable/disable the motor controls.
|
||||
"""
|
||||
|
||||
precision_signal = pyqtSignal(int)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
# Loading UI
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_relative.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI."""
|
||||
self._init_ui_motor_control()
|
||||
self._init_keyboard_shortcuts()
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""
|
||||
Update config dict
|
||||
Args:
|
||||
config(dict): New config dict
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
# Update step precision
|
||||
self.precision = self.config["motor_control"]["precision"]
|
||||
self.ui.spinBox_precision.setValue(self.precision)
|
||||
|
||||
# Update step sizes
|
||||
self.ui.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"])
|
||||
self.ui.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"])
|
||||
|
||||
# Checkboxes for keyboard shortcuts and x/y step size link
|
||||
self.ui.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
|
||||
self.ui.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui_motor_control(self) -> None:
|
||||
"""Initialize the motor control elements"""
|
||||
|
||||
# Connect checkbox and spinBoxes
|
||||
self.ui.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes)
|
||||
self.ui.spinBox_step_x.valueChanged.connect(self._update_step_size_x)
|
||||
self.ui.spinBox_step_y.valueChanged.connect(self._update_step_size_y)
|
||||
|
||||
self.ui.toolButton_right.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_x, "x", 1)
|
||||
)
|
||||
self.ui.toolButton_left.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_x, "x", -1)
|
||||
)
|
||||
self.ui.toolButton_up.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_y, "y", 1)
|
||||
)
|
||||
self.ui.toolButton_down.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_y, "y", -1)
|
||||
)
|
||||
|
||||
# Switch between key shortcuts active
|
||||
self.ui.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts)
|
||||
self._update_arrow_key_shortcuts()
|
||||
|
||||
# Enable/Disable GUI
|
||||
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
||||
|
||||
# Precision update
|
||||
self.ui.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
|
||||
|
||||
# Error messages
|
||||
self.motor_thread.motor_error.connect(
|
||||
lambda error: MotorControlErrors.display_error_message(error)
|
||||
)
|
||||
|
||||
# Stop Button
|
||||
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
|
||||
|
||||
def _init_keyboard_shortcuts(self) -> None:
|
||||
"""Initialize the keyboard shortcuts"""
|
||||
|
||||
# Increase/decrease step size for X motor
|
||||
increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
|
||||
decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
|
||||
increase_x_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_x, 2)
|
||||
)
|
||||
decrease_x_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_x, 0.5)
|
||||
)
|
||||
self.ui.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z")
|
||||
|
||||
# Increase/decrease step size for Y motor
|
||||
increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
|
||||
decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
|
||||
increase_y_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_y, 2)
|
||||
)
|
||||
decrease_y_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_y, 0.5)
|
||||
)
|
||||
self.ui.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z")
|
||||
|
||||
# Stop Button
|
||||
self.ui.pushButton_stop.setShortcut("Ctrl+X")
|
||||
self.ui.pushButton_stop.setToolTip("Ctrl+X")
|
||||
|
||||
def _update_arrow_key_shortcuts(self) -> None:
|
||||
"""Update the arrow key shortcuts based on the checkbox state."""
|
||||
if self.ui.checkBox_enableArrows.isChecked():
|
||||
# Set the arrow key shortcuts for motor movement
|
||||
self.ui.toolButton_right.setShortcut(Qt.Key_Right)
|
||||
self.ui.toolButton_left.setShortcut(Qt.Key_Left)
|
||||
self.ui.toolButton_up.setShortcut(Qt.Key_Up)
|
||||
self.ui.toolButton_down.setShortcut(Qt.Key_Down)
|
||||
else:
|
||||
# Clear the shortcuts
|
||||
self.ui.toolButton_right.setShortcut("")
|
||||
self.ui.toolButton_left.setShortcut("")
|
||||
self.ui.toolButton_up.setShortcut("")
|
||||
self.ui.toolButton_down.setShortcut("")
|
||||
|
||||
def _update_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Update the precision of the coordinates.
|
||||
Args:
|
||||
precision(int): Precision of the coordinates.
|
||||
"""
|
||||
self.ui.spinBox_step_x.setDecimals(precision)
|
||||
self.ui.spinBox_step_y.setDecimals(precision)
|
||||
self.precision_signal.emit(precision)
|
||||
|
||||
def _change_step_size(self, spinBox: QDoubleSpinBox, factor: float) -> None:
|
||||
"""
|
||||
Change the step size of the spinbox.
|
||||
Args:
|
||||
spinBox(QDoubleSpinBox): Spinbox to change the step size.
|
||||
factor(float): Factor to change the step size.
|
||||
"""
|
||||
old_step = spinBox.value()
|
||||
new_step = old_step * factor
|
||||
spinBox.setValue(new_step)
|
||||
|
||||
def _sync_step_sizes(self):
|
||||
"""Sync step sizes based on checkbox state."""
|
||||
if self.ui.checkBox_same_xy.isChecked():
|
||||
value = self.ui.spinBox_step_x.value()
|
||||
self.ui.spinBox_step_y.setValue(value)
|
||||
|
||||
def _update_step_size_x(self):
|
||||
"""Update step size for x if checkbox is checked."""
|
||||
if self.ui.checkBox_same_xy.isChecked():
|
||||
value = self.ui.spinBox_step_x.value()
|
||||
self.ui.spinBox_step_y.setValue(value)
|
||||
|
||||
def _update_step_size_y(self):
|
||||
"""Update step size for y if checkbox is checked."""
|
||||
if self.ui.checkBox_same_xy.isChecked():
|
||||
value = self.ui.spinBox_step_y.value()
|
||||
self.ui.spinBox_step_x.setValue(value)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def change_motors(self, motor_x: str, motor_y: str):
|
||||
"""
|
||||
Change the active motors and update config.
|
||||
Can be connected to the selected_motors_signal from MotorControlSelection.
|
||||
Args:
|
||||
motor_x(str): New motor X to be controlled.
|
||||
motor_y(str): New motor Y to be controlled.
|
||||
"""
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.config["motor_control"]["motor_x"] = motor_x
|
||||
self.config["motor_control"]["motor_y"] = motor_y
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def enable_motor_controls(self, disable: bool) -> None:
|
||||
"""
|
||||
Enable or disable the motor controls.
|
||||
Args:
|
||||
disable(bool): True to disable, False to enable.
|
||||
"""
|
||||
|
||||
# Disable or enable all controls within the motorControl_absolute group box
|
||||
for widget in self.ui.motorControl.findChildren(QWidget):
|
||||
widget.setEnabled(disable)
|
||||
|
||||
# Enable the pushButton_stop if the motor is moving
|
||||
self.ui.pushButton_stop.setEnabled(True)
|
||||
|
||||
def move_motor_relative(self, motor, axis: str, direction: int) -> None:
|
||||
"""
|
||||
Move the motor relative to the current position.
|
||||
Args:
|
||||
motor: Motor to move.
|
||||
axis(str): Axis to move.
|
||||
direction(int): Direction to move. 1 for positive, -1 for negative.
|
||||
"""
|
||||
if axis == "x":
|
||||
step = direction * self.ui.spinBox_step_x.value()
|
||||
elif axis == "y":
|
||||
step = direction * self.ui.spinBox_step_y.value()
|
||||
self.motor_thread.move_relative(motor, step)
|
||||
@@ -1,298 +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>285</width>
|
||||
<height>405</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>405</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Control Relative</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>394</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Control 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>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_stop">
|
||||
<property name="text">
|
||||
<string>Stop Movement</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,110 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
|
||||
|
||||
|
||||
class MotorControlSelection(MotorControlWidget):
|
||||
"""
|
||||
Widget for selecting the motors to control.
|
||||
|
||||
Signals:
|
||||
selected_motors_signal (pyqtSignal(str,str)): Signal to emit the selected motors.
|
||||
Slots:
|
||||
get_available_motors (pyqtSlot): Slot to populate the available motors in the combo boxes and set the index based on the configuration.
|
||||
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls GUI.
|
||||
on_config_update (pyqtSlot(dict)): Slot to update the config dict.
|
||||
"""
|
||||
|
||||
selected_motors_signal = pyqtSignal(str, str)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
current_path = os.path.dirname(__file__)
|
||||
uic.loadUi(os.path.join(current_path, "selection.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI."""
|
||||
# Lock GUI while motors are moving
|
||||
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
||||
|
||||
self.pushButton_connecMotors.clicked.connect(self.select_motor)
|
||||
self.get_available_motors()
|
||||
|
||||
# Connect change signals to change color
|
||||
self.comboBox_motor_x.currentIndexChanged.connect(
|
||||
lambda: self.set_combobox_style(self.comboBox_motor_x, "#ffa700")
|
||||
)
|
||||
self.comboBox_motor_y.currentIndexChanged.connect(
|
||||
lambda: self.set_combobox_style(self.comboBox_motor_y, "#ffa700")
|
||||
)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""
|
||||
Update config dict
|
||||
Args:
|
||||
config(dict): New config dict
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def enable_motor_controls(self, enable: bool) -> None:
|
||||
"""
|
||||
Enable or disable the motor controls.
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
self.motorSelection.setEnabled(enable)
|
||||
|
||||
@pyqtSlot()
|
||||
def get_available_motors(self) -> None:
|
||||
"""
|
||||
Slot to populate the available motors in the combo boxes and set the index based on the configuration.
|
||||
"""
|
||||
# Get all available motors
|
||||
self.motor_list = self.motor_thread.get_all_motors_names()
|
||||
|
||||
# Populate the combo boxes
|
||||
self.comboBox_motor_x.addItems(self.motor_list)
|
||||
self.comboBox_motor_y.addItems(self.motor_list)
|
||||
|
||||
# Set the index based on the config if provided
|
||||
if self.config:
|
||||
index_x = self.comboBox_motor_x.findText(self.motor_x)
|
||||
index_y = self.comboBox_motor_y.findText(self.motor_y)
|
||||
self.comboBox_motor_x.setCurrentIndex(index_x if index_x != -1 else 0)
|
||||
self.comboBox_motor_y.setCurrentIndex(index_y if index_y != -1 else 0)
|
||||
|
||||
def set_combobox_style(self, combobox, color: str) -> None:
|
||||
"""
|
||||
Set the combobox style to a specific color.
|
||||
Args:
|
||||
combobox(QComboBox): Combobox to change the color.
|
||||
color(str): Color to set the combobox to.
|
||||
"""
|
||||
combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
|
||||
|
||||
def select_motor(self):
|
||||
"""Emit the selected motors"""
|
||||
motor_x = self.comboBox_motor_x.currentText()
|
||||
motor_y = self.comboBox_motor_y.currentText()
|
||||
|
||||
# Reset the combobox color to normal after selection
|
||||
self.set_combobox_style(self.comboBox_motor_x, "")
|
||||
self.set_combobox_style(self.comboBox_motor_y, "")
|
||||
|
||||
self.selected_motors_signal.emit(motor_x, motor_y)
|
||||
@@ -1,69 +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>285</width>
|
||||
<height>156</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>156</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Control Selection</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<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="1" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Motor X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_x"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Motor Y</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>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_y"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -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 |
@@ -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 |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user