Compare commits
266 Commits
feat/ivans
...
v0.99.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50dbef52c0 | ||
| bb385f07ca | |||
| cf28730515 | |||
|
|
13ae383455 | ||
| 2265458dcc | |||
|
|
0a59f08fcc | ||
| c70724a456 | |||
| 406c263746 | |||
| df35aabff3 | |||
| cc8c166b5c | |||
| c4f3308dc0 | |||
|
|
8f3824c0e7 | ||
| afdf4e8782 | |||
| 2a82032644 | |||
| 88a2f66758 | |||
| 3f3b207295 | |||
| 44cfda1c07 | |||
| e42b84c636 | |||
| 77c5aa741c | |||
| 2b4449afeb | |||
| 36ad464159 | |||
|
|
e8ae6f2e43 | ||
| 3ecbd60627 | |||
| 82a55ddf3e | |||
|
|
7d190719b1 | ||
| 8c2e7c8259 | |||
| dd7c71bb1e | |||
|
|
7b5b7a8cbb | ||
| af28574bd5 | |||
| 617db36ed4 | |||
|
|
ebc2e44c7c | ||
| 44738057a3 | |||
| f98a9f9771 | |||
| 2fe72c9ccb | |||
| f0203d9bf6 | |||
| 37835cbf76 | |||
|
|
e005be33d1 | ||
| 9d7718c3d9 | |||
| 9d8fb0b761 | |||
|
|
9df1e0899b | ||
| 640464a654 | |||
| 84abe46050 | |||
| 1d2afaa09e | |||
| 2bf5c7096e | |||
|
|
41dc6e6cfd | ||
| 650039303a | |||
| 196504b533 | |||
| 2c31cc90ae | |||
| e870e5ba08 | |||
| 73f5a2f085 | |||
| 4790afde3d | |||
| 7357f3d2a1 | |||
| e9ecd268c6 | |||
| 91ba30e8d0 | |||
|
|
d36d801ef1 | ||
| 939f834a26 | |||
|
|
bee51bd86e | ||
| bc2abe945f | |||
|
|
49a5a23d41 | ||
| 4f96d0e4a1 | |||
| ea9240d2f7 | |||
| 4d02b42f11 | |||
|
|
9509be14be | ||
| 198c1d1064 | |||
| 2af5c94913 | |||
|
|
a4a0bac3c1 | ||
| f285b35b49 | |||
| 7aeb2b5c26 | |||
| d56ea95ef9 | |||
|
|
5733fea98c | ||
| 98b79aac7b | |||
|
|
4212fe0e32 | ||
| 93d397759c | |||
|
|
8c5b901a37 | ||
| 0273bf4856 | |||
| c80a7cd108 | |||
|
|
a50d9c7b3f | ||
| 281633deff | |||
| 0d190c5c59 | |||
| 6269009e54 | |||
| 6d2442d23c | |||
|
|
110b27351b | ||
| 37aa371e7c | |||
| eb54e9f788 | |||
|
|
482efeb340 | ||
| 99ee545e41 | |||
| cf94599c25 | |||
| b50b3a27e6 | |||
| bf6294ecbf | |||
| a3d4f5ac4b | |||
| bc264975b1 | |||
| ad07bbf85e | |||
| 9856857f4c | |||
| f9e5897900 | |||
|
|
39fb22b716 | ||
| a372925fff | |||
|
|
ec54440569 | ||
| af86860bf3 | |||
| 302ae90139 | |||
|
|
1405068925 | ||
| 5aad401ef8 | |||
|
|
885dcfda89 | ||
| 30fef929cf | |||
| 1f30dd73a9 | |||
| 73cd11e472 | |||
| 7616ca0e14 | |||
|
|
ca29a69779 | ||
| dcc5fd71ee | |||
|
|
fee4901657 | ||
| 71873ddf35 | |||
|
|
f8552ca551 | ||
| 995a795060 | |||
|
|
7ab81c5797 | ||
| bc1e23944c | |||
| a3fe20500a | |||
| 61a4e32deb | |||
| 3d681f77e1 | |||
| 5a9ccfd1f6 | |||
| fc57b7a126 | |||
| 06205e0790 | |||
| 4be6fd6b83 | |||
| 714e1e139e | |||
|
|
01c0e0b1df | ||
| 4457ef2147 | |||
| 8ca60d54b3 | |||
| 5696c993dc | |||
| 1206e15309 | |||
|
|
4f2b51b211 | ||
| 1b9c55a46a | |||
| f4844d2e06 | |||
| 06fab0eab9 | |||
| a16b87ac28 | |||
| cce1367a72 | |||
| 28f26e92a4 | |||
|
|
2f5cc3030d | ||
| 1cf6e32303 | |||
| 7f49893d2c | |||
| ba0d1ea903 | |||
| 70fb276fdf | |||
| 43711680ba | |||
| 3d2ca4855c | |||
| fe7e542b19 | |||
| 501eb923f1 | |||
| c15035b6b7 | |||
| 6a9317facd | |||
|
|
95e515114a | ||
| fd6ae91993 | |||
| 3798714369 | |||
|
|
067496b18c | ||
| ad112d1f08 | |||
| a3dff7decc | |||
| 2bcaa4256d | |||
|
|
006f7c01fd | ||
| 2c8764a27d | |||
| 50135b5fe9 | |||
| 8d764e2d46 | |||
| 6eb313fa76 | |||
| 1f8ef52b60 | |||
| 2be009c647 | |||
| 8df6b003e5 | |||
| 7089cf356a | |||
| 1e551d6e96 | |||
| 8e588d79c8 | |||
| 8d93405399 | |||
| 6ff6111091 | |||
| a8b6ef20cc | |||
| a8ff1d4cd0 | |||
| 47fcb9ebfe | |||
| e8305652fd | |||
| 33495cfe03 | |||
| 8ac35d7280 | |||
| c926a75a79 | |||
| fa9b17191d | |||
| 755b394c1c | |||
|
|
717df63d62 | ||
| d75c55b2b1 | |||
| e52ee2604c | |||
| c7feb6952d | |||
| d64758f268 | |||
| 6202d224fe | |||
|
|
4ac90e1e56 | ||
| 8f104cf402 | |||
| 787f74949b | |||
| 196ef7afe1 | |||
|
|
18ac3ffac0 | ||
| ba69e7957c | |||
|
|
5acb47532c | ||
| b5b0aa4f82 | |||
|
|
9a91583ed0 | ||
| b98fd00ade | |||
|
|
d5c5e12589 | ||
| e495fd30c4 | |||
| 8516a1d639 | |||
| 006992e43c | |||
| 48911e9348 | |||
| e4e1a905d1 | |||
| fc935d9fc8 | |||
| 0c6a9f2310 | |||
| d23fd8bd07 | |||
| 9d6ae87d0f | |||
| fc5a8bdd8b | |||
| b8717f1327 | |||
| 0aa317aae5 | |||
|
|
edc19bdff8 | ||
| 11a7204c98 | |||
| eab7883979 | |||
| 2d4249e73a | |||
| 63db1352ee | |||
| 8308115f36 | |||
|
|
02fce838db | ||
| 360d171355 | |||
| eb26e2a11b | |||
| 2b29e34b52 | |||
| fd8766ed87 | |||
| 5de8804da1 | |||
| 2988fd387e | |||
| 1b017edfad | |||
| 903ce7d46b | |||
| 41bcb80167 | |||
|
|
d3f85060ca | ||
| 90178e2f61 | |||
|
|
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 |
@@ -43,13 +43,20 @@ stages:
|
||||
- export QTWEBENGINE_DISABLE_SANDBOX=1
|
||||
|
||||
.clone-repos: &clone-repos
|
||||
- echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
|
||||
.install-repos: &install-repos
|
||||
- pip install -e ./ophyd_devices
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e ./bec/bec_ipython_client
|
||||
|
||||
.install-os-packages: &install-os-packages
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
- *install-qt-webengine-deps
|
||||
|
||||
before_script:
|
||||
@@ -131,10 +138,9 @@ tests:
|
||||
script:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e ./bec/bec_ipython_client
|
||||
- *install-repos
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --maxfail=2 --random-order --full-trace ./tests/unit_tests
|
||||
- coverage report
|
||||
- coverage xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
@@ -144,6 +150,9 @@ tests:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
paths:
|
||||
- tests/reference_failures/
|
||||
when: always
|
||||
|
||||
test-matrix:
|
||||
parallel:
|
||||
@@ -154,7 +163,6 @@ test-matrix:
|
||||
- "3.12"
|
||||
QT_PCKG:
|
||||
- "pyside6"
|
||||
- "pyqt5"
|
||||
- "pyqt6"
|
||||
|
||||
stage: AdditionalTests
|
||||
@@ -167,11 +175,9 @@ test-matrix:
|
||||
script:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e ./bec/bec_ipython_client
|
||||
- *install-repos
|
||||
- pip install -e .[dev,$QT_PCKG]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
- pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
|
||||
end-2-end-conda:
|
||||
stage: End2End
|
||||
@@ -193,10 +199,9 @@ end-2-end-conda:
|
||||
|
||||
- cd ./bec
|
||||
- source ./bin/install_bec_dev.sh -t
|
||||
|
||||
- pip install -e ./bec_lib[dev]
|
||||
- pip install -e ./bec_ipython_client[dev]
|
||||
- cd ../
|
||||
- pip install -e ./ophyd_devices
|
||||
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- cd ./tests/end-2-end
|
||||
- pytest -v --start-servers --flush-redis --random-order
|
||||
|
||||
@@ -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
|
||||
|
||||
180
CHANGELOG.md
@@ -1,155 +1,161 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.77.0 (2024-07-02)
|
||||
## v0.99.2 (2024-08-27)
|
||||
|
||||
### Feature
|
||||
### Ci
|
||||
|
||||
* feat(bec_connector): export config to yaml ([`a391f30`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a391f3018c50fee6a4a06884491b957df80c3cd3))
|
||||
|
||||
* feat(utils): colors added convertor for rgba to hex ([`572f2fb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/572f2fb8110d5cb0e80f3ca45ce57ef405572456))
|
||||
* ci: additional tests are not allowed to fail ([`bb385f0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bb385f07ca18904461a541b5cadde05398c84438))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(waveform): scatter 2D brush error ([`215d59c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/215d59c8bfe7fda9aff8cec8353bef9e1ce2eca1))
|
||||
* fix(widgets): fixed default theme for widgets
|
||||
|
||||
* fix(figure): API cleanup ([`008a33a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/008a33a9b192473cc58e90cd6d98c5bcb5f7b8c0))
|
||||
If not theme is set, the init of the BECWidget base class sets the default theme to "dark" ([`cf28730`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf28730515e3c2d5914e0205768734c578711e5c))
|
||||
|
||||
* 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))
|
||||
|
||||
* fix(bec_figure): full reconstruction with config from other bec figure ([`b6e1e20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b6e1e20b7c8549bb092e981062329e601411dda6))
|
||||
|
||||
* fix(motor_map): API changes updates current visualisation; motor_map can be initialised from config ([`2e2d422`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2e2d422910685a2527a3d961a468c787f771ca44))
|
||||
|
||||
* fix(image): image add_custom_image fixed, closes #225 ([`f0556e4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0556e44113ffee66cf735aa2dd758c62cb634f4))
|
||||
|
||||
* fix(figure): subplot methods consolidated; added subplot factory ([`4a97105`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4a97105e4bd2ce77d72dfe5f8307dd9ee65b21b0))
|
||||
|
||||
* fix(image): image can be fully reconstructed from config ([`797f73c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/797f73c39aa73e07d6311f3de4baea53f6c380e0))
|
||||
|
||||
* fix(image_item): vrange added int for pydantic model check ([`b8f796f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8f796fd3fcc15641e8fc6a3ca75c344ce90fc45))
|
||||
|
||||
* fix(bec_figure): waveforms can be initialised from the config; widgets are deleteLater after removal ([`78673ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/78673ea11a47aad878128197ae6213925228ed59))
|
||||
|
||||
### Unknown
|
||||
|
||||
* Resolve "add VT100 console executing BEC as a widget" ([`c6a14c0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c6a14c0768a90695567a83a7895247ed0c64f3ce))
|
||||
|
||||
## v0.76.1 (2024-06-29)
|
||||
## v0.99.1 (2024-08-27)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(plugins): fixes and tests for auto-gen plugins ([`c42511d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c42511dd44cc13577e108a6cef3166376e594f54))
|
||||
* fix(crosshair): emit all crosshair events, not just line coordinates ([`2265458`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2265458dcc57970db18c62619f5877d542d72e81))
|
||||
|
||||
## v0.76.0 (2024-06-28)
|
||||
## v0.99.0 (2024-08-25)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(darkmodebutton): added dark mode button docs ([`406c263`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/406c263746f0e809c1a4d98356c48f40428c23d7))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(designer): added support for creating designer plugins automatically ([`c1dd0ee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c1dd0ee1906dba1f2e2ae9ce40a84d55c26a1cce))
|
||||
* feat(darkmodebutton): added button to toggle between dark and light mode ([`cc8c166`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cc8c166b5c1d37e0f64c83801b2347a54a6550b6))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: fixed qwidget inheritance for ring progress bar ([`0610d2f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0610d2f9f027f8659e7149f2dfbb316ff30e337d))
|
||||
|
||||
### Unknown
|
||||
|
||||
* fix:parent set as first kwarg TextBox and WebsiteWidget ([`a45c407`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a45c4075684b93bfdcee03e5a416b84f61d3bc6f))
|
||||
|
||||
## v0.75.0 (2024-06-26)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(widgets): added simple bec queue widget ([`3faee98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3faee98ec80041a27e4c1f1156178de6f9dcdc63))
|
||||
* fix(toggle): emit state change ([`c4f3308`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c4f3308dc0c3e4b2064760ccd7372d71b3e49f96))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(dispatcher): cleanup ([`ca02132`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca02132c8d18535b37e9192e00459d2aca6ba5cf))
|
||||
* refactor(darkmodebutton): renamed set_dark_mode_enabled to toggle_dark_mode ([`c70724a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c70724a456900bcb06b040407a2c5d497e49ce77))
|
||||
|
||||
## v0.74.1 (2024-06-26)
|
||||
### Test
|
||||
|
||||
### Build
|
||||
* test(dark_mode_button): added tests for dark mode button ([`df35aab`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df35aabff30c5d00b1c441132bd370446653741e))
|
||||
|
||||
* build: added missing pytest-bec-e2e dependency; closes #219 ([`56fdae4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56fdae42757bdb9fa301c1e425a77e98b6eaf92b))
|
||||
## v0.98.0 (2024-08-25)
|
||||
|
||||
* build: fixed dependency ranges; closes #135 ([`e6a06c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6a06c9f43e0ad6bbfcfa550a2f580d2a27aff66))
|
||||
### Feature
|
||||
|
||||
### Chore
|
||||
* feat(themes): added set_theme method ([`2b4449a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2b4449afebdda0a97f95712a1353cf40ec55c283))
|
||||
|
||||
* chore: sorted dependencies alphabetically ([`21c807f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/21c807f35831fdd1ef2e488ab90edae4719f0cb7))
|
||||
### Fix
|
||||
|
||||
* fix(toolbar): removed hardcoded color values ([`afdf4e8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/afdf4e8782a22566932180224fa1c924d24c810f))
|
||||
|
||||
* fix: transitioning to material icons ([`2a82032`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2a82032644a84e38df04e2035a6aa63f4a046360))
|
||||
|
||||
* fix(dock_area): transitioned to MaterialIconAction ([`88a2f66`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/88a2f667588e9aeb34ae556fa327898824052bc3))
|
||||
|
||||
* fix: fix color palette if qtheme was not called ([`3f3b207`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3f3b207295ebd406ebaeecee465c774965161b8b))
|
||||
|
||||
* fix(figure): removed theme from figure init ([`e42b84c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e42b84c63650297d67feffccc02a2c2ba111ca79))
|
||||
|
||||
* fix: use globally set theme instead of the internal bec widgets theme ([`77c5aa7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77c5aa741cf1f5b969a42aa878aa2965176dbf41))
|
||||
|
||||
* fix(waveform): fixed icon appearance ([`36ad464`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/36ad4641594b67c9b789515c28f7db78a12757ee))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(waveform): use set theme for demo ([`44cfda1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/44cfda1c07306669c9a4e09706d95e6b91dee370))
|
||||
|
||||
## v0.97.0 (2024-08-23)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(designer): added designer icon factory ([`82a55dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/82a55ddf3eafb589cb63408db1c0e7e5c9d629da))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(toolbar icon): fixed material icon toolbar for theme changes ([`3ecbd60`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3ecbd60627994417c9175364e5909710dbcdceb2))
|
||||
|
||||
## v0.96.3 (2024-08-23)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: fixed doc string ([`f979a63`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f979a63d3d1a008f80e500510909750878ff4303))
|
||||
* docs(dispatcher): docs added ([`dd7c71b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dd7c71bb1e0b7ef5398b1e1a05fc1147c772420a))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(rings): rings properties updated right after setting ([`c8b7367`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c8b7367815b095f8e4aa8b819481efb701f2e542))
|
||||
* fix: minor fixes for type annotations ([`8c2e7c8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8c2e7c82592ace50e4e1f47e392a0ddc988f57ae))
|
||||
|
||||
* fix(motor_map): motor map can be removed from BECFigure with .remove() ([`6b25abf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b25abff70280271e2eeb70450553c05d4b7c99c))
|
||||
## v0.96.2 (2024-08-22)
|
||||
|
||||
### Test
|
||||
### Fix
|
||||
|
||||
* test(bec_figure): tests for removing widgets with rpc e2e ([`a268caa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a268caaa30711fcc7ece542d24578d74cbf65c77))
|
||||
* fix(waveform): validation of custom curves removed ([`af28574`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af28574bd58457a05f1269f121db01ad627b5769))
|
||||
|
||||
## v0.74.0 (2024-06-25)
|
||||
* fix(waveform): skip validation for curves that are not BECCurve instances ([`617db36`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/617db36ed4932c8a0633724079b695bc67d5c77b))
|
||||
|
||||
## v0.96.1 (2024-08-22)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: fail pytest after 2 failed tests ([`f0203d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0203d9bf60c4975ba5ab93a057d9091762454d5))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(crosshair): update markers if necessary ([`4473805`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/44738057a36f5de2bbb55affdd309f92286d4a0f))
|
||||
|
||||
* fix(waveform_widget): fixed icon appearance ([`f98a9f9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f98a9f9771b93226d47830aa52f45739624f51b4))
|
||||
|
||||
* fix: bubble-up signals ([`2fe72c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2fe72c9ccb71bcb196a1b78197b73acf9aa3f506))
|
||||
|
||||
* fix(crosshair): fixed crosshair for image and waveforms ([`37835cb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37835cbf76ca3ba1081f514ee7793244ac500e7f))
|
||||
|
||||
## v0.96.0 (2024-08-22)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(becfigure): docs added ([`a51b15d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a51b15da3f5e83e0c897a0342bdb05b9c677a179))
|
||||
* docs(scan_control): added designer options ([`9d7718c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d7718c3d9badf14150174410b9958a3134a1e23))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(waveform1d): dap LMFit model can be added to plot ([`1866ba6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1866ba66c8e3526661beb13fff3e13af6a0ae562))
|
||||
* feat(scan_control): added the ability to configure the scan control widget from designer ([`9d8fb0b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d8fb0b761efa92972399bcd9aea28e956074380))
|
||||
|
||||
### Test
|
||||
## v0.95.1 (2024-08-22)
|
||||
|
||||
* test(waveform1d): dap e2e test added ([`7271b42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7271b422f98ef9264970d708811c414b69a644db))
|
||||
### Documentation
|
||||
|
||||
## v0.73.2 (2024-06-25)
|
||||
* docs: links section added ([`2bf5c70`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2bf5c7096e7d822713e1b50bde89f072e6356e17))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(vscode): only run terminate if the process is still alive ([`7120f3e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7120f3e93b054b788f15e2d5bcd688e3c140c1ce))
|
||||
* fix(docs): changed link to scan gui config in main docs ([`640464a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/640464a6543b2111bdb58d0174f2ce86c5836cbe))
|
||||
|
||||
* fix(rpc): trigger shutdown of server when gui is terminated ([`acc1318`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/acc13183e28030e3ca9af21bb081e1eed081622b))
|
||||
### Refactor
|
||||
|
||||
* fix(rpc): remove of calling "close" and waiting for gui_is_alive ([`f75fc19`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f75fc19c5b10022763252917ca473f404a25165a))
|
||||
* refactor: removed designer pngs ([`84abe46`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/84abe460502d838aac41bb8ff63d93c9fcec9214))
|
||||
|
||||
## v0.73.1 (2024-06-25)
|
||||
* refactor: moved to dynamically loaded material design icons ([`1d2afaa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1d2afaa09e64b7f714d72796e87e2cb49b2a75a7))
|
||||
|
||||
### Fix
|
||||
## v0.95.0 (2024-08-21)
|
||||
|
||||
* fix(ringprogressbar): removed hard-coded endpoint strings ([`1de3cbf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1de3cbf65a1832150917a7549a1bf3efdee6371a))
|
||||
### Documentation
|
||||
|
||||
## v0.73.0 (2024-06-25)
|
||||
* docs(device_browser): added user docs ([`2c31cc9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2c31cc90ae751f14a653cbbdd6c353d6359aaafe))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: add new default scaling of image_item ([`df812ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df812eaad5989f2930dde41d87491868505af946))
|
||||
* feat(cli): added device_browser to cli ([`196504b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/196504b533367a899c19b88af4ccd5b39dc46aac))
|
||||
|
||||
### Test
|
||||
|
||||
* test: add test for imageitem ([`88ecd05`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/88ecd05b95974938ef1efff40e81854baf004cb4))
|
||||
|
||||
## v0.72.2 (2024-06-25)
|
||||
* feat(widgets): added device_browser widget ([`73f5a2f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/73f5a2f085b289ac18fa8a918b6ad7cfed595fb4))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(designer): fixed designer for pyenv and venv; closes #237 ([`e631fc1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e631fc15d8707b73d58cb64316e115a7e43961ea))
|
||||
* fix(device_browser): fixed plugin assignment for designer ([`6500393`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/650039303aae9bbec62c676285938416fff146ce))
|
||||
|
||||
## v0.72.1 (2024-06-24)
|
||||
### Refactor
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: renamed spiral progress bar to ring progress bar; closes #235 ([`e5c0087`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e5c0087c9aed831edbe1c172746325a772a3bafa))
|
||||
* refactor(docs): review response ([`4790afd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4790afde3d61fc9beb073c2775c339d4f80779e3))
|
||||
|
||||
### 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))
|
||||
* test: added test for device browser ([`e870e5b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e870e5ba083c61df581c9c0305adabe72967f997))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
bec_widgets/assets/app_icons/BEC-Dark.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
3
bec_widgets/assets/status_icons/error.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#EA3323">
|
||||
<path d="M479.85-265.87q19.8 0 32.69-12.46 12.9-12.46 12.9-32.26 0-19.8-12.75-32.98-12.74-13.17-32.54-13.17-19.8 0-32.69 13.16-12.9 13.15-12.9 32.95 0 19.8 12.75 32.28 12.74 12.48 32.54 12.48Zm-36.46-166.56h79.22v-262.61h-79.22v262.61Zm36.95 366.56q-86.2 0-161.5-32.39-75.3-32.4-131.74-88.84-56.44-56.44-88.84-131.73-32.39-75.3-32.39-161.59t32.39-161.67q32.4-75.37 88.75-131.34t131.69-88.62q75.34-32.65 161.67-32.65 86.34 0 161.78 32.61 75.45 32.6 131.37 88.5 55.93 55.89 88.55 131.45 32.63 75.56 32.63 161.87 0 86.29-32.65 161.58t-88.62 131.48q-55.97 56.18-131.42 88.76-75.46 32.58-161.67 32.58Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 718 B |
3
bec_widgets/assets/status_icons/not_connected.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#EA3323">
|
||||
<path d="m759.04-283.09-63.13-62q49.31-9.43 84.44-46.02 35.13-36.59 35.13-86.14 0-55.49-39.42-95.08-39.43-39.58-94.93-39.58h-153.3v-79.79h152.74q89.28 0 151.7 62.71Q894.7-566.28 894.7-477q0 63.7-38.26 115.96-38.27 52.26-97.4 77.95ZM596.83-443.61l-65.66-66.78h110.05v66.78h-44.39ZM804.96-56 58.48-802.48 106-850l746.48 746.48L804.96-56ZM443.22-265.87H279.43q-89.28 0-151.7-62.42Q65.3-390.72 65.3-480q0-72.57 43.09-129.54 43.09-56.98 112.09-76.07l70.13 70.7h-11.18q-55.73 0-95.32 39.3-39.59 39.31-39.59 95.61t39.66 95.61q39.66 39.3 95.5 39.3h163.54v79.22ZM319.35-446.61v-66.78h77.3l66.78 66.78H319.35Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 721 B |
3
bec_widgets/assets/status_icons/refresh.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFF55">
|
||||
<path d="M478.3-145.87q-138.65 0-236.39-97.74-97.74-97.74-97.74-236.25t97.74-236.68q97.74-98.16 236.39-98.16 88.4 0 155.45 35.76 67.04 35.76 115.86 98.9V-814.7h66.78v274.92H540.91V-606h165.74q-38.56-57.74-95.3-93.33-56.74-35.58-133.05-35.58-106.88 0-180.89 73.98-74.02 73.99-74.02 180.83 0 106.84 74.02 180.93 74.02 74.08 180.91 74.08 80.16 0 147.74-46.08 67.59-46.09 95.16-121.83H803q-29.56 110.65-119.67 178.89-90.1 68.24-205.03 68.24Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 559 B |
3
bec_widgets/assets/status_icons/running.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#75FB4C">
|
||||
<path d="m419.87-289.52 289.22-289.22-57.31-56.87L419.87-403.7 304.96-518.61l-56.31 56.87 171.22 172.22Zm60.21 223.65q-85.47 0-161.01-32.39-75.53-32.4-131.97-88.84-56.44-56.44-88.84-131.89-32.39-75.46-32.39-160.93 0-86.47 32.39-162.01 32.4-75.53 88.75-131.5t131.85-88.62q75.5-32.65 161.01-32.65 86.52 0 162.12 32.61 75.61 32.6 131.53 88.5 55.93 55.89 88.55 131.45Q894.7-566.58 894.7-480q0 85.55-32.65 161.07-32.65 75.53-88.62 131.9-55.97 56.37-131.42 88.77-75.46 32.39-161.93 32.39Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 604 B |
3
bec_widgets/assets/status_icons/warning.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#F19E39">
|
||||
<path d="M27.56-112.65 480-894.7l452.44 782.05H27.56Zm456.62-125.48q13.15 0 22.61-9.64 9.47-9.65 9.47-22.8t-9.64-22.33q-9.65-9.19-22.8-9.19t-22.61 9.36q-9.47 9.36-9.47 22.51 0 13.15 9.64 22.62 9.65 9.47 22.8 9.47ZM454-348h60v-219.48h-60V-348Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 364 B |
3
bec_widgets/assets/toolbar_icons/add.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M440.39-440.39H185.87v-79.22h254.52V-774.7h79.22v255.09H774.7v79.22H519.61v254.52h-79.22v-254.52Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
3
bec_widgets/assets/toolbar_icons/attach_all.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="m145.26-88.13-57.13-57.13 137.39-137.39H105.87v-79.22h256v256h-79.22v-119.65L145.26-88.13Zm669.48 0L677.91-225.52v119.65h-79.78v-256H854.7v79.22H734.48l137.39 137.39-57.13 57.13Zm-708.87-510v-79.78h119.65L88.13-814.74l57.13-57.13 137.39 137.39V-854.7h79.22v256.57h-256Zm492.26 0V-854.7h79.78v120.22l137.83-138.39 57.13 57.13-138.39 137.83H854.7v79.78H598.13Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 489 B |
3
bec_widgets/assets/toolbar_icons/auto_range.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M114.02-114.02v-308.13h68.13v192.02l547.72-547.72H537.85v-68.37h308.37v308.37h-68.37v-192.02L230.13-182.15h192.02v68.13H114.02Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
3
bec_widgets/assets/toolbar_icons/compare.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="m311.5-154.02-47.74-47.74 116.94-116.7H74.02v-68.37H380.7L263.76-503.76l47.74-47.74 198.98 198.74L311.5-154.02Zm337-254.72L449.76-607.48 648.5-806.22l47.74 47.74-116.7 116.94h306.68v68.37H579.54l116.7 116.69-47.74 47.74Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
4
bec_widgets/assets/toolbar_icons/connection.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 341 B |
3
bec_widgets/assets/toolbar_icons/device_line_edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M180-404.87v-50.26h289.75v50.26H180Zm0-162.57v-50.25h454.62v50.25H180Zm0-162.3V-780h454.62v50.26H180ZM524.62-180v-105.69l217.15-216.16q7.46-7.07 16.11-10.3 8.65-3.23 17.3-3.23 9.43 0 18.25 3.53 8.82 3.54 16.03 10.62l37 37.38q6.87 7.47 10.21 16.16Q860-439 860-430.31t-3.37 17.69q-3.37 9-10.52 16.46L630.31-180H524.62Zm250.69-211.69 37-38.62-37-37.38-38 38 38 38Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 492 B |
3
bec_widgets/assets/toolbar_icons/drag_pan_mode.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M480-54 303.43-230.56 361-288.13l79.39 78.83v-231.09H209.3L283.13-366l-57.57 57.57L54-480l172.56-172.57L284.13-595l-74.83 75.39h231.09v-231.65L366-676.87l-57.57-57.57L480-906l171.57 171.56L594-676.87l-74.39-74.39v231.65h231.65L676.87-594l57.57-57.57L906-480 734.44-308.43 676.87-366l74.39-74.39H519.61v231.09L599-288.13l57.57 57.57L480-54Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
9
bec_widgets/assets/toolbar_icons/export.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
|
||||
fill="#FFFFFF">
|
||||
<g>
|
||||
<rect fill="none" height="24" width="24"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5-5L7,9z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
11
bec_widgets/assets/toolbar_icons/fft.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 100 96" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<rect id="Artboard1" x="0" y="0" width="100" height="96.486" style="fill:none;"/>
|
||||
<g id="Artboard11" serif:id="Artboard1">
|
||||
<path d="M11.379,24.832C11.379,24.261 11.843,23.798 12.414,23.798C18.11,23.798 20.117,19.072 22.06,14.503C23.704,10.634 25.403,6.634 29.483,6.634C33.902,6.634 35.376,12.91 36.934,19.555C38.473,26.113 40.065,32.893 44.138,32.893C48.25,32.893 50.78,28.962 53.457,24.8C56.279,20.412 59.198,15.876 64.31,15.876C69.322,15.876 72.165,20.305 74.915,24.588C77.707,28.935 80.343,33.04 84.999,33.04C85.571,33.04 86.034,33.503 86.034,34.075C86.034,34.647 85.571,35.11 84.999,35.11C79.212,35.11 76.004,30.115 73.175,25.708C70.612,21.716 68.192,17.946 64.31,17.946C60.328,17.946 57.835,21.819 55.197,25.92C52.338,30.366 49.381,34.963 44.138,34.963C38.425,34.963 36.643,27.371 34.92,20.028C33.613,14.46 32.262,8.703 29.482,8.703C26.953,8.703 25.71,11.2 23.963,15.311C21.964,20.014 19.479,25.867 12.413,25.867C11.843,25.867 11.379,25.403 11.379,24.832M44.361,44.584C43.504,44.584 42.557,44.882 42.557,45.739L42.586,50.703L39.522,50.703L43.922,61.878L48.807,50.703L45.691,50.703L45.604,46.255C45.602,45.398 45.218,44.584 44.361,44.584ZM6.034,37.487L6.034,6.674L5,6.674L5,38.522L95,38.522L95,37.487L6.034,37.487M77.414,91.881L77.414,63.849C77.414,63.277 76.951,62.814 76.379,62.814C75.808,62.814 75.345,63.277 75.345,63.849L75.345,91.881C75.345,92.045 75.391,92.194 75.458,92.332L61.955,92.332C62.022,92.194 62.068,92.045 62.068,91.881L62.068,82.718C62.068,82.146 61.605,81.683 61.034,81.683C60.462,81.683 59.999,82.146 59.999,82.718L59.999,91.881C59.999,92.045 60.045,92.194 60.112,92.332L45.059,92.332C45.126,92.194 45.172,92.045 45.172,91.881L45.172,75.943C45.172,75.372 44.709,74.909 44.138,74.909C43.567,74.909 43.104,75.372 43.104,75.943L43.104,91.881C43.104,92.045 43.15,92.194 43.217,92.332L23.852,92.332C23.92,92.194 23.966,92.045 23.966,91.881L23.966,63.849C23.966,63.277 23.502,62.814 22.931,62.814C22.36,62.814 21.897,63.277 21.897,63.849L21.897,91.881C21.897,92.045 21.943,92.194 22.011,92.332L6.034,92.332L6.034,62.881L5,62.881L5,93.366L95,93.366L95,92.332L77.301,92.332C77.368,92.194 77.414,92.045 77.414,91.881"
|
||||
style="fill:white;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
3
bec_widgets/assets/toolbar_icons/fitting_parameters.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="m78.89-112.59 263.02-367.17h202l303.5-354.22v721.39H78.89Zm62.24-262.74-54.7-39.78 166.59-233.02h201L640.46-864.8l51.45 44.26-205.82 240.78H287.33l-146.2 204.43Zm70.54 194.37h567.37v-468.78L574.98-411.63H376.22L211.67-180.96Zm567.37 0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
4
bec_widgets/assets/toolbar_icons/history.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 392 B |
3
bec_widgets/assets/toolbar_icons/image.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M185.09-105.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-589.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h589.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v589.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H185.09Zm43.56-166.04h503.7L578-481.48l-132 171-93-127-124.35 165.57Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
3
bec_widgets/assets/toolbar_icons/image_autorange.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M283.85-289.91h67.41l42.32-112.66h172.59l43.33 112.66h68.85l-164-428h-67.5l-163 428Zm127.74-165.72 67.34-182.87H481l68.41 182.87H411.59Zm68.53 381.61q-86.32 0-160.51-31t-128.89-85.7q-54.7-54.7-85.7-128.89-31-74.19-31-160.51 0-85.31 30.94-159.4t85.7-128.9q54.76-54.8 128.95-86.3t160.51-31.5q85.31 0 159.42 31.47 74.1 31.47 128.91 86.27 54.82 54.8 86.29 128.88 31.48 74.08 31.48 159.6 0 86.2-31.5 160.39-31.5 74.19-86.3 128.95-54.81 54.76-128.9 85.7-74.09 30.94-159.4 30.94ZM480-480Zm-.04 337.85q144.08 0 240.99-96.74 96.9-96.74 96.9-241.07 0-144.32-96.86-241.11-96.86-96.78-240.95-96.78-144.08 0-240.99 96.74-96.9 96.74-96.9 241.07 0 144.32 96.86 241.11 96.86 96.78 240.95 96.78Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 809 B |
9
bec_widgets/assets/toolbar_icons/import.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
|
||||
fill="#FFFFFF">
|
||||
<g>
|
||||
<rect fill="none" height="24" width="24"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
3
bec_widgets/assets/toolbar_icons/line_axis.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M110.39-110.39v-89.57l77.52-77.52v167.09h-77.52Zm165.57 0v-250.13l77.52-77.52v327.65h-77.52Zm165.56 0v-327.65l77.52 77.95v249.7h-77.52Zm165.57 0v-250.83l77.52-76.96v327.79h-77.52Zm165.56 0v-411.83l76.96-76.96v488.79h-76.96ZM110.39-335.65v-112.7L400-735.96l160 160 289.61-290.61v112.14L560-463.26l-160-160-289.61 287.61Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 450 B |
3
bec_widgets/assets/toolbar_icons/line_curve.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M725.93-155.93q0-118.18-45-222.09t-122-180.91q-77-77-180.91-122t-222.09-45v-68.14q132.68 0 248.61 50.23 115.92 50.23 202.5 136.75 86.57 86.53 136.8 202.53 50.23 116.01 50.23 248.63h-68.14Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 319 B |
3
bec_widgets/assets/toolbar_icons/lock_aspect_ratio.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M571.91-279.09h191v-194h-60v134h-131v60ZM198.09-486.91h60v-134h131v-60h-191v194Zm-53 341.04q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-509.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h669.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v509.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H145.09Zm0-79.22h669.82v-509.82H145.09v509.82Zm0 0v-509.82 509.82Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 487 B |
BIN
bec_widgets/assets/toolbar_icons/log_scale.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
3
bec_widgets/assets/toolbar_icons/motor_map.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M443.78-28.43v-75Q305.65-118 211.54-212.11q-94.11-94.11-108.11-231.67h-75v-72.44h75Q118-654.35 212.11-748.46q94.11-94.11 231.67-108.11v-75h72.44v75q137.56 14 231.67 108.11Q842-654.35 856.57-516.22h75v72.44h-75q-14 137.56-108.11 231.67Q654.35-118 516.22-103.43v75h-72.44Zm36.12-152.66q123.4 0 211.21-87.7 87.8-87.71 87.8-211.11 0-123.4-87.7-211.21-87.71-87.8-211.11-87.8-123.4 0-211.21 87.7-87.8 87.71-87.8 211.11 0 123.4 87.7 211.21 87.71 87.8 211.11 87.8ZM480-330q-63 0-106.5-43.5T330-480q0-63 43.5-106.5T480-630q63 0 106.5 43.5T630-480q0 63-43.5 106.5T480-330Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 693 B |
3
bec_widgets/assets/toolbar_icons/photo_library.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M354.61-386.61h391l-127-171-103 135-68-87-93 123ZM274.7-195.48q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-549.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h549.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v549.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H274.7Zm0-79.22h549.82v-549.82H274.7v549.82ZM135.48-55.69q-32.74 0-56.26-23.53-23.53-23.52-23.53-56.26v-629.04h79.79v629.04h629.04v79.79H135.48ZM274.7-824.52v549.82-549.82Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 564 B |
3
bec_widgets/assets/toolbar_icons/positioner_box.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M403.39-158.74 82.13-480l321.26-321.26v642.52Zm153.22 0v-642.52L878.44-480 556.61-158.74Zm81.57-195.74L763.13-480 638.18-605.52v251.04Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 266 B |
3
bec_widgets/assets/toolbar_icons/progress.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M480-65.87q-87.51 0-162.97-31.89-75.47-31.89-131.43-87.84-55.95-55.96-87.84-131.43Q65.87-392.49 65.87-480q0-87.47 31.88-162.87 31.89-75.39 87.75-131.51 55.87-56.11 131.41-88.21Q392.44-894.7 480-894.7q15.96 0 27.78 12.16 11.83 12.16 11.83 28.07 0 15.9-11.83 27.73-11.82 11.83-27.78 11.83-139.31 0-237.11 97.8-97.8 97.8-97.8 237.1 0 139.31 97.8 237.12 97.8 97.8 237.1 97.8 139.31 0 237.12-97.8 97.8-97.8 97.8-237.11 0-15.96 11.83-27.78 11.83-11.83 27.73-11.83 15.91 0 28.07 11.83Q894.7-495.96 894.7-480q0 87.56-32.14 163.1-32.14 75.54-88.11 131.44-55.97 55.9-131.44 87.74Q567.55-65.87 480-65.87Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 724 B |
3
bec_widgets/assets/toolbar_icons/queue.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M854.7-105.87H105.87v-209.61H854.7v209.61Zm0-269.61H105.87v-209.04H854.7v209.04Zm0-269.04H105.87V-854.7H854.7v210.18Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 248 B |
3
bec_widgets/assets/toolbar_icons/rectangle_mode.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M444.19-421.3q39.72 0 67.4-27.78 27.67-27.78 27.67-66.91 0-39.14-27.71-66.57Q483.83-610 444.4-610q-39.44 0-66.92 27.28Q350-555.44 350-516.3q0 39.13 27.36 67.06 27.35 27.94 66.83 27.94ZM634.52-274 532.78-375.74q-22.56 12.31-44.41 19.02-21.84 6.72-43.28 6.72-69.57 0-117.7-48.62-48.13-48.62-48.13-117.88 0-67.98 48.29-116.39t117.08-48.41q68.79 0 117.08 48.41Q610-584.48 610-515.75q0 21.72-6.93 44.13-6.94 22.4-20.37 44.97l102.73 102.74L634.52-274ZM185.09-105.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87V-352h79.22v166.91H352v79.22H185.09Zm422.91 0v-79.22h166.91V-352h79.79v166.91q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H608ZM105.87-608v-166.91q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53H352v79.79H185.09V-608h-79.22Zm669.04 0v-166.91H608v-79.79h166.91q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26V-608h-79.79Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 946 B |
5
bec_widgets/assets/toolbar_icons/remove.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
|
||||
fill="#8B1A10">
|
||||
<rect fill="none" height="24" width="24"/>
|
||||
<path d="M19,19H5V5h14V19z M3,3v18h18V3H3z M17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41L8.41,7L12,10.59L15.59,7 L17,8.41L13.41,12L17,15.59z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 357 B |
3
bec_widgets/assets/toolbar_icons/reset_settings.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M473.54-344.48v-63.59h187.18v63.59H473.54Zm81.92 230.46v-56.15h-81.92v-63.59h81.92v-56.39h63.58v176.13h-63.58Zm103.58-56.15v-63.59h187.18v63.59H659.04Zm41.68-116.92v-177.13h63.58v56.15h81.92v63.59H764.3v57.39h-63.58ZM841.22-540h-68.46q-22.98-101.89-103.94-170.83-80.95-68.93-188.89-68.93-125.29 0-212.37 87.18T180.48-480q0 78.61 36.59 143.32 36.58 64.7 96.95 104.46v-108.26h66.46v226.46H154.02v-66.46h117.41q-71.56-48.72-114.48-127.36-42.93-78.64-42.93-172.16 0-76.22 28.86-142.78 28.86-66.57 78.29-116.04 49.42-49.47 116.01-78.43 66.58-28.97 142.82-28.97 136.52 0 237.87 87.8Q819.22-670.63 841.22-540Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 733 B |
3
bec_widgets/assets/toolbar_icons/restore_state.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M336.04-487.39 145.09-678.35v156.65H65.87v-293H358.3v79.79H201.22l191.39 190.95-56.57 56.57ZM145.09-145.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87V-451.7h79.22v226.61H493v79.22H145.09Zm669.82-278.3v-310.74H428.3v-79.79h386.61q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v310.74h-79.79Zm79.79 60v218.87H553v-218.87h341.7Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 455 B |
3
bec_widgets/assets/toolbar_icons/ring_progress.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M479.87-54q-87.96 0-165.74-33.42-77.79-33.43-135.53-91.18-57.75-57.74-91.18-135.66Q54-392.17 54-480.13q0-87.96 33.42-165.74 33.43-77.79 91.18-135.53 57.74-57.75 135.62-91.18Q392.09-906 480-906h36.22v340.7q24.82 10.69 40.8 33.52Q573-508.96 573-480.17q0 38.43-27.38 65.8Q518.24-387 479.79-387q-38.44 0-65.62-27.37Q387-441.74 387-480.17q0-28.79 15.98-51.61 15.98-22.83 40.8-33.52v-97.22Q378.22-650.39 336-599.76q-42.22 50.63-42.22 119.5 0 78.19 54.12 132.33 54.12 54.15 132.02 54.15 77.91 0 132.1-54.09 54.2-54.1 54.2-131.79 0-42.47-16.28-77.88-16.29-35.42-45.42-60.98l52.05-52.05q38.18 35.64 60.7 84.75 22.51 49.11 22.51 105.82 0 108.57-75.58 183.9-75.59 75.32-184.13 75.32-108.55 0-183.92-75.32-75.37-75.33-75.37-183.89 0-99.62 63.68-172.01 63.67-72.39 159.32-85.65v-93.22Q309.39-817.61 218.2-717.8 127-617.98 127-480.14q0 147.23 103.1 250.18Q333.19-127 480.14-127t249.9-103.02Q833-333.04 833-479.81q0-76.45-29.58-141.91-29.57-65.46-80.81-114.89l51.48-51.48q61.05 58.34 96.48 137.6Q906-571.23 906-479.73q0 87.82-33.42 165.6-33.43 77.79-91.18 135.53-57.74 57.75-135.66 91.18Q567.83-54 479.87-54Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
bec_widgets/assets/toolbar_icons/rotate_left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M435-72.11q-49.67-7-95.99-25.36-46.31-18.36-86.55-49.55l49.21-49.74q31.76 24 65.29 37.26 33.52 13.26 68.04 19.02v68.37Zm90 0v-68.37q109.28-21.24 181.07-102.9 71.78-81.66 71.78-197.71 0-124.37-84.11-210.87t-208.72-86.5h-18.8L540.67-664l-49.74 49.74L332.2-773l158.73-158.74 49.74 49.26-75.41 75.65h19.28q75.48 0 141.34 28.72t114.98 78.56q49.12 49.83 77.24 116.29 28.12 66.46 28.12 142.17 0 142.39-90.8 245.07Q664.63-93.35 525-72.11ZM189.46-209.78q-28.96-38.72-47.82-86.3-18.86-47.57-25.62-100.01h68.89q5 37.76 18.38 72.17 13.38 34.4 36.14 64.16l-49.97 49.98Zm-73.44-276.31q6.52-50.71 25-97.29 18.48-46.58 48.44-87.77l50.21 48.26q-22.76 33-36.14 67.52-13.38 34.52-18.62 69.28h-68.89Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 811 B |
3
bec_widgets/assets/toolbar_icons/rotate_right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M527-72.11v-68.37q34.52-5.76 68.04-19.02 33.53-13.26 65.29-37.26l49.21 49.74q-40.24 31.19-86.55 49.55Q576.67-79.11 527-72.11Zm-90 0Q297.37-93.35 206.7-196.02q-90.68-102.68-90.68-245.07 0-75.71 28-142.17t77.12-116.29q49.12-49.84 114.98-78.56 65.86-28.72 141.34-28.72h19.28l-75.41-75.65 49.74-49.26L629.8-773 471.07-614.26 421.33-664l74.69-74.46h-19.04q-124.61 0-208.72 86.5t-84.11 210.87q0 116.05 71.78 197.71 71.79 81.66 181.07 102.9v68.37Zm335.54-137.67-49.97-49.98q22.76-29.76 36.14-64.16 13.38-34.41 18.38-72.17h69.13q-7 52.44-25.86 100.01-18.86 47.58-47.82 86.3Zm73.68-276.31h-69.13q-5.24-34.76-18.62-69.28t-36.14-67.52l50.21-48.26q29.96 41.19 48.44 87.77 18.48 46.58 25.24 97.29Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 815 B |
3
bec_widgets/assets/toolbar_icons/save.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M854.7-689.22v504.13q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H185.09q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-589.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h504.13L854.7-689.22Zm-79.79 35.48L653.74-774.91H185.09v589.82h589.82v-468.65ZM479.76-250.09q43.24 0 73.74-30.26 30.5-30.27 30.5-73.5 0-43.24-30.26-73.74-30.27-30.5-73.5-30.5-43.24 0-73.74 30.27-30.5 30.26-30.5 73.5 0 43.23 30.26 73.73 30.27 30.5 73.5 30.5ZM238.09-578.91h358v-143h-358v143Zm-53-74.83v468.65-589.82 121.17Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 621 B |
3
bec_widgets/assets/toolbar_icons/save_state.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M185.87-98.52v-681.39q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h429.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v681.39L480-224.17 185.87-98.52Zm79.22-120.39L480-309.18l214.91 90.27v-561H265.09v561Zm0-561h429.82-429.82Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
3
bec_widgets/assets/toolbar_icons/scan_control.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M185.09-105.87q-32.93 0-56.08-23.14-23.14-23.15-23.14-56.08v-589.82q0-33.16 23.14-56.47 23.15-23.32 56.08-23.32h589.82q33.16 0 56.47 23.32 23.32 23.31 23.32 56.47v589.82q0 32.93-23.32 56.08-23.31 23.14-56.47 23.14H185.09Zm439.21-79.22h71l79.61-79.61v-40.39H744.3l-120 120ZM281-389.48l141-140 90 90L725.52-654 679-700.52l-167 167-90-90L234.48-436 281-389.48Zm-95.91 204.39h34.56l120-120h-71l-83.56 83.57v36.43Zm350.91 0 120-120h-71l-120 120h71Zm-159.87 0 120-120h-71l-120 120h71Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 609 B |
4
bec_widgets/assets/toolbar_icons/settings.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.09-.16-.26-.25-.44-.25-.06 0-.12.01-.17.03l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.06-.02-.12-.03-.18-.03-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.09.16.26.25.44.25.06 0 .12-.01.17-.03l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
bec_widgets/assets/toolbar_icons/status.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M286.83-277h60v-205h-60v205Zm326.91 0h60v-420h-60v420ZM450-277h60v-118h-60v118Zm0-205h60v-60h-60v60ZM185.09-105.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-589.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h589.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v589.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H185.09Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 452 B |
3
bec_widgets/assets/toolbar_icons/terminal.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M145.09-145.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-509.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h669.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v509.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H145.09Zm0-79.22h669.82v-425.82H145.09v425.82ZM300-292l-42-42 103-104-104-104 43-42 146 146-146 146Zm190 4v-60h220v60H490Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
3
bec_widgets/assets/toolbar_icons/transform.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M636.17-32.59 480.35-188.41l45.82-46.07 77.13 77.37v-137.32H356.93q-28.1 0-46.8-18.55-18.7-18.54-18.7-46.95V-600.3H74.02v-65.27h217.41v-137.32l-77.36 77.37-45.59-45.83 155.59-155.82 156.06 155.82-46.06 45.83-77.14-77.37v442.96h529.29v65.5H669.04v137.32l77.13-77.37L792-188.41 636.17-32.59ZM603.3-419.93V-600.3H416.93v-65.27H603.3q28.37 0 47.06 18.56 18.68 18.55 18.68 46.71v180.37H603.3Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 518 B |
3
bec_widgets/assets/toolbar_icons/waveform.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
|
||||
<path d="M126-206.43 66.43-266 380-579.57 539.43-419.7l298-335 56.14 54.57-352.44 399.26L380-460.43l-254 254Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from queue import Queue
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -25,6 +27,17 @@ class AutoUpdates:
|
||||
|
||||
def __init__(self, gui: BECDockArea):
|
||||
self.gui = gui
|
||||
self.msg_queue = Queue()
|
||||
self.auto_update_thread = None
|
||||
self._shutdown_sentinel = object()
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the auto update thread.
|
||||
"""
|
||||
self.auto_update_thread = threading.Thread(target=self.process_queue)
|
||||
self.auto_update_thread.start()
|
||||
|
||||
def start_default_dock(self):
|
||||
"""
|
||||
@@ -79,6 +92,16 @@ class AutoUpdates:
|
||||
info = self.get_scan_info(msg)
|
||||
self.handler(info)
|
||||
|
||||
def process_queue(self):
|
||||
"""
|
||||
Process the message queue.
|
||||
"""
|
||||
while True:
|
||||
msg = self.msg_queue.get()
|
||||
if msg is self._shutdown_sentinel:
|
||||
break
|
||||
self.run(msg)
|
||||
|
||||
@staticmethod
|
||||
def get_selected_device(monitored_devices, selected_device):
|
||||
"""
|
||||
@@ -151,3 +174,11 @@ class AutoUpdates:
|
||||
fig.clear_all()
|
||||
plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number} - {dev_y}")
|
||||
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shutdown the auto update thread.
|
||||
"""
|
||||
self.msg_queue.put(self._shutdown_sentinel)
|
||||
if self.auto_update_thread:
|
||||
self.auto_update_thread.join()
|
||||
|
||||
@@ -13,13 +13,23 @@ class Widgets(str, enum.Enum):
|
||||
Enum for the available widgets.
|
||||
"""
|
||||
|
||||
BECQueue = "BECQueue"
|
||||
BECStatusBox = "BECStatusBox"
|
||||
BECDock = "BECDock"
|
||||
BECDockArea = "BECDockArea"
|
||||
BECFigure = "BECFigure"
|
||||
BECImageWidget = "BECImageWidget"
|
||||
BECMotorMapWidget = "BECMotorMapWidget"
|
||||
BECQueue = "BECQueue"
|
||||
BECStatusBox = "BECStatusBox"
|
||||
BECWaveformWidget = "BECWaveformWidget"
|
||||
DarkModeButton = "DarkModeButton"
|
||||
DeviceBrowser = "DeviceBrowser"
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerControlLine = "PositionerControlLine"
|
||||
RingProgressBar = "RingProgressBar"
|
||||
ScanControl = "ScanControl"
|
||||
StopButton = "StopButton"
|
||||
TextBox = "TextBox"
|
||||
VSCodeEditor = "VSCodeEditor"
|
||||
WebsiteWidget = "WebsiteWidget"
|
||||
@@ -179,7 +189,7 @@ class BECDock(RPCBase):
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def widget_list(self) -> "list[BECConnector]":
|
||||
def widget_list(self) -> "list[BECWidget]":
|
||||
"""
|
||||
Get the widgets in the dock.
|
||||
|
||||
@@ -220,13 +230,13 @@ class BECDock(RPCBase):
|
||||
@rpc_call
|
||||
def add_widget(
|
||||
self,
|
||||
widget: "BECConnector | str",
|
||||
widget: "BECWidget | str",
|
||||
row=None,
|
||||
col=0,
|
||||
rowspan=1,
|
||||
colspan=1,
|
||||
shift: "Literal['down', 'up', 'left', 'right']" = "down",
|
||||
) -> "BECConnector":
|
||||
) -> "BECWidget":
|
||||
"""
|
||||
Add a widget to the dock.
|
||||
|
||||
@@ -344,7 +354,7 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
|
||||
name: "str" = None,
|
||||
position: "Literal['bottom', 'top', 'left', 'right', 'above', 'below']" = None,
|
||||
relative_to: "BECDock | None" = None,
|
||||
closable: "bool" = False,
|
||||
closable: "bool" = True,
|
||||
floating: "bool" = False,
|
||||
prefix: "str" = "dock",
|
||||
widget: "str | QWidget | None" = None,
|
||||
@@ -463,8 +473,9 @@ class BECFigure(RPCBase):
|
||||
@rpc_call
|
||||
def plot(
|
||||
self,
|
||||
x: "list | np.ndarray | None" = None,
|
||||
arg1: "list | np.ndarray | str | None" = None,
|
||||
y: "list | np.ndarray | None" = None,
|
||||
x: "list | np.ndarray | None" = None,
|
||||
x_name: "str | None" = None,
|
||||
y_name: "str | None" = None,
|
||||
z_name: "str | None" = None,
|
||||
@@ -486,8 +497,9 @@ class BECFigure(RPCBase):
|
||||
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
|
||||
|
||||
Args:
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
@@ -607,6 +619,12 @@ class BECFigure(RPCBase):
|
||||
theme(Literal["dark","light"]): The theme to set for the figure widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export(self):
|
||||
"""
|
||||
Export the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def clear_all(self):
|
||||
"""
|
||||
@@ -808,7 +826,7 @@ class BECImageShow(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_monitor_image(
|
||||
def image(
|
||||
self,
|
||||
monitor: "str",
|
||||
color_map: "Optional[str]" = "magma",
|
||||
@@ -819,7 +837,17 @@ class BECImageShow(RPCBase):
|
||||
**kwargs,
|
||||
) -> "BECImageItem":
|
||||
"""
|
||||
None
|
||||
Add an image to the figure. Always access the first image widget in the figure.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
vrange(tuple[float, float]): The range of values to display.
|
||||
|
||||
Returns:
|
||||
BECImageItem: The image item.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -1089,6 +1117,12 @@ class BECImageShow(RPCBase):
|
||||
lock(bool): True to lock, False to unlock.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export(self):
|
||||
"""
|
||||
Show the Export Dialog of the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
@@ -1105,6 +1139,180 @@ class BECImageShow(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECImageWidget(RPCBase):
|
||||
@rpc_call
|
||||
def image(
|
||||
self,
|
||||
monitor: "str",
|
||||
color_map: "Optional[str]" = "magma",
|
||||
color_bar: "Optional[Literal['simple', 'full']]" = "full",
|
||||
downsample: "Optional[bool]" = True,
|
||||
opacity: "Optional[float]" = 1.0,
|
||||
vrange: "Optional[tuple[int, int]]" = None,
|
||||
**kwargs,
|
||||
) -> "BECImageItem":
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_title(self, title: "str"):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_label(self, x_label: "str"):
|
||||
"""
|
||||
Set the x-axis label of the plot widget.
|
||||
|
||||
Args:
|
||||
x_label(str): Label of the x-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_label(self, y_label: "str"):
|
||||
"""
|
||||
Set the y-axis label of the plot widget.
|
||||
|
||||
Args:
|
||||
y_label(str): Label of the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_scale(self, x_scale: "Literal['linear', 'log']"):
|
||||
"""
|
||||
Set the scale of the x-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
x_scale(Literal["linear", "log"]): Scale of the x-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_scale(self, y_scale: "Literal['linear', 'log']"):
|
||||
"""
|
||||
Set the scale of the y-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
y_scale(Literal["linear", "log"]): Scale of the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_lim(self, x_lim: "tuple"):
|
||||
"""
|
||||
Set the limits of the x-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
x_lim(tuple): Limits of the x-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_lim(self, y_lim: "tuple"):
|
||||
"""
|
||||
Set the limits of the y-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
y_lim(tuple): Limits of the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_vrange(self, vmin: "float", vmax: "float", name: "str" = None):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
If name is not specified, then set vrange for all images.
|
||||
|
||||
Args:
|
||||
vmin(float): Minimum value of the color bar.
|
||||
vmax(float): Maximum value of the color bar.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_fft(self, enable: "bool" = False, name: "str" = None):
|
||||
"""
|
||||
Set the FFT of the image.
|
||||
If name is not specified, then set FFT for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform FFT on the monitor data.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_transpose(self, enable: "bool" = False, name: "str" = None):
|
||||
"""
|
||||
Set the transpose of the image.
|
||||
If name is not specified, then set transpose for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to transpose the monitor data before displaying.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_rotation(self, deg_90: "int" = 0, name: "str" = None):
|
||||
"""
|
||||
Set the rotation of the image.
|
||||
If name is not specified, then set rotation for all images.
|
||||
|
||||
Args:
|
||||
deg_90(int): The rotation angle of the monitor data before displaying.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_log(self, enable: "bool" = False, name: "str" = None):
|
||||
"""
|
||||
Set the log of the image.
|
||||
If name is not specified, then set log for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform log on the monitor data.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_grid(self, x_grid: "bool", y_grid: "bool"):
|
||||
"""
|
||||
Set the grid visibility of the plot widget.
|
||||
|
||||
Args:
|
||||
x_grid(bool): Visibility of the x-axis grid.
|
||||
y_grid(bool): Visibility of the y-axis grid.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock: "bool"):
|
||||
"""
|
||||
Lock the aspect ratio of the plot widget.
|
||||
|
||||
Args:
|
||||
lock(bool): Lock the aspect ratio.
|
||||
"""
|
||||
|
||||
|
||||
class BECMotorMap(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -1192,16 +1400,117 @@ class BECMotorMap(RPCBase):
|
||||
def get_data(self) -> "dict":
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export(self):
|
||||
"""
|
||||
Show the Export Dialog of the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the plot widget from the figure.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def reset_history(self):
|
||||
"""
|
||||
Reset the history of the motor map.
|
||||
"""
|
||||
|
||||
|
||||
class BECMotorMapWidget(RPCBase):
|
||||
@rpc_call
|
||||
def change_motors(
|
||||
self,
|
||||
motor_x: "str",
|
||||
motor_y: "str",
|
||||
motor_x_entry: "str" = None,
|
||||
motor_y_entry: "str" = None,
|
||||
validate_bec: "bool" = True,
|
||||
) -> "None":
|
||||
"""
|
||||
Change the active motors for the plot.
|
||||
|
||||
Args:
|
||||
motor_x(str): Motor name for the X axis.
|
||||
motor_y(str): Motor name for the Y axis.
|
||||
motor_x_entry(str): Motor entry for the X axis.
|
||||
motor_y_entry(str): Motor entry for the Y axis.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_max_points(self, max_points: "int") -> "None":
|
||||
"""
|
||||
Set the maximum number of points to display on the motor map.
|
||||
|
||||
Args:
|
||||
max_points(int): Maximum number of points to display.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_precision(self, precision: "int") -> "None":
|
||||
"""
|
||||
Set the precision of the motor map.
|
||||
|
||||
Args:
|
||||
precision(int): Precision to set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_num_dim_points(self, num_dim_points: "int") -> "None":
|
||||
"""
|
||||
Set the number of points to display on the motor map.
|
||||
|
||||
Args:
|
||||
num_dim_points(int): Number of points to display.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_background_value(self, background_value: "int") -> "None":
|
||||
"""
|
||||
Set the background value of the motor map.
|
||||
|
||||
Args:
|
||||
background_value(int): Background value of the motor map.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_scatter_size(self, scatter_size: "int") -> "None":
|
||||
"""
|
||||
Set the scatter size of the motor map.
|
||||
|
||||
Args:
|
||||
scatter_size(int): Scatter size of the motor map.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_data(self) -> "dict":
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def reset_history(self) -> "None":
|
||||
"""
|
||||
Reset the history of the motor map.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export(self):
|
||||
"""
|
||||
Show the export dialog for the motor map.
|
||||
"""
|
||||
|
||||
|
||||
class BECPlotBase(RPCBase):
|
||||
@property
|
||||
@@ -1330,6 +1639,12 @@ class BECPlotBase(RPCBase):
|
||||
lock(bool): True to lock, False to unlock.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export(self):
|
||||
"""
|
||||
Show the Export Dialog of the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
@@ -1403,8 +1718,9 @@ class BECWaveform(RPCBase):
|
||||
@rpc_call
|
||||
def plot(
|
||||
self,
|
||||
x: "list | np.ndarray | None" = None,
|
||||
arg1: "list | np.ndarray | str | None" = None,
|
||||
y: "list | np.ndarray | None" = None,
|
||||
x: "list | np.ndarray | None" = None,
|
||||
x_name: "str | None" = None,
|
||||
y_name: "str | None" = None,
|
||||
z_name: "str | None" = None,
|
||||
@@ -1416,13 +1732,20 @@ class BECWaveform(RPCBase):
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
dap: "str | None" = None,
|
||||
**kwargs,
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
|
||||
Args:
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
arg1(list | np.ndarray | str | None): First argument which can be x data, y data, or y_name.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
x(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): Name of the x signal.
|
||||
- "best_effort": Use the best effort signal.
|
||||
- "timestamp": Use the timestamp signal.
|
||||
- "index": Use the index signal.
|
||||
- Custom signal name of device from BEC.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
@@ -1432,7 +1755,7 @@ class BECWaveform(RPCBase):
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
dap(str): The dap model to use for the curve. If not specified, none will be added.
|
||||
dap(str): The dap model to use for the curve, only available for sync devices. If not specified, none will be added.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
@@ -1441,12 +1764,13 @@ class BECWaveform(RPCBase):
|
||||
@rpc_call
|
||||
def add_dap(
|
||||
self,
|
||||
x_name: "str",
|
||||
y_name: "str",
|
||||
x_name: "str | None" = None,
|
||||
y_name: "str | None" = None,
|
||||
x_entry: "Optional[str]" = None,
|
||||
y_entry: "Optional[str]" = None,
|
||||
color: "Optional[str]" = None,
|
||||
dap: "str" = "GaussianModel",
|
||||
validate_bec: "bool" = True,
|
||||
**kwargs,
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
@@ -1458,15 +1782,28 @@ class BECWaveform(RPCBase):
|
||||
y_name(str): Name of the y signal.
|
||||
y_entry(str): Entry of the y signal.
|
||||
color(str, optional): Color of the curve. Defaults to None.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
dap(str): The dap model to use for the curve.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x(self, x_name: "str", x_entry: "str | None" = None):
|
||||
"""
|
||||
Change the x axis of the plot widget.
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
- "best_effort": Use the best effort signal.
|
||||
- "timestamp": Use the timestamp signal.
|
||||
- "index": Use the index signal.
|
||||
- Custom signal name of device from BEC.
|
||||
x_entry(str): Entry of the x signal.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_dap_params(self) -> "dict":
|
||||
"""
|
||||
@@ -1636,6 +1973,15 @@ class BECWaveform(RPCBase):
|
||||
y(bool): Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_colormap(self, colormap: "str | None" = None):
|
||||
"""
|
||||
Set the colormap of the plot widget.
|
||||
|
||||
Args:
|
||||
colormap(str, optional): Scale the colors of curves to colormap. If None, use the default color palette.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock):
|
||||
"""
|
||||
@@ -1645,12 +1991,24 @@ class BECWaveform(RPCBase):
|
||||
lock(bool): True to lock, False to unlock.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export(self):
|
||||
"""
|
||||
Show the Export Dialog of the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the plot widget from the figure.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def clear_all(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_legend_label_size(self, size: "int" = None):
|
||||
"""
|
||||
@@ -1661,6 +2019,304 @@ class BECWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECWaveformWidget(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def curves(self) -> "list[BECCurve]":
|
||||
"""
|
||||
Get the curves of the plot widget as a list
|
||||
Returns:
|
||||
list: List of curves.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def plot(
|
||||
self,
|
||||
arg1: "list | np.ndarray | str | None" = None,
|
||||
x: "list | np.ndarray | None" = None,
|
||||
y: "list | np.ndarray | None" = None,
|
||||
x_name: "str | None" = None,
|
||||
y_name: "str | None" = None,
|
||||
z_name: "str | None" = None,
|
||||
x_entry: "str | None" = None,
|
||||
y_entry: "str | None" = None,
|
||||
z_entry: "str | None" = None,
|
||||
color: "str | None" = None,
|
||||
color_map_z: "str | None" = "plasma",
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
dap: "str | None" = None,
|
||||
**kwargs,
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
Args:
|
||||
arg1(list | np.ndarray | str | None): First argument which can be x data(list | np.ndarray), y data(list | np.ndarray), or y_name(str).
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
dap(str): The dap model to use for the curve. If not specified, none will be added.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_dap(
|
||||
self,
|
||||
x_name: "str",
|
||||
y_name: "str",
|
||||
x_entry: "str | None" = None,
|
||||
y_entry: "str | None" = None,
|
||||
color: "str | None" = None,
|
||||
dap: "str" = "GaussianModel",
|
||||
validate_bec: "bool" = True,
|
||||
**kwargs,
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Add LMFIT dap model curve to the plot widget.
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
x_entry(str): Entry of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
y_entry(str): Entry of the y signal.
|
||||
color(str, optional): Color of the curve. Defaults to None.
|
||||
dap(str): The dap model to use for the curve.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_dap_params(self) -> "dict":
|
||||
"""
|
||||
Get the DAP parameters of all DAP curves.
|
||||
|
||||
Returns:
|
||||
dict: DAP parameters of all DAP curves.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove_curve(self, *identifiers):
|
||||
"""
|
||||
Remove a curve from the plot widget.
|
||||
|
||||
Args:
|
||||
*identifiers: Identifier of the curve to be removed. Can be either an integer (index) or a string (curve_id).
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def scan_history(self, scan_index: "int" = None, scan_id: "str" = None):
|
||||
"""
|
||||
Update the scan curves with the data from the scan storage.
|
||||
Provide only one of scan_id or scan_index.
|
||||
|
||||
Args:
|
||||
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
|
||||
scan_index(int, optional): Index of the scan to be updated. Defaults to None.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict | pd.DataFrame":
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
|
||||
Args:
|
||||
output (Literal["dict", "pandas"]): Format of the output data.
|
||||
|
||||
Returns:
|
||||
dict | pd.DataFrame: Data of all curves in the specified format.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x(self, x_name: "str", x_entry: "str | None" = None):
|
||||
"""
|
||||
Change the x axis of the plot widget.
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
- "best_effort": Use the best effort signal.
|
||||
- "timestamp": Use the timestamp signal.
|
||||
- "index": Use the index signal.
|
||||
- Custom signal name of device from BEC.
|
||||
x_entry(str): Entry of the x signal.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_title(self, title: "str"):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_label(self, x_label: "str"):
|
||||
"""
|
||||
Set the x-axis label of the plot widget.
|
||||
|
||||
Args:
|
||||
x_label(str): Label of the x-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_label(self, y_label: "str"):
|
||||
"""
|
||||
Set the y-axis label of the plot widget.
|
||||
|
||||
Args:
|
||||
y_label(str): Label of the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_scale(self, x_scale: "Literal['linear', 'log']"):
|
||||
"""
|
||||
Set the scale of the x-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
x_scale(Literal["linear", "log"]): Scale of the x-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_scale(self, y_scale: "Literal['linear', 'log']"):
|
||||
"""
|
||||
Set the scale of the y-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
y_scale(Literal["linear", "log"]): Scale of the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_lim(self, x_lim: "tuple"):
|
||||
"""
|
||||
Set the limits of the x-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
x_lim(tuple): Limits of the x-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_lim(self, y_lim: "tuple"):
|
||||
"""
|
||||
Set the limits of the y-axis of the plot widget.
|
||||
|
||||
Args:
|
||||
y_lim(tuple): Limits of the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_legend_label_size(self, legend_label_size: "int"):
|
||||
"""
|
||||
Set the size of the legend labels of the plot widget.
|
||||
|
||||
Args:
|
||||
legend_label_size(int): Size of the legend labels.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_auto_range(self, enabled: "bool", axis: "str" = "xy"):
|
||||
"""
|
||||
Set the auto range of the plot widget.
|
||||
|
||||
Args:
|
||||
enabled(bool): If True, enable the auto range.
|
||||
axis(str, optional): The axis to enable the auto range.
|
||||
- "xy": Enable auto range for both x and y axis.
|
||||
- "x": Enable auto range for x axis.
|
||||
- "y": Enable auto range for y axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_grid(self, x_grid: "bool", y_grid: "bool"):
|
||||
"""
|
||||
Set the grid visibility of the plot widget.
|
||||
|
||||
Args:
|
||||
x_grid(bool): Visibility of the x-axis grid.
|
||||
y_grid(bool): Visibility of the y-axis grid.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock: "bool"):
|
||||
"""
|
||||
Lock the aspect ratio of the plot widget.
|
||||
|
||||
Args:
|
||||
lock(bool): Lock the aspect ratio.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export(self):
|
||||
"""
|
||||
Show the export dialog for the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def export_to_matplotlib(self):
|
||||
"""
|
||||
Export the plot widget to Matplotlib.
|
||||
"""
|
||||
|
||||
|
||||
class DarkModeButton(RPCBase):
|
||||
@rpc_call
|
||||
def toggle_dark_mode(self) -> None:
|
||||
"""
|
||||
Toggle the dark mode state. This will change the theme of the entire
|
||||
application to dark or light mode.
|
||||
"""
|
||||
|
||||
|
||||
class DeviceBrowser(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
|
||||
class DeviceComboBox(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -1715,6 +2371,28 @@ class DeviceLineEdit(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox(RPCBase):
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: str):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerControlLine(RPCBase):
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: str):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class Ring(RPCBase):
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
|
||||
@@ -2,20 +2,20 @@ from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import importlib.metadata as imd
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
|
||||
from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer
|
||||
|
||||
import bec_widgets.cli.client as client
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates
|
||||
@@ -23,10 +23,6 @@ from bec_widgets.cli.auto_updates import AutoUpdates
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.device import DeviceBase
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea, BECFigure
|
||||
|
||||
from bec_lib.serialization import MsgpackSerialization
|
||||
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
# from bec_lib.connector import MessageObject
|
||||
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
|
||||
@@ -87,7 +83,7 @@ def _get_output(process, logger) -> None:
|
||||
print(f"Error reading process output: {str(e)}")
|
||||
|
||||
|
||||
def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
|
||||
def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger=None) -> None:
|
||||
"""
|
||||
Start the plot in a new process.
|
||||
|
||||
@@ -98,10 +94,13 @@ def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
|
||||
# pylint: disable=subprocess-run-check
|
||||
command = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__]
|
||||
if config:
|
||||
if isinstance(config, dict):
|
||||
config = json.dumps(config)
|
||||
command.extend(["--config", config])
|
||||
|
||||
env_dict = os.environ.copy()
|
||||
env_dict["PYTHONUNBUFFERED"] = "1"
|
||||
|
||||
if logger is None:
|
||||
stdout_redirect = subprocess.DEVNULL
|
||||
stderr_redirect = subprocess.DEVNULL
|
||||
@@ -181,7 +180,7 @@ class BECGuiClientMixin:
|
||||
if isinstance(msg, messages.ScanStatusMessage):
|
||||
if not self.gui_is_alive():
|
||||
return
|
||||
self.auto_updates.run(msg)
|
||||
self.auto_updates.msg_queue.put(msg)
|
||||
|
||||
def show(self) -> None:
|
||||
"""
|
||||
@@ -190,7 +189,7 @@ class BECGuiClientMixin:
|
||||
if self._process is None or self._process.poll() is not None:
|
||||
self._start_update_script()
|
||||
self._process, self._process_output_processing_thread = _start_plot_process(
|
||||
self._gui_id, self.__class__, self._client._service_config.config_path
|
||||
self._gui_id, self.__class__, self._client._service_config.config
|
||||
)
|
||||
while not self.gui_is_alive():
|
||||
print("Waiting for GUI to start...")
|
||||
@@ -210,6 +209,8 @@ class BECGuiClientMixin:
|
||||
self._process_output_processing_thread.join()
|
||||
self._process.wait()
|
||||
self._process = None
|
||||
if self.auto_updates is not None:
|
||||
self.auto_updates.shutdown()
|
||||
|
||||
|
||||
class RPCResponseTimeoutError(Exception):
|
||||
@@ -221,54 +222,14 @@ class RPCResponseTimeoutError(Exception):
|
||||
)
|
||||
|
||||
|
||||
class QtRedisMessageWaiter:
|
||||
def __init__(self, redis_connector, message_to_wait):
|
||||
self.ev_loop = QEventLoop()
|
||||
self.response = None
|
||||
self.connector = redis_connector
|
||||
self.message_to_wait = message_to_wait
|
||||
self.pubsub = redis_connector._redis_conn.pubsub()
|
||||
self.pubsub.subscribe(self.message_to_wait.endpoint)
|
||||
fd = self.pubsub.connection._sock.fileno()
|
||||
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
|
||||
self.notifier.activated.connect(self._pubsub_readable)
|
||||
|
||||
def _msg_received(self, msg_obj):
|
||||
self.response = msg_obj.value
|
||||
self.ev_loop.quit()
|
||||
|
||||
def wait(self, timeout=1):
|
||||
timer = QTimer()
|
||||
timer.singleShot(timeout * 1000, self.ev_loop.quit)
|
||||
self.ev_loop.exec_()
|
||||
timer.stop()
|
||||
self.notifier.setEnabled(False)
|
||||
self.pubsub.close()
|
||||
return self.response
|
||||
|
||||
def _pubsub_readable(self, fd):
|
||||
while True:
|
||||
msg = self.pubsub.get_message()
|
||||
if msg:
|
||||
if msg["type"] == "subscribe":
|
||||
# get_message buffers, so we may already have the answer
|
||||
# let's check...
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
return
|
||||
channel = msg["channel"].decode()
|
||||
msg = MessageObject(topic=channel, value=MsgpackSerialization.loads(msg["data"]))
|
||||
self.connector._execute_callback(self._msg_received, msg, {})
|
||||
|
||||
|
||||
class RPCBase:
|
||||
def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
|
||||
self._client = BECDispatcher().client
|
||||
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
|
||||
self._config = config if config is not None else {}
|
||||
self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())
|
||||
self._parent = parent
|
||||
self._msg_wait_event = threading.Event()
|
||||
self._rpc_response = None
|
||||
super().__init__()
|
||||
# print(f"RPCBase: {self._gui_id}")
|
||||
|
||||
@@ -312,24 +273,39 @@ class RPCBase:
|
||||
# pylint: disable=protected-access
|
||||
receiver = self._root._gui_id
|
||||
if wait_for_rpc_response:
|
||||
redis_msg = QtRedisMessageWaiter(
|
||||
self._client.connector, MessageEndpoints.gui_instruction_response(request_id)
|
||||
self._rpc_response = None
|
||||
self._msg_wait_event.clear()
|
||||
self._client.connector.register(
|
||||
MessageEndpoints.gui_instruction_response(request_id),
|
||||
cb=self._on_rpc_response,
|
||||
parent=self,
|
||||
)
|
||||
|
||||
self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
|
||||
|
||||
if wait_for_rpc_response:
|
||||
response = redis_msg.wait(timeout)
|
||||
|
||||
if response is None:
|
||||
raise RPCResponseTimeoutError(request_id, timeout)
|
||||
|
||||
try:
|
||||
finished = self._msg_wait_event.wait(10)
|
||||
if not finished:
|
||||
raise RPCResponseTimeoutError(request_id, timeout)
|
||||
finally:
|
||||
self._msg_wait_event.clear()
|
||||
self._client.connector.unregister(
|
||||
MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
|
||||
)
|
||||
# get class name
|
||||
if not response.accepted:
|
||||
raise ValueError(response.message["error"])
|
||||
msg_result = response.message.get("result")
|
||||
if not self._rpc_response.accepted:
|
||||
raise ValueError(self._rpc_response.message["error"])
|
||||
msg_result = self._rpc_response.message.get("result")
|
||||
self._rpc_response = None
|
||||
return self._create_widget_from_msg_result(msg_result)
|
||||
|
||||
@staticmethod
|
||||
def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
|
||||
msg = msg.value
|
||||
parent._msg_wait_event.set()
|
||||
parent._rpc_response = msg
|
||||
|
||||
def _create_widget_from_msg_result(self, msg_result):
|
||||
if msg_result is None:
|
||||
return None
|
||||
@@ -356,4 +332,8 @@ class RPCBase:
|
||||
Check if the GUI is alive.
|
||||
"""
|
||||
heart = self._client.connector.get(MessageEndpoints.gui_heartbeat(self._root._gui_id))
|
||||
return heart is not None
|
||||
if heart is None:
|
||||
return False
|
||||
if heart.status == messages.BECStatus.RUNNING:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -5,13 +5,12 @@ import argparse
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
import black
|
||||
import isort
|
||||
|
||||
from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator
|
||||
from bec_widgets.utils.plugin_utils import get_rpc_classes
|
||||
from bec_widgets.utils.plugin_utils import BECClassContainer, get_rpc_classes
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import get_overloads
|
||||
@@ -40,17 +39,20 @@ from bec_widgets.cli.client_utils import RPCBase, rpc_call, BECGuiClientMixin
|
||||
|
||||
self.content = ""
|
||||
|
||||
def generate_client(
|
||||
self, published_classes: dict[Literal["connector_classes", "top_level_classes"], list[type]]
|
||||
):
|
||||
def generate_client(self, class_container: BECClassContainer):
|
||||
"""
|
||||
Generate the client for the published classes.
|
||||
|
||||
Args:
|
||||
published_classes(dict): A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
|
||||
class_container: The class container with the classes to generate the client for.
|
||||
"""
|
||||
self.write_client_enum(published_classes["top_level_classes"])
|
||||
for cls in published_classes["connector_classes"]:
|
||||
rpc_top_level_classes = class_container.rpc_top_level_classes
|
||||
rpc_top_level_classes.sort(key=lambda x: x.__name__)
|
||||
connector_classes = class_container.connector_classes
|
||||
connector_classes.sort(key=lambda x: x.__name__)
|
||||
|
||||
self.write_client_enum(rpc_top_level_classes)
|
||||
for cls in connector_classes:
|
||||
self.content += "\n\n"
|
||||
self.generate_content_for_class(cls)
|
||||
|
||||
@@ -156,13 +158,12 @@ def main():
|
||||
client_path = os.path.join(current_path, "client.py")
|
||||
|
||||
rpc_classes = get_rpc_classes("bec_widgets")
|
||||
rpc_classes["connector_classes"].sort(key=lambda x: x.__name__)
|
||||
|
||||
generator = ClientGenerator()
|
||||
generator.generate_client(rpc_classes)
|
||||
generator.write(client_path)
|
||||
|
||||
for cls in rpc_classes["top_level_classes"]:
|
||||
for cls in rpc_classes.plugins:
|
||||
plugin = DesignerPluginGenerator(cls)
|
||||
if not hasattr(plugin, "info"):
|
||||
continue
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import signal
|
||||
import sys
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
@@ -27,12 +28,13 @@ class BECWidgetsCLIServer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gui_id: str = None,
|
||||
gui_id: str,
|
||||
dispatcher: BECDispatcher = None,
|
||||
client=None,
|
||||
config=None,
|
||||
gui_class: Union[BECFigure, BECDockArea] = BECFigure,
|
||||
) -> None:
|
||||
self.status = messages.BECStatus.BUSY
|
||||
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
|
||||
self.client = self.dispatcher.client if client is None else client
|
||||
self.client.start()
|
||||
@@ -46,11 +48,12 @@ class BECWidgetsCLIServer:
|
||||
)
|
||||
|
||||
# Setup QTimer for heartbeat
|
||||
self._shutdown_event = False
|
||||
self._heartbeat_timer = QTimer()
|
||||
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
||||
self._heartbeat_timer.start(200)
|
||||
|
||||
self.status = messages.BECStatus.RUNNING
|
||||
|
||||
def on_rpc_update(self, msg: dict, metadata: dict):
|
||||
request_id = metadata.get("request_id")
|
||||
try:
|
||||
@@ -110,16 +113,16 @@ class BECWidgetsCLIServer:
|
||||
return obj
|
||||
|
||||
def emit_heartbeat(self):
|
||||
if self._shutdown_event is False:
|
||||
self.client.connector.set(
|
||||
MessageEndpoints.gui_heartbeat(self.gui_id),
|
||||
messages.StatusMessage(name=self.gui_id, status=1, info={}),
|
||||
expire=1,
|
||||
)
|
||||
self.client.connector.set(
|
||||
MessageEndpoints.gui_heartbeat(self.gui_id),
|
||||
messages.StatusMessage(name=self.gui_id, status=self.status, info={}),
|
||||
expire=10,
|
||||
)
|
||||
|
||||
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
||||
self._shutdown_event = True
|
||||
self.status = messages.BECStatus.IDLE
|
||||
self._heartbeat_timer.stop()
|
||||
self.emit_heartbeat()
|
||||
self.gui.close()
|
||||
self.client.shutdown()
|
||||
|
||||
@@ -127,24 +130,44 @@ class BECWidgetsCLIServer:
|
||||
class SimpleFileLikeFromLogOutputFunc:
|
||||
def __init__(self, log_func):
|
||||
self._log_func = log_func
|
||||
self._buffer = []
|
||||
|
||||
def write(self, buffer):
|
||||
for line in buffer.rstrip().splitlines():
|
||||
line = line.rstrip()
|
||||
if line:
|
||||
self._log_func(line)
|
||||
self._buffer.append(buffer)
|
||||
|
||||
def flush(self):
|
||||
return
|
||||
lines, _, remaining = "".join(self._buffer).rpartition("\n")
|
||||
self._log_func(lines)
|
||||
self._buffer = [remaining]
|
||||
|
||||
def close(self):
|
||||
return
|
||||
|
||||
|
||||
def _start_server(gui_id: str, gui_class: Union[BECFigure, BECDockArea], config: str | None = None):
|
||||
if config:
|
||||
try:
|
||||
config = json.loads(config)
|
||||
service_config = ServiceConfig(config=config)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
service_config = ServiceConfig(config_path=config)
|
||||
else:
|
||||
# if no config is provided, use the default config
|
||||
service_config = ServiceConfig()
|
||||
|
||||
bec_logger.configure(
|
||||
service_config.redis,
|
||||
QtRedisConnector,
|
||||
service_name="BECWidgetsCLIServer",
|
||||
service_config=service_config.service_config,
|
||||
)
|
||||
server = BECWidgetsCLIServer(gui_id=gui_id, config=service_config, gui_class=gui_class)
|
||||
return server
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
@@ -159,7 +182,7 @@ def main():
|
||||
type=str,
|
||||
help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
|
||||
)
|
||||
parser.add_argument("--config", type=str, help="Config file")
|
||||
parser.add_argument("--config", type=str, help="Config file or config string.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -181,21 +204,15 @@ def main():
|
||||
module_path = os.path.dirname(bec_widgets.__file__)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(module_path, "assets", "bec_widgets_icon.png"), size=QSize(48, 48)
|
||||
os.path.join(module_path, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||
size=QSize(48, 48),
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
win = QMainWindow()
|
||||
win.setWindowTitle("BEC Widgets")
|
||||
|
||||
service_config = ServiceConfig(args.config)
|
||||
bec_logger.configure(
|
||||
service_config.redis,
|
||||
QtRedisConnector,
|
||||
service_name="BECWidgetsCLIServer",
|
||||
service_config=service_config.service_config,
|
||||
)
|
||||
server = BECWidgetsCLIServer(gui_id=args.id, config=service_config, gui_class=gui_class)
|
||||
server = _start_server(args.id, gui_class, args.config)
|
||||
|
||||
gui = server.gui
|
||||
win.setCentralWidget(gui)
|
||||
@@ -216,4 +233,5 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.argv = ["bec_widgets.cli.server", "--id", "test", "--gui_class", "BECDockArea"]
|
||||
main()
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from .motor_movement import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorControlPanelRelative,
|
||||
MotorCoordinateTable,
|
||||
MotorThread,
|
||||
)
|
||||
|
||||
92
bec_widgets/examples/general_app/general_app.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QActionGroup, QIcon
|
||||
from qtpy.QtWidgets import QApplication, QMainWindow, QStyle
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.examples.general_app.web_links import BECWebLinksMixin
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.ui_loader import UILoader
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class BECGeneralApp(QMainWindow):
|
||||
def __init__(self, parent=None):
|
||||
super(BECGeneralApp, self).__init__(parent)
|
||||
ui_file_path = os.path.join(os.path.dirname(__file__), "general_app.ui")
|
||||
self.load_ui(ui_file_path)
|
||||
|
||||
self.resize(1280, 720)
|
||||
|
||||
self.ini_ui()
|
||||
|
||||
def ini_ui(self):
|
||||
self._setup_icons()
|
||||
self._hook_menubar_docs()
|
||||
self._hook_theme_bar()
|
||||
|
||||
def load_ui(self, ui_file):
|
||||
loader = UILoader(self)
|
||||
self.ui = loader.loader(ui_file)
|
||||
self.setCentralWidget(self.ui)
|
||||
|
||||
def _hook_menubar_docs(self):
|
||||
# BEC Docs
|
||||
self.ui.action_BEC_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
|
||||
# BEC Widgets Docs
|
||||
self.ui.action_BEC_widgets_docs.triggered.connect(BECWebLinksMixin.open_bec_widgets_docs)
|
||||
# Bug report
|
||||
self.ui.action_bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
|
||||
|
||||
def change_theme(self, theme):
|
||||
apply_theme(theme)
|
||||
|
||||
def _setup_icons(self):
|
||||
help_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxQuestion)
|
||||
bug_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation)
|
||||
computer_icon = QIcon.fromTheme("computer")
|
||||
widget_icon = QIcon(os.path.join(MODULE_PATH, "assets", "designer_icons", "dock_area.png"))
|
||||
|
||||
self.ui.action_BEC_docs.setIcon(help_icon)
|
||||
self.ui.action_BEC_widgets_docs.setIcon(help_icon)
|
||||
self.ui.action_bug_report.setIcon(bug_icon)
|
||||
|
||||
self.ui.central_tab.setTabIcon(0, widget_icon)
|
||||
self.ui.central_tab.setTabIcon(1, computer_icon)
|
||||
|
||||
def _hook_theme_bar(self):
|
||||
self.ui.action_light.setCheckable(True)
|
||||
self.ui.action_dark.setCheckable(True)
|
||||
|
||||
# Create an action group to make sure only one can be checked at a time
|
||||
theme_group = QActionGroup(self)
|
||||
theme_group.addAction(self.ui.action_light)
|
||||
theme_group.addAction(self.ui.action_dark)
|
||||
theme_group.setExclusive(True)
|
||||
|
||||
# Connect the actions to the theme change method
|
||||
|
||||
self.ui.action_light.triggered.connect(lambda: self.change_theme("light"))
|
||||
self.ui.action_dark.triggered.connect(lambda: self.change_theme("dark"))
|
||||
|
||||
self.ui.action_dark.trigger()
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "BEC-Dark.png"), size=QSize(48, 48)
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
main_window = BECGeneralApp()
|
||||
main_window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
262
bec_widgets/examples/general_app/general_app.ui
Normal file
@@ -0,0 +1,262 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1718</width>
|
||||
<height>1139</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<property name="tabShape">
|
||||
<enum>QTabWidget::TabShape::Rounded</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="central_tab">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="dock_area_tab">
|
||||
<attribute name="title">
|
||||
<string>Dock Area</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="BECDockArea" name="dock_area"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="vscode_tab">
|
||||
<attribute name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::Computer"/>
|
||||
</attribute>
|
||||
<attribute name="title">
|
||||
<string>Visual Studio Code</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="VSCodeEditor" name="vscode"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1718</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
<property name="title">
|
||||
<string>Help</string>
|
||||
</property>
|
||||
<addaction name="action_BEC_docs"/>
|
||||
<addaction name="action_BEC_widgets_docs"/>
|
||||
<addaction name="action_bug_report"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuTheme">
|
||||
<property name="title">
|
||||
<string>Theme</string>
|
||||
</property>
|
||||
<addaction name="action_light"/>
|
||||
<addaction name="action_dark"/>
|
||||
</widget>
|
||||
<addaction name="menuTheme"/>
|
||||
<addaction name="menuHelp"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QDockWidget" name="dock_scan_control">
|
||||
<property name="windowTitle">
|
||||
<string>Scan Control</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="ScanControl" name="scan_control"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="dock_status_2">
|
||||
<property name="windowTitle">
|
||||
<string>BEC Service Status</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_3">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="BECStatusBox" name="bec_status_box_2"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="dock_queue">
|
||||
<property name="windowTitle">
|
||||
<string>Scan Queue</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_4">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="BECQueue" name="bec_queue">
|
||||
<row/>
|
||||
<column/>
|
||||
<column/>
|
||||
<column/>
|
||||
<item row="0" column="0"/>
|
||||
<item row="0" column="1"/>
|
||||
<item row="0" column="2"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<action name="action_BEC_docs">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BEC Docs</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_BEC_widgets_docs">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BEC Widgets Docs</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_bug_report">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::DialogError"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Bug Report</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_light">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Light</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_dark">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Dark</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>WebsiteWidget</class>
|
||||
<extends>QWebEngineView</extends>
|
||||
<header>website_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECQueue</class>
|
||||
<extends>QTableWidget</extends>
|
||||
<header>bec_queue</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ScanControl</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>scan_control</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>VSCodeEditor</class>
|
||||
<extends>WebsiteWidget</extends>
|
||||
<header>vs_code_editor</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECStatusBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_status_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECDockArea</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dock_area</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QWebEngineView</class>
|
||||
<extends></extends>
|
||||
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
15
bec_widgets/examples/general_app/web_links.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import webbrowser
|
||||
|
||||
|
||||
class BECWebLinksMixin:
|
||||
@staticmethod
|
||||
def open_bec_docs():
|
||||
webbrowser.open("https://beamline-experiment-control.readthedocs.io/en/latest/")
|
||||
|
||||
@staticmethod
|
||||
def open_bec_widgets_docs():
|
||||
webbrowser.open("https://bec.readthedocs.io/projects/bec-widgets/en/latest/")
|
||||
|
||||
@staticmethod
|
||||
def open_bec_bug_report():
|
||||
webbrowser.open("https://gitlab.psi.ch/groups/bec/-/issues/")
|
||||
@@ -2,14 +2,20 @@ import os
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import qdarktheme
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QSplitter,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils import BECDispatcher, UILoader
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
@@ -21,14 +27,8 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "jupyter_console_window.ui"), self)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
self.ui.splitter.setSizes([200, 100])
|
||||
self.safe_close = False
|
||||
|
||||
# console push
|
||||
if self.console.inprocess is True:
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
@@ -40,27 +40,49 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"w1": self.w1,
|
||||
"w2": self.w2,
|
||||
"w3": self.w3,
|
||||
"w1_c": self.w1_c,
|
||||
"w2_c": self.w2_c,
|
||||
"w3_c": self.w3_c,
|
||||
"w4": self.w4,
|
||||
"w5": self.w5,
|
||||
"w6": self.w6,
|
||||
"w7": self.w7,
|
||||
"w8": self.w8,
|
||||
"w9": self.w9,
|
||||
"w10": self.w10,
|
||||
"d0": self.d0,
|
||||
"d1": self.d1,
|
||||
"d2": self.d2,
|
||||
"plt": self.plt,
|
||||
"bar": self.bar,
|
||||
"wave": self.wf,
|
||||
# "bar": self.bar,
|
||||
# "cm": self.colormap,
|
||||
"im": self.im,
|
||||
"mm": self.mm,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_ui(self):
|
||||
# Plotting window
|
||||
self.glw_1_layout = QVBoxLayout(self.ui.glw) # Create a new QVBoxLayout
|
||||
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
|
||||
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
|
||||
self.layout = QHBoxLayout(self)
|
||||
|
||||
self.dock_layout = QVBoxLayout(self.ui.dock_placeholder)
|
||||
self.dock = BECDockArea(gui_id="remote")
|
||||
self.dock_layout.addWidget(self.dock)
|
||||
# Horizontal splitter
|
||||
splitter = QSplitter(self)
|
||||
self.layout.addWidget(splitter)
|
||||
|
||||
tab_widget = QTabWidget(splitter)
|
||||
|
||||
first_tab = QWidget()
|
||||
first_tab_layout = QVBoxLayout(first_tab)
|
||||
self.dock = BECDockArea(gui_id="dock")
|
||||
first_tab_layout.addWidget(self.dock)
|
||||
tab_widget.addTab(first_tab, "Dock Area")
|
||||
|
||||
second_tab = QWidget()
|
||||
second_tab_layout = QVBoxLayout(second_tab)
|
||||
self.figure = BECFigure(parent=self, gui_id="figure")
|
||||
second_tab_layout.addWidget(self.figure)
|
||||
tab_widget.addTab(second_tab, "BEC Figure")
|
||||
|
||||
group_box = QGroupBox("Jupyter Console", splitter)
|
||||
group_box_layout = QVBoxLayout(group_box)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
group_box_layout.addWidget(self.console)
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
@@ -68,72 +90,102 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
# init dock for testing
|
||||
self._init_dock()
|
||||
|
||||
self.console_layout = QVBoxLayout(self.ui.widget_console)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
self.console_layout.addWidget(self.console)
|
||||
self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
def _init_figure(self):
|
||||
self.figure.plot(x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="cividis")
|
||||
self.figure.motor_map("samx", "samy")
|
||||
self.figure.image("eiger", color_map="viridis", vrange=(0, 100))
|
||||
self.figure.plot(
|
||||
x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="magma", new=True
|
||||
self.w1 = self.figure.plot(
|
||||
x_name="samx",
|
||||
y_name="bpm4i",
|
||||
# title="Standard Plot with sync device, custom labels - w1",
|
||||
# x_label="Motor Position",
|
||||
# y_label="Intensity (A.U.)",
|
||||
row=0,
|
||||
col=0,
|
||||
)
|
||||
self.w1.set(
|
||||
title="Standard Plot with sync device, custom labels - w1",
|
||||
x_label="Motor Position",
|
||||
y_label="Intensity (A.U.)",
|
||||
)
|
||||
self.w2 = self.figure.motor_map("samx", "samy", row=0, col=1)
|
||||
self.w3 = self.figure.image(
|
||||
"eiger", color_map="viridis", vrange=(0, 100), title="Eiger Image - w3", row=0, col=2
|
||||
)
|
||||
self.w4 = self.figure.plot(
|
||||
x_name="samx",
|
||||
y_name="samy",
|
||||
z_name="bpm4i",
|
||||
color_map_z="magma",
|
||||
new=True,
|
||||
title="2D scatter plot - w4",
|
||||
row=0,
|
||||
col=3,
|
||||
)
|
||||
self.w5 = self.figure.plot(
|
||||
y_name="bpm4i",
|
||||
new=True,
|
||||
title="Best Effort Plot - w5",
|
||||
dap="GaussianModel",
|
||||
row=1,
|
||||
col=0,
|
||||
)
|
||||
self.w6 = self.figure.plot(
|
||||
x_name="timestamp", y_name="bpm4i", new=True, title="Timestamp Plot - w6", row=1, col=1
|
||||
)
|
||||
self.w7 = self.figure.plot(
|
||||
x_name="index", y_name="bpm4i", new=True, title="Index Plot - w7", row=1, col=2
|
||||
)
|
||||
self.w8 = self.figure.plot(
|
||||
y_name="monitor_async", new=True, title="Async Plot - Best Effort - w8", row=2, col=0
|
||||
)
|
||||
self.w9 = self.figure.plot(
|
||||
x_name="timestamp",
|
||||
y_name="monitor_async",
|
||||
new=True,
|
||||
title="Async Plot - timestamp - w9",
|
||||
row=2,
|
||||
col=1,
|
||||
)
|
||||
self.w10 = self.figure.plot(
|
||||
x_name="index",
|
||||
y_name="monitor_async",
|
||||
new=True,
|
||||
title="Async Plot - index - w10",
|
||||
row=2,
|
||||
col=2,
|
||||
)
|
||||
|
||||
self.figure.change_layout(2, 2)
|
||||
|
||||
self.w1 = self.figure[0, 0]
|
||||
self.w2 = self.figure[0, 1]
|
||||
self.w3 = self.figure[1, 0]
|
||||
self.w4 = self.figure[1, 1]
|
||||
|
||||
# Plot Customisation
|
||||
self.w1.set_title("Waveform 1")
|
||||
self.w1.set_x_label("Motor Position (samx)")
|
||||
self.w1.set_y_label("Intensity A.U.")
|
||||
|
||||
# Image Customisation
|
||||
self.w3.set_title("Eiger Image")
|
||||
self.w3.set_x_label("X")
|
||||
self.w3.set_y_label("Y")
|
||||
|
||||
# Configs to try to pass
|
||||
self.w1_c = self.w1._config_dict
|
||||
self.w2_c = self.w2._config_dict
|
||||
self.w3_c = self.w3._config_dict
|
||||
|
||||
# curves for w1
|
||||
self.c1 = self.w1.get_config()
|
||||
|
||||
self.fig_c = self.figure._config_dict
|
||||
|
||||
def _init_dock(self):
|
||||
|
||||
self.d0 = self.dock.add_dock(name="dock_0")
|
||||
self.fig0 = self.d0.add_widget("BECFigure")
|
||||
data = np.random.rand(10, 2)
|
||||
self.fig0.plot(data, label="2d Data")
|
||||
self.fig0.image("eiger", vrange=(0, 100))
|
||||
self.mm = self.d0.add_widget("BECMotorMapWidget")
|
||||
self.mm.change_motors("samx", "samy")
|
||||
|
||||
self.d1 = self.dock.add_dock(name="dock_1", position="right")
|
||||
self.fig1 = self.d1.add_widget("BECFigure")
|
||||
self.fig1.plot(x_name="samx", y_name="bpm4i")
|
||||
self.fig1.plot(x_name="samx", y_name="bpm3a")
|
||||
self.im = self.d1.add_widget("BECImageWidget")
|
||||
self.im.image("eiger")
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.fig2 = self.d2.add_widget("BECFigure", row=0, col=0)
|
||||
self.plt = self.fig2.plot(x_name="samx", y_name="bpm3a")
|
||||
self.plt.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
||||
self.bar = self.d2.add_widget("RingProgressBar", row=0, col=1)
|
||||
self.bar.set_diameter(200)
|
||||
self.wf = self.d2.add_widget("BECWaveformWidget", row=0, col=0)
|
||||
self.wf.plot(x_name="samx", y_name="bpm3a")
|
||||
self.wf.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
||||
# self.bar = self.d2.add_widget("RingProgressBar", row=0, col=1)
|
||||
# self.bar.set_diameter(200)
|
||||
|
||||
# self.d3 = self.dock.add_dock(name="dock_3", position="bottom")
|
||||
# self.colormap = pg.GradientWidget()
|
||||
# self.d3.add_widget(self.colormap, row=0, col=0)
|
||||
|
||||
self.dock.save_state()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Override to handle things when main window is closed."""
|
||||
self.dock.cleanup()
|
||||
self.figure.clear_all()
|
||||
self.figure.client.shutdown()
|
||||
self.dock.close()
|
||||
self.figure.cleanup()
|
||||
self.figure.close()
|
||||
self.console.close()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
@@ -147,9 +199,11 @@ if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Jupyter Console")
|
||||
app.setApplicationDisplayName("Jupyter Console")
|
||||
qdarktheme.setup_theme("auto")
|
||||
apply_theme("dark")
|
||||
icon = QIcon()
|
||||
icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
|
||||
icon.addFile(
|
||||
os.path.join(module_path, "assets", "app_icons", "terminal_icon.png"), size=QSize(48, 48)
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>2104</width>
|
||||
<height>966</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Plotting Console</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab_1">
|
||||
<attribute name="title">
|
||||
<string>BECDock</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="dock_placeholder" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>BECFigure</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QWidget" name="glw" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="widget_console" native="true"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -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>
|
||||
@@ -7,7 +7,8 @@ import sys
|
||||
|
||||
from bec_ipython_client.main import BECIPythonClient
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from tictactoe import TicTacToe
|
||||
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
from tictactoe import TicTacToe
|
||||
from tictactoeplugin import TicTacToePlugin
|
||||
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoeplugin import TicTacToePlugin
|
||||
|
||||
# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class TicTacToe(QWidget): # pragma: no cover
|
||||
super().__init__(parent)
|
||||
self._state = DEFAULT_STATE
|
||||
self._turn_number = 0
|
||||
print("TicTac HERE !!!!!!")
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return QSize(200, 200)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QIcon
|
||||
from tictactoe import TicTacToe
|
||||
from tictactoetaskmenu import TicTacToeTaskMenuFactory
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoetaskmenu import TicTacToeTaskMenuFactory
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
@@ -24,6 +27,8 @@ DOM_XML = """
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class TicTacToePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -38,10 +43,10 @@ class TicTacToePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
return "Games"
|
||||
|
||||
def icon(self):
|
||||
return QIcon()
|
||||
return designer_material_icon("sports_esports")
|
||||
|
||||
def includeFile(self):
|
||||
return "tictactoe"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
|
||||
from qtpy.QtGui import QAction
|
||||
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
|
||||
from tictactoe import TicTacToe
|
||||
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
|
||||
|
||||
class TicTacToeDialog(QDialog): # pragma: no cover
|
||||
|
||||
214
bec_widgets/qt_utils/error_popups.py
Normal file
@@ -0,0 +1,214 @@
|
||||
import functools
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from qtpy.QtCore import QObject, Qt, Signal, Slot
|
||||
from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
||||
"""Function with args, acting like a decorator, applying "error_managed" decorator + Qt Slot
|
||||
to the passed function, to display errors instead of potentially raising an exception
|
||||
|
||||
'popup_error' keyword argument can be passed with boolean value if a dialog should pop up,
|
||||
otherwise error display is left to the original exception hook
|
||||
"""
|
||||
popup_error = bool(slot_kwargs.pop("popup_error", False))
|
||||
|
||||
def error_managed(method):
|
||||
@Slot(*slot_args, **slot_kwargs)
|
||||
@functools.wraps(method)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return method(*args, **kwargs)
|
||||
except Exception:
|
||||
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=popup_error)
|
||||
|
||||
return wrapper
|
||||
|
||||
return error_managed
|
||||
|
||||
|
||||
class WarningPopupUtility(QObject):
|
||||
"""
|
||||
Utility class to show warning popups in the application.
|
||||
"""
|
||||
|
||||
@SafeSlot(str, str, str, QWidget)
|
||||
def show_warning_message(self, title, message, detailed_text, widget):
|
||||
msg = QMessageBox(widget)
|
||||
msg.setIcon(QMessageBox.Warning)
|
||||
msg.setWindowTitle(title)
|
||||
msg.setText(message)
|
||||
msg.setStandardButtons(QMessageBox.Ok)
|
||||
msg.setDetailedText(detailed_text)
|
||||
msg.exec_()
|
||||
|
||||
def show_warning(self, title: str, message: str, detailed_text: str, widget: QWidget = None):
|
||||
"""
|
||||
Show a warning message with the given title, message, and detailed text.
|
||||
|
||||
Args:
|
||||
title (str): The title of the warning message.
|
||||
message (str): The main text of the warning message.
|
||||
detailed_text (str): The detailed text to show when the user expands the message.
|
||||
widget (QWidget): The parent widget for the message box.
|
||||
"""
|
||||
self.show_warning_message(title, message, detailed_text, widget)
|
||||
|
||||
|
||||
_popup_utility_instance = None
|
||||
|
||||
|
||||
class _ErrorPopupUtility(QObject):
|
||||
"""
|
||||
Utility class to manage error popups in the application to show error messages to the users.
|
||||
This class is singleton and the error popup can be enabled or disabled globally or attach to widget methods with decorator @error_managed.
|
||||
"""
|
||||
|
||||
error_occurred = Signal(str, str, QWidget)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.error_occurred.connect(self.show_error_message)
|
||||
self.enable_error_popup = False
|
||||
self._initialized = True
|
||||
sys.excepthook = self.custom_exception_hook
|
||||
|
||||
@SafeSlot(str, str, QWidget)
|
||||
def show_error_message(self, title, message, widget):
|
||||
detailed_text = self.format_traceback(message)
|
||||
error_message = self.parse_error_message(detailed_text)
|
||||
|
||||
msg = QMessageBox(widget)
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
msg.setWindowTitle(title)
|
||||
msg.setText(error_message)
|
||||
msg.setStandardButtons(QMessageBox.Ok)
|
||||
msg.setDetailedText(detailed_text)
|
||||
msg.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
msg.setMinimumWidth(600)
|
||||
msg.setMinimumHeight(400)
|
||||
msg.exec_()
|
||||
|
||||
def format_traceback(self, traceback_message: str) -> str:
|
||||
"""
|
||||
Format the traceback message to be displayed in the error popup by adding indentation to each line.
|
||||
|
||||
Args:
|
||||
traceback_message(str): The traceback message to be formatted.
|
||||
|
||||
Returns:
|
||||
str: The formatted traceback message.
|
||||
"""
|
||||
formatted_lines = []
|
||||
lines = traceback_message.split("\n")
|
||||
for line in lines:
|
||||
formatted_lines.append(" " + line) # Add indentation to each line
|
||||
return "\n".join(formatted_lines)
|
||||
|
||||
def parse_error_message(self, traceback_message):
|
||||
lines = traceback_message.split("\n")
|
||||
error_message = "Error occurred. See details."
|
||||
capture = False
|
||||
captured_message = []
|
||||
|
||||
for line in lines:
|
||||
if "raise" in line:
|
||||
capture = True
|
||||
continue
|
||||
if capture:
|
||||
if line.strip() and not line.startswith(" File "):
|
||||
captured_message.append(line.strip())
|
||||
else:
|
||||
break
|
||||
|
||||
if captured_message:
|
||||
error_message = " ".join(captured_message)
|
||||
return error_message
|
||||
|
||||
def custom_exception_hook(self, exctype, value, tb, popup_error=False):
|
||||
if popup_error or self.enable_error_popup:
|
||||
error_message = traceback.format_exception(exctype, value, tb)
|
||||
self.error_occurred.emit(
|
||||
"Method error" if popup_error else "Application Error",
|
||||
"".join(error_message),
|
||||
self.parent(),
|
||||
)
|
||||
else:
|
||||
sys.__excepthook__(exctype, value, tb) # Call the original excepthook
|
||||
|
||||
def enable_global_error_popups(self, state: bool):
|
||||
"""
|
||||
Enable or disable global error popups for all applications.
|
||||
|
||||
Args:
|
||||
state(bool): True to enable error popups, False to disable error popups.
|
||||
"""
|
||||
self.enable_error_popup = bool(state)
|
||||
|
||||
|
||||
def ErrorPopupUtility():
|
||||
global _popup_utility_instance
|
||||
if not _popup_utility_instance:
|
||||
_popup_utility_instance = _ErrorPopupUtility()
|
||||
return _popup_utility_instance
|
||||
|
||||
|
||||
class ExampleWidget(QWidget): # pragma: no cover
|
||||
"""
|
||||
Example widget to demonstrate error handling with the ErrorPopupUtility.
|
||||
|
||||
Warnings -> This example works properly only with PySide6, PyQt6 has a bug with the error handling.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.init_ui()
|
||||
self.warning_utility = WarningPopupUtility(self)
|
||||
|
||||
def init_ui(self):
|
||||
self.layout = QVBoxLayout(self)
|
||||
|
||||
# Button to trigger method with error handling
|
||||
self.error_button = QPushButton("Trigger Handled Error", self)
|
||||
self.error_button.clicked.connect(self.method_with_error_handling)
|
||||
self.layout.addWidget(self.error_button)
|
||||
|
||||
# Button to trigger method without error handling
|
||||
self.normal_button = QPushButton("Trigger Normal Error", self)
|
||||
self.normal_button.clicked.connect(self.method_without_error_handling)
|
||||
self.layout.addWidget(self.normal_button)
|
||||
|
||||
# Button to trigger warning popup
|
||||
self.warning_button = QPushButton("Trigger Warning", self)
|
||||
self.warning_button.clicked.connect(self.trigger_warning)
|
||||
self.layout.addWidget(self.warning_button)
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def method_with_error_handling(self):
|
||||
"""This method raises an error and the exception is handled by the decorator."""
|
||||
raise ValueError("This is a handled error.")
|
||||
|
||||
@SafeSlot()
|
||||
def method_without_error_handling(self):
|
||||
"""This method raises an error and the exception is not handled here."""
|
||||
raise ValueError("This is an unhandled error.")
|
||||
|
||||
@SafeSlot()
|
||||
def trigger_warning(self):
|
||||
"""Trigger a warning using the WarningPopupUtility."""
|
||||
self.warning_utility.show_warning(
|
||||
title="Warning",
|
||||
message="This is a warning message.",
|
||||
detailed_text="This is the detailed text of the warning message.",
|
||||
widget=self,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = ExampleWidget()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
47
bec_widgets/qt_utils/redis_message_waiter.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from bec_lib.serialization import MsgpackSerialization
|
||||
from bec_lib.utils import lazy_import_from
|
||||
from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer
|
||||
|
||||
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
|
||||
|
||||
|
||||
class QtRedisMessageWaiter:
|
||||
def __init__(self, redis_connector, message_to_wait):
|
||||
self.ev_loop = QEventLoop()
|
||||
self.response = None
|
||||
self.connector = redis_connector
|
||||
self.message_to_wait = message_to_wait
|
||||
self.pubsub = redis_connector._redis_conn.pubsub()
|
||||
self.pubsub.subscribe(self.message_to_wait.endpoint)
|
||||
fd = self.pubsub.connection._sock.fileno()
|
||||
self.notifier = QSocketNotifier(fd, QSocketNotifier.Read)
|
||||
self.notifier.activated.connect(self._pubsub_readable)
|
||||
|
||||
def _msg_received(self, msg_obj):
|
||||
self.response = msg_obj.value
|
||||
self.ev_loop.quit()
|
||||
|
||||
def wait(self, timeout=1):
|
||||
timer = QTimer()
|
||||
timer.singleShot(timeout * 1000, self.ev_loop.quit)
|
||||
self.ev_loop.exec_()
|
||||
timer.stop()
|
||||
self.notifier.setEnabled(False)
|
||||
self.pubsub.close()
|
||||
return self.response
|
||||
|
||||
def _pubsub_readable(self, fd):
|
||||
while True:
|
||||
msg = self.pubsub.get_message()
|
||||
if msg:
|
||||
if msg["type"] == "subscribe":
|
||||
# get_message buffers, so we may already have the answer
|
||||
# let's check...
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
return
|
||||
channel = msg["channel"].decode()
|
||||
msg = MessageObject(topic=channel, value=MsgpackSerialization.loads(msg["data"]))
|
||||
self.connector._execute_callback(self._msg_received, msg, {})
|
||||
119
bec_widgets/qt_utils/settings_dialog.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the dialog.
|
||||
"""
|
||||
self.button_box.close()
|
||||
self.button_box.deleteLater()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.cleanup()
|
||||
super().closeEvent(event)
|
||||
221
bec_widgets/qt_utils/toolbar.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# pylint: disable=no-name-in-module
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QAction, QIcon
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
|
||||
|
||||
import bec_widgets
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class ToolBarAction(ABC):
|
||||
"""
|
||||
Abstract base class for toolbar actions.
|
||||
|
||||
Args:
|
||||
icon_path (str, optional): The name of the icon file from `assets/toolbar_icons`. Defaults to None.
|
||||
tooltip (bool, optional): The tooltip for the action. Defaults to None.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(self, icon_path: str = None, tooltip: str = None, checkable: bool = False):
|
||||
self.icon_path = (
|
||||
os.path.join(MODULE_PATH, "assets", "toolbar_icons", icon_path) if icon_path else None
|
||||
)
|
||||
self.tooltip = tooltip
|
||||
self.checkable = checkable
|
||||
self.action = None
|
||||
|
||||
@abstractmethod
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
"""Adds an action or widget to a toolbar.
|
||||
|
||||
Args:
|
||||
toolbar (QToolBar): The toolbar to add the action or widget to.
|
||||
target (QWidget): The target widget for the action.
|
||||
"""
|
||||
|
||||
|
||||
class SeparatorAction(ToolBarAction):
|
||||
"""Separator action for the toolbar."""
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
self.separator = QToolButton()
|
||||
self.separator.setFixedSize(2, 22)
|
||||
toolbar.addWidget(self.separator)
|
||||
|
||||
|
||||
class IconAction(ToolBarAction):
|
||||
"""
|
||||
Action with an icon for the toolbar.
|
||||
|
||||
Args:
|
||||
icon_path (str): The path to the icon file.
|
||||
tooltip (str): The tooltip for the action.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(self, icon_path: str = None, tooltip: str = None, checkable: bool = False):
|
||||
super().__init__(icon_path, tooltip, checkable)
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
icon = QIcon()
|
||||
icon.addFile(self.icon_path, size=QSize(20, 20))
|
||||
self.action = QAction(icon, self.tooltip, target)
|
||||
self.action.setCheckable(self.checkable)
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
|
||||
class MaterialIconAction:
|
||||
"""
|
||||
Action with a Material icon for the toolbar.
|
||||
|
||||
Args:
|
||||
icon_path (str, optional): The name of the Material icon. Defaults to None.
|
||||
tooltip (bool, optional): The tooltip for the action. Defaults to None.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
filled (bool, optional): Whether the icon is filled. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
icon_name: str = None,
|
||||
tooltip: str = None,
|
||||
checkable: bool = False,
|
||||
filled: bool = False,
|
||||
):
|
||||
self.icon_name = icon_name
|
||||
self.tooltip = tooltip
|
||||
self.checkable = checkable
|
||||
self.action = None
|
||||
self.filled = filled
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
icon = self.get_icon()
|
||||
self.action = QAction(icon, self.tooltip, target)
|
||||
self.action.setCheckable(self.checkable)
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
def get_icon(self):
|
||||
|
||||
icon = material_icon(
|
||||
self.icon_name, size=(20, 20), convert_to_pixmap=False, filled=self.filled
|
||||
)
|
||||
return icon
|
||||
|
||||
|
||||
class DeviceSelectionAction(ToolBarAction):
|
||||
"""
|
||||
Action for selecting a device in a combobox.
|
||||
|
||||
Args:
|
||||
label (str): The label for the combobox.
|
||||
device_combobox (DeviceComboBox): The combobox for selecting the device.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label: str, device_combobox):
|
||||
super().__init__()
|
||||
self.label = label
|
||||
self.device_combobox = device_combobox
|
||||
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
|
||||
|
||||
def add_to_toolbar(self, toolbar, target):
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
label = QLabel(f"{self.label}")
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(self.device_combobox)
|
||||
toolbar.addWidget(widget)
|
||||
|
||||
def set_combobox_style(self, color: str):
|
||||
self.device_combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
|
||||
|
||||
|
||||
class ExpandableMenuAction(ToolBarAction):
|
||||
"""
|
||||
Action for an expandable menu in the toolbar.
|
||||
|
||||
Args:
|
||||
label (str): The label for the menu.
|
||||
actions (dict): A dictionary of actions to populate the menu.
|
||||
icon_path (str, optional): The path to the icon file. Defaults to None.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label: str, actions: dict, icon_path: str = None):
|
||||
super().__init__(icon_path, label)
|
||||
self.actions = actions
|
||||
self.widgets = defaultdict(dict)
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
button = QToolButton(toolbar)
|
||||
if self.icon_path:
|
||||
button.setIcon(QIcon(self.icon_path))
|
||||
button.setText(self.tooltip)
|
||||
button.setPopupMode(QToolButton.InstantPopup)
|
||||
button.setStyleSheet(
|
||||
"""
|
||||
QToolButton {
|
||||
font-size: 14px;
|
||||
}
|
||||
QMenu {
|
||||
font-size: 14px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
menu = QMenu(button)
|
||||
for action_id, action in self.actions.items():
|
||||
sub_action = QAction(action.tooltip, target)
|
||||
if hasattr(action, "icon_path"):
|
||||
icon = QIcon()
|
||||
icon.addFile(action.icon_path, size=QSize(20, 20))
|
||||
sub_action.setIcon(icon)
|
||||
elif hasattr(action, "get_icon"):
|
||||
sub_action.setIcon(action.get_icon())
|
||||
sub_action.setCheckable(action.checkable)
|
||||
menu.addAction(sub_action)
|
||||
self.widgets[action_id] = sub_action
|
||||
button.setMenu(menu)
|
||||
toolbar.addWidget(button)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, actions: dict | None = None, target_widget=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.widgets = defaultdict(dict)
|
||||
self.set_background_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):
|
||||
self.setIconSize(QSize(20, 20))
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -10,9 +10,11 @@ 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 qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.cli.rpc_register import RPCRegister
|
||||
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot
|
||||
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
||||
|
||||
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
||||
@@ -64,15 +66,29 @@ class Worker(QRunnable):
|
||||
|
||||
|
||||
class BECConnector:
|
||||
"""Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
|
||||
"""Connection mixin class to handle BEC client and device manager"""
|
||||
|
||||
USER_ACCESS = ["_config_dict", "_get_all_rpc"]
|
||||
EXIT_HANDLERS = {}
|
||||
|
||||
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
|
||||
# BEC related connections
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
|
||||
if not self.client in BECConnector.EXIT_HANDLERS:
|
||||
# register function to clean connections at exit;
|
||||
# the function depends on BECClient, and BECDispatcher
|
||||
@pyqtSlot()
|
||||
def terminate(client=self.client, dispatcher=self.bec_dispatcher):
|
||||
print("Disconnecting", repr(dispatcher))
|
||||
dispatcher.disconnect_all()
|
||||
print("Shutting down BEC Client", repr(client))
|
||||
client.shutdown()
|
||||
|
||||
BECConnector.EXIT_HANDLERS[self.client] = terminate
|
||||
QApplication.instance().aboutToQuit.connect(terminate)
|
||||
|
||||
if config:
|
||||
self.config = config
|
||||
self.config.widget_class = self.__class__.__name__
|
||||
@@ -90,9 +106,14 @@ class BECConnector:
|
||||
self.gui_id = self.config.gui_id
|
||||
|
||||
# register widget to rpc register
|
||||
# be careful: when registering, and the object is not a BECWidget,
|
||||
# cleanup has to called manually since there is no 'closeEvent'
|
||||
self.rpc_register = RPCRegister()
|
||||
self.rpc_register.add_rpc(self)
|
||||
|
||||
# Error popups
|
||||
self.error_utility = ErrorPopupUtility()
|
||||
|
||||
self._thread_pool = QThreadPool.globalInstance()
|
||||
|
||||
def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker:
|
||||
@@ -279,16 +300,3 @@ class BECConnector:
|
||||
return self.config.model_dump()
|
||||
else:
|
||||
return self.config
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
self.rpc_register.remove_rpc(self)
|
||||
all_connections = self.rpc_register.list_all_connections()
|
||||
if len(all_connections) == 0:
|
||||
print("No more connections. Shutting down GUI BEC client.")
|
||||
self.bec_dispatcher.disconnect_all()
|
||||
self.client.shutdown()
|
||||
|
||||
# def closeEvent(self, event):
|
||||
# self.cleanup()
|
||||
# super().closeEvent(event)
|
||||
|
||||
@@ -6,7 +6,9 @@ import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy import PYSIDE6
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
if PYSIDE6:
|
||||
from PySide6.scripts.pyside_tool import (
|
||||
@@ -21,6 +23,19 @@ if PYSIDE6:
|
||||
import bec_widgets
|
||||
|
||||
|
||||
def designer_material_icon(icon_name: str) -> QIcon:
|
||||
"""
|
||||
Create a QIcon for the BECDesigner with the given material icon name.
|
||||
|
||||
Args:
|
||||
icon_name (str): The name of the material icon.
|
||||
|
||||
Returns:
|
||||
QIcon: The QIcon for the material icon.
|
||||
"""
|
||||
return QIcon(material_icon(icon_name, filled=True, convert_to_pixmap=True))
|
||||
|
||||
|
||||
def list_editable_packages() -> set[str]:
|
||||
"""
|
||||
List all editable packages in the environment.
|
||||
@@ -73,26 +88,28 @@ def patch_designer(): # pragma: no cover
|
||||
os.environ["PY_MAJOR_VERSION"] = str(major_version)
|
||||
os.environ["PY_MINOR_VERSION"] = str(minor_version)
|
||||
|
||||
if sys.platform == "linux":
|
||||
if sys.platform == "win32":
|
||||
if is_virtual_env():
|
||||
_extend_path_var("PATH", os.fspath(Path(sys._base_executable).parent), True)
|
||||
else:
|
||||
if sys.platform == "linux":
|
||||
suffix = f"{sys.abiflags}.so"
|
||||
env_var = "LD_PRELOAD"
|
||||
elif sys.platform == "darwin":
|
||||
suffix = ".dylib"
|
||||
env_var = "DYLD_INSERT_LIBRARIES"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
||||
version = f"{major_version}.{minor_version}"
|
||||
library_name = f"libpython{version}{sys.abiflags}.so"
|
||||
if is_pyenv_python():
|
||||
library_name = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
|
||||
os.environ["LD_PRELOAD"] = library_name
|
||||
elif sys.platform == "darwin":
|
||||
library_name = f"libpython{major_version}.{minor_version}.dylib"
|
||||
library_name = f"libpython{version}{suffix}"
|
||||
lib_path = str(Path(sysconfig.get_config_var("LIBDIR")) / library_name)
|
||||
os.environ["DYLD_INSERT_LIBRARIES"] = lib_path
|
||||
os.environ[env_var] = lib_path
|
||||
|
||||
if is_pyenv_python() or is_virtual_env():
|
||||
# append all editable packages to the PYTHONPATH
|
||||
editable_packages = list_editable_packages()
|
||||
for pckg in editable_packages:
|
||||
_extend_path_var("PYTHONPATH", pckg, True)
|
||||
elif sys.platform == "win32":
|
||||
if is_virtual_env():
|
||||
_extend_path_var("PATH", os.fspath(Path(sys._base_executable).parent), True)
|
||||
|
||||
qt_tool_wrapper(ui_tool_binary("designer"), sys.argv[1:])
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import redis
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.redis_connector import MessageObject, RedisConnector
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from qtpy.QtCore import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QCoreApplication, QObject
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -75,7 +75,6 @@ class BECDispatcher:
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
qapp = None
|
||||
|
||||
def __new__(cls, client=None, config: str = None, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
@@ -87,9 +86,6 @@ class BECDispatcher:
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
if not QCoreApplication.instance():
|
||||
BECDispatcher.qapp = QCoreApplication([])
|
||||
|
||||
self._slots = collections.defaultdict(set)
|
||||
self.client = client
|
||||
|
||||
@@ -123,20 +119,13 @@ class BECDispatcher:
|
||||
cls._instance = None
|
||||
cls._initialized = False
|
||||
|
||||
if not cls.qapp:
|
||||
return
|
||||
|
||||
# shutdown QCoreApp if it exists
|
||||
if PYQT5 or PYQT6:
|
||||
cls.qapp.exit()
|
||||
elif PYSIDE2 or PYSIDE6:
|
||||
cls.qapp.shutdown()
|
||||
cls.qapp = None
|
||||
|
||||
def connect_slot(
|
||||
self, slot: Callable, topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]]
|
||||
self,
|
||||
slot: Callable,
|
||||
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Connect widget's pyqt slot, so that it is called on new pub/sub topic message.
|
||||
"""Connect widget's qt slot, so that it is called on new pub/sub topic message.
|
||||
|
||||
Args:
|
||||
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
||||
@@ -144,11 +133,18 @@ class BECDispatcher:
|
||||
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
|
||||
"""
|
||||
slot = QtThreadSafeCallback(slot)
|
||||
self.client.connector.register(topics, cb=slot)
|
||||
self.client.connector.register(topics, cb=slot, **kwargs)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._slots[slot].update(set(topics_str))
|
||||
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
||||
"""
|
||||
Disconnect a slot from a topic.
|
||||
|
||||
Args:
|
||||
slot(Callable): The slot to disconnect
|
||||
topics(Union[str, list]): The topic(s) to disconnect from
|
||||
"""
|
||||
# find the right slot to disconnect from ;
|
||||
# slot callbacks are wrapped in QtThreadSafeCallback objects,
|
||||
# but the slot we receive here is the original callable
|
||||
@@ -159,11 +155,17 @@ class BECDispatcher:
|
||||
return
|
||||
self.client.connector.unregister(topics, cb=connected_slot)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._slots[slot].difference_update(set(topics_str))
|
||||
if not self._slots[slot]:
|
||||
del self._slots[slot]
|
||||
self._slots[connected_slot].difference_update(set(topics_str))
|
||||
if not self._slots[connected_slot]:
|
||||
del self._slots[connected_slot]
|
||||
|
||||
def disconnect_topics(self, topics: Union[str, list]):
|
||||
"""
|
||||
Disconnect all slots from a topic.
|
||||
|
||||
Args:
|
||||
topics(Union[str, list]): The topic(s) to disconnect from
|
||||
"""
|
||||
self.client.connector.unregister(topics)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
for slot in list(self._slots.keys()):
|
||||
@@ -173,4 +175,11 @@ class BECDispatcher:
|
||||
del self._slots[slot]
|
||||
|
||||
def disconnect_all(self, *args, **kwargs):
|
||||
"""
|
||||
Disconnect all slots from all topics.
|
||||
|
||||
Args:
|
||||
*args: Arbitrary positional arguments
|
||||
**kwargs: Arbitrary keyword arguments
|
||||
"""
|
||||
self.disconnect_topics(self.client.connector._topics_cb)
|
||||
|
||||
28
bec_widgets/utils/bec_widget.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
|
||||
class BECWidget(BECConnector):
|
||||
"""Mixin class for all BEC widgets, to handle cleanup"""
|
||||
|
||||
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
|
||||
if not isinstance(self, QWidget):
|
||||
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
||||
super().__init__(client, config, gui_id)
|
||||
|
||||
# Set the theme to auto if it is not set yet
|
||||
app = QApplication.instance()
|
||||
if not hasattr(app, "theme"):
|
||||
set_theme("dark")
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.rpc_register.remove_rpc(self)
|
||||
try:
|
||||
self.cleanup()
|
||||
finally:
|
||||
super().closeEvent(event)
|
||||
@@ -1,10 +1,60 @@
|
||||
import itertools
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
import bec_qthemes
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_qthemes._os_appearance.listener import OSThemeSwitchListener
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
|
||||
def get_theme_palette():
|
||||
if QApplication.instance() is None or not hasattr(QApplication.instance(), "theme"):
|
||||
theme = "dark"
|
||||
else:
|
||||
theme = QApplication.instance().theme["theme"]
|
||||
return bec_qthemes.load_palette(theme)
|
||||
|
||||
|
||||
def set_theme(theme: Literal["dark", "light", "auto"]):
|
||||
"""
|
||||
Set the theme for the application.
|
||||
|
||||
Args:
|
||||
theme (Literal["dark", "light", "auto"]): The theme to set. "auto" will automatically switch between dark and light themes based on the system theme.
|
||||
"""
|
||||
app = QApplication.instance()
|
||||
bec_qthemes.setup_theme(theme)
|
||||
pg.setConfigOption("background", "w" if app.theme["theme"] == "light" else "k")
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if theme != "auto":
|
||||
return
|
||||
|
||||
def callback():
|
||||
app.theme["theme"] = listener._theme.lower()
|
||||
apply_theme(listener._theme.lower())
|
||||
|
||||
listener = OSThemeSwitchListener(callback)
|
||||
|
||||
app.installEventFilter(listener)
|
||||
|
||||
|
||||
def apply_theme(theme: Literal["dark", "light"]):
|
||||
app = QApplication.instance()
|
||||
# go through all pyqtgraph widgets and set background
|
||||
children = itertools.chain.from_iterable(
|
||||
top.findChildren(pg.GraphicsLayoutWidget) for top in app.topLevelWidgets()
|
||||
)
|
||||
for pg_widget in children:
|
||||
pg_widget.setBackground("k" if theme == "dark" else "w")
|
||||
|
||||
# now define stylesheet according to theme and apply it
|
||||
style = bec_qthemes.load_stylesheet(theme)
|
||||
app.setStyleSheet(style)
|
||||
|
||||
|
||||
class Colors:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import Type
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
# from qtpy.QtCore import QObject, pyqtSignal
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import QObject, Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
|
||||
class Crosshair(QObject):
|
||||
positionChanged = pyqtSignal(tuple)
|
||||
positionClicked = pyqtSignal(tuple)
|
||||
# Signal for 1D plot
|
||||
coordinatesChanged1D = pyqtSignal(tuple)
|
||||
coordinatesClicked1D = pyqtSignal(tuple)
|
||||
@@ -26,10 +30,13 @@ class Crosshair(QObject):
|
||||
super().__init__(parent)
|
||||
self.is_log_y = None
|
||||
self.is_log_x = None
|
||||
self.is_derivative = None
|
||||
self.plot_item = plot_item
|
||||
self.precision = precision
|
||||
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
||||
self.v_line.skip_auto_range = True
|
||||
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
||||
self.h_line.skip_auto_range = True
|
||||
self.plot_item.addItem(self.v_line, ignoreBounds=True)
|
||||
self.plot_item.addItem(self.h_line, ignoreBounds=True)
|
||||
self.proxy = pg.SignalProxy(
|
||||
@@ -37,74 +44,75 @@ class Crosshair(QObject):
|
||||
)
|
||||
self.plot_item.scene().sigMouseClicked.connect(self.mouse_clicked)
|
||||
|
||||
self.plot_item.ctrl.derivativeCheck.checkStateChanged.connect(self.check_derivatives)
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
||||
|
||||
# Initialize markers
|
||||
self.marker_moved_1d = []
|
||||
self.marker_clicked_1d = []
|
||||
self.marker_moved_1d = {}
|
||||
self.marker_clicked_1d = {}
|
||||
self.marker_2d = None
|
||||
self.update_markers()
|
||||
|
||||
def update_markers(self):
|
||||
"""Update the markers for the crosshair, creating new ones if necessary."""
|
||||
|
||||
# Clear existing markers
|
||||
for marker in self.marker_moved_1d + self.marker_clicked_1d:
|
||||
self.plot_item.removeItem(marker)
|
||||
if self.marker_2d:
|
||||
self.plot_item.removeItem(self.marker_2d)
|
||||
|
||||
# Create new markers
|
||||
self.marker_moved_1d = []
|
||||
self.marker_clicked_1d = []
|
||||
self.marker_2d = None
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||
if item.name() in self.marker_moved_1d:
|
||||
continue
|
||||
pen = item.opts["pen"]
|
||||
color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
|
||||
marker_moved = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
||||
)
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(None), brush=pg.mkBrush(color)
|
||||
)
|
||||
self.marker_moved_1d.append(marker_moved)
|
||||
marker_moved.skip_auto_range = True
|
||||
self.marker_moved_1d[item.name()] = marker_moved
|
||||
self.plot_item.addItem(marker_moved)
|
||||
|
||||
# Create glowing effect markers for clicked events
|
||||
marker_clicked_list = []
|
||||
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
size=size,
|
||||
pen=pg.mkPen(None),
|
||||
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
|
||||
)
|
||||
marker_clicked_list.append(marker_clicked)
|
||||
marker_clicked.skip_auto_range = True
|
||||
self.marker_clicked_1d[item.name()] = marker_clicked
|
||||
self.plot_item.addItem(marker_clicked)
|
||||
|
||||
self.marker_clicked_1d.append(marker_clicked_list)
|
||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||
if self.marker_2d is not None:
|
||||
continue
|
||||
self.marker_2d = pg.ROI(
|
||||
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
||||
)
|
||||
self.plot_item.addItem(self.marker_2d)
|
||||
|
||||
def snap_to_data(self, x, y) -> tuple:
|
||||
def snap_to_data(self, x, y) -> tuple[defaultdict[list], defaultdict[list]]:
|
||||
"""
|
||||
Finds the nearest data points to the given x and y coordinates.
|
||||
|
||||
Args:
|
||||
x: The x-coordinate
|
||||
y: The y-coordinate
|
||||
x: The x-coordinate of the mouse cursor
|
||||
y: The y-coordinate of the mouse cursor
|
||||
|
||||
Returns:
|
||||
tuple: The nearest x and y values
|
||||
tuple: x and y values snapped to the nearest data
|
||||
"""
|
||||
y_values_1d = []
|
||||
x_values_1d = []
|
||||
y_values = defaultdict(list)
|
||||
x_values = defaultdict(list)
|
||||
image_2d = None
|
||||
|
||||
# Iterate through items in the plot
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||
x_data, y_data = item.xData, item.yData
|
||||
name = item.name()
|
||||
plot_data = item._getDisplayDataset()
|
||||
if plot_data is None:
|
||||
continue
|
||||
x_data, y_data = plot_data.x, plot_data.y
|
||||
if x_data is not None and y_data is not None:
|
||||
if self.is_log_x:
|
||||
min_x_data = np.min(x_data[x_data > 0])
|
||||
@@ -112,25 +120,25 @@ class Crosshair(QObject):
|
||||
min_x_data = np.min(x_data)
|
||||
max_x_data = np.max(x_data)
|
||||
if x < min_x_data or x > max_x_data:
|
||||
return None, None
|
||||
y_values[name] = None
|
||||
x_values[name] = None
|
||||
continue
|
||||
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
|
||||
y_values_1d.append(closest_y)
|
||||
x_values_1d.append(closest_x)
|
||||
y_values[name] = closest_y
|
||||
x_values[name] = closest_x
|
||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||
name = item.config.monitor
|
||||
image_2d = item.image
|
||||
# clip the x and y values to the image dimensions to avoid out of bounds errors
|
||||
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||
x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||
|
||||
# Handle 1D plot
|
||||
if y_values_1d:
|
||||
if all(v is None for v in x_values_1d) or all(v is None for v in y_values_1d):
|
||||
if x_values and y_values:
|
||||
if all(v is None for v in x_values.values()) or all(
|
||||
v is None for v in y_values.values()
|
||||
):
|
||||
return None, None
|
||||
closest_x = min(x_values_1d, key=lambda xi: abs(xi - x)) # Snap x to closest data point
|
||||
return closest_x, y_values_1d
|
||||
|
||||
# Handle 2D plot
|
||||
if image_2d is not None:
|
||||
x_idx = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||
y_idx = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||
return x_idx, y_idx
|
||||
return x_values, y_values
|
||||
|
||||
return None, None
|
||||
|
||||
@@ -156,8 +164,9 @@ class Crosshair(QObject):
|
||||
Args:
|
||||
event: The mouse moved event
|
||||
"""
|
||||
self.check_log()
|
||||
pos = event[0]
|
||||
self.update_markers()
|
||||
self.positionChanged.emit((pos.x(), pos.y()))
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
||||
self.v_line.setPos(mouse_point.x())
|
||||
@@ -168,27 +177,34 @@ class Crosshair(QObject):
|
||||
x = 10**x
|
||||
if self.is_log_y:
|
||||
y = 10**y
|
||||
x, y_values = self.snap_to_data(x, y)
|
||||
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
||||
if x_snap_values is None or y_snap_values is None:
|
||||
return
|
||||
if all(v is None for v in x_snap_values.values()) or all(
|
||||
v is None for v in y_snap_values.values()
|
||||
):
|
||||
# not sure how we got here, but just to be safe...
|
||||
return
|
||||
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
if x is None or all(v is None for v in y_values):
|
||||
return
|
||||
coordinate_to_emit = (
|
||||
round(x, self.precision),
|
||||
[round(y_val, self.precision) for y_val in y_values],
|
||||
)
|
||||
name = item.name()
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_moved_1d[name].setData([x], [y])
|
||||
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
||||
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
||||
for i, y_val in enumerate(y_values):
|
||||
self.marker_moved_1d[i].setData(
|
||||
[x if not self.is_log_x else np.log10(x)],
|
||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
||||
)
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
if x is None or y_values is None:
|
||||
return
|
||||
coordinate_to_emit = (x, y_values)
|
||||
name = item.config.monitor
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_2d.setPos([x, y])
|
||||
coordinate_to_emit = (name, x, y)
|
||||
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
||||
else:
|
||||
continue
|
||||
|
||||
def mouse_clicked(self, event):
|
||||
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
|
||||
@@ -196,40 +212,69 @@ class Crosshair(QObject):
|
||||
Args:
|
||||
event: The mouse clicked event
|
||||
"""
|
||||
self.check_log()
|
||||
|
||||
# we only accept left mouse clicks
|
||||
if event.button() != Qt.MouseButton.LeftButton:
|
||||
return
|
||||
self.update_markers()
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
|
||||
x, y = mouse_point.x(), mouse_point.y()
|
||||
self.positionClicked.emit((x, y))
|
||||
|
||||
if self.is_log_x:
|
||||
x = 10**x
|
||||
if self.is_log_y:
|
||||
y = 10**y
|
||||
x, y_values = self.snap_to_data(x, y)
|
||||
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
||||
|
||||
if x_snap_values is None or y_snap_values is None:
|
||||
return
|
||||
if all(v is None for v in x_snap_values.values()) or all(
|
||||
v is None for v in y_snap_values.values()
|
||||
):
|
||||
# not sure how we got here, but just to be safe...
|
||||
return
|
||||
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
if x is None or all(v is None for v in y_values):
|
||||
return
|
||||
coordinate_to_emit = (
|
||||
round(x, self.precision),
|
||||
[round(y_val, self.precision) for y_val in y_values],
|
||||
)
|
||||
name = item.name()
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_clicked_1d[name].setData([x], [y])
|
||||
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
||||
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
||||
for i, y_val in enumerate(y_values):
|
||||
for marker in self.marker_clicked_1d[i]:
|
||||
marker.setData(
|
||||
[x if not self.is_log_x else np.log10(x)],
|
||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
||||
)
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
if x is None or y_values is None:
|
||||
return
|
||||
coordinate_to_emit = (x, y_values)
|
||||
name = item.config.monitor
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_2d.setPos([x, y])
|
||||
coordinate_to_emit = (name, x, y)
|
||||
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
||||
self.marker_2d.setPos([x, y_values])
|
||||
else:
|
||||
continue
|
||||
|
||||
def clear_markers(self):
|
||||
"""Clears the markers from the plot."""
|
||||
for marker in self.marker_moved_1d.values():
|
||||
marker.clear()
|
||||
for marker in self.marker_clicked_1d.values():
|
||||
marker.clear()
|
||||
|
||||
def check_log(self):
|
||||
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
||||
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
||||
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
||||
self.clear_markers()
|
||||
|
||||
def check_derivatives(self):
|
||||
"""Checks if the derivatives are enabled and updates the internal state accordingly."""
|
||||
self.is_derivative = self.plot_item.ctrl.derivativeCheck.isChecked()
|
||||
self.clear_markers()
|
||||
|
||||
def cleanup(self):
|
||||
self.v_line.deleteLater()
|
||||
self.h_line.deleteLater()
|
||||
self.clear_markers()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock"]
|
||||
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
|
||||
|
||||
|
||||
class DesignerPluginInfo:
|
||||
@@ -58,11 +58,12 @@ class DesignerPluginGenerator:
|
||||
os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
|
||||
)
|
||||
|
||||
def run(self):
|
||||
def run(self, validate=True):
|
||||
if self._excluded:
|
||||
print(f"Plugin {self.widget.__name__} is excluded from generation.")
|
||||
return
|
||||
self._check_class_validity()
|
||||
if validate:
|
||||
self._check_class_validity()
|
||||
self._load_templates()
|
||||
self._write_templates()
|
||||
|
||||
@@ -142,7 +143,7 @@ class DesignerPluginGenerator:
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
||||
from bec_widgets.widgets.dock import BECDockArea
|
||||
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
|
||||
|
||||
generator = DesignerPluginGenerator(BECDockArea)
|
||||
generator.run()
|
||||
generator = DesignerPluginGenerator(SpinnerWidget)
|
||||
generator.run(validate=False)
|
||||
|
||||
@@ -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
|
||||
|
||||
92
bec_widgets/utils/reference_utils.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageChops
|
||||
from qtpy.QtGui import QPixmap
|
||||
|
||||
import bec_widgets
|
||||
|
||||
REFERENCE_DIR = os.path.join(
|
||||
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/references"
|
||||
)
|
||||
REFERENCE_DIR_FAILURES = os.path.join(
|
||||
os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/reference_failures"
|
||||
)
|
||||
|
||||
|
||||
def compare_images(image1_path: str, reference_image_path: str):
|
||||
"""
|
||||
Load two images and compare them pixel by pixel
|
||||
|
||||
Args:
|
||||
image1_path(str): The path to the first image
|
||||
reference_image_path(str): The path to the reference image
|
||||
|
||||
Raises:
|
||||
ValueError: If the images are different
|
||||
"""
|
||||
image1 = Image.open(image1_path)
|
||||
image2 = Image.open(reference_image_path)
|
||||
if image1.size != image2.size:
|
||||
raise ValueError("Image size has changed")
|
||||
diff = ImageChops.difference(image1, image2)
|
||||
if diff.getbbox():
|
||||
# copy image1 to the reference directory to upload as artifact
|
||||
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
|
||||
image_name = os.path.join(REFERENCE_DIR_FAILURES, os.path.basename(image1_path))
|
||||
image1.save(image_name)
|
||||
print(f"Image saved to {image_name}")
|
||||
|
||||
raise ValueError("Images are different")
|
||||
|
||||
|
||||
def snap_and_compare(widget: any, output_directory: str, suffix: str = ""):
|
||||
"""
|
||||
Save a rendering of a widget and compare it to a reference image
|
||||
|
||||
Args:
|
||||
widget(any): The widget to render
|
||||
output_directory(str): The directory to save the image to
|
||||
suffix(str): A suffix to append to the image name
|
||||
|
||||
Raises:
|
||||
ValueError: If the images are different
|
||||
|
||||
Examples:
|
||||
snap_and_compare(widget, tmpdir, suffix="started")
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(output_directory, str):
|
||||
output_directory = str(output_directory)
|
||||
|
||||
os_suffix = sys.platform
|
||||
|
||||
name = (
|
||||
f"{widget.__class__.__name__}_{suffix}_{os_suffix}.png"
|
||||
if suffix
|
||||
else f"{widget.__class__.__name__}_{os_suffix}.png"
|
||||
)
|
||||
|
||||
# Save the widget to a pixmap
|
||||
test_image_path = os.path.join(output_directory, name)
|
||||
pixmap = QPixmap(widget.size())
|
||||
widget.render(pixmap)
|
||||
pixmap.save(test_image_path)
|
||||
|
||||
try:
|
||||
reference_path = os.path.join(REFERENCE_DIR, f"{widget.__class__.__name__}")
|
||||
reference_image_path = os.path.join(reference_path, name)
|
||||
|
||||
if not os.path.exists(reference_image_path):
|
||||
raise ValueError(f"Reference image not found: {reference_image_path}")
|
||||
|
||||
compare_images(test_image_path, reference_image_path)
|
||||
|
||||
except ValueError:
|
||||
image = Image.open(test_image_path)
|
||||
os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
|
||||
image_name = os.path.join(REFERENCE_DIR_FAILURES, name)
|
||||
image.save(image_name)
|
||||
print(f"Image saved to {image_name}")
|
||||
raise
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
|
||||
class DeviceInputConfig(ConnectionConfig):
|
||||
@@ -9,7 +10,7 @@ class DeviceInputConfig(ConnectionConfig):
|
||||
arg_name: str | None = None
|
||||
|
||||
|
||||
class DeviceInputBase(BECConnector):
|
||||
class DeviceInputBase(BECWidget):
|
||||
"""
|
||||
Mixin class for device input widgets. This class provides methods to get the device list and device object based
|
||||
on the current text of the widget.
|
||||
@@ -25,6 +26,7 @@ class DeviceInputBase(BECConnector):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
self._device_filter = None
|
||||
self._devices = []
|
||||
|
||||
@property
|
||||
@@ -56,6 +58,7 @@ class DeviceInputBase(BECConnector):
|
||||
"""
|
||||
self.validate_device_filter(device_filter)
|
||||
self.config.device_filter = device_filter
|
||||
self._device_filter = device_filter
|
||||
|
||||
def set_default_device(self, default_device: str):
|
||||
"""
|
||||
@@ -118,6 +121,3 @@ class DeviceInputBase(BECConnector):
|
||||
"""
|
||||
if device not in self.get_device_list(self.config.device_filter):
|
||||
raise ValueError(f"Device {device} is not valid.")
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
@@ -1,11 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import Qt, Slot
|
||||
from qtpy.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem, QWidget
|
||||
from qtpy.QtWidgets import QHBoxLayout, QHeaderView, QTableWidget, QTableWidgetItem, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
|
||||
class BECQueue(BECConnector, QTableWidget):
|
||||
class BECQueue(BECWidget, QWidget):
|
||||
"""
|
||||
Widget to display the BEC queue.
|
||||
"""
|
||||
@@ -18,10 +21,14 @@ class BECQueue(BECConnector, QTableWidget):
|
||||
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()
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.table = QTableWidget(self)
|
||||
self.layout = QHBoxLayout(self)
|
||||
self.layout.addWidget(self.table)
|
||||
|
||||
self.table.setColumnCount(3)
|
||||
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
|
||||
self.reset_content()
|
||||
@@ -37,8 +44,8 @@ class BECQueue(BECConnector, QTableWidget):
|
||||
"""
|
||||
# only show the primary queue for now
|
||||
queue_info = content.get("queue", {}).get("primary", {}).get("info", [])
|
||||
self.setRowCount(len(queue_info))
|
||||
self.clearContents()
|
||||
self.table.setRowCount(len(queue_info))
|
||||
self.table.clearContents()
|
||||
|
||||
if not queue_info:
|
||||
self.reset_content()
|
||||
@@ -72,6 +79,8 @@ class BECQueue(BECConnector, QTableWidget):
|
||||
Returns:
|
||||
QTableWidgetItem: The formatted item.
|
||||
"""
|
||||
if not content or not isinstance(content, str):
|
||||
content = ""
|
||||
item = QTableWidgetItem(content)
|
||||
item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
||||
return item
|
||||
@@ -87,16 +96,16 @@ class BECQueue(BECConnector, QTableWidget):
|
||||
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))
|
||||
self.table.setItem(index, 0, self.format_item(scan_number))
|
||||
self.table.setItem(index, 1, self.format_item(scan_type))
|
||||
self.table.setItem(index, 2, self.format_item(status))
|
||||
|
||||
def reset_content(self):
|
||||
"""
|
||||
Reset the content of the table.
|
||||
"""
|
||||
|
||||
self.setRowCount(1)
|
||||
self.table.setRowCount(1)
|
||||
self.set_row(0, "", "", "")
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +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
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
||||
|
||||
DOM_XML = """
|
||||
@@ -13,6 +15,8 @@ DOM_XML = """
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
@@ -27,10 +31,10 @@ class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
return "BEC Services"
|
||||
|
||||
def icon(self):
|
||||
return QIcon()
|
||||
return designer_material_icon("edit_note")
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_queue"
|
||||
|
||||
@@ -5,15 +5,16 @@ The widget automatically updates the status of all running BEC services, and dis
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import qdarktheme
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy.QtCore import QObject, QTimer, Signal, Slot
|
||||
from qtpy.QtWidgets import QTreeWidget, QTreeWidgetItem
|
||||
from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.bec_status_box.status_item import StatusItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -23,45 +24,18 @@ if TYPE_CHECKING:
|
||||
BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
|
||||
|
||||
|
||||
class BECStatusBoxConfig(ConnectionConfig):
|
||||
pass
|
||||
|
||||
|
||||
class BECServiceInfoContainer(BaseModel):
|
||||
@dataclass
|
||||
class BECServiceInfoContainer:
|
||||
"""Container to store information about the BEC services."""
|
||||
|
||||
service_name: str
|
||||
status: BECStatus | str = Field(
|
||||
default="NOTCONNECTED",
|
||||
description="The status of the service. Can be any of the BECStatus names, or NOTCONNECTED.",
|
||||
)
|
||||
status: str
|
||||
info: dict
|
||||
metrics: dict | None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def validate_status(cls, v):
|
||||
"""Validate input for status. Accept BECStatus and NOTCONNECTED.
|
||||
|
||||
Args:
|
||||
v (BECStatus | str): The input value.
|
||||
|
||||
Returns:
|
||||
str: The validated status.
|
||||
"""
|
||||
if v in list(BECStatus.__members__.values()):
|
||||
return v.name
|
||||
if v in list(BECStatus.__members__.keys()) or v == "NOTCONNECTED":
|
||||
return v
|
||||
raise ValueError(
|
||||
f"Status must be one of {BECStatus.__members__.values()} or 'NOTCONNECTED'. Input {v}"
|
||||
)
|
||||
|
||||
|
||||
class BECServiceStatusMixin(QObject):
|
||||
"""A mixin class to update the service status, and metrics.
|
||||
It emits a signal 'services_update' when the service status is updated.
|
||||
"""Mixin to receive the latest service status from the BEC server and emit it via services_update signal.
|
||||
|
||||
Args:
|
||||
client (BECClient): The client object to connect to the BEC server.
|
||||
@@ -69,29 +43,31 @@ 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)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the BECServiceStatusMixin."""
|
||||
self._service_update_timer.stop()
|
||||
self._service_update_timer.deleteLater()
|
||||
|
||||
class BECStatusBox(BECConnector, QTreeWidget):
|
||||
"""A widget to display the status of different BEC services.
|
||||
This widget automatically updates the status of all running BEC services, and displays their status.
|
||||
Information about the individual services is collapsible, and double clicking on
|
||||
the individual service will display the metrics about the service.
|
||||
|
||||
class BECStatusBox(BECWidget, QWidget):
|
||||
"""An autonomous widget to display the status of BEC services.
|
||||
|
||||
Args:
|
||||
parent Optional : The parent widget for the BECStatusBox. Defaults to None.
|
||||
service_name Optional(str): The name of the top service label. Defaults to "BEC Server".
|
||||
box_name Optional(str): The name of the top service label. Defaults to "BEC Server".
|
||||
client Optional(BECClient): The client object to connect to the BEC server. Defaults to None
|
||||
config Optional(BECStatusBoxConfig | dict): The configuration for the status box. Defaults to None.
|
||||
gui_id Optional(str): The unique id for the widget. Defaults to None.
|
||||
@@ -99,58 +75,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 +148,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 +160,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 +199,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 +207,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 +228,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 +258,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 +272,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 +291,14 @@ 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):
|
||||
super().cleanup()
|
||||
QTreeWidget().closeEvent(event)
|
||||
def cleanup(self):
|
||||
"""Cleanup the BECStatusBox widget."""
|
||||
self.bec_service_status.cleanup()
|
||||
return super().cleanup()
|
||||
|
||||
|
||||
def main():
|
||||
@@ -346,7 +307,7 @@ def main():
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("auto")
|
||||
apply_theme("dark")
|
||||
main_window = BECStatusBox()
|
||||
main_window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['bec_status_box.py']}
|
||||
58
bec_widgets/widgets/bec_status_box/bec_status_box_plugin.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.bec_status_box.bec_status_box import BECStatusBox
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECStatusBox' name='bec_status_box'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
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 Services"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon("dns")
|
||||
|
||||
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,40 +2,30 @@
|
||||
The widget is bound to be used with the BECStatusBox widget."""
|
||||
|
||||
import enum
|
||||
import sys
|
||||
import os
|
||||
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 qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
import bec_widgets
|
||||
|
||||
# TODO : Put normal imports back when Pydantic gets faster
|
||||
BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class IconsEnum(enum.Enum):
|
||||
"""Enum class for icons in the status item widget."""
|
||||
|
||||
RUNNING = "SP_DialogApplyButton"
|
||||
BUSY = "SP_BrowserReload"
|
||||
IDLE = "SP_MessageBoxWarning"
|
||||
ERROR = "SP_DialogCancelButton"
|
||||
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.")
|
||||
RUNNING = os.path.join(MODULE_PATH, "assets", "status_icons", "running.svg")
|
||||
BUSY = os.path.join(MODULE_PATH, "assets", "status_icons", "refresh.svg")
|
||||
IDLE = os.path.join(MODULE_PATH, "assets", "status_icons", "warning.svg")
|
||||
ERROR = os.path.join(MODULE_PATH, "assets", "status_icons", "error.svg")
|
||||
NOTCONNECTED = os.path.join(MODULE_PATH, "assets", "status_icons", "not_connected.svg")
|
||||
|
||||
|
||||
class StatusItem(QWidget):
|
||||
@@ -43,23 +33,24 @@ class StatusItem(QWidget):
|
||||
|
||||
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 +65,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 +88,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}"
|
||||
@@ -108,9 +97,9 @@ class StatusItem(QWidget):
|
||||
|
||||
def set_status(self) -> None:
|
||||
"""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))
|
||||
icon_path = IconsEnum[self.config.status].value
|
||||
icon = QIcon(icon_path)
|
||||
self._icon.setPixmap(icon.pixmap(*self.icon_size))
|
||||
self._icon.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
def show_popup(self) -> None:
|
||||
@@ -153,19 +142,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
|
||||