1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-16 21:45:35 +02:00

Compare commits

..

1 Commits

168 changed files with 4688 additions and 6228 deletions

View File

@@ -144,9 +144,6 @@ tests:
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- tests/reference_failures/
when: always
test-matrix:
parallel:

View File

@@ -1,147 +1,143 @@
# CHANGELOG
## v0.88.1 (2024-07-22)
### Documentation
* docs: readthedocs icon path fixed ([`2bcaa42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2bcaa4256d6daaefacb3ead8c72458d7b1498e29))
### Fix
* fix(plot_base): set_xy autorange moved to plotbase from waveform ([`a3dff7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3dff7decc16115c12dc6b4ef1572552368da309))
### Refactor
* refactor(toolbar): generalizations of the ToolBarAction ([`ad112d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad112d1f08157f6987edd48a0bacf9f669ef1997))
## v0.88.0 (2024-07-19)
## v0.81.0 (2024-07-06)
### Feature
* feat(waveform_widget): designer plugin added ([`1f8ef52`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f8ef52b606283038052640849094f515a463403))
* feat(color_button): can get colors in RGBA or HEX ([`9594be2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9594be260680d11c8550ff74ffb8d679e5a5b8f6))
* feat(waveform_widget): switch between drag and rectangle mode ([`2be009c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2be009c6477ba26c5cfb4d827534c5d5eb428999))
* feat(waveform_widget): autorange button ([`8df6b00`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8df6b003e5c6a942fa2e875d9790e492c087bf26))
* feat(waveform_widget): dap parameter window ([`1e551d6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1e551d6e9696f79ea2e0a179d13a4fc6c2a128b2))
* feat(waveform): export to matplotlib window of current scene ([`8d93405`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d9340539967b06b1e15f21a2106a39d5c740f31))
* feat(figure): export dialog can be launched from CLI and from toolbar ([`6ff6111`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ff611109153b9412dce37c527b19e839d99bba7))
* feat(waveform_widget): added error handle utility ([`a8ff1d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8ff1d4cd09cae5eaeb4bd0ea90fdd102e32f3a3))
* feat(curve_dialog): add DAP functionality ([`e830565`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e8305652fde384da037242cf8f7e3606f22bcfb6))
* feat(curve_dialog): curves can be added ([`c926a75`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c926a75a7927d672c044ea8f68771209ae5accc6))
* feat(waveform_widget): BECWaveformWidget toolbar added import/export config ([`fa9b171`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fa9b17191ddbb4043a658dae9aa0801e1dc22b84))
* feat(waveform_widget): BECWaveformWidget added with toolbar ([`755b394`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/755b394c1c4d7c443c442d89c630d08ce5415554))
## v0.80.1 (2024-07-06)
### Fix
* fix(waveform_widget): plot API unified with BECFigure ([`2c8764a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2c8764a27de89b39b717032b58465e120ec57fbc))
* fix(entry_validator): check for entry == "" ([`61de7e9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61de7e9e221c766b9fb3ec23246da6a11c96a986))
* fix(colormap_selector): compatibility for PyQt6 when using designer fixed ([`50135b5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/50135b5fe90a88618291e9357f180cb19251dace))
* fix(waveform_widget): adapted for BECWidget base class ([`6eb313f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6eb313fa76e559d62ecd8fa8849142b83817e47c))
* fix(waveform_widget): temporary disabled save/load config ([`7089cf3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7089cf356a43d805241d5621952e544d690e65e0))
* fix(waveform_widget): use @SafeSlot decorator for automatic error message ([`8e588d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e588d79c86e950f6915e89c08fa9415c4bd8033))
* fix(waveform): colormaps of curves can be changed and normalised
feat(waveform): colormap can be changed from curve dialog
fix(curve_dialog): default dialog parameters fixed
curve Dialog colormap WIP ([`33495cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33495cfe03b363f18db61d8af2983f49027b7a43))
* fix(waveform_widget): adapted for changes from improved scan logic from waveform widget ([`8ac35d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ac35d7280b1ff007c10612228d163cc0c5d1a99))
### Refactor
* refactor(icons): icons moved to the assets directory ([`a8b6ef2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8b6ef20cccae87515b10f054d0ed5b10e152769))
* refactor(waveform_widget): removed PYSIDE6 check ([`47fcb9e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/47fcb9ebfe35ae600cced95a1edc68f6f6e37a04))
### Test
* test(waveform_widget): test added ([`8d764e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d764e2d46a1e017dadc3c4630648c1ca708afc2))
## v0.87.1 (2024-07-18)
### Fix
* fix(dock): added hasattr to cleanup method for widgets ([`d75c55b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d75c55b2b1ccf156fb789c7813f1c5bdf256f860))
* fix: add missing close() call, ensure jupyter console client.shutdown() is called in closeEvent ([`e52ee26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e52ee2604cb35096f1bd833ca9516d8a34197d35))
* fix: BECWidget checks if it is a widget, and implements closeEvent and cleanup ([`d64758f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d64758f268cad69e6a17bd52dc9913a6367d3cde))
* fix: add exit handlers for BECConnection objects ([`6202d22`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6202d224fe85c103a4c33bd8c255f18cfd027303))
### Refactor
* refactor: BECWidget is a mixin based on BECConnector, for each QWidget in BEC
Handles closeEvent() and RPC registering/unregistering ([`c7feb69`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c7feb6952d590b569f7b0cba3b019a9af0ce0c93))
## v0.87.0 (2024-07-17)
## v0.80.0 (2024-07-06)
### Feature
* feat(qt_utils): added warning utility with simple API to setup warning message ([`787f749`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/787f74949bac27aaa51cbb43911919071481707c))
* feat(qt5): dropped support for qt5; pyside2 and pyqt5 ([`fadbf77`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fadbf77866903beff6580802bc203d53367fc7e7))
* feat(qt_utils): added error handle utility with popup messageBoxes ([`196ef7a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/196ef7afe11a1b5dcc536f8859dc3b6044ea628e))
* feat(plugins): moved plugin dict to dataclass and container ([`03819a3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/03819a3d902b4a51f3e882d52aedd971b2a8e127))
* feat(plugins): added support for pyqt6 ui files ([`d6d0777`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d6d07771135335cb78dc648508ce573b8970261a))
* feat(plugins): added bec widgets base class ([`1aa83e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1aa83e0ef1ffe45b01677b0b4590535cb0ca1cff))
## v0.79.3 (2024-07-05)
### Fix
* fix: changed inheritance to adress qt designer bug in rendering ([`e403870`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e403870874bd5e45840a034d6f1b3dd576d9c846))
* fix: add designer plugin classes ([`1586ce2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1586ce2d6cba2bb086b2ef596e724bb9e40ab4f2))
### Refactor
* refactor: simplify logic in bec_status_box ([`576353c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/576353cfe8c6fd64db561f0b6e2bc951300643d3))
## v0.79.2 (2024-07-04)
### Fix
* fix: overwrite closeEvent and call super class ([`bc0ef78`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bc0ef7893ef100b71b62101c459655509b534a56))
## v0.79.1 (2024-07-03)
### Fix
* fix: use libdir env var to preload Python library, also for Linux platform ([`d7718d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d7718d4dcb9728c050b6421388af4d484f3741f2))
## v0.79.0 (2024-07-03)
### Feature
* feat(motor_map_widget): standalone MotorMap Widget with toolbar + plugin ([`6e75642`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6e756420907d7093557e945bc92bc4cfc0138d07))
* feat(motor_map): method to reset history trace ([`5960918`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5960918137dd41cdeb94e50f8abc4f169cf45c11))
### Fix
* fix(toolbar): change default color to black to match BECFigure theme ([`b8774e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8774e0b0bc43dcd00f94f42539a778e507ca27d))
* fix(motor_map): fixed bug with residual trace after changing motors ([`aaa0d10`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aaa0d1003d2e94b45bafe4f700852c2c05288aea))
* fix(widget_io): widget handler adjusted for spinboxes and comboboxes ([`3dc0532`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3dc0532df05b6ec0a2522107fa0b1e210ce7d91b))
### Refactor
* refactor(toolbar): cleanup and adjusted colors ([`96863ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96863adf53c15112645d20eb6200733617801c6d))
## v0.78.1 (2024-07-02)
### Fix
* fix(ui_loader): ui loader is compatible with bec plugins ([`b787759`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b787759f44486dc7af2c03811efb156041e4b6cb))
## v0.78.0 (2024-07-02)
### Feature
* feat(color_button): patched ColorButton from pyqtgraph to be able to be opened in another QDialog ([`c36bb80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c36bb80d6a4939802a4a1c8e5452c7b94bac185e))
## v0.77.0 (2024-07-02)
### Feature
* feat(bec_connector): export config to yaml ([`a391f30`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a391f3018c50fee6a4a06884491b957df80c3cd3))
* feat(utils): colors added convertor for rgba to hex ([`572f2fb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/572f2fb8110d5cb0e80f3ca45ce57ef405572456))
### Fix
* fix(waveform): scatter 2D brush error ([`215d59c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/215d59c8bfe7fda9aff8cec8353bef9e1ce2eca1))
* fix(figure): API cleanup ([`008a33a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/008a33a9b192473cc58e90cd6d98c5bcb5f7b8c0))
* fix(figure): if/else logic corrected in subplot_factory ([`3e78723`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3e787234c7274b0698423d7bf9a4c54ec46bad5f))
* fix(image): processing of already displayed data; closes #106 ([`1173510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1173510105d2d70d7e498c2ac1e122cea3a16597))
* 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
* tests: add unit tests for error and warning message boxes ([`8f104cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8f104cf4024d3a4516e6aba5daa8fb78c85e2bfd))
* Resolve "add VT100 console executing BEC as a widget" ([`c6a14c0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c6a14c0768a90695567a83a7895247ed0c64f3ce))
## v0.86.0 (2024-07-17)
### Feature
* feat(toolbar): added separator action ([`ba69e79`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ba69e7957cd20df1557ac0c3a9ca43a54493c34d))
## v0.85.1 (2024-07-17)
## v0.76.1 (2024-06-29)
### Fix
* fix(waveform): readout_priority dict fixed, not overwritten to 'baseline' key ([`b5b0aa4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b5b0aa4f82a998bb0162dc319591e854204a7354))
* fix(plugins): fixes and tests for auto-gen plugins ([`c42511d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c42511dd44cc13577e108a6cef3166376e594f54))
## v0.85.0 (2024-07-16)
## v0.76.0 (2024-06-28)
### Feature
* feat(color_map_selector): added colormap selector with plugin ([`b98fd00`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b98fd00adef97adf57f49b60ade99972b9f5a6bc))
## v0.84.0 (2024-07-15)
### Feature
* feat(waveform): async readback update implemented for async devices ([`0c6a9f2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0c6a9f2310df31ddcd68050a17cfbf52c3e2e226))
* feat(designer): added support for creating designer plugins automatically ([`c1dd0ee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c1dd0ee1906dba1f2e2ae9ce40a84d55c26a1cce))
### Fix
* fix(waveform): timestamp are not converted to human readable format ([`e495fd3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e495fd30c4c16474689943c7263e3060cb09ffb4))
* fix: fixed qwidget inheritance for ring progress bar ([`0610d2f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0610d2f9f027f8659e7149f2dfbb316ff30e337d))
* fix(waveform): set_x method various bugs fixed ([`8516a1d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8516a1d639925a877f174fa13f427a71131cc918))
### Unknown
* fix(waveform): x axis switching logic fixed when axis are not compatible ([`e4e1a90`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e4e1a905d19def22f970b364c18c953f00e10389))
* fix:parent set as first kwarg TextBox and WebsiteWidget ([`a45c407`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a45c4075684b93bfdcee03e5a416b84f61d3bc6f))
* fix(waveform): dap leaked RID for all daps in current process; dap RID is now f"{scan_id}-{gui_id}" to distinguish for each plot instance ([`d23fd8b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d23fd8bd074ede6e14eb8e85e025cbced4bd45ef))
## v0.75.0 (2024-06-26)
### Refactor
### Feature
* refactor(waveform): plot can be prompted without specifying kwargs ([`48911e9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/48911e934815923c94edb5ced6042058a11a97f5))
* refactor(jupyter_console_window): added more examples of waveforms ([`fc935d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc935d9fc81067c3a67389ff88ea97da2e0c903e))
### Test
* test(waveform): tests extended ([`006992e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/006992e43cc56d56261bc4fd3e9cae9abcab2153))
* feat(widgets): added simple bec queue widget ([`3faee98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3faee98ec80041a27e4c1f1156178de6f9dcdc63))

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 228 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 487 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 470 B

View File

@@ -1,9 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 371 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 366 B

View File

@@ -1,9 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 377 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 450 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 564 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 946 B

View File

@@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 357 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 621 B

View File

@@ -19,13 +19,8 @@ class Widgets(str, enum.Enum):
BECMotorMapWidget = "BECMotorMapWidget"
BECQueue = "BECQueue"
BECStatusBox = "BECStatusBox"
BECWaveformWidget = "BECWaveformWidget"
DeviceBox = "DeviceBox"
DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
StopButton = "StopButton"
TextBox = "TextBox"
VSCodeEditor = "VSCodeEditor"
WebsiteWidget = "WebsiteWidget"
@@ -185,7 +180,7 @@ class BECDock(RPCBase):
@property
@rpc_call
def widget_list(self) -> "list[BECWidget]":
def widget_list(self) -> "list[BECConnector]":
"""
Get the widgets in the dock.
@@ -226,13 +221,13 @@ class BECDock(RPCBase):
@rpc_call
def add_widget(
self,
widget: "BECWidget | str",
widget: "BECConnector | str",
row=None,
col=0,
rowspan=1,
colspan=1,
shift: "Literal['down', 'up', 'left', 'right']" = "down",
) -> "BECWidget":
) -> "BECConnector":
"""
Add a widget to the dock.
@@ -469,9 +464,8 @@ class BECFigure(RPCBase):
@rpc_call
def plot(
self,
arg1: "list | np.ndarray | str | None" = None,
y: "list | np.ndarray | 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,
@@ -493,9 +487,8 @@ class BECFigure(RPCBase):
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
Args:
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.
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.
@@ -615,12 +608,6 @@ 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):
"""
@@ -1103,12 +1090,6 @@ 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):
"""
@@ -1217,12 +1198,6 @@ class BECMotorMap(RPCBase):
dict: Data of the motor map.
"""
@rpc_call
def export(self):
"""
Show the Export Dialog of the plot widget.
"""
@rpc_call
def remove(self):
"""
@@ -1317,12 +1292,6 @@ class BECMotorMapWidget(RPCBase):
Reset the history of the motor map.
"""
@rpc_call
def export(self):
"""
Show the export dialog for the motor map.
"""
class BECPlotBase(RPCBase):
@property
@@ -1451,12 +1420,6 @@ 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):
"""
@@ -1530,9 +1493,8 @@ class BECWaveform(RPCBase):
@rpc_call
def plot(
self,
arg1: "list | np.ndarray | str | None" = None,
y: "list | np.ndarray | 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,
@@ -1544,20 +1506,13 @@ class BECWaveform(RPCBase):
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, y data, or y_name.
x(list | np.ndarray): Custom x data to plot.
y(list | np.ndarray): Custom y data to plot.
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.
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.
@@ -1567,7 +1522,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, only available for sync devices. If not specified, none will be added.
dap(str): The dap model to use for the curve. If not specified, none will be added.
Returns:
BECCurve: The curve object.
@@ -1576,13 +1531,12 @@ class BECWaveform(RPCBase):
@rpc_call
def add_dap(
self,
x_name: "str | None" = None,
y_name: "str | None" = None,
x_name: "str",
y_name: "str",
x_entry: "Optional[str]" = None,
y_entry: "Optional[str]" = None,
color: "Optional[str]" = None,
dap: "str" = "GaussianModel",
validate_bec: "bool" = True,
**kwargs,
) -> "BECCurve":
"""
@@ -1594,28 +1548,15 @@ 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":
"""
@@ -1785,15 +1726,6 @@ 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):
"""
@@ -1803,24 +1735,12 @@ 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):
"""
@@ -1831,295 +1751,6 @@ class BECWaveform(RPCBase):
"""
class BECWaveformWidget(RPCBase):
@property
@rpc_call
def curves(self) -> "list[BECCurve]":
"""
Get the curves of the plot widget as a list
Returns:
list: List of curves.
"""
@rpc_call
def plot(
self,
arg1: "list | np.ndarray | str | None" = None,
x: "list | np.ndarray | None" = None,
y: "list | np.ndarray | None" = None,
x_name: "str | None" = None,
y_name: "str | None" = None,
z_name: "str | None" = None,
x_entry: "str | None" = None,
y_entry: "str | None" = None,
z_entry: "str | None" = None,
color: "str | None" = None,
color_map_z: "str | None" = "plasma",
label: "str | None" = None,
validate: "bool" = True,
dap: "str | None" = None,
**kwargs,
) -> "BECCurve":
"""
Plot a curve to the plot widget.
Args:
arg1(list | np.ndarray | str | None): First argument which can be x data(list | np.ndarray), y data(list | np.ndarray), or y_name(str).
x(list | np.ndarray): Custom x data to plot.
y(list | np.ndarray): Custom y data to plot.
x_name(str): The name of the device for the x-axis.
y_name(str): The name of the device for the y-axis.
z_name(str): The name of the device for the z-axis.
x_entry(str): The name of the entry for the x-axis.
y_entry(str): The name of the entry for the y-axis.
z_entry(str): The name of the entry for the z-axis.
color(str): The color of the curve.
color_map_z(str): The color map to use for the z-axis.
label(str): The label of the curve.
validate(bool): If True, validate the device names and entries.
dap(str): The dap model to use for the curve. If not specified, none will be added.
Returns:
BECCurve: The curve object.
"""
@rpc_call
def add_dap(
self,
x_name: "str",
y_name: "str",
x_entry: "str | None" = None,
y_entry: "str | None" = None,
color: "str | None" = None,
dap: "str" = "GaussianModel",
validate_bec: "bool" = True,
**kwargs,
) -> "BECCurve":
"""
Add LMFIT dap model curve to the plot widget.
Args:
x_name(str): Name of the x signal.
x_entry(str): Entry of the x signal.
y_name(str): Name of the y signal.
y_entry(str): Entry of the y signal.
color(str, optional): Color of the curve. Defaults to None.
dap(str): The dap model to use for the curve.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
**kwargs: Additional keyword arguments for the curve configuration.
Returns:
BECCurve: The curve object.
"""
@rpc_call
def get_dap_params(self) -> "dict":
"""
Get the DAP parameters of all DAP curves.
Returns:
dict: DAP parameters of all DAP curves.
"""
@rpc_call
def remove_curve(self, *identifiers):
"""
Remove a curve from the plot widget.
Args:
*identifiers: Identifier of the curve to be removed. Can be either an integer (index) or a string (curve_id).
"""
@rpc_call
def scan_history(self, scan_index: "int" = None, scan_id: "str" = None):
"""
Update the scan curves with the data from the scan storage.
Provide only one of scan_id or scan_index.
Args:
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
scan_index(int, optional): Index of the scan to be updated. Defaults to None.
"""
@rpc_call
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict | pd.DataFrame":
"""
Extract all curve data into a dictionary or a pandas DataFrame.
Args:
output (Literal["dict", "pandas"]): Format of the output data.
Returns:
dict | pd.DataFrame: Data of all curves in the specified format.
"""
@rpc_call
def set(self, **kwargs):
"""
Set the properties of the plot widget.
Args:
**kwargs: Keyword arguments for the properties to be set.
Possible properties:
- title: str
- x_label: str
- y_label: str
- x_scale: Literal["linear", "log"]
- y_scale: Literal["linear", "log"]
- x_lim: tuple
- y_lim: tuple
- legend_label_size: int
"""
@rpc_call
def set_x(self, x_name: "str", x_entry: "str | None" = None):
"""
Change the x axis of the plot widget.
Args:
x_name(str): Name of the x signal.
- "best_effort": Use the best effort signal.
- "timestamp": Use the timestamp signal.
- "index": Use the index signal.
- Custom signal name of device from BEC.
x_entry(str): Entry of the x signal.
"""
@rpc_call
def set_title(self, title: "str"):
"""
Set the title of the plot widget.
Args:
title(str): Title of the plot.
"""
@rpc_call
def set_x_label(self, x_label: "str"):
"""
Set the x-axis label of the plot widget.
Args:
x_label(str): Label of the x-axis.
"""
@rpc_call
def set_y_label(self, y_label: "str"):
"""
Set the y-axis label of the plot widget.
Args:
y_label(str): Label of the y-axis.
"""
@rpc_call
def set_x_scale(self, x_scale: "Literal['linear', 'log']"):
"""
Set the scale of the x-axis of the plot widget.
Args:
x_scale(Literal["linear", "log"]): Scale of the x-axis.
"""
@rpc_call
def set_y_scale(self, y_scale: "Literal['linear', 'log']"):
"""
Set the scale of the y-axis of the plot widget.
Args:
y_scale(Literal["linear", "log"]): Scale of the y-axis.
"""
@rpc_call
def set_x_lim(self, x_lim: "tuple"):
"""
Set the limits of the x-axis of the plot widget.
Args:
x_lim(tuple): Limits of the x-axis.
"""
@rpc_call
def set_y_lim(self, y_lim: "tuple"):
"""
Set the limits of the y-axis of the plot widget.
Args:
y_lim(tuple): Limits of the y-axis.
"""
@rpc_call
def set_legend_label_size(self, legend_label_size: "int"):
"""
Set the size of the legend labels of the plot widget.
Args:
legend_label_size(int): Size of the legend labels.
"""
@rpc_call
def set_auto_range(self, enabled: "bool", axis: "str" = "xy"):
"""
Set the auto range of the plot widget.
Args:
enabled(bool): If True, enable the auto range.
axis(str, optional): The axis to enable the auto range.
- "xy": Enable auto range for both x and y axis.
- "x": Enable auto range for x axis.
- "y": Enable auto range for y axis.
"""
@rpc_call
def set_grid(self, x_grid: "bool", y_grid: "bool"):
"""
Set the grid visibility of the plot widget.
Args:
x_grid(bool): Visibility of the x-axis grid.
y_grid(bool): Visibility of the y-axis grid.
"""
@rpc_call
def lock_aspect_ratio(self, lock: "bool"):
"""
Lock the aspect ratio of the plot widget.
Args:
lock(bool): Lock the aspect ratio.
"""
@rpc_call
def export(self):
"""
Show the export dialog for the plot widget.
"""
@rpc_call
def export_to_matplotlib(self):
"""
Export the plot widget to Matplotlib.
"""
class DeviceBox(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class DeviceComboBox(RPCBase):
@property
@rpc_call

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import importlib
import importlib.metadata as imd
import json
import os
import select
import subprocess
@@ -88,7 +87,7 @@ def _get_output(process, logger) -> None:
print(f"Error reading process output: {str(e)}")
def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger=None) -> None:
def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
"""
Start the plot in a new process.
@@ -99,8 +98,6 @@ def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger
# 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()
@@ -193,7 +190,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
self._gui_id, self.__class__, self._client._service_config.config_path
)
while not self.gui_is_alive():
print("Waiting for GUI to start...")

View File

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

View File

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

View File

@@ -2,20 +2,14 @@ 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,
QGroupBox,
QHBoxLayout,
QSplitter,
QTabWidget,
QVBoxLayout,
QWidget,
)
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils import BECDispatcher, UILoader
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
@@ -27,8 +21,14 @@ 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,46 +40,27 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
"w1_c": self.w1_c,
"w2_c": self.w2_c,
"w3_c": self.w3_c,
"w4": self.w4,
"w5": self.w5,
"w6": self.w6,
"w7": self.w7,
"w8": self.w8,
"w9": self.w9,
"d0": self.d0,
"d1": self.d1,
"d2": self.d2,
"wave": self.wave,
"plt": self.plt,
"bar": self.bar,
"cm": self.colormap,
}
)
def _init_ui(self):
self.layout = QHBoxLayout(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
# 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)
self.dock_layout = QVBoxLayout(self.ui.dock_placeholder)
self.dock = BECDockArea(gui_id="remote")
self.dock_layout.addWidget(self.dock)
# add stuff to figure
self._init_figure()
@@ -87,71 +68,45 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
# init dock for testing
self._init_dock()
self.setWindowTitle("Jupyter Console Window")
self.console_layout = QVBoxLayout(self.ui.widget_console)
self.console = BECJupyterConsole(inprocess=True)
self.console_layout.addWidget(self.console)
def _init_figure(self):
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.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.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")
@@ -166,26 +121,19 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.fig1.plot(x_name="samx", y_name="bpm3a")
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
self.wave = self.d2.add_widget("BECWaveformWidget", row=0, col=0)
# self.wave.plot(x_name="samx", y_name="bpm3a")
# self.wave.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
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.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.dock.close()
self.figure.cleanup()
self.figure.close()
self.console.close()
self.figure.clear_all()
self.figure.client.shutdown()
super().closeEvent(event)
@@ -199,11 +147,9 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console")
app.setApplicationDisplayName("Jupyter Console")
apply_theme("dark")
qdarktheme.setup_theme("auto")
icon = QIcon()
icon.addFile(
os.path.join(module_path, "assets", "app_icons", "terminal_icon.png"), size=QSize(48, 48)
)
icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
app.setWindowIcon(icon)
bec_dispatcher = BECDispatcher()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,9 @@ from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QApplication
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
@@ -65,30 +64,16 @@ class Worker(QRunnable):
self.signals.completed.emit()
class BECConnector:
"""Connection mixin class to handle BEC client and device manager"""
class BECConnector(BECWidget):
"""Connection mixin class for all BEC widgets, 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__
@@ -106,14 +91,9 @@ 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:
@@ -300,3 +280,16 @@ class BECConnector:
return self.config.model_dump()
else:
return self.config
def cleanup(self):
"""Cleanup the widget."""
self.rpc_register.remove_rpc(self)
all_connections = self.rpc_register.list_all_connections()
if len(all_connections) == 0:
print("No more connections. Shutting down GUI BEC client.")
self.bec_dispatcher.disconnect_all()
self.client.shutdown()
# def closeEvent(self, event):
# self.cleanup()
# super().closeEvent(event)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,12 +9,12 @@ 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 qtpy.QtCore import QObject, QTimer, Signal, Slot
from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.widgets.bec_status_box.status_item import StatusItem
if TYPE_CHECKING:
@@ -57,7 +57,7 @@ class BECServiceStatusMixin(QObject):
self.services_update.emit(self.client._services_info, self.client._services_metric)
class BECStatusBox(BECWidget, QWidget):
class BECStatusBox(BECConnector, QWidget):
"""An autonomous widget to display the status of BEC services.
Args:
@@ -290,6 +290,15 @@ class BECStatusBox(BECWidget, QWidget):
if objects["item"] == item:
objects["widget"].show_popup()
def closeEvent(self, event):
"""Upon closing the widget, clean up the BECStatusBox and the QWidget.
Args:
event: The close event.
"""
super().cleanup()
super().closeEvent(event)
def main():
"""Main method to run the BECStatusBox widget."""
@@ -297,7 +306,7 @@ def main():
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
apply_theme("dark")
qdarktheme.setup_theme("auto")
main_window = BECStatusBox()
main_window.show()
sys.exit(app.exec())

View File

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

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -3,7 +3,7 @@ import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.color_button.color_button import ColorButton
from bec_widgets.widgets.buttons.color_button.color_button import ColorButton
DOM_XML = """
<ui language='c++'>

View File

@@ -6,7 +6,7 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.color_button.color_button_plugin import ColorButtonPlugin
from bec_widgets.widgets.buttons.color_button.color_button_plugin import ColorButtonPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(ColorButtonPlugin())

View File

@@ -1,10 +1,9 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QPushButton
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils import BECConnector
class StopButton(BECWidget, QPushButton):
class StopButton(BECConnector, QPushButton):
"""A button that stops the current scan."""
def __init__(self, parent=None, client=None, config=None, gui_id=None):
@@ -13,13 +12,21 @@ class StopButton(BECWidget, QPushButton):
self.get_bec_shortcuts()
self.setText("Stop")
self.setStyleSheet(
"background-color: #cc181e; color: white; font-weight: bold; font-size: 12px;"
)
self.setStyleSheet("background-color: #cc181e; color: white")
self.clicked.connect(self.stop_scan)
@Slot()
def stop_scan(self):
"""Stop the scan."""
self.queue.request_scan_abortion()
self.queue.request_queue_reset()
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = StopButton()
widget.show()
sys.exit(app.exec_())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,54 +0,0 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.device_box.device_box import DeviceBox
DOM_XML = """
<ui language='c++'>
<widget class='DeviceBox' name='device_box'>
</widget>
</ui>
"""
class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = DeviceBox(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "Device Control"
def icon(self):
return QIcon()
def includeFile(self):
return "device_box"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "DeviceBox"
def toolTip(self):
return "A widget for controlling a single positioner. "
def whatsThis(self):
return self.toolTip()

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,10 @@ from typing import TYPE_CHECKING
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputBase, DeviceInputConfig
if TYPE_CHECKING:
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig
from bec_widgets.widgets.device_inputs.device_input_base import DeviceInputConfig
class DeviceLineEdit(DeviceInputBase, QLineEdit):
@@ -34,8 +33,8 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
default: str | None = None,
arg_name: str | None = None,
):
super().__init__(client=client, config=config, gui_id=gui_id)
QLineEdit.__init__(self, parent=parent)
DeviceInputBase.__init__(self, client=client, config=config, gui_id=gui_id)
self.completer = QCompleter(self)
self.setCompleter(self.completer)
@@ -95,3 +94,11 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
if device_obj is None:
raise ValueError(f"Device {device_name} is not found.")
return device_obj
def cleanup(self):
"""Cleanup the widget."""
super().cleanup()
def closeEvent(self, event):
super().cleanup()
return QLineEdit.closeEvent(self, event)

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

View File

@@ -6,8 +6,7 @@ from pydantic import Field
from pyqtgraph.dockarea import Dock
from bec_widgets.cli.rpc_wigdet_handler import widget_handler
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils import BECConnector, ConnectionConfig, GridLayoutManager
if TYPE_CHECKING:
from qtpy.QtWidgets import QWidget
@@ -25,7 +24,7 @@ class DockConfig(ConnectionConfig):
)
class BECDock(BECWidget, Dock):
class BECDock(BECConnector, Dock):
USER_ACCESS = [
"_config_dict",
"_rpc_id",
@@ -92,7 +91,7 @@ class BECDock(BECWidget, Dock):
super().float()
@property
def widget_list(self) -> list[BECWidget]:
def widget_list(self) -> list[BECConnector]:
"""
Get the widgets in the dock.
@@ -102,7 +101,7 @@ class BECDock(BECWidget, Dock):
return self.widgets
@widget_list.setter
def widget_list(self, value: list[BECWidget]):
def widget_list(self, value: list[BECConnector]):
self.widgets = value
def hide_title_bar(self):
@@ -154,13 +153,13 @@ class BECDock(BECWidget, Dock):
def add_widget(
self,
widget: BECWidget | str,
widget: BECConnector | str,
row=None,
col=0,
rowspan=1,
colspan=1,
shift: Literal["down", "up", "left", "right"] = "down",
) -> BECWidget:
) -> BECConnector:
"""
Add a widget to the dock.
@@ -239,7 +238,6 @@ class BECDock(BECWidget, Dock):
for widget in self.widgets:
if hasattr(widget, "cleanup"):
widget.cleanup()
self.widgets.clear()
super().cleanup()
def close(self):

View File

@@ -9,8 +9,7 @@ from qtpy.QtCore import Qt
from qtpy.QtGui import QPainter, QPaintEvent
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils import BECConnector, ConnectionConfig, WidgetContainerUtils
from .dock import BECDock, DockConfig
@@ -22,7 +21,7 @@ class DockAreaConfig(ConnectionConfig):
)
class BECDockArea(BECWidget, DockArea):
class BECDockArea(BECConnector, DockArea):
USER_ACCESS = [
"_config_dict",
"panels",
@@ -228,7 +227,6 @@ class BECDockArea(BECWidget, DockArea):
self.attach_all()
for dock in dict(self.docks).values():
dock.remove()
self.docks.clear()
def cleanup(self):
"""

View File

@@ -7,14 +7,13 @@ from typing import Literal, Optional
import numpy as np
import pyqtgraph as pg
import qdarktheme
from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtWidgets import QWidget
from typeguard import typechecked
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils import BECConnector, ConnectionConfig, WidgetContainerUtils
from bec_widgets.widgets.figure.plots.image.image import BECImageShow, ImageConfig
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap, MotorMapConfig
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
@@ -109,7 +108,7 @@ class WidgetHandler:
return widget
class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
USER_ACCESS = [
"_rpc_id",
"_config_dict",
@@ -122,7 +121,6 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
"remove",
"change_layout",
"change_theme",
"export",
"clear_all",
"widget_list",
]
@@ -229,23 +227,106 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
"""
self._widgets = value
def export(self):
"""Export the plot widget."""
try:
plot_item = self.widget_list[0]
except:
raise ValueError("No plot widget available to export.")
def _init_waveform(
self,
waveform,
x_name: str = None,
y_name: str = None,
z_name: str = None,
x_entry: str = None,
y_entry: str = None,
z_entry: str = None,
x: list | np.ndarray = None,
y: list | np.ndarray = None,
color: str | None = None,
color_map_z: str | None = "plasma",
label: str | None = None,
validate: bool = True,
dap: str | None = None,
) -> BECWaveform:
"""
Configure the waveform based on the provided parameters.
scene = plot_item.scene()
scene.contextMenuItem = plot_item
scene.showExportDialog()
Args:
waveform (BECWaveform): The waveform to configure.
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 x is not None and y is None:
if isinstance(x, np.ndarray):
if x.ndim == 1:
y = np.arange(x.size)
waveform.add_curve_custom(x=np.arange(x.size), y=x, color=color, label=label)
return waveform
if x.ndim == 2:
waveform.add_curve_custom(x=x[:, 0], y=x[:, 1], color=color, label=label)
return waveform
elif isinstance(x, list):
y = np.arange(len(x))
waveform.add_curve_custom(x=np.arange(len(x)), y=x, color=color, label=label)
return waveform
else:
raise ValueError(
"Invalid input. Provide either device names (x_name, y_name) or custom data."
)
if x is not None and y is not None:
waveform.add_curve_custom(x=x, y=y, color=color, label=label)
return waveform
# User wants to add scan curve -> 1D Waveform
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
waveform.plot(
x_name=x_name,
y_name=y_name,
x_entry=x_entry,
y_entry=y_entry,
validate=validate,
color=color,
label=label,
dap=dap,
)
# User wants to add scan curve -> 2D Waveform Scatter
if (
x_name is not None
and y_name is not None
and z_name is not None
and x is None
and y is None
):
waveform.plot(
x_name=x_name,
y_name=y_name,
z_name=z_name,
x_entry=x_entry,
y_entry=y_entry,
z_entry=z_entry,
color=color,
color_map_z=color_map_z,
label=label,
validate=validate,
dap=dap,
)
# User wants to add custom curve
elif x is not None and y is not None and x_name is None and y_name is None:
waveform.add_curve_custom(x=x, y=y, color=color, label=label)
return waveform
@typechecked
def plot(
self,
arg1: list | np.ndarray | str | None = None,
y: list | np.ndarray | 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,
@@ -267,9 +348,8 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
Args:
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.
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.
@@ -296,23 +376,23 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
if config is not None:
return waveform
if arg1 is not None or y_name is not None or (y is not None and x is not None):
waveform.plot(
arg1=arg1,
y=y,
x=x,
x_name=x_name,
y_name=y_name,
z_name=z_name,
x_entry=x_entry,
y_entry=y_entry,
z_entry=z_entry,
color=color,
color_map_z=color_map_z,
label=label,
validate=validate,
dap=dap,
)
# Passing args to init_waveform
waveform = self._init_waveform(
waveform=waveform,
x=x,
y=y,
x_name=x_name,
y_name=y_name,
z_name=z_name,
x_entry=x_entry,
y_entry=y_entry,
z_entry=z_entry,
color=color,
color_map_z=color_map_z,
label=label,
validate=validate,
dap=dap,
)
return waveform
def _init_image(
@@ -590,8 +670,9 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
Args:
theme(Literal["dark","light"]): The theme to set for the figure widget.
"""
qdarktheme.setup_theme(theme)
self.setBackground("k" if theme == "dark" else "w")
self.config.theme = theme
apply_theme(theme)
for plot in self.widget_list:
plot.set_x_label(plot.plot_item.getAxis("bottom").label.toPlainText())
plot.set_y_label(plot.plot_item.getAxis("left").label.toPlainText())
@@ -741,9 +822,14 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
"""Clear all widgets from the figure and reset to default state"""
for widget in list(self._widgets.values()):
widget.remove()
self._widgets.clear()
# self.clear()
self._widgets = defaultdict(dict)
self.grid = []
theme = self.config.theme
self.config = FigureConfig(
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
)
def cleanup(self):
self.clear_all()
super().cleanup()

View File

@@ -1,14 +1,14 @@
import os
import qdarktheme
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QVBoxLayout
from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.qt_utils.settings_dialog import SettingWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO
class AxisSettings(SettingWidget):
class AxisSettings(QWidget):
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent=parent, *args, **kwargs)
@@ -25,10 +25,6 @@ class AxisSettings(SettingWidget):
@Slot(dict)
def display_current_settings(self, axis_config: dict):
if axis_config == {}:
return
# Top Box
WidgetIO.set_value(self.ui.plot_title, axis_config["title"])
@@ -41,10 +37,6 @@ class AxisSettings(SettingWidget):
WidgetIO.check_and_adjust_limits(self.ui.x_max, axis_config["x_lim"][1])
WidgetIO.set_value(self.ui.x_min, axis_config["x_lim"][0])
WidgetIO.set_value(self.ui.x_max, axis_config["x_lim"][1])
if axis_config["x_lim"] is None:
x_range = self.target_widget.fig.widget_list[0].plot_item.viewRange()[0]
WidgetIO.set_value(self.ui.x_min, x_range[0])
WidgetIO.set_value(self.ui.x_max, x_range[1])
# Y Axis Box
WidgetIO.set_value(self.ui.y_label, axis_config["y_label"])
@@ -55,34 +47,15 @@ class AxisSettings(SettingWidget):
WidgetIO.check_and_adjust_limits(self.ui.y_max, axis_config["y_lim"][1])
WidgetIO.set_value(self.ui.y_min, axis_config["y_lim"][0])
WidgetIO.set_value(self.ui.y_max, axis_config["y_lim"][1])
if axis_config["y_lim"] is None:
y_range = self.target_widget.fig.widget_list[0].plot_item.viewRange()[1]
WidgetIO.set_value(self.ui.y_min, y_range[0])
WidgetIO.set_value(self.ui.y_max, y_range[1])
@Slot()
def accept_changes(self):
title = WidgetIO.get_value(self.ui.plot_title)
# X Axis
x_label = WidgetIO.get_value(self.ui.x_label)
x_scale = self.ui.x_scale.currentText()
x_grid = WidgetIO.get_value(self.ui.x_grid)
x_lim = (WidgetIO.get_value(self.ui.x_min), WidgetIO.get_value(self.ui.x_max))
if __name__ == "__main__":
import sys
# Y Axis
y_label = WidgetIO.get_value(self.ui.y_label)
y_scale = self.ui.y_scale.currentText()
y_grid = WidgetIO.get_value(self.ui.y_grid)
y_lim = (WidgetIO.get_value(self.ui.y_min), WidgetIO.get_value(self.ui.y_max))
from qtpy.QtWidgets import QApplication
self.target_widget.set(
title=title,
x_label=x_label,
x_scale=x_scale,
x_lim=x_lim,
y_label=y_label,
y_scale=y_scale,
y_lim=y_lim,
)
self.target_widget.set_grid(x_grid, y_grid)
app = QApplication(sys.argv)
qdarktheme.setup_theme("dark")
window = AxisSettings()
window.show()
sys.exit(app.exec_())

View File

@@ -55,7 +55,6 @@ class BECImageShow(BECPlotBase):
"set_y_lim",
"set_grid",
"lock_aspect_ratio",
"export",
"remove",
"images",
]
@@ -597,4 +596,7 @@ class BECImageShow(BECPlotBase):
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(monitor)
)
self.images.clear()
for image in self.images:
image.cleanup()
super().cleanup()

View File

@@ -58,7 +58,6 @@ class BECMotorMap(BECPlotBase):
"set_background_value",
"set_scatter_size",
"get_data",
"export",
"remove",
"reset_history",
]
@@ -272,12 +271,11 @@ class BECMotorMap(BECPlotBase):
def _swap_limit_map(self):
"""Swap the limit map."""
self.plot_item.removeItem(self.plot_components["limit_map"])
if self.config.signals.x.limits is not None and self.config.signals.y.limits is not None:
self.plot_components["limit_map"] = self._make_limit_map(
self.config.signals.x.limits, self.config.signals.y.limits
)
self.plot_components["limit_map"].setZValue(-1)
self.plot_item.addItem(self.plot_components["limit_map"])
self.plot_components["limit_map"] = self._make_limit_map(
self.config.signals.x.limits, self.config.signals.y.limits
)
self.plot_components["limit_map"].setZValue(-1)
self.plot_item.addItem(self.plot_components["limit_map"])
def _make_motor_map(self):
"""
@@ -286,10 +284,9 @@ class BECMotorMap(BECPlotBase):
# Create limit map
motor_x_limit = self.config.signals.x.limits
motor_y_limit = self.config.signals.y.limits
if motor_x_limit is not None or motor_y_limit is not None:
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
self.plot_item.addItem(self.plot_components["limit_map"])
self.plot_components["limit_map"].setZValue(-1)
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
self.plot_item.addItem(self.plot_components["limit_map"])
self.plot_components["limit_map"].setZValue(-1)
# Create scatter plot
scatter_size = self.config.scatter_size
@@ -519,3 +516,4 @@ class BECMotorMap(BECPlotBase):
def cleanup(self):
"""Cleanup the widget."""
self._disconnect_current_motors()
super().cleanup()

View File

@@ -2,8 +2,11 @@ from __future__ import annotations
from typing import Literal, Optional
import numpy as np
import pyqtgraph as pg
from pydantic import BaseModel, Field
from qtpy import QT_VERSION
from qtpy.QtGui import QFont, QFontDatabase, QFontInfo
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import BECConnector, ConnectionConfig
@@ -54,7 +57,6 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
"set_y_lim",
"set_grid",
"lock_aspect_ratio",
"export",
"remove",
"set_legend_label_size",
]
@@ -291,26 +293,12 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
"""
self.plot_item.setAspectLocked(lock)
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.
"""
self.plot_item.enableAutoRange(axis, enabled)
def export(self):
"""Show the Export Dialog of the plot widget."""
scene = self.plot_item.scene()
scene.contextMenuItem = self.plot_item
scene.showExportDialog()
def remove(self):
"""Remove the plot widget from the figure."""
if self.figure is not None:
self.cleanup()
self.figure.remove(widget_id=self.gui_id)
def cleanup(self):
"""Cleanup the plot widget."""
super().cleanup()

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, Optional
from typing import TYPE_CHECKING, Literal, Optional
import numpy as np
import pyqtgraph as pg
from bec_lib.endpoints import MessageEndpoints
from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError
from qtpy import QtCore
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
if TYPE_CHECKING:
import numpy as np
from bec_widgets.widgets.figure.plots.waveform import BECWaveform1D
@@ -29,7 +30,7 @@ class Signal(BaseModel):
"""The configuration of a signal in the 1D waveform widget."""
source: str
x: Optional[SignalData] = None
x: SignalData # TODO maybe add metadata for config gui later
y: SignalData
z: Optional[SignalData] = None
dap: Optional[str] = None
@@ -101,10 +102,17 @@ class BECCurve(BECConnector, pg.PlotDataItem):
self.parent_item = parent_item
self.apply_config()
self.dap_params = None
self.dap_summary = None
if kwargs:
self.set(**kwargs)
self._async_info = {}
if self.config.source == "async_readback":
# get updates on new scans in order to change the subscription to the correct async readback
self.bec_dispatcher.connect_slot(
self.on_scan_status_update, MessageEndpoints.scan_status()
)
def apply_config(self):
pen_style_map = {
"solid": QtCore.Qt.SolidLine,
@@ -133,20 +141,55 @@ class BECCurve(BECConnector, pg.PlotDataItem):
def dap_params(self, value):
self._dap_params = value
@property
def dap_summary(self):
return self._dap_report
@dap_summary.setter
def dap_summary(self, value):
self._dap_report = value
def set_data(self, x, y):
if self.config.source == "custom":
self.setData(x, y)
else:
raise ValueError(f"Source {self.config.source} do not allow custom data setting.")
def on_scan_status_update(self, content: dict, metadata: dict):
"""
Update the async info with the latest scan status.
"""
scan_id = content.get("scan_id")
if scan_id == self._async_info.get("scan_id"):
return
active_subscription = self._async_info.get("active_subscription")
if active_subscription:
self.bec_dispatcher.disconnect_slot(self.on_async_update, active_subscription)
self._async_info["scan_id"] = scan_id
self._async_info["active_subscription"] = MessageEndpoints.device_async_readback(
scan_id, self.config.signals.y.name
)
self._async_info["data"] = []
self.bec_dispatcher.connect_slot(
self.on_async_update,
MessageEndpoints.device_async_readback(scan_id, self.config.signals.y.name),
from_start=True,
)
def on_async_update(self, content: dict, metadata: dict):
"""
Update the curve with the latest async readback data.
"""
async_update = metadata.get("async_update")
if not async_update:
return
signals = content.get("signals")
if not signals:
return
y_data = signals.get(self.config.signals.y.name, {}).get("value", [])
if y_data is None:
return
if async_update == "extend":
self._async_info["data"].extend(y_data)
elif async_update == "replace":
self._async_info["data"] = y_data
else:
print(f"Warning: Unsupported async update type {async_update}.")
self.setData(self._async_info["data"])
def set(self, **kwargs):
"""
Set the properties of the curve.
@@ -258,17 +301,11 @@ class BECCurve(BECConnector, pg.PlotDataItem):
Returns:
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
"""
try:
x_data, y_data = self.getData()
except TypeError:
x_data, y_data = np.array([]), np.array([])
x_data, y_data = self.getData()
return x_data, y_data
def clear_data(self):
self.setData([], [])
def remove(self):
"""Remove the curve from the plot."""
# self.parent_item.removeItem(self)
self.parent_item.remove_curve(self.name())
self.rpc_register.remove_rpc(self)
self.cleanup()

View File

@@ -10,7 +10,6 @@ class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
super().__init__()
self.inprocess = None
self.client = None
self.kernel_manager, self.kernel_client = self._init_kernel(inprocess=inprocess)
self.set_default_style("linux")
@@ -61,11 +60,6 @@ class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
self.kernel_client.stop_channels()
self.kernel_manager.shutdown_kernel()
def closeEvent(self, event):
self.shutdown_kernel()
if self.client:
self.client.shutdown()
if __name__ == "__main__": # pragma: no cover
import sys

View File

@@ -0,0 +1,252 @@
# pylint: disable = no-name-in-module,missing-module-docstring
from enum import Enum
from bec_lib.alarm_handler import AlarmBase
from bec_lib.device import Positioner
from qtpy.QtCore import QThread
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QMessageBox, QWidget
from bec_widgets.utils.bec_dispatcher import BECDispatcher
CONFIG_DEFAULT = {
"motor_control": {
"motor_x": "samx",
"motor_y": "samy",
"step_size_x": 3,
"step_size_y": 50,
"precision": 4,
"step_x_y_same": False,
"move_with_arrows": False,
}
}
class MotorControlWidget(QWidget):
"""Base class for motor control widgets."""
def __init__(self, parent=None, client=None, motor_thread=None, config=None):
super().__init__(parent)
self.client = client
self.motor_thread = motor_thread
self.config = config
self.motor_x = None
self.motor_y = None
if not self.client:
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client
if not self.motor_thread:
self.motor_thread = MotorThread(client=self.client)
self._load_ui()
if self.config is None:
print(f"No initial config found for {self.__class__.__name__}")
self._init_ui()
else:
self.on_config_update(self.config)
def _load_ui(self):
"""Load the UI from the .ui file."""
def _init_ui(self):
"""Initialize the UI components specific to the widget."""
@pyqtSlot(dict)
def on_config_update(self, config):
"""Handle configuration updates."""
self.config = config
self._init_ui()
class MotorControlErrors:
"""Class for displaying formatted error messages."""
@staticmethod
def display_error_message(error_message: str) -> None:
"""
Display a critical error message.
Args:
error_message(str): Error message to display.
"""
# Create a QMessageBox
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setWindowTitle("Critical Error")
# Format the message
formatted_message = MotorControlErrors._format_error_message(error_message)
msg.setText(formatted_message)
# Display the message box
msg.exec_()
@staticmethod
def _format_error_message(error_message: str) -> str:
"""
Format the error message.
Args:
error_message(str): Error message to format.
Returns:
str: Formatted error message.
"""
# Split the message into lines
lines = error_message.split("\n")
formatted_lines = [
f"<b>{line.strip()}</b>" if i == 0 else line.strip()
for i, line in enumerate(lines)
if line.strip()
]
# Join the lines with double breaks for empty lines in between
formatted_message = "<br><br>".join(formatted_lines)
return formatted_message
class MotorActions(Enum):
"""Enum for motor actions."""
MOVE_ABSOLUTE = "move_absolute"
MOVE_RELATIVE = "move_relative"
class MotorThread(QThread):
"""
QThread subclass for controlling motor actions asynchronously.
Signals:
coordinates_updated (pyqtSignal): Signal to emit current coordinates.
motor_error (pyqtSignal): Signal to emit when there is an error with the motors.
lock_gui (pyqtSignal): Signal to lock/unlock the GUI.
"""
coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates
motor_error = pyqtSignal(str) # Signal to emit when there is an error with the motors
lock_gui = pyqtSignal(bool) # Signal to lock/unlock the GUI
def __init__(self, parent=None, client=None):
super().__init__(parent)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.dev = self.client.device_manager.devices
self.scans = self.client.scans
self.queue = self.client.queue
self.action = None
self.motor = None
self.motor_x = None
self.motor_y = None
self.target_coordinates = None
self.value = None
def get_all_motors_names(self) -> list:
"""
Get all the motors names.
Returns:
list: List of all the motors names.
"""
all_devices = self.client.device_manager.devices.enabled_devices
all_motors_names = [motor.name for motor in all_devices if isinstance(motor, Positioner)]
return all_motors_names
def get_coordinates(self, motor_x: str, motor_y: str) -> tuple:
"""
Get the current coordinates of the motors.
Args:
motor_x(str): Motor X to get positions from.
motor_y(str): Motor Y to get positions from.
Returns:
tuple: Current coordinates of the motors.
"""
x = self.dev[motor_x].readback.get()
y = self.dev[motor_y].readback.get()
return x, y
def move_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
"""
Wrapper for moving the motor to the target coordinates.
Args:
motor_x(str): Motor X to move.
motor_y(str): Motor Y to move.
target_coordinates(tuple): Target coordinates.
"""
self.action = MotorActions.MOVE_ABSOLUTE
self.motor_x = motor_x
self.motor_y = motor_y
self.target_coordinates = target_coordinates
self.start()
def move_relative(self, motor: str, value: float) -> None:
"""
Wrapper for moving the motor relative to the current position.
Args:
motor(str): Motor to move.
value(float): Value to move.
"""
self.action = MotorActions.MOVE_RELATIVE
self.motor = motor
self.value = value
self.start()
def run(self):
"""
Run the thread.
Possible actions:
- Move to coordinates
- Move relative
"""
if self.action == MotorActions.MOVE_ABSOLUTE:
self._move_motor_absolute(self.motor_x, self.motor_y, self.target_coordinates)
elif self.action == MotorActions.MOVE_RELATIVE:
self._move_motor_relative(self.motor, self.value)
def _move_motor_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
"""
Move the motor to the target coordinates.
Args:
motor_x(str): Motor X to move.
motor_y(str): Motor Y to move.
target_coordinates(tuple): Target coordinates.
"""
self.lock_gui.emit(False)
try:
status = self.scans.mv(
self.dev[motor_x],
target_coordinates[0],
self.dev[motor_y],
target_coordinates[1],
relative=False,
)
status.wait()
except AlarmBase as e:
self.motor_error.emit(str(e))
finally:
self.lock_gui.emit(True)
def _move_motor_relative(self, motor, value: float) -> None:
"""
Move the motor relative to the current position.
Args:
motor(str): Motor to move.
value(float): Value to move.
"""
self.lock_gui.emit(False)
try:
status = self.scans.mv(self.dev[motor], value, relative=True)
status.wait()
except AlarmBase as e:
self.motor_error.emit(str(e))
finally:
self.lock_gui.emit(True)
def stop_movement(self):
self.queue.request_scan_abortion()
self.queue.request_queue_reset()

View File

@@ -0,0 +1,484 @@
# pylint: disable = no-name-in-module,missing-module-docstring
import os
from qtpy import uic
from qtpy.QtCore import Qt
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QDoubleValidator, QKeySequence
from qtpy.QtWidgets import (
QCheckBox,
QLineEdit,
QMessageBox,
QPushButton,
QShortcut,
QTableWidget,
QTableWidgetItem,
)
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
class MotorCoordinateTable(MotorControlWidget):
"""
Widget to save coordinates from motor, display them in the table and move back to them.
There are two modes of operation:
- Individual: Each row is a single coordinate.
- Start/Stop: Each pair of rows is a start and end coordinate.
Signals:
plot_coordinates_signal (pyqtSignal(list, str, str)): Signal to plot the coordinates in the MotorMap.
Slots:
add_coordinate (pyqtSlot(tuple)): Slot to add a coordinate to the table.
mode_switch (pyqtSlot): Slot to switch between individual and start/stop mode.
"""
plot_coordinates_signal = pyqtSignal(list, str, str)
def _load_ui(self):
"""Load the UI for the coordinate table."""
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "motor_table.ui"), self)
def _init_ui(self):
"""Initialize the UI"""
# Setup table behaviour
self._setup_table()
self.ui.table.setSelectionBehavior(QTableWidget.SelectRows)
# for tag columns default tag
self.tag_counter = 1
# Connect signals and slots
self.ui.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
self.ui.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
# Keyboard shortcuts for deleting a row
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.ui.table)
self.delete_shortcut.activated.connect(self.delete_selected_row)
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.ui.table)
self.backspace_shortcut.activated.connect(self.delete_selected_row)
# Warning message for mode switch enable/disable
self.warning_message = True
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""
Update config dict
Args:
config(dict): New config dict
"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
# Decimal precision of the table coordinates
self.precision = self.config["motor_control"].get("precision", 3)
# Mode switch default option
self.mode = self.config["motor_control"].get("mode", "Individual")
# Set combobox to default mode
self.ui.comboBox_mode.setCurrentText(self.mode)
self._init_ui()
def _setup_table(self):
"""Setup the table with appropriate headers and configurations."""
mode = self.ui.comboBox_mode.currentText()
if mode == "Individual":
self._setup_individual_mode()
elif mode == "Start/Stop":
self._setup_start_stop_mode()
self.start_stop_counter = 0 # TODO: remove this??
self.wipe_motor_map_coordinates()
def _setup_individual_mode(self):
"""Setup the table for individual mode."""
self.ui.table.setColumnCount(5)
self.ui.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
self.ui.table.verticalHeader().setVisible(False)
def _setup_start_stop_mode(self):
"""Setup the table for start/stop mode."""
self.ui.table.setColumnCount(8)
self.ui.table.setHorizontalHeaderLabels(
[
"Show",
"Move [start]",
"Move [end]",
"Tag",
"X [start]",
"Y [start]",
"X [end]",
"Y [end]",
]
)
self.ui.table.verticalHeader().setVisible(False)
# Set flag to track if the coordinate is stat or the end of the entry
self.is_next_entry_end = False
def mode_switch(self):
"""Switch between individual and start/stop mode."""
last_selected_index = self.ui.comboBox_mode.currentIndex()
if self.ui.table.rowCount() > 0 and self.warning_message is True:
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Critical)
msgBox.setText(
"Switching modes will delete all table entries. Do you want to continue?"
)
msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
returnValue = msgBox.exec()
if returnValue is QMessageBox.Cancel:
self.ui.comboBox_mode.blockSignals(True) # Block signals
self.ui.comboBox_mode.setCurrentIndex(last_selected_index)
self.ui.comboBox_mode.blockSignals(False) # Unblock signals
return
# Wipe table
self.wipe_motor_map_coordinates()
# Initiate new table with new mode
self._setup_table()
@pyqtSlot(tuple)
def add_coordinate(self, coordinates: tuple):
"""
Add a coordinate to the table.
Args:
coordinates(tuple): Coordinates (x,y) to add to the table.
"""
tag = f"Pos {self.tag_counter}"
self.tag_counter += 1
x, y = coordinates
self._add_row(tag, x, y)
def _add_row(self, tag: str, x: float, y: float) -> None:
"""
Add a row to the table.
Args:
tag(str): Tag of the coordinate.
x(float): X coordinate.
y(float): Y coordinate.
"""
mode = self.ui.comboBox_mode.currentText()
if mode == "Individual":
checkbox_pos = 0
button_pos = 1
tag_pos = 2
x_pos = 3
y_pos = 4
coordinate_reference = "Individual"
color = "green"
# Add new row -> new entry
row_count = self.ui.table.rowCount()
self.ui.table.insertRow(row_count)
# Add Widgets
self._add_widgets(
tag,
x,
y,
row_count,
checkbox_pos,
tag_pos,
button_pos,
x_pos,
y_pos,
coordinate_reference,
color,
)
if mode == "Start/Stop":
# These positions are always fixed
checkbox_pos = 0
tag_pos = 3
if self.is_next_entry_end is False: # It is the start position of the entry
print("Start position")
button_pos = 1
x_pos = 4
y_pos = 5
coordinate_reference = "Start"
color = "blue"
# Add new row -> new entry
row_count = self.ui.table.rowCount()
self.ui.table.insertRow(row_count)
# Add Widgets
self._add_widgets(
tag,
x,
y,
row_count,
checkbox_pos,
tag_pos,
button_pos,
x_pos,
y_pos,
coordinate_reference,
color,
)
# Next entry will be the end of the current entry
self.is_next_entry_end = True
elif self.is_next_entry_end is True: # It is the end position of the entry
print("End position")
row_count = self.ui.table.rowCount() - 1 # Current row
button_pos = 2
x_pos = 6
y_pos = 7
coordinate_reference = "Stop"
color = "red"
# Add Widgets
self._add_widgets(
tag,
x,
y,
row_count,
checkbox_pos,
tag_pos,
button_pos,
x_pos,
y_pos,
coordinate_reference,
color,
)
self.is_next_entry_end = False # Next entry will be the start of the new entry
# Auto table resize
self.resize_table_auto()
def _add_widgets(
self,
tag: str,
x: float,
y: float,
row: int,
checkBox_pos: int,
tag_pos: int,
button_pos: int,
x_pos: int,
y_pos: int,
coordinate_reference: str,
color: str,
) -> None:
"""
Add widgets to the table.
Args:
tag(str): Tag of the coordinate.
x(float): X coordinate.
y(float): Y coordinate.
row(int): Row of the QTableWidget where to add the widgets.
checkBox_pos(int): Column where to put CheckBox.
tag_pos(int): Column where to put Tag.
button_pos(int): Column where to put Move button.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
coordinate_reference(str): Reference to the coordinate for MotorMap.
color(str): Color of the coordinate for MotorMap.
"""
# Add widgets
self._add_checkbox(row, checkBox_pos, x_pos, y_pos)
self._add_move_button(row, button_pos, x_pos, y_pos)
self.ui.table.setItem(row, tag_pos, QTableWidgetItem(tag))
self._add_line_edit(x, row, x_pos, x_pos, y_pos, coordinate_reference, color)
self._add_line_edit(y, row, y_pos, x_pos, y_pos, coordinate_reference, color)
# # Emit the coordinates to be plotted
self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
# Connect item edit to emit coordinates
self.ui.table.itemChanged.connect(
lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}")
)
self.ui.table.itemChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
)
def _add_checkbox(self, row: int, checkBox_pos: int, x_pos: int, y_pos: int):
"""
Add a checkbox to the table.
Args:
row(int): Row of QTableWidget where to add the checkbox.
checkBox_pos(int): Column where to put CheckBox.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
"""
show_checkbox = QCheckBox()
show_checkbox.setChecked(True)
show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos))
self.ui.table.setCellWidget(row, checkBox_pos, show_checkbox)
def _add_move_button(self, row: int, button_pos: int, x_pos: int, y_pos: int) -> None:
"""
Add a move button to the table.
Args:
row(int): Row of QTableWidget where to add the move button.
button_pos(int): Column where to put move button.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
"""
move_button = QPushButton("Move")
move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos))
self.ui.table.setCellWidget(row, button_pos, move_button)
def _add_line_edit(
self,
value: float,
row: int,
line_pos: int,
x_pos: int,
y_pos: int,
coordinate_reference: str,
color: str,
) -> None:
"""
Add a QLineEdit to the table.
Args:
value(float): Initial value of the QLineEdit.
row(int): Row of QTableWidget where to add the QLineEdit.
line_pos(int): Column where to put QLineEdit.
x_pos(int): Column where to link x coordinate.
y_pos(int): Column where to link y coordinate.
coordinate_reference(str): Reference to the coordinate for MotorMap.
color(str): Color of the coordinate for MotorMap.
"""
# Adding validator
validator = QDoubleValidator()
validator.setDecimals(self.precision)
# Create line edit
edit = QLineEdit(str(f"{value:.{self.precision}f}"))
edit.setValidator(validator)
edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Add line edit to the table
self.ui.table.setCellWidget(row, line_pos, edit)
edit.textChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
)
def wipe_motor_map_coordinates(self):
"""Wipe the motor map coordinates."""
try:
self.ui.table.itemChanged.disconnect() # Disconnect all previous connections
except TypeError:
print("No previous connections to disconnect")
self.ui.table.setRowCount(0)
reference_tags = ["Individual", "Start", "Stop"]
for reference_tag in reference_tags:
self.plot_coordinates_signal.emit([], reference_tag, "green")
def handle_move_button_click(self, x_pos: int, y_pos: int) -> None:
"""
Handle the move button click.
Args:
x_pos(int): X position of the coordinate.
y_pos(int): Y position of the coordinate.
"""
button = self.sender()
row = self.ui.table.indexAt(button.pos()).row()
x = self.get_coordinate(row, x_pos)
y = self.get_coordinate(row, y_pos)
self.move_motor(x, y)
def emit_plot_coordinates(self, x_pos: float, y_pos: float, reference_tag: str, color: str):
"""
Emit the coordinates to be plotted.
Args:
x_pos(float): X position of the coordinate.
y_pos(float): Y position of the coordinate.
reference_tag(str): Reference tag of the coordinate.
color(str): Color of the coordinate.
"""
print(
f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}"
)
coordinates = []
for row in range(self.ui.table.rowCount()):
show = self.ui.table.cellWidget(row, 0).isChecked()
x = self.get_coordinate(row, x_pos)
y = self.get_coordinate(row, y_pos)
coordinates.append((x, y, show)) # (x, y, show_flag)
self.plot_coordinates_signal.emit(coordinates, reference_tag, color)
def get_coordinate(self, row: int, column: int) -> float:
"""
Helper function to get the coordinate from the table QLineEdit cells.
Args:
row(int): Row of the table.
column(int): Column of the table.
Returns:
float: Value of the coordinate.
"""
edit = self.ui.table.cellWidget(row, column)
value = float(edit.text()) if edit and edit.text() != "" else None
if value:
return value
def delete_selected_row(self):
"""Delete the selected row from the table."""
selected_rows = self.ui.table.selectionModel().selectedRows()
for row in selected_rows:
self.ui.table.removeRow(row.row())
if self.ui.comboBox_mode.currentText() == "Start/Stop":
self.emit_plot_coordinates(x_pos=4, y_pos=5, reference_tag="Start", color="blue")
self.emit_plot_coordinates(x_pos=6, y_pos=7, reference_tag="Stop", color="red")
self.is_next_entry_end = False
elif self.ui.comboBox_mode.currentText() == "Individual":
self.emit_plot_coordinates(x_pos=3, y_pos=4, reference_tag="Individual", color="green")
def resize_table_auto(self):
"""Resize the table to fit the contents."""
if self.ui.checkBox_resize_auto.isChecked():
self.ui.table.resizeColumnsToContents()
def move_motor(self, x: float, y: float) -> None:
"""
Move the motor to the target coordinates.
Args:
x(float): Target x coordinate.
y(float): Target y coordinate.
"""
self.motor_thread.move_absolute(self.motor_x, self.motor_y, (x, y))
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str) -> None:
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
"""
self.motor_x = motor_x
self.motor_y = motor_y
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
@pyqtSlot(int)
def set_precision(self, precision: int) -> None:
"""
Set the precision of the coordinates.
Args:
precision(int): Precision of the coordinates.
"""
self.precision = precision
self.config["motor_control"]["precision"] = precision

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>676</width>
<height>667</height>
</rect>
</property>
<property name="windowTitle">
<string>Motor Coordinates Table</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="checkBox_resize_auto">
<property name="text">
<string>Resize Auto</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_editColumns">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Edit Custom Column</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Entries Mode:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<property name="enabled">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>Individual</string>
</property>
</item>
<item>
<property name="text">
<string>Start/Stop</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="table">
<property name="gridStyle">
<enum>Qt::SolidLine</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pushButton_importCSV">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Import CSV</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_exportCSV">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Export CSV</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,159 @@
import os
from qtpy import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlErrors, MotorControlWidget
class MotorControlAbsolute(MotorControlWidget):
"""
Widget for controlling the motors to absolute coordinates.
Signals:
coordinates_signal (pyqtSignal(tuple)): Signal to emit the coordinates.
Slots:
change_motors (pyqtSlot): Slot to change the active motors.
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls.
"""
coordinates_signal = pyqtSignal(tuple)
def _load_ui(self):
"""Load the UI from the .ui file."""
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_absolute.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
# Check if there are any motors connected
if self.motor_x is None or self.motor_y is None:
self.ui.motorControl_absolute.setEnabled(False)
return
# Move to absolute coordinates
self.ui.pushButton_go_absolute.clicked.connect(
lambda: self.move_motor_absolute(
self.ui.spinBox_absolute_x.value(), self.ui.spinBox_absolute_y.value()
)
)
self.ui.pushButton_set.clicked.connect(self.save_absolute_coordinates)
self.ui.pushButton_save.clicked.connect(self.save_current_coordinates)
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
# Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# Error messages
self.motor_thread.motor_error.connect(
lambda error: MotorControlErrors.display_error_message(error)
)
# Keyboard shortcuts
self._init_keyboard_shortcuts()
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""Update config dict"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
# Update step precision
self.precision = self.config["motor_control"]["precision"]
self._init_ui()
@pyqtSlot(bool)
def enable_motor_controls(self, enable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
enable(bool): True to enable, False to disable.
"""
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.ui.motorControl_absolute.findChildren(QWidget):
widget.setEnabled(enable)
# Enable the pushButton_stop if the motor is moving
self.ui.pushButton_stop.setEnabled(True)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
"""
self.motor_x = motor_x
self.motor_y = motor_y
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
@pyqtSlot(int)
def set_precision(self, precision: int) -> None:
"""
Set the precision of the coordinates.
Args:
precision(int): Precision of the coordinates.
"""
self.precision = precision
self.config["motor_control"]["precision"] = precision
self.ui.spinBox_absolute_x.setDecimals(precision)
self.ui.spinBox_absolute_y.setDecimals(precision)
def move_motor_absolute(self, x: float, y: float) -> None:
"""
Move the motor to the target coordinates.
Args:
x(float): Target x coordinate.
y(float): Target y coordinate.
"""
# self._enable_motor_controls(False)
target_coordinates = (x, y)
self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
if self.ui.checkBox_save_with_go.isChecked():
self.save_absolute_coordinates()
def _init_keyboard_shortcuts(self):
"""Initialize the keyboard shortcuts."""
# Go absolute button
self.ui.pushButton_go_absolute.setShortcut("Ctrl+G")
self.ui.pushButton_go_absolute.setToolTip("Ctrl+G")
# Set absolute coordinates
self.ui.pushButton_set.setShortcut("Ctrl+D")
self.ui.pushButton_set.setToolTip("Ctrl+D")
# Save Current coordinates
self.ui.pushButton_save.setShortcut("Ctrl+S")
self.ui.pushButton_save.setToolTip("Ctrl+S")
# Stop Button
self.ui.pushButton_stop.setShortcut("Ctrl+X")
self.ui.pushButton_stop.setToolTip("Ctrl+X")
def save_absolute_coordinates(self):
"""Emit the setup coordinates from the spinboxes"""
x, y = round(self.ui.spinBox_absolute_x.value(), self.precision), round(
self.ui.spinBox_absolute_y.value(), self.precision
)
self.coordinates_signal.emit((x, y))
def save_current_coordinates(self):
"""Emit the current coordinates from the motor thread"""
x, y = self.motor_thread.get_coordinates(self.motor_x, self.motor_y)
self.coordinates_signal.emit((round(x, self.precision), round(y, self.precision)))

View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>220</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>285</width>
<height>220</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>285</width>
<height>220</height>
</size>
</property>
<property name="windowTitle">
<string>Move Movement Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="motorControl_absolute">
<property name="minimumSize">
<size>
<width>261</width>
<height>195</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>261</width>
<height>195</height>
</size>
</property>
<property name="title">
<string>Move Movement Absolute</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox_save_with_go">
<property name="text">
<string>Save position with Go</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>-500.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Y</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>X</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="pushButton_save">
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_set">
<property name="text">
<string>Set</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_go_absolute">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>Stop Movement</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,230 @@
import os
from qtpy import uic
from qtpy.QtCore import Qt
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
class MotorControlRelative(MotorControlWidget):
"""
Widget for controlling the motors to relative coordinates.
Signals:
precision_signal (pyqtSignal): Signal to emit the precision of the coordinates.
Slots:
change_motors (pyqtSlot(str,str)): Slot to change the active motors.
enable_motor_controls (pyqtSlot): Slot to enable/disable the motor controls.
"""
precision_signal = pyqtSignal(int)
def _load_ui(self):
"""Load the UI from the .ui file."""
# Loading UI
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_relative.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
self._init_ui_motor_control()
self._init_keyboard_shortcuts()
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""
Update config dict
Args:
config(dict): New config dict
"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
# Update step precision
self.precision = self.config["motor_control"]["precision"]
self.ui.spinBox_precision.setValue(self.precision)
# Update step sizes
self.ui.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"])
self.ui.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"])
# Checkboxes for keyboard shortcuts and x/y step size link
self.ui.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
self.ui.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
self._init_ui()
def _init_ui_motor_control(self) -> None:
"""Initialize the motor control elements"""
# Connect checkbox and spinBoxes
self.ui.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes)
self.ui.spinBox_step_x.valueChanged.connect(self._update_step_size_x)
self.ui.spinBox_step_y.valueChanged.connect(self._update_step_size_y)
self.ui.toolButton_right.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", 1)
)
self.ui.toolButton_left.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", -1)
)
self.ui.toolButton_up.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, "y", 1)
)
self.ui.toolButton_down.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, "y", -1)
)
# Switch between key shortcuts active
self.ui.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts)
self._update_arrow_key_shortcuts()
# Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# Precision update
self.ui.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
# Error messages
self.motor_thread.motor_error.connect(
lambda error: MotorControlErrors.display_error_message(error)
)
# Stop Button
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
def _init_keyboard_shortcuts(self) -> None:
"""Initialize the keyboard shortcuts"""
# Increase/decrease step size for X motor
increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
increase_x_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_x, 2)
)
decrease_x_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_x, 0.5)
)
self.ui.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z")
# Increase/decrease step size for Y motor
increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
increase_y_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_y, 2)
)
decrease_y_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_y, 0.5)
)
self.ui.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z")
# Stop Button
self.ui.pushButton_stop.setShortcut("Ctrl+X")
self.ui.pushButton_stop.setToolTip("Ctrl+X")
def _update_arrow_key_shortcuts(self) -> None:
"""Update the arrow key shortcuts based on the checkbox state."""
if self.ui.checkBox_enableArrows.isChecked():
# Set the arrow key shortcuts for motor movement
self.ui.toolButton_right.setShortcut(Qt.Key_Right)
self.ui.toolButton_left.setShortcut(Qt.Key_Left)
self.ui.toolButton_up.setShortcut(Qt.Key_Up)
self.ui.toolButton_down.setShortcut(Qt.Key_Down)
else:
# Clear the shortcuts
self.ui.toolButton_right.setShortcut("")
self.ui.toolButton_left.setShortcut("")
self.ui.toolButton_up.setShortcut("")
self.ui.toolButton_down.setShortcut("")
def _update_precision(self, precision: int) -> None:
"""
Update the precision of the coordinates.
Args:
precision(int): Precision of the coordinates.
"""
self.ui.spinBox_step_x.setDecimals(precision)
self.ui.spinBox_step_y.setDecimals(precision)
self.precision_signal.emit(precision)
def _change_step_size(self, spinBox: QDoubleSpinBox, factor: float) -> None:
"""
Change the step size of the spinbox.
Args:
spinBox(QDoubleSpinBox): Spinbox to change the step size.
factor(float): Factor to change the step size.
"""
old_step = spinBox.value()
new_step = old_step * factor
spinBox.setValue(new_step)
def _sync_step_sizes(self):
"""Sync step sizes based on checkbox state."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_x.value()
self.ui.spinBox_step_y.setValue(value)
def _update_step_size_x(self):
"""Update step size for x if checkbox is checked."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_x.value()
self.ui.spinBox_step_y.setValue(value)
def _update_step_size_y(self):
"""Update step size for y if checkbox is checked."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_y.value()
self.ui.spinBox_step_x.setValue(value)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
"""
Change the active motors and update config.
Can be connected to the selected_motors_signal from MotorControlSelection.
Args:
motor_x(str): New motor X to be controlled.
motor_y(str): New motor Y to be controlled.
"""
self.motor_x = motor_x
self.motor_y = motor_y
self.config["motor_control"]["motor_x"] = motor_x
self.config["motor_control"]["motor_y"] = motor_y
@pyqtSlot(bool)
def enable_motor_controls(self, disable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
disable(bool): True to disable, False to enable.
"""
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.ui.motorControl.findChildren(QWidget):
widget.setEnabled(disable)
# Enable the pushButton_stop if the motor is moving
self.ui.pushButton_stop.setEnabled(True)
def move_motor_relative(self, motor, axis: str, direction: int) -> None:
"""
Move the motor relative to the current position.
Args:
motor: Motor to move.
axis(str): Axis to move.
direction(int): Direction to move. 1 for positive, -1 for negative.
"""
if axis == "x":
step = direction * self.ui.spinBox_step_x.value()
elif axis == "y":
step = direction * self.ui.spinBox_step_y.value()
self.motor_thread.move_relative(motor, step)

View File

@@ -0,0 +1,298 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>405</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>285</width>
<height>405</height>
</size>
</property>
<property name="windowTitle">
<string>Motor Control Relative</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="motorControl">
<property name="minimumSize">
<size>
<width>261</width>
<height>394</height>
</size>
</property>
<property name="title">
<string>Motor Control Relative</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QCheckBox" name="checkBox_enableArrows">
<property name="text">
<string>Move with arrow keys</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_same_xy">
<property name="text">
<string>Step [X] = Step [Y]</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="step_grid">
<item row="2" column="0">
<widget class="QLabel" name="label_step_y">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [Y]</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Decimal</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_x">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_step_x">
<property name="minimumSize">
<size>
<width>111</width>
<height>19</height>
</size>
</property>
<property name="text">
<string>Step [X]</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="spinBox_step_y">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinBox_precision">
<property name="minimumSize">
<size>
<width>110</width>
<height>19</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="direction_grid">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_up">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
<item row="2" column="4">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
<widget class="QToolButton" name="toolButton_down">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::DownArrow</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="toolButton_left">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::LeftArrow</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="toolButton_right">
<property name="minimumSize">
<size>
<width>26</width>
<height>26</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::RightArrow</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>Stop Movement</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,110 @@
# pylint: disable = no-name-in-module,missing-module-docstring
import os
from qtpy import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QComboBox
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
class MotorControlSelection(MotorControlWidget):
"""
Widget for selecting the motors to control.
Signals:
selected_motors_signal (pyqtSignal(str,str)): Signal to emit the selected motors.
Slots:
get_available_motors (pyqtSlot): Slot to populate the available motors in the combo boxes and set the index based on the configuration.
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls GUI.
on_config_update (pyqtSlot(dict)): Slot to update the config dict.
"""
selected_motors_signal = pyqtSignal(str, str)
def _load_ui(self):
"""Load the UI from the .ui file."""
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "selection.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
# Lock GUI while motors are moving
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
self.pushButton_connecMotors.clicked.connect(self.select_motor)
self.get_available_motors()
# Connect change signals to change color
self.comboBox_motor_x.currentIndexChanged.connect(
lambda: self.set_combobox_style(self.comboBox_motor_x, "#ffa700")
)
self.comboBox_motor_y.currentIndexChanged.connect(
lambda: self.set_combobox_style(self.comboBox_motor_y, "#ffa700")
)
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""
Update config dict
Args:
config(dict): New config dict
"""
self.config = config
# Get motor names
self.motor_x, self.motor_y = (
self.config["motor_control"]["motor_x"],
self.config["motor_control"]["motor_y"],
)
self._init_ui()
@pyqtSlot(bool)
def enable_motor_controls(self, enable: bool) -> None:
"""
Enable or disable the motor controls.
Args:
enable(bool): True to enable, False to disable.
"""
self.motorSelection.setEnabled(enable)
@pyqtSlot()
def get_available_motors(self) -> None:
"""
Slot to populate the available motors in the combo boxes and set the index based on the configuration.
"""
# Get all available motors
self.motor_list = self.motor_thread.get_all_motors_names()
# Populate the combo boxes
self.comboBox_motor_x.addItems(self.motor_list)
self.comboBox_motor_y.addItems(self.motor_list)
# Set the index based on the config if provided
if self.config:
index_x = self.comboBox_motor_x.findText(self.motor_x)
index_y = self.comboBox_motor_y.findText(self.motor_y)
self.comboBox_motor_x.setCurrentIndex(index_x if index_x != -1 else 0)
self.comboBox_motor_y.setCurrentIndex(index_y if index_y != -1 else 0)
def set_combobox_style(self, combobox, color: str) -> None:
"""
Set the combobox style to a specific color.
Args:
combobox(QComboBox): Combobox to change the color.
color(str): Color to set the combobox to.
"""
combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
def select_motor(self):
"""Emit the selected motors"""
motor_x = self.comboBox_motor_x.currentText()
motor_y = self.comboBox_motor_y.currentText()
# Reset the combobox color to normal after selection
self.set_combobox_style(self.comboBox_motor_x, "")
self.set_combobox_style(self.comboBox_motor_y, "")
self.selected_motors_signal.emit(motor_x, motor_y)

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>156</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>285</width>
<height>156</height>
</size>
</property>
<property name="windowTitle">
<string>Motor Control Selection</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="motorSelection">
<property name="minimumSize">
<size>
<width>261</width>
<height>145</height>
</size>
</property>
<property name="title">
<string>Motor Selection</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Motor X</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_motor_x"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Motor Y</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="pushButton_connecMotors">
<property name="text">
<string>Connect Motors</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_motor_y"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 341 B

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