mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-19 23:05:36 +02:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65345187b3 | ||
| d48243483e | |||
| 1ca9499edd | |||
| 060935ffc5 | |||
|
|
50dbef52c0 | ||
| bb385f07ca | |||
| cf28730515 | |||
|
|
13ae383455 | ||
| 2265458dcc | |||
|
|
0a59f08fcc | ||
| c70724a456 | |||
| 406c263746 | |||
| df35aabff3 | |||
| cc8c166b5c | |||
| c4f3308dc0 | |||
|
|
8f3824c0e7 | ||
| afdf4e8782 | |||
| 2a82032644 | |||
| 88a2f66758 | |||
| 3f3b207295 | |||
| 44cfda1c07 | |||
| e42b84c636 | |||
| 77c5aa741c | |||
| 2b4449afeb | |||
| 36ad464159 | |||
|
|
e8ae6f2e43 | ||
| 3ecbd60627 | |||
| 82a55ddf3e | |||
|
|
7d190719b1 | ||
| 8c2e7c8259 | |||
| dd7c71bb1e | |||
|
|
7b5b7a8cbb | ||
| af28574bd5 | |||
| 617db36ed4 | |||
|
|
ebc2e44c7c | ||
| 44738057a3 | |||
| f98a9f9771 | |||
| 2fe72c9ccb | |||
| f0203d9bf6 | |||
| 37835cbf76 | |||
|
|
e005be33d1 | ||
| 9d7718c3d9 | |||
| 9d8fb0b761 |
@@ -140,7 +140,7 @@ tests:
|
||||
- *install-os-packages
|
||||
- *install-repos
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --maxfail=2 --random-order --full-trace ./tests/unit_tests
|
||||
- coverage report
|
||||
- coverage xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
@@ -177,8 +177,7 @@ test-matrix:
|
||||
- *install-os-packages
|
||||
- *install-repos
|
||||
- pip install -e .[dev,$QT_PCKG]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
- pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
|
||||
end-2-end-conda:
|
||||
stage: End2End
|
||||
|
||||
264
CHANGELOG.md
264
CHANGELOG.md
@@ -1,5 +1,137 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.99.3 (2024-08-27)
|
||||
|
||||
### Build
|
||||
|
||||
* build: updated min version of bec qthemes ([`d482434`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d48243483ef8228cc5eb85e40a6b8f5da3b45520))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(cmaps): unified all defaults to magma cmap ([`1ca9499`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1ca9499edd334c19fe1e7aac71d3940a80a1ec95))
|
||||
|
||||
* fix(color maps): color maps should take the background color into account; fixed min colors to 10 ([`060935f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/060935ffc5472a958c337bf60834c5291f104ece))
|
||||
|
||||
## v0.99.2 (2024-08-27)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: additional tests are not allowed to fail ([`bb385f0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bb385f07ca18904461a541b5cadde05398c84438))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(widgets): fixed default theme for widgets
|
||||
|
||||
If not theme is set, the init of the BECWidget base class sets the default theme to "dark" ([`cf28730`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf28730515e3c2d5914e0205768734c578711e5c))
|
||||
|
||||
## v0.99.1 (2024-08-27)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(crosshair): emit all crosshair events, not just line coordinates ([`2265458`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2265458dcc57970db18c62619f5877d542d72e81))
|
||||
|
||||
## v0.99.0 (2024-08-25)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(darkmodebutton): added dark mode button docs ([`406c263`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/406c263746f0e809c1a4d98356c48f40428c23d7))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(darkmodebutton): added button to toggle between dark and light mode ([`cc8c166`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cc8c166b5c1d37e0f64c83801b2347a54a6550b6))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(toggle): emit state change ([`c4f3308`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c4f3308dc0c3e4b2064760ccd7372d71b3e49f96))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(darkmodebutton): renamed set_dark_mode_enabled to toggle_dark_mode ([`c70724a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c70724a456900bcb06b040407a2c5d497e49ce77))
|
||||
|
||||
### Test
|
||||
|
||||
* test(dark_mode_button): added tests for dark mode button ([`df35aab`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df35aabff30c5d00b1c441132bd370446653741e))
|
||||
|
||||
## v0.98.0 (2024-08-25)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(themes): added set_theme method ([`2b4449a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2b4449afebdda0a97f95712a1353cf40ec55c283))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(toolbar): removed hardcoded color values ([`afdf4e8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/afdf4e8782a22566932180224fa1c924d24c810f))
|
||||
|
||||
* fix: transitioning to material icons ([`2a82032`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2a82032644a84e38df04e2035a6aa63f4a046360))
|
||||
|
||||
* fix(dock_area): transitioned to MaterialIconAction ([`88a2f66`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/88a2f667588e9aeb34ae556fa327898824052bc3))
|
||||
|
||||
* fix: fix color palette if qtheme was not called ([`3f3b207`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3f3b207295ebd406ebaeecee465c774965161b8b))
|
||||
|
||||
* fix(figure): removed theme from figure init ([`e42b84c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e42b84c63650297d67feffccc02a2c2ba111ca79))
|
||||
|
||||
* fix: use globally set theme instead of the internal bec widgets theme ([`77c5aa7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/77c5aa741cf1f5b969a42aa878aa2965176dbf41))
|
||||
|
||||
* fix(waveform): fixed icon appearance ([`36ad464`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/36ad4641594b67c9b789515c28f7db78a12757ee))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(waveform): use set theme for demo ([`44cfda1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/44cfda1c07306669c9a4e09706d95e6b91dee370))
|
||||
|
||||
## v0.97.0 (2024-08-23)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(designer): added designer icon factory ([`82a55dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/82a55ddf3eafb589cb63408db1c0e7e5c9d629da))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(toolbar icon): fixed material icon toolbar for theme changes ([`3ecbd60`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3ecbd60627994417c9175364e5909710dbcdceb2))
|
||||
|
||||
## v0.96.3 (2024-08-23)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(dispatcher): docs added ([`dd7c71b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dd7c71bb1e0b7ef5398b1e1a05fc1147c772420a))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: minor fixes for type annotations ([`8c2e7c8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8c2e7c82592ace50e4e1f47e392a0ddc988f57ae))
|
||||
|
||||
## v0.96.2 (2024-08-22)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(waveform): validation of custom curves removed ([`af28574`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af28574bd58457a05f1269f121db01ad627b5769))
|
||||
|
||||
* fix(waveform): skip validation for curves that are not BECCurve instances ([`617db36`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/617db36ed4932c8a0633724079b695bc67d5c77b))
|
||||
|
||||
## v0.96.1 (2024-08-22)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: fail pytest after 2 failed tests ([`f0203d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0203d9bf60c4975ba5ab93a057d9091762454d5))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(crosshair): update markers if necessary ([`4473805`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/44738057a36f5de2bbb55affdd309f92286d4a0f))
|
||||
|
||||
* fix(waveform_widget): fixed icon appearance ([`f98a9f9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f98a9f9771b93226d47830aa52f45739624f51b4))
|
||||
|
||||
* fix: bubble-up signals ([`2fe72c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2fe72c9ccb71bcb196a1b78197b73acf9aa3f506))
|
||||
|
||||
* fix(crosshair): fixed crosshair for image and waveforms ([`37835cb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37835cbf76ca3ba1081f514ee7793244ac500e7f))
|
||||
|
||||
## v0.96.0 (2024-08-22)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(scan_control): added designer options ([`9d7718c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d7718c3d9badf14150174410b9958a3134a1e23))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(scan_control): added the ability to configure the scan control widget from designer ([`9d8fb0b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d8fb0b761efa92972399bcd9aea28e956074380))
|
||||
|
||||
## v0.95.1 (2024-08-22)
|
||||
|
||||
### Documentation
|
||||
@@ -18,142 +150,10 @@
|
||||
|
||||
## v0.95.0 (2024-08-21)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(device_browser): added user docs ([`2c31cc9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2c31cc90ae751f14a653cbbdd6c353d6359aaafe))
|
||||
|
||||
* docs(user): widget gallery with documentation added ([`7357f3d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7357f3d2a189f9f04954a027f39ce07c394d57ec))
|
||||
|
||||
* docs: added sphinx-inline-tabs as sphinx dependency ([`e9ecd26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e9ecd268c602ea9572df0e8d508e49ee62d0c170))
|
||||
|
||||
* docs(cards): changed index cards to custom css class instead of overwriting the default sd-card theme ([`91ba30e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/91ba30e8d054a9c7f6c6d98b21113a5d0b1bbbbb))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(cli): added device_browser to cli ([`196504b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/196504b533367a899c19b88af4ccd5b39dc46aac))
|
||||
|
||||
* feat(widgets): added device_browser widget ([`73f5a2f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/73f5a2f085b289ac18fa8a918b6ad7cfed595fb4))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(device_browser): fixed plugin assignment for designer ([`6500393`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/650039303aae9bbec62c676285938416fff146ce))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(docs): review response ([`4790afd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4790afde3d61fc9beb073c2775c339d4f80779e3))
|
||||
|
||||
### Test
|
||||
|
||||
* test: added test for device browser ([`e870e5b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e870e5ba083c61df581c9c0305adabe72967f997))
|
||||
|
||||
## v0.94.7 (2024-08-20)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: formatting of stdout, stderr captured text for logger ([`939f834`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/939f834a26ddbac0bdead0b60b1cdf52014f182f))
|
||||
|
||||
## v0.94.6 (2024-08-14)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(server): emit heartbeat with state ([`bc2abe9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bc2abe945fb5adeec89ed5ac45e966db86ce6ffc))
|
||||
|
||||
## v0.94.5 (2024-08-14)
|
||||
|
||||
### Build
|
||||
|
||||
* build: increased min version of bec to 2.21.4
|
||||
|
||||
Since we now rely on reusing the BECClient singleton, we need the fix introduced with 2.21.4 in BEC. ([`4f96d0e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f96d0e4a14edc4b2839c1dddeda384737dc7a8a))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(rpc): use client singleton instead of dispatcher ([`ea9240d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ea9240d2f71931082f33fb6b68231469875c3d63))
|
||||
|
||||
* fix: removed qcoreapplication for polling events ([`4d02b42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4d02b42f11e9882b843317255a4975565c8a536f))
|
||||
|
||||
## v0.94.4 (2024-08-14)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: review developer section; add introduction ([`2af5c94`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2af5c94913a3435c1839034df4f45f885b56d08b))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: do not shutdown client in "close"
|
||||
|
||||
Terminating client connections has to be done at the application level ([`198c1d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/198c1d1064cc2dae55de4b941929341faddacb28))
|
||||
|
||||
## v0.94.3 (2024-08-13)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(curve_dialog): async curves are shown in curve dialog after addition. ([`7aeb2b5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7aeb2b5c26c7c2851e8d663d32521da8daec95ef))
|
||||
|
||||
* fix(waveform): async device entry is correctly passed, updated and with new scan the previous data are cleared ([`d56ea95`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d56ea95ef97bfdd0bc3eeddc4505d20b38e28559))
|
||||
|
||||
### Test
|
||||
|
||||
* test(waveform_widget): added tests for axis setting and curve dialog ([`f285b35`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f285b35b491660549e74349318119f7c2c44f619))
|
||||
|
||||
## v0.94.2 (2024-08-13)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(image): image is single image mode do not raise popup error when connected twice with the same monitor ([`98b79aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/98b79aac7b47b73137f4d582f7f1d552b1d95366))
|
||||
|
||||
## v0.94.1 (2024-08-12)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: issue #292, wrong key was used to clean _slots internal dictionary ([`93d3977`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/93d397759c756397604ebff5e24f3a580be8620d))
|
||||
|
||||
## v0.94.0 (2024-08-08)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: add PositionerControlLine ([`c80a7cd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c80a7cd1083baa9543a2cee2e3c3a51dfd209b19))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: adjust dimensions ([`0273bf4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0273bf485694609325b5b556a3c69fb53c18446e))
|
||||
|
||||
## v0.93.5 (2024-08-08)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(positioner_box): icons fixed ([`281633d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/281633deff15b6879dac3a4f0770fa6949aaecdc))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: add button for positioner selection ([`0d190c5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0d190c5c5996e59fec4bdd44d2003e10e200b009))
|
||||
|
||||
### Test
|
||||
|
||||
* test(dap): wait for fit ([`6269009`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6269009e5451f830cdee58a514c7858483488a8d))
|
||||
|
||||
* test(auto-update): wait for rendering ([`6d2442d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6d2442d23c683fe92af13df982ce681c07e99cde))
|
||||
|
||||
## v0.93.4 (2024-08-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: rename DeviceBox to PositionerBox, fix test for validation ([`37aa371`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37aa371e7c4c62d70abf37abc125db0c088790fe))
|
||||
|
||||
* fix: add validation for bec_lib.device.Positioner; closes #268 ([`eb54e9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb54e9f788e97af23db8fe0c78f8facb8688bb99))
|
||||
|
||||
## v0.93.3 (2024-08-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(dock): properly shut down docks and temp areas ([`99ee545`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99ee545e41c6078654958b668b5b329f85553d16))
|
||||
|
||||
* fix(settings): shut down settings dialog ([`b50b3a2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b50b3a27e68956e10e8169a0aa698c911d2d9642))
|
||||
|
||||
* fix(website): fixed teardown of website widgets ([`a3d4f5a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3d4f5ac4bc52acfed2791a1724fade6972ed320))
|
||||
|
||||
### Test
|
||||
|
||||
* test: removed quit from teardown ([`cf94599`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf94599c2544d6831c8afbe7b340082077557ed1))
|
||||
|
||||
* test: removed explicit call to close the widget ([`bf6294e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bf6294ecbfd494565d2dc215e4d7e0c280ac7745))
|
||||
|
||||
@@ -21,6 +21,7 @@ class Widgets(str, enum.Enum):
|
||||
BECQueue = "BECQueue"
|
||||
BECStatusBox = "BECStatusBox"
|
||||
BECWaveformWidget = "BECWaveformWidget"
|
||||
DarkModeButton = "DarkModeButton"
|
||||
DeviceBrowser = "DeviceBrowser"
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
@@ -482,7 +483,7 @@ class BECFigure(RPCBase):
|
||||
y_entry: "str | None" = None,
|
||||
z_entry: "str | None" = None,
|
||||
color: "str | None" = None,
|
||||
color_map_z: "str | None" = "plasma",
|
||||
color_map_z: "str | None" = "magma",
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
new: "bool" = False,
|
||||
@@ -1727,7 +1728,7 @@ class BECWaveform(RPCBase):
|
||||
y_entry: "str | None" = None,
|
||||
z_entry: "str | None" = None,
|
||||
color: "str | None" = None,
|
||||
color_map_z: "str | None" = "plasma",
|
||||
color_map_z: "str | None" = "magma",
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
dap: "str | None" = None,
|
||||
@@ -2041,7 +2042,7 @@ class BECWaveformWidget(RPCBase):
|
||||
y_entry: "str | None" = None,
|
||||
z_entry: "str | None" = None,
|
||||
color: "str | None" = None,
|
||||
color_map_z: "str | None" = "plasma",
|
||||
color_map_z: "str | None" = "magma",
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
dap: "str | None" = None,
|
||||
@@ -2289,6 +2290,15 @@ class BECWaveformWidget(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class DarkModeButton(RPCBase):
|
||||
@rpc_call
|
||||
def toggle_dark_mode(self) -> None:
|
||||
"""
|
||||
Toggle the dark mode state. This will change the theme of the entire
|
||||
application to dark or light mode.
|
||||
"""
|
||||
|
||||
|
||||
class DeviceBrowser(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
|
||||
from bec_widgets.examples.plugin_example_pyside.tictactoetaskmenu import TicTacToeTaskMenuFactory
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
@@ -47,9 +46,7 @@ class TicTacToePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "Games"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("sports_esports", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("sports_esports")
|
||||
|
||||
def includeFile(self):
|
||||
return "tictactoe"
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QAction, QIcon
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
|
||||
@@ -70,6 +71,44 @@ class IconAction(ToolBarAction):
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
|
||||
class MaterialIconAction:
|
||||
"""
|
||||
Action with a Material icon for the toolbar.
|
||||
|
||||
Args:
|
||||
icon_path (str, optional): The name of the Material icon. Defaults to None.
|
||||
tooltip (bool, optional): The tooltip for the action. Defaults to None.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
filled (bool, optional): Whether the icon is filled. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
icon_name: str = None,
|
||||
tooltip: str = None,
|
||||
checkable: bool = False,
|
||||
filled: bool = False,
|
||||
):
|
||||
self.icon_name = icon_name
|
||||
self.tooltip = tooltip
|
||||
self.checkable = checkable
|
||||
self.action = None
|
||||
self.filled = filled
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
icon = self.get_icon()
|
||||
self.action = QAction(icon, self.tooltip, target)
|
||||
self.action.setCheckable(self.checkable)
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
def get_icon(self):
|
||||
|
||||
icon = material_icon(
|
||||
self.icon_name, size=(20, 20), convert_to_pixmap=False, filled=self.filled
|
||||
)
|
||||
return icon
|
||||
|
||||
|
||||
class DeviceSelectionAction(ToolBarAction):
|
||||
"""
|
||||
Action for selecting a device in a combobox.
|
||||
@@ -133,10 +172,12 @@ class ExpandableMenuAction(ToolBarAction):
|
||||
menu = QMenu(button)
|
||||
for action_id, action in self.actions.items():
|
||||
sub_action = QAction(action.tooltip, target)
|
||||
if action.icon_path:
|
||||
if hasattr(action, "icon_path"):
|
||||
icon = QIcon()
|
||||
icon.addFile(action.icon_path, size=QSize(20, 20))
|
||||
sub_action.setIcon(icon)
|
||||
elif hasattr(action, "get_icon"):
|
||||
sub_action.setIcon(action.get_icon())
|
||||
sub_action.setCheckable(action.checkable)
|
||||
menu.addAction(sub_action)
|
||||
self.widgets[action_id] = sub_action
|
||||
@@ -150,20 +191,13 @@ class ModularToolBar(QToolBar):
|
||||
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: dict | None = None,
|
||||
target_widget=None,
|
||||
color: str = "rgba(255, 255, 255, 0)",
|
||||
):
|
||||
def __init__(self, parent=None, actions: dict | None = None, target_widget=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.widgets = defaultdict(dict)
|
||||
self.set_background_color(color)
|
||||
self.set_background_color()
|
||||
|
||||
if actions is not None and target_widget is not None:
|
||||
self.populate_toolbar(actions, target_widget)
|
||||
@@ -180,8 +214,7 @@ class ModularToolBar(QToolBar):
|
||||
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}; }}")
|
||||
def set_background_color(self):
|
||||
self.setIconSize(QSize(20, 20))
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
|
||||
@@ -6,7 +6,9 @@ import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy import PYSIDE6
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
if PYSIDE6:
|
||||
from PySide6.scripts.pyside_tool import (
|
||||
@@ -21,6 +23,19 @@ if PYSIDE6:
|
||||
import bec_widgets
|
||||
|
||||
|
||||
def designer_material_icon(icon_name: str) -> QIcon:
|
||||
"""
|
||||
Create a QIcon for the BECDesigner with the given material icon name.
|
||||
|
||||
Args:
|
||||
icon_name (str): The name of the material icon.
|
||||
|
||||
Returns:
|
||||
QIcon: The QIcon for the material icon.
|
||||
"""
|
||||
return QIcon(material_icon(icon_name, filled=True, convert_to_pixmap=True))
|
||||
|
||||
|
||||
def list_editable_packages() -> set[str]:
|
||||
"""
|
||||
List all editable packages in the environment.
|
||||
|
||||
@@ -125,7 +125,7 @@ class BECDispatcher:
|
||||
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Connect widget's pyqt slot, so that it is called on new pub/sub topic message.
|
||||
"""Connect widget's qt slot, so that it is called on new pub/sub topic message.
|
||||
|
||||
Args:
|
||||
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
||||
@@ -138,6 +138,13 @@ class BECDispatcher:
|
||||
self._slots[slot].update(set(topics_str))
|
||||
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
||||
"""
|
||||
Disconnect a slot from a topic.
|
||||
|
||||
Args:
|
||||
slot(Callable): The slot to disconnect
|
||||
topics(Union[str, list]): The topic(s) to disconnect from
|
||||
"""
|
||||
# find the right slot to disconnect from ;
|
||||
# slot callbacks are wrapped in QtThreadSafeCallback objects,
|
||||
# but the slot we receive here is the original callable
|
||||
@@ -153,6 +160,12 @@ class BECDispatcher:
|
||||
del self._slots[connected_slot]
|
||||
|
||||
def disconnect_topics(self, topics: Union[str, list]):
|
||||
"""
|
||||
Disconnect all slots from a topic.
|
||||
|
||||
Args:
|
||||
topics(Union[str, list]): The topic(s) to disconnect from
|
||||
"""
|
||||
self.client.connector.unregister(topics)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
for slot in list(self._slots.keys()):
|
||||
@@ -162,4 +175,11 @@ class BECDispatcher:
|
||||
del self._slots[slot]
|
||||
|
||||
def disconnect_all(self, *args, **kwargs):
|
||||
"""
|
||||
Disconnect all slots from all topics.
|
||||
|
||||
Args:
|
||||
*args: Arbitrary positional arguments
|
||||
**kwargs: Arbitrary keyword arguments
|
||||
"""
|
||||
self.disconnect_topics(self.client.connector._topics_cb)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
|
||||
class BECWidget(BECConnector):
|
||||
@@ -11,9 +12,13 @@ class BECWidget(BECConnector):
|
||||
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
||||
super().__init__(client, config, gui_id)
|
||||
|
||||
# Set the theme to auto if it is not set yet
|
||||
app = QApplication.instance()
|
||||
if not hasattr(app, "theme"):
|
||||
set_theme("dark")
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
pass
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.rpc_register.remove_rpc(self)
|
||||
|
||||
@@ -5,21 +5,45 @@ from typing import Literal
|
||||
import bec_qthemes
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_qthemes._os_appearance.listener import OSThemeSwitchListener
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
CURRENT_THEME = "dark"
|
||||
|
||||
|
||||
def get_theme_palette():
|
||||
return bec_qthemes.load_palette(CURRENT_THEME)
|
||||
if QApplication.instance() is None or not hasattr(QApplication.instance(), "theme"):
|
||||
theme = "dark"
|
||||
else:
|
||||
theme = QApplication.instance().theme["theme"]
|
||||
return bec_qthemes.load_palette(theme)
|
||||
|
||||
|
||||
def set_theme(theme: Literal["dark", "light", "auto"]):
|
||||
"""
|
||||
Set the theme for the application.
|
||||
|
||||
Args:
|
||||
theme (Literal["dark", "light", "auto"]): The theme to set. "auto" will automatically switch between dark and light themes based on the system theme.
|
||||
"""
|
||||
app = QApplication.instance()
|
||||
bec_qthemes.setup_theme(theme)
|
||||
pg.setConfigOption("background", "w" if app.theme["theme"] == "light" else "k")
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if theme != "auto":
|
||||
return
|
||||
|
||||
def callback():
|
||||
app.theme["theme"] = listener._theme.lower()
|
||||
apply_theme(listener._theme.lower())
|
||||
|
||||
listener = OSThemeSwitchListener(callback)
|
||||
|
||||
app.installEventFilter(listener)
|
||||
|
||||
|
||||
def apply_theme(theme: Literal["dark", "light"]):
|
||||
global CURRENT_THEME
|
||||
CURRENT_THEME = theme
|
||||
|
||||
app = QApplication.instance()
|
||||
# go through all pyqtgraph widgets and set background
|
||||
children = itertools.chain.from_iterable(
|
||||
@@ -81,8 +105,19 @@ class Colors:
|
||||
angles = Colors.golden_ratio(len(cmap_colors))
|
||||
color_selection = np.round(np.interp(angles, (-np.pi, np.pi), (0, len(cmap_colors))))
|
||||
colors = []
|
||||
for ii in color_selection[:num]:
|
||||
color = cmap_colors[int(ii)]
|
||||
ii = 0
|
||||
while len(colors) < num:
|
||||
color_index = int(color_selection[ii])
|
||||
color = cmap_colors[color_index]
|
||||
app = QApplication.instance()
|
||||
if hasattr(app, "theme") and app.theme["theme"] == "light":
|
||||
background = 255
|
||||
else:
|
||||
background = 0
|
||||
if np.abs(np.mean(color[:3] * 255) - background) < 50:
|
||||
ii += 1
|
||||
continue
|
||||
|
||||
if format.upper() == "HEX":
|
||||
colors.append(QColor.fromRgbF(*color).name())
|
||||
elif format.upper() == "RGB":
|
||||
@@ -91,6 +126,7 @@ class Colors:
|
||||
colors.append(QColor.fromRgbF(*color))
|
||||
else:
|
||||
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
||||
ii += 1
|
||||
return colors
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import Type
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
# from qtpy.QtCore import QObject, pyqtSignal
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import QObject, Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
|
||||
class Crosshair(QObject):
|
||||
positionChanged = pyqtSignal(tuple)
|
||||
positionClicked = pyqtSignal(tuple)
|
||||
# Signal for 1D plot
|
||||
coordinatesChanged1D = pyqtSignal(tuple)
|
||||
coordinatesClicked1D = pyqtSignal(tuple)
|
||||
@@ -26,10 +30,13 @@ class Crosshair(QObject):
|
||||
super().__init__(parent)
|
||||
self.is_log_y = None
|
||||
self.is_log_x = None
|
||||
self.is_derivative = None
|
||||
self.plot_item = plot_item
|
||||
self.precision = precision
|
||||
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
||||
self.v_line.skip_auto_range = True
|
||||
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
||||
self.h_line.skip_auto_range = True
|
||||
self.plot_item.addItem(self.v_line, ignoreBounds=True)
|
||||
self.plot_item.addItem(self.h_line, ignoreBounds=True)
|
||||
self.proxy = pg.SignalProxy(
|
||||
@@ -37,74 +44,75 @@ class Crosshair(QObject):
|
||||
)
|
||||
self.plot_item.scene().sigMouseClicked.connect(self.mouse_clicked)
|
||||
|
||||
self.plot_item.ctrl.derivativeCheck.checkStateChanged.connect(self.check_derivatives)
|
||||
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
||||
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
||||
|
||||
# Initialize markers
|
||||
self.marker_moved_1d = []
|
||||
self.marker_clicked_1d = []
|
||||
self.marker_moved_1d = {}
|
||||
self.marker_clicked_1d = {}
|
||||
self.marker_2d = None
|
||||
self.update_markers()
|
||||
|
||||
def update_markers(self):
|
||||
"""Update the markers for the crosshair, creating new ones if necessary."""
|
||||
|
||||
# Clear existing markers
|
||||
for marker in self.marker_moved_1d + self.marker_clicked_1d:
|
||||
self.plot_item.removeItem(marker)
|
||||
if self.marker_2d:
|
||||
self.plot_item.removeItem(self.marker_2d)
|
||||
|
||||
# Create new markers
|
||||
self.marker_moved_1d = []
|
||||
self.marker_clicked_1d = []
|
||||
self.marker_2d = None
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||
if item.name() in self.marker_moved_1d:
|
||||
continue
|
||||
pen = item.opts["pen"]
|
||||
color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
|
||||
marker_moved = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
||||
)
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(None), brush=pg.mkBrush(color)
|
||||
)
|
||||
self.marker_moved_1d.append(marker_moved)
|
||||
marker_moved.skip_auto_range = True
|
||||
self.marker_moved_1d[item.name()] = marker_moved
|
||||
self.plot_item.addItem(marker_moved)
|
||||
|
||||
# Create glowing effect markers for clicked events
|
||||
marker_clicked_list = []
|
||||
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
size=size,
|
||||
pen=pg.mkPen(None),
|
||||
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
|
||||
)
|
||||
marker_clicked_list.append(marker_clicked)
|
||||
marker_clicked.skip_auto_range = True
|
||||
self.marker_clicked_1d[item.name()] = marker_clicked
|
||||
self.plot_item.addItem(marker_clicked)
|
||||
|
||||
self.marker_clicked_1d.append(marker_clicked_list)
|
||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||
if self.marker_2d is not None:
|
||||
continue
|
||||
self.marker_2d = pg.ROI(
|
||||
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
||||
)
|
||||
self.plot_item.addItem(self.marker_2d)
|
||||
|
||||
def snap_to_data(self, x, y) -> tuple:
|
||||
def snap_to_data(self, x, y) -> tuple[defaultdict[list], defaultdict[list]]:
|
||||
"""
|
||||
Finds the nearest data points to the given x and y coordinates.
|
||||
|
||||
Args:
|
||||
x: The x-coordinate
|
||||
y: The y-coordinate
|
||||
x: The x-coordinate of the mouse cursor
|
||||
y: The y-coordinate of the mouse cursor
|
||||
|
||||
Returns:
|
||||
tuple: The nearest x and y values
|
||||
tuple: x and y values snapped to the nearest data
|
||||
"""
|
||||
y_values_1d = []
|
||||
x_values_1d = []
|
||||
y_values = defaultdict(list)
|
||||
x_values = defaultdict(list)
|
||||
image_2d = None
|
||||
|
||||
# Iterate through items in the plot
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||
x_data, y_data = item.xData, item.yData
|
||||
name = item.name()
|
||||
plot_data = item._getDisplayDataset()
|
||||
if plot_data is None:
|
||||
continue
|
||||
x_data, y_data = plot_data.x, plot_data.y
|
||||
if x_data is not None and y_data is not None:
|
||||
if self.is_log_x:
|
||||
min_x_data = np.min(x_data[x_data > 0])
|
||||
@@ -112,25 +120,25 @@ class Crosshair(QObject):
|
||||
min_x_data = np.min(x_data)
|
||||
max_x_data = np.max(x_data)
|
||||
if x < min_x_data or x > max_x_data:
|
||||
return None, None
|
||||
y_values[name] = None
|
||||
x_values[name] = None
|
||||
continue
|
||||
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
|
||||
y_values_1d.append(closest_y)
|
||||
x_values_1d.append(closest_x)
|
||||
y_values[name] = closest_y
|
||||
x_values[name] = closest_x
|
||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||
name = item.config.monitor
|
||||
image_2d = item.image
|
||||
# clip the x and y values to the image dimensions to avoid out of bounds errors
|
||||
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||
x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||
|
||||
# Handle 1D plot
|
||||
if y_values_1d:
|
||||
if all(v is None for v in x_values_1d) or all(v is None for v in y_values_1d):
|
||||
if x_values and y_values:
|
||||
if all(v is None for v in x_values.values()) or all(
|
||||
v is None for v in y_values.values()
|
||||
):
|
||||
return None, None
|
||||
closest_x = min(x_values_1d, key=lambda xi: abs(xi - x)) # Snap x to closest data point
|
||||
return closest_x, y_values_1d
|
||||
|
||||
# Handle 2D plot
|
||||
if image_2d is not None:
|
||||
x_idx = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||
y_idx = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||
return x_idx, y_idx
|
||||
return x_values, y_values
|
||||
|
||||
return None, None
|
||||
|
||||
@@ -156,8 +164,9 @@ class Crosshair(QObject):
|
||||
Args:
|
||||
event: The mouse moved event
|
||||
"""
|
||||
self.check_log()
|
||||
pos = event[0]
|
||||
self.update_markers()
|
||||
self.positionChanged.emit((pos.x(), pos.y()))
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
||||
self.v_line.setPos(mouse_point.x())
|
||||
@@ -168,27 +177,34 @@ class Crosshair(QObject):
|
||||
x = 10**x
|
||||
if self.is_log_y:
|
||||
y = 10**y
|
||||
x, y_values = self.snap_to_data(x, y)
|
||||
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
||||
if x_snap_values is None or y_snap_values is None:
|
||||
return
|
||||
if all(v is None for v in x_snap_values.values()) or all(
|
||||
v is None for v in y_snap_values.values()
|
||||
):
|
||||
# not sure how we got here, but just to be safe...
|
||||
return
|
||||
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
if x is None or all(v is None for v in y_values):
|
||||
return
|
||||
coordinate_to_emit = (
|
||||
round(x, self.precision),
|
||||
[round(y_val, self.precision) for y_val in y_values],
|
||||
)
|
||||
name = item.name()
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_moved_1d[name].setData([x], [y])
|
||||
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
||||
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
||||
for i, y_val in enumerate(y_values):
|
||||
self.marker_moved_1d[i].setData(
|
||||
[x if not self.is_log_x else np.log10(x)],
|
||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
||||
)
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
if x is None or y_values is None:
|
||||
return
|
||||
coordinate_to_emit = (x, y_values)
|
||||
name = item.config.monitor
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_2d.setPos([x, y])
|
||||
coordinate_to_emit = (name, x, y)
|
||||
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
||||
else:
|
||||
continue
|
||||
|
||||
def mouse_clicked(self, event):
|
||||
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
|
||||
@@ -196,40 +212,69 @@ class Crosshair(QObject):
|
||||
Args:
|
||||
event: The mouse clicked event
|
||||
"""
|
||||
self.check_log()
|
||||
|
||||
# we only accept left mouse clicks
|
||||
if event.button() != Qt.MouseButton.LeftButton:
|
||||
return
|
||||
self.update_markers()
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
|
||||
x, y = mouse_point.x(), mouse_point.y()
|
||||
self.positionClicked.emit((x, y))
|
||||
|
||||
if self.is_log_x:
|
||||
x = 10**x
|
||||
if self.is_log_y:
|
||||
y = 10**y
|
||||
x, y_values = self.snap_to_data(x, y)
|
||||
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
||||
|
||||
if x_snap_values is None or y_snap_values is None:
|
||||
return
|
||||
if all(v is None for v in x_snap_values.values()) or all(
|
||||
v is None for v in y_snap_values.values()
|
||||
):
|
||||
# not sure how we got here, but just to be safe...
|
||||
return
|
||||
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
if x is None or all(v is None for v in y_values):
|
||||
return
|
||||
coordinate_to_emit = (
|
||||
round(x, self.precision),
|
||||
[round(y_val, self.precision) for y_val in y_values],
|
||||
)
|
||||
name = item.name()
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_clicked_1d[name].setData([x], [y])
|
||||
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
||||
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
||||
for i, y_val in enumerate(y_values):
|
||||
for marker in self.marker_clicked_1d[i]:
|
||||
marker.setData(
|
||||
[x if not self.is_log_x else np.log10(x)],
|
||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
||||
)
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
if x is None or y_values is None:
|
||||
return
|
||||
coordinate_to_emit = (x, y_values)
|
||||
name = item.config.monitor
|
||||
x, y = x_snap_values[name], y_snap_values[name]
|
||||
if x is None or y is None:
|
||||
continue
|
||||
self.marker_2d.setPos([x, y])
|
||||
coordinate_to_emit = (name, x, y)
|
||||
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
||||
self.marker_2d.setPos([x, y_values])
|
||||
else:
|
||||
continue
|
||||
|
||||
def clear_markers(self):
|
||||
"""Clears the markers from the plot."""
|
||||
for marker in self.marker_moved_1d.values():
|
||||
marker.clear()
|
||||
for marker in self.marker_clicked_1d.values():
|
||||
marker.clear()
|
||||
|
||||
def check_log(self):
|
||||
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
||||
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
||||
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
||||
self.clear_markers()
|
||||
|
||||
def check_derivatives(self):
|
||||
"""Checks if the derivatives are enabled and updates the internal state accordingly."""
|
||||
self.is_derivative = self.plot_item.ctrl.derivativeCheck.isChecked()
|
||||
self.clear_markers()
|
||||
|
||||
def cleanup(self):
|
||||
self.v_line.deleteLater()
|
||||
self.h_line.deleteLater()
|
||||
self.clear_markers()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import Qt, Slot
|
||||
from qtpy.QtWidgets import QHBoxLayout, QHeaderView, QTableWidget, QTableWidgetItem, QWidget
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class BECQueuePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Services"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("edit_note", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("edit_note")
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_queue"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.bec_status_box.bec_status_box import BECStatusBox
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class BECStatusBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Services"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("dns", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("dns")
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_status_box"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.color_button.color_button import ColorButton
|
||||
|
||||
DOM_XML = """
|
||||
@@ -32,9 +31,7 @@ class ColorButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Buttons"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("colors", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("colors")
|
||||
|
||||
def includeFile(self):
|
||||
return "color_button"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.colormap_selector.colormap_selector import ColormapSelector
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class ColormapSelectorPlugin(QDesignerCustomWidgetInterface): # pragma: no cove
|
||||
return "BEC Buttons"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("palette", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("palette")
|
||||
|
||||
def includeFile(self):
|
||||
return "colormap_selector"
|
||||
|
||||
0
bec_widgets/widgets/dark_mode_button/__init__.py
Normal file
0
bec_widgets/widgets/dark_mode_button/__init__.py
Normal file
71
bec_widgets/widgets/dark_mode_button/dark_mode_button.py
Normal file
71
bec_widgets/widgets/dark_mode_button/dark_mode_button.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Property, Qt, Slot
|
||||
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
|
||||
class DarkModeButton(BECWidget, QWidget):
|
||||
USER_ACCESS = ["toggle_dark_mode"]
|
||||
|
||||
def __init__(
|
||||
self, parent: QWidget | None = None, client=None, gui_id: str | None = None
|
||||
) -> None:
|
||||
super().__init__(client=client, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent)
|
||||
|
||||
self._dark_mode_enabled = False
|
||||
self.layout = QHBoxLayout(self)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
icon = material_icon("dark_mode", size=(20, 20), convert_to_pixmap=False)
|
||||
self.mode_button = QPushButton(icon=icon)
|
||||
self.update_mode_button()
|
||||
self.mode_button.clicked.connect(self.toggle_dark_mode)
|
||||
self.layout.addWidget(self.mode_button)
|
||||
self.setLayout(self.layout)
|
||||
self.setFixedSize(40, 40)
|
||||
|
||||
@Property(bool)
|
||||
def dark_mode_enabled(self) -> bool:
|
||||
"""
|
||||
The dark mode state. If True, dark mode is enabled. If False, light mode is enabled.
|
||||
"""
|
||||
return self._dark_mode_enabled
|
||||
|
||||
@dark_mode_enabled.setter
|
||||
def dark_mode_enabled(self, state: bool) -> None:
|
||||
self._dark_mode_enabled = state
|
||||
|
||||
@Slot()
|
||||
def toggle_dark_mode(self) -> None:
|
||||
"""
|
||||
Toggle the dark mode state. This will change the theme of the entire
|
||||
application to dark or light mode.
|
||||
"""
|
||||
self.dark_mode_enabled = not self.dark_mode_enabled
|
||||
self.update_mode_button()
|
||||
set_theme("dark" if self.dark_mode_enabled else "light")
|
||||
|
||||
def update_mode_button(self):
|
||||
icon = material_icon(
|
||||
"light_mode" if self.dark_mode_enabled else "dark_mode",
|
||||
size=(20, 20),
|
||||
convert_to_pixmap=False,
|
||||
)
|
||||
self.mode_button.setIcon(icon)
|
||||
self.mode_button.setToolTip("Set Light Mode" if self.dark_mode_enabled else "Set Dark Mode")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
w = DarkModeButton()
|
||||
w.show()
|
||||
|
||||
app.exec_()
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['dark_mode_button.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='DarkModeButton' name='dark_mode_button'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class DarkModeButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = DarkModeButton(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Buttons"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon("dark_mode")
|
||||
|
||||
def includeFile(self):
|
||||
return "dark_mode_button"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "DarkModeButton"
|
||||
|
||||
def toolTip(self):
|
||||
return "Button to toggle between dark and light mode."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.dark_mode_button.dark_mode_button_plugin import DarkModeButtonPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(DarkModeButtonPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.device_browser.device_browser import DeviceBrowser
|
||||
|
||||
DOM_XML = """
|
||||
@@ -31,9 +30,7 @@ class DeviceBrowserPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Services"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("lists", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("lists")
|
||||
|
||||
def includeFile(self):
|
||||
return "device_browser"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "Device Control"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("list_alt", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("list_alt")
|
||||
|
||||
def includeFile(self):
|
||||
return "device_combobox"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "Device Control"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("edit_note", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("edit_note")
|
||||
|
||||
def includeFile(self):
|
||||
return "device_line_edit"
|
||||
|
||||
@@ -9,17 +9,16 @@ from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QPainter, QPaintEvent
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.toolbar import (
|
||||
ExpandableMenuAction,
|
||||
IconAction,
|
||||
MaterialIconAction,
|
||||
ModularToolBar,
|
||||
SeparatorAction,
|
||||
)
|
||||
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
from ...qt_utils.error_popups import SafeSlot
|
||||
from .dock import BECDock, DockConfig
|
||||
from bec_widgets.widgets.dock.dock import BECDock, DockConfig
|
||||
|
||||
|
||||
class DockAreaConfig(ConnectionConfig):
|
||||
@@ -71,20 +70,26 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"menu_plots": ExpandableMenuAction(
|
||||
label="Add Plot ",
|
||||
actions={
|
||||
"waveform": IconAction(icon_path="waveform.svg", tooltip="Add Waveform"),
|
||||
"image": IconAction(icon_path="image.svg", tooltip="Add Image"),
|
||||
"motor_map": IconAction(icon_path="motor_map.svg", tooltip="Add Motor Map"),
|
||||
"waveform": MaterialIconAction(
|
||||
icon_name="show_chart", tooltip="Add Waveform", filled=True
|
||||
),
|
||||
"image": MaterialIconAction(
|
||||
icon_name="image", tooltip="Add Image", filled=True
|
||||
),
|
||||
"motor_map": MaterialIconAction(
|
||||
icon_name="my_location", tooltip="Add Motor Map", filled=True
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_0": SeparatorAction(),
|
||||
"menu_devices": ExpandableMenuAction(
|
||||
label="Add Device Control ",
|
||||
actions={
|
||||
"scan_control": IconAction(
|
||||
icon_path="scan_control.svg", tooltip="Add Scan Control"
|
||||
"scan_control": MaterialIconAction(
|
||||
icon_name="stacked_line_chart", tooltip="Add Scan Control", filled=True
|
||||
),
|
||||
"positioner_box": IconAction(
|
||||
icon_path="positioner_box.svg", tooltip="Add Device Box"
|
||||
"positioner_box": MaterialIconAction(
|
||||
icon_name="switch_right", tooltip="Add Device Box", filled=True
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -92,21 +97,29 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"menu_utils": ExpandableMenuAction(
|
||||
label="Add Utils ",
|
||||
actions={
|
||||
"queue": IconAction(icon_path="queue.svg", tooltip="Add Scan Queue"),
|
||||
"vs_code": IconAction(icon_path="terminal.svg", tooltip="Add VS Code"),
|
||||
"status": IconAction(icon_path="status.svg", tooltip="Add BEC Status Box"),
|
||||
"progress_bar": IconAction(
|
||||
icon_path="ring_progress.svg", tooltip="Add Circular ProgressBar"
|
||||
"queue": MaterialIconAction(
|
||||
icon_name="edit_note", tooltip="Add Scan Queue", filled=True
|
||||
),
|
||||
"vs_code": MaterialIconAction(
|
||||
icon_name="show_chart", tooltip="Add VS Code", filled=True
|
||||
),
|
||||
"status": MaterialIconAction(
|
||||
icon_name="dns", tooltip="Add BEC Status Box", filled=True
|
||||
),
|
||||
"progress_bar": MaterialIconAction(
|
||||
icon_name="track_changes",
|
||||
tooltip="Add Circular ProgressBar",
|
||||
filled=True,
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_2": SeparatorAction(),
|
||||
"attach_all": IconAction(
|
||||
icon_path="attach_all.svg", tooltip="Attach all floating docks"
|
||||
"attach_all": MaterialIconAction(
|
||||
icon_name="zoom_in_map", tooltip="Attach all floating docks"
|
||||
),
|
||||
"save_state": IconAction(icon_path="save_state.svg", tooltip="Save Dock State"),
|
||||
"restore_state": IconAction(
|
||||
icon_path="restore_state.svg", tooltip="Restore Dock State"
|
||||
"save_state": MaterialIconAction(icon_name="bookmark", tooltip="Save Dock State"),
|
||||
"restore_state": MaterialIconAction(
|
||||
icon_name="frame_reload", tooltip="Restore Dock State"
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
@@ -368,3 +381,15 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"""
|
||||
self.cleanup()
|
||||
super().close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("auto")
|
||||
dock_area = BECDockArea()
|
||||
dock_area.show()
|
||||
app.exec_()
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.dock import BECDockArea
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Plots"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("widgets", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("widgets")
|
||||
|
||||
def includeFile(self):
|
||||
return "dock_area"
|
||||
|
||||
@@ -182,7 +182,6 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
print(f"Error in applying config: {e}")
|
||||
return
|
||||
self.config = config
|
||||
self.change_theme(self.config.theme)
|
||||
|
||||
# widget_config has to be reset for not have each widget config twice when added to the figure
|
||||
widget_configs = list(self.config.widgets.values())
|
||||
@@ -253,7 +252,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
color_map_z: str | None = "magma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
new: bool = False,
|
||||
|
||||
@@ -4,9 +4,11 @@ from typing import Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import Signal, Slot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.crosshair import Crosshair
|
||||
|
||||
|
||||
class AxisConfig(BaseModel):
|
||||
@@ -41,7 +43,23 @@ class SubplotConfig(ConnectionConfig):
|
||||
)
|
||||
|
||||
|
||||
class BECViewBox(pg.ViewBox):
|
||||
|
||||
def itemBoundsChanged(self, item):
|
||||
self._itemBoundsCache.pop(item, None)
|
||||
if (self.state["autoRange"][0] is not False) or (self.state["autoRange"][1] is not False):
|
||||
# check if the call is coming from a mouse-move event
|
||||
if hasattr(item, "skip_auto_range") and item.skip_auto_range:
|
||||
return
|
||||
self._autoRangeNeedsUpdate = True
|
||||
self.update()
|
||||
|
||||
|
||||
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
crosshair_position_changed = Signal(tuple)
|
||||
crosshair_position_clicked = Signal(tuple)
|
||||
crosshair_coordinates_changed = Signal(tuple)
|
||||
crosshair_coordinates_clicked = Signal(tuple)
|
||||
USER_ACCESS = [
|
||||
"_config_dict",
|
||||
"set",
|
||||
@@ -73,9 +91,13 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
pg.GraphicsLayout.__init__(self, parent)
|
||||
|
||||
self.figure = parent_figure
|
||||
self.plot_item = self.addPlot(row=0, col=0)
|
||||
|
||||
# self.plot_item = self.addPlot(row=0, col=0)
|
||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(parent=self, enableMenu=True), parent=self)
|
||||
self.addItem(self.plot_item, row=0, col=0)
|
||||
|
||||
self.add_legend()
|
||||
self.crosshair = None
|
||||
|
||||
def set(self, **kwargs) -> None:
|
||||
"""
|
||||
@@ -304,6 +326,44 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"""
|
||||
self.plot_item.enableAutoRange(axis, enabled)
|
||||
|
||||
def hook_crosshair(self) -> None:
|
||||
"""Hook the crosshair to all plots."""
|
||||
if self.crosshair is None:
|
||||
self.crosshair = Crosshair(self.plot_item, precision=3)
|
||||
self.crosshair.positionChanged.connect(self.crosshair_position_changed)
|
||||
self.crosshair.positionClicked.connect(self.crosshair_position_clicked)
|
||||
self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked1D.connect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.coordinatesChanged2D.connect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked2D.connect(self.crosshair_coordinates_clicked)
|
||||
|
||||
def unhook_crosshair(self) -> None:
|
||||
"""Unhook the crosshair from all plots."""
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.positionChanged.disconnect(self.crosshair_position_changed)
|
||||
self.crosshair.positionClicked.disconnect(self.crosshair_position_clicked)
|
||||
self.crosshair.coordinatesChanged1D.disconnect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked1D.disconnect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.coordinatesChanged2D.disconnect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked2D.disconnect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.cleanup()
|
||||
self.crosshair.deleteLater()
|
||||
self.crosshair = None
|
||||
|
||||
def toggle_crosshair(self) -> None:
|
||||
"""Toggle the crosshair on all plots."""
|
||||
if self.crosshair is None:
|
||||
return self.hook_crosshair()
|
||||
|
||||
self.unhook_crosshair()
|
||||
|
||||
@Slot()
|
||||
def reset(self) -> None:
|
||||
"""Reset the plot widget."""
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.clear_markers()
|
||||
self.crosshair.update_markers()
|
||||
|
||||
def export(self):
|
||||
"""Show the Export Dialog of the plot widget."""
|
||||
scene = self.plot_item.scene()
|
||||
@@ -317,6 +377,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
|
||||
def cleanup_pyqtgraph(self):
|
||||
"""Cleanup pyqtgraph items."""
|
||||
self.unhook_crosshair()
|
||||
item = self.plot_item
|
||||
item.vb.menu.close()
|
||||
item.vb.menu.deleteLater()
|
||||
|
||||
@@ -26,7 +26,7 @@ from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
|
||||
|
||||
class Waveform1DConfig(SubplotConfig):
|
||||
color_palette: Optional[str] = Field(
|
||||
"plasma", description="The color palette of the figure widget.", validate_default=True
|
||||
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||
)
|
||||
curves: dict[str, CurveConfig] = Field(
|
||||
{}, description="The list of curves to be added to the 1D waveform widget."
|
||||
@@ -77,6 +77,7 @@ class BECWaveform(BECPlotBase):
|
||||
dap_params_update = pyqtSignal(dict)
|
||||
dap_summary_update = pyqtSignal(dict)
|
||||
autorange_signal = pyqtSignal()
|
||||
new_scan = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -268,7 +269,7 @@ class BECWaveform(BECPlotBase):
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
color_map_z: str | None = "magma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
dap: str | None = None, # TODO add dap custom curve wrapper
|
||||
@@ -375,6 +376,10 @@ class BECWaveform(BECPlotBase):
|
||||
if len(self.curves) > 0:
|
||||
# validate all curves
|
||||
for curve in self.curves:
|
||||
if not isinstance(curve, BECCurve):
|
||||
continue
|
||||
if curve.config.source == "custom":
|
||||
continue
|
||||
self._validate_x_axis_behaviour(curve.config.signals.y.name, x_name, x_entry, False)
|
||||
self._switch_x_axis_item(
|
||||
f"{x_name}-{x_entry}"
|
||||
@@ -382,9 +387,12 @@ class BECWaveform(BECPlotBase):
|
||||
else x_name
|
||||
)
|
||||
for curve_id, curve_config in zip(curve_ids, curve_configs):
|
||||
if curve_config.signals.x:
|
||||
curve_config.signals.x.name = x_name
|
||||
curve_config.signals.x.entry = x_entry
|
||||
if curve_config.signals is None:
|
||||
continue
|
||||
if curve_config.signals.x is None:
|
||||
continue
|
||||
curve_config.signals.x.name = x_name
|
||||
curve_config.signals.x.entry = x_entry
|
||||
self.remove_curve(curve_id)
|
||||
self.add_curve_by_config(curve_config)
|
||||
|
||||
@@ -408,23 +416,6 @@ class BECWaveform(BECPlotBase):
|
||||
"""
|
||||
self.plot_item.enableAutoRange(axis, enabled)
|
||||
|
||||
@Slot()
|
||||
def auto_range(self):
|
||||
self.plot_item.autoRange()
|
||||
|
||||
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 add_curve_custom(
|
||||
self,
|
||||
x: list | np.ndarray,
|
||||
@@ -460,8 +451,10 @@ class BECWaveform(BECPlotBase):
|
||||
color = (
|
||||
color
|
||||
or Colors.golden_angle_color(
|
||||
colormap=self.config.color_palette, num=len(self.plot_item.curves) + 1, format="HEX"
|
||||
)[-1]
|
||||
colormap=self.config.color_palette,
|
||||
num=max(10, len(self.plot_item.curves) + 1),
|
||||
format="HEX",
|
||||
)[len(self.plot_item.curves)]
|
||||
)
|
||||
|
||||
# Create curve by config
|
||||
@@ -488,7 +481,7 @@ class BECWaveform(BECPlotBase):
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
color_map_z: str | None = "magma",
|
||||
label: str | None = None,
|
||||
validate_bec: bool = True,
|
||||
dap: str | None = None,
|
||||
@@ -555,9 +548,12 @@ class BECWaveform(BECPlotBase):
|
||||
color = (
|
||||
color
|
||||
or Colors.golden_angle_color(
|
||||
colormap=self.config.color_palette, num=len(self.plot_item.curves) + 1, format="HEX"
|
||||
)[-1]
|
||||
colormap=self.config.color_palette,
|
||||
num=max(10, len(self.plot_item.curves) + 1),
|
||||
format="HEX",
|
||||
)[len(self.plot_item.curves)]
|
||||
)
|
||||
print(f"Color: {color}")
|
||||
|
||||
# Create curve by config
|
||||
curve_config = CurveConfig(
|
||||
@@ -935,6 +931,8 @@ class BECWaveform(BECPlotBase):
|
||||
return
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self.reset()
|
||||
self.new_scan.emit()
|
||||
self.set_auto_range(True, "xy")
|
||||
self.old_scan_id = self.scan_id
|
||||
self.scan_id = current_scan_id
|
||||
|
||||
@@ -44,15 +44,15 @@ class CurveConfig(ConnectionConfig):
|
||||
symbol_color: Optional[str | tuple] = Field(
|
||||
None, description="The color of the symbol of the curve."
|
||||
)
|
||||
symbol_size: Optional[int] = Field(5, description="The size of the symbol of the curve.")
|
||||
pen_width: Optional[int] = Field(2, description="The width of the pen of the curve.")
|
||||
symbol_size: Optional[int] = Field(7, description="The size of the symbol of the curve.")
|
||||
pen_width: Optional[int] = Field(4, description="The width of the pen of the curve.")
|
||||
pen_style: Optional[Literal["solid", "dash", "dot", "dashdot"]] = Field(
|
||||
"solid", description="The style of the pen of the curve."
|
||||
)
|
||||
source: Optional[str] = Field(None, description="The source of the curve.")
|
||||
signals: Optional[Signal] = Field(None, description="The signal of the curve.")
|
||||
color_map_z: Optional[str] = Field(
|
||||
"plasma", description="The colormap of the curves z gradient.", validate_default=True
|
||||
"magma", description="The colormap of the curves z gradient.", validate_default=True
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.image.image_widget import BECImageWidget
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class BECImageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Plots"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("image", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("image")
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_image_widget"
|
||||
|
||||
@@ -10,7 +10,7 @@ from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.qt_utils.toolbar import (
|
||||
DeviceSelectionAction,
|
||||
IconAction,
|
||||
MaterialIconAction,
|
||||
ModularToolBar,
|
||||
SeparatorAction,
|
||||
)
|
||||
@@ -66,45 +66,45 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
"monitor": DeviceSelectionAction(
|
||||
"Monitor:", DeviceComboBox(device_filter="Device")
|
||||
),
|
||||
"connect": IconAction(icon_path="connection.svg", tooltip="Connect Device"),
|
||||
"connect": MaterialIconAction(icon_name="link", tooltip="Connect Device"),
|
||||
"separator_0": SeparatorAction(),
|
||||
"save": IconAction(icon_path="save.svg", tooltip="Open Export Dialog"),
|
||||
"save": MaterialIconAction(icon_name="save", tooltip="Open Export Dialog"),
|
||||
"separator_1": SeparatorAction(),
|
||||
"drag_mode": IconAction(
|
||||
icon_path="drag_pan_mode.svg", tooltip="Drag Mouse Mode", checkable=True
|
||||
"drag_mode": MaterialIconAction(
|
||||
icon_name="open_with", tooltip="Drag Mouse Mode", checkable=True
|
||||
),
|
||||
"rectangle_mode": IconAction(
|
||||
icon_path="rectangle_mode.svg", tooltip="Rectangle Zoom Mode", checkable=True
|
||||
"rectangle_mode": MaterialIconAction(
|
||||
icon_name="frame_inspect", tooltip="Rectangle Zoom Mode", checkable=True
|
||||
),
|
||||
"auto_range": IconAction(icon_path="auto_range.svg", tooltip="Autorange Plot"),
|
||||
"auto_range_image": IconAction(
|
||||
icon_path="image_autorange.svg",
|
||||
tooltip="Autorange Image Intensity",
|
||||
checkable=True,
|
||||
"auto_range": MaterialIconAction(
|
||||
icon_name="open_in_full", tooltip="Autorange Plot"
|
||||
),
|
||||
"aspect_ratio": IconAction(
|
||||
icon_path="lock_aspect_ratio.svg",
|
||||
tooltip="Lock image aspect ratio",
|
||||
checkable=True,
|
||||
"auto_range_image": MaterialIconAction(
|
||||
icon_name="hdr_auto", tooltip="Autorange Image Intensity", checkable=True
|
||||
),
|
||||
"aspect_ratio": MaterialIconAction(
|
||||
icon_name="aspect_ratio", tooltip="Lock image aspect ratio", checkable=True
|
||||
),
|
||||
"separator_2": SeparatorAction(),
|
||||
"FFT": IconAction(icon_path="fft.svg", tooltip="Toggle FFT", checkable=True),
|
||||
"log": IconAction(
|
||||
icon_path="log_scale.png", tooltip="Toggle log scale", checkable=True
|
||||
"FFT": MaterialIconAction(icon_name="fft", tooltip="Toggle FFT", checkable=True),
|
||||
"log": MaterialIconAction(
|
||||
icon_name="log_scale", tooltip="Toggle log scale", checkable=True
|
||||
),
|
||||
"transpose": IconAction(
|
||||
icon_path="transform.svg", tooltip="Transpose Image", checkable=True
|
||||
"transpose": MaterialIconAction(
|
||||
icon_name="transform", tooltip="Transpose Image", checkable=True
|
||||
),
|
||||
"rotate_right": IconAction(
|
||||
icon_path="rotate_right.svg", tooltip="Rotate image clockwise by 90 deg"
|
||||
"rotate_right": MaterialIconAction(
|
||||
icon_name="rotate_right", tooltip="Rotate image clockwise by 90 deg"
|
||||
),
|
||||
"rotate_left": IconAction(
|
||||
icon_path="rotate_left.svg", tooltip="Rotate image counterclockwise by 90 deg"
|
||||
"rotate_left": MaterialIconAction(
|
||||
icon_name="rotate_left", tooltip="Rotate image counterclockwise by 90 deg"
|
||||
),
|
||||
"reset": MaterialIconAction(
|
||||
icon_name="reset_settings", tooltip="Reset Image Settings"
|
||||
),
|
||||
"reset": IconAction(icon_path="reset_settings.svg", tooltip="Reset Image Settings"),
|
||||
"separator_3": SeparatorAction(),
|
||||
"axis_settings": IconAction(
|
||||
icon_path="settings.svg", tooltip="Open Configuration Dialog"
|
||||
"axis_settings": MaterialIconAction(
|
||||
icon_name="settings", tooltip="Open Configuration Dialog"
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.motor_map.motor_map_widget import BECMotorMapWidget
|
||||
|
||||
DOM_XML = """
|
||||
@@ -33,9 +32,7 @@ class BECMotorMapWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
||||
return "BEC Plots"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("my_location", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("my_location")
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_motor_map_widget"
|
||||
|
||||
@@ -5,7 +5,7 @@ import sys
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.qt_utils.toolbar import DeviceSelectionAction, IconAction, ModularToolBar
|
||||
from bec_widgets.qt_utils.toolbar import DeviceSelectionAction, MaterialIconAction, ModularToolBar
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
@@ -54,9 +54,11 @@ class BECMotorMapWidget(BECWidget, QWidget):
|
||||
"motor_y": DeviceSelectionAction(
|
||||
"Motor Y:", DeviceComboBox(device_filter="Positioner")
|
||||
),
|
||||
"connect": IconAction(icon_path="connection.svg", tooltip="Connect Motors"),
|
||||
"history": IconAction(icon_path="history.svg", tooltip="Reset Trace History"),
|
||||
"config": IconAction(icon_path="settings.svg", tooltip="Open Configuration Dialog"),
|
||||
"connect": MaterialIconAction(icon_name="link", tooltip="Connect Motors"),
|
||||
"history": MaterialIconAction(icon_name="history", tooltip="Reset Trace History"),
|
||||
"config": MaterialIconAction(
|
||||
icon_name="settings", tooltip="Open Configuration Dialog"
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
)
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.position_indicator.position_indicator import PositionIndicator
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class PositionIndicatorPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("horizontal_distribute", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("horizontal_distribute")
|
||||
|
||||
def includeFile(self):
|
||||
return "position_indicator"
|
||||
|
||||
@@ -7,13 +7,14 @@ from bec_lib.device import Positioner
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.messages import ScanQueueMessage
|
||||
from qtpy.QtCore import Property, QSize, Signal, Slot
|
||||
from qtpy.QtGui import QDoubleValidator, QIcon
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtGui import QDoubleValidator
|
||||
from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -78,11 +79,7 @@ class PositionerBox(BECWidget, QWidget):
|
||||
self.ui.setpoint.setValidator(self.setpoint_validator)
|
||||
self.ui.spinner_widget.start()
|
||||
self.ui.tool_button.clicked.connect(self._open_dialog_selection)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "toolbar_icons", "device_line_edit.svg"),
|
||||
size=QSize(16, 16),
|
||||
)
|
||||
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
|
||||
self.ui.tool_button.setIcon(icon)
|
||||
|
||||
def _open_dialog_selection(self):
|
||||
@@ -303,7 +300,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("dark")
|
||||
set_theme("dark")
|
||||
widget = PositionerBox(device="bpm4i")
|
||||
|
||||
widget.show()
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
|
||||
|
||||
DOM_XML = """
|
||||
@@ -34,9 +33,7 @@ class PositionerBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "Device Control"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("switch_right", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("switch_right")
|
||||
|
||||
def includeFile(self):
|
||||
return "positioner_box"
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.positioner_box.positioner_control_line import PositionerControlLine
|
||||
|
||||
DOM_XML = """
|
||||
@@ -34,9 +33,7 @@ class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no
|
||||
return "Device Control"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("switch_left", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("switch_left")
|
||||
|
||||
def includeFile(self):
|
||||
return "positioner_control_line"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
|
||||
DOM_XML = """
|
||||
@@ -34,9 +33,7 @@ class RingProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("track_changes", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("track_changes")
|
||||
|
||||
def includeFile(self):
|
||||
return "ring_progress_bar"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
@@ -18,6 +20,9 @@ from bec_widgets.widgets.stop_button.stop_button import StopButton
|
||||
|
||||
class ScanControl(BECWidget, QWidget):
|
||||
|
||||
scan_started = Signal()
|
||||
scan_selected = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self, parent=None, client=None, gui_id: str | None = None, allowed_scans: list | None = None
|
||||
):
|
||||
@@ -50,11 +55,26 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.layout.addWidget(self.scan_selection_group)
|
||||
|
||||
# Connect signals
|
||||
self.comboBox_scan_selection.currentIndexChanged.connect(self.on_scan_selected)
|
||||
self.comboBox_scan_selection.currentIndexChanged.connect(self.on_scan_selection_changed)
|
||||
self.button_run_scan.clicked.connect(self.run_scan)
|
||||
|
||||
# Add bundle button
|
||||
self.button_add_bundle = QPushButton("Add Bundle")
|
||||
self.button_add_bundle.setVisible(False)
|
||||
# Remove bundle button
|
||||
self.button_remove_bundle = QPushButton("Remove Bundle")
|
||||
self.button_remove_bundle.setVisible(False)
|
||||
|
||||
bundle_layout = QHBoxLayout()
|
||||
bundle_layout.addWidget(self.button_add_bundle)
|
||||
bundle_layout.addWidget(self.button_remove_bundle)
|
||||
self.layout.addLayout(bundle_layout)
|
||||
|
||||
self.button_add_bundle.clicked.connect(self.add_arg_bundle)
|
||||
self.button_remove_bundle.clicked.connect(self.remove_arg_bundle)
|
||||
|
||||
self.scan_selected.connect(self.scan_select)
|
||||
|
||||
# Initialize scan selection
|
||||
self.populate_scans()
|
||||
|
||||
@@ -69,21 +89,16 @@ class ScanControl(BECWidget, QWidget):
|
||||
scan_selection_group = QGroupBox("Scan Selection", self)
|
||||
self.scan_selection_layout = QGridLayout(scan_selection_group)
|
||||
self.comboBox_scan_selection = QComboBox(scan_selection_group)
|
||||
|
||||
# Run button
|
||||
self.button_run_scan = QPushButton("Start", scan_selection_group)
|
||||
self.button_run_scan.setStyleSheet("background-color: #559900; color: white")
|
||||
# Stop button
|
||||
self.button_stop_scan = StopButton(parent=scan_selection_group)
|
||||
# Add bundle button
|
||||
self.button_add_bundle = QPushButton("Add Bundle", scan_selection_group)
|
||||
# Remove bundle button
|
||||
self.button_remove_bundle = QPushButton("Remove Bundle", scan_selection_group)
|
||||
|
||||
self.scan_selection_layout.addWidget(self.comboBox_scan_selection, 0, 0, 1, 2)
|
||||
self.scan_selection_layout.addWidget(self.button_run_scan, 1, 0)
|
||||
self.scan_selection_layout.addWidget(self.button_stop_scan, 1, 1)
|
||||
self.scan_selection_layout.addWidget(self.button_add_bundle, 2, 0)
|
||||
self.scan_selection_layout.addWidget(self.button_remove_bundle, 2, 1)
|
||||
|
||||
return scan_selection_group
|
||||
|
||||
@@ -104,23 +119,65 @@ class ScanControl(BECWidget, QWidget):
|
||||
allowed_scans = self.allowed_scans
|
||||
self.comboBox_scan_selection.addItems(allowed_scans)
|
||||
|
||||
def on_scan_selected(self):
|
||||
def on_scan_selection_changed(self, index: int):
|
||||
"""Callback for scan selection combo box"""
|
||||
self.reset_layout()
|
||||
selected_scan_name = self.comboBox_scan_selection.currentText()
|
||||
selected_scan_info = self.available_scans.get(selected_scan_name, {})
|
||||
self.scan_selected.emit(selected_scan_name)
|
||||
|
||||
@Property(bool)
|
||||
def hide_scan_control_buttons(self):
|
||||
return not self.button_run_scan.isVisible()
|
||||
|
||||
@hide_scan_control_buttons.setter
|
||||
def hide_scan_control_buttons(self, hide: bool):
|
||||
self.show_scan_control_buttons(not hide)
|
||||
|
||||
@Slot(bool)
|
||||
def show_scan_control_buttons(self, show: bool):
|
||||
"""Shows or hides the scan control buttons."""
|
||||
self.button_run_scan.setVisible(show)
|
||||
self.button_stop_scan.setVisible(show)
|
||||
|
||||
show_group = show or self.button_run_scan.isVisible()
|
||||
self.scan_selection_group.setVisible(show_group)
|
||||
|
||||
@Property(bool)
|
||||
def hide_scan_selection_combobox(self):
|
||||
return not self.comboBox_scan_selection.isVisible()
|
||||
|
||||
@hide_scan_selection_combobox.setter
|
||||
def hide_scan_selection_combobox(self, hide: bool):
|
||||
self.show_scan_selection_combobox(not hide)
|
||||
|
||||
@Slot(bool)
|
||||
def show_scan_selection_combobox(self, show: bool):
|
||||
"""Shows or hides the scan selection combobox."""
|
||||
self.comboBox_scan_selection.setVisible(show)
|
||||
|
||||
show_group = show or self.button_run_scan.isVisible()
|
||||
self.scan_selection_group.setVisible(show_group)
|
||||
|
||||
@Slot(str)
|
||||
def scan_select(self, scan_name: str):
|
||||
"""
|
||||
Slot for scan selection. Updates the scan control layout based on the selected scan.
|
||||
|
||||
Args:
|
||||
scan_name(str): Name of the selected scan.
|
||||
"""
|
||||
self.reset_layout()
|
||||
selected_scan_info = self.available_scans.get(scan_name, {})
|
||||
|
||||
gui_config = selected_scan_info.get("gui_config", {})
|
||||
self.arg_group = gui_config.get("arg_group", None)
|
||||
self.kwarg_groups = gui_config.get("kwarg_groups", None)
|
||||
|
||||
if self.arg_box is None:
|
||||
self.button_add_bundle.setEnabled(False)
|
||||
self.button_remove_bundle.setEnabled(False)
|
||||
show_bundle_buttons = bool(self.arg_group["arg_inputs"])
|
||||
|
||||
if len(self.arg_group["arg_inputs"]) > 0:
|
||||
self.button_add_bundle.setEnabled(True)
|
||||
self.button_remove_bundle.setEnabled(True)
|
||||
self.button_add_bundle.setVisible(show_bundle_buttons)
|
||||
self.button_remove_bundle.setVisible(show_bundle_buttons)
|
||||
|
||||
if show_bundle_buttons:
|
||||
self.add_arg_group(self.arg_group)
|
||||
if len(self.kwarg_groups) > 0:
|
||||
self.add_kwargs_boxes(self.kwarg_groups)
|
||||
@@ -151,9 +208,11 @@ class ScanControl(BECWidget, QWidget):
|
||||
self.arg_box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||
self.layout.addWidget(self.arg_box)
|
||||
|
||||
@Slot()
|
||||
def add_arg_bundle(self):
|
||||
self.arg_box.add_widget_bundle()
|
||||
|
||||
@Slot()
|
||||
def remove_arg_bundle(self):
|
||||
self.arg_box.remove_widget_bundle()
|
||||
|
||||
@@ -172,7 +231,9 @@ class ScanControl(BECWidget, QWidget):
|
||||
box.deleteLater()
|
||||
self.kwarg_boxes = []
|
||||
|
||||
@Slot()
|
||||
def run_scan(self):
|
||||
self.scan_started.emit()
|
||||
args = []
|
||||
kwargs = {}
|
||||
if self.arg_box is not None:
|
||||
@@ -199,10 +260,12 @@ class ScanControl(BECWidget, QWidget):
|
||||
|
||||
# Application example
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
scan_control = ScanControl()
|
||||
|
||||
apply_theme("dark")
|
||||
set_theme("auto")
|
||||
window = scan_control
|
||||
window.show()
|
||||
app.exec()
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.scan_control.scan_control import ScanControl
|
||||
|
||||
DOM_XML = """
|
||||
@@ -34,9 +33,7 @@ class ScanControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "Device Control"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("stacked_line_chart", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("stacked_line_chart")
|
||||
|
||||
def includeFile(self):
|
||||
return "scan_control"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.spinner.spinner import SpinnerWidget
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class SpinnerWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("progress_activity", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("progress_activity")
|
||||
|
||||
def includeFile(self):
|
||||
return "spinner_widget"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.stop_button.stop_button import StopButton
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class StopButtonPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("dangerous", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("dangerous")
|
||||
|
||||
def includeFile(self):
|
||||
return "stop_button"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.text_box.text_box import TextBox
|
||||
|
||||
DOM_XML = """
|
||||
@@ -34,9 +33,7 @@ class TextBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("chat", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("chat")
|
||||
|
||||
def includeFile(self):
|
||||
return "text_box"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import Property, QEasingCurve, QPointF, QPropertyAnimation, Qt
|
||||
from qtpy.QtCore import Property, QEasingCurve, QPointF, QPropertyAnimation, Qt, Signal
|
||||
from qtpy.QtGui import QColor, QPainter
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
@@ -10,6 +10,8 @@ class ToggleSwitch(QWidget):
|
||||
A simple toggle.
|
||||
"""
|
||||
|
||||
enabled = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedSize(40, 21)
|
||||
@@ -41,6 +43,7 @@ class ToggleSwitch(QWidget):
|
||||
self._checked = state
|
||||
self.update_colors()
|
||||
self.set_thumb_pos_to_state()
|
||||
self.enabled.emit(self._checked)
|
||||
|
||||
@Property(QPointF)
|
||||
def thumb_pos(self):
|
||||
@@ -109,9 +112,7 @@ class ToggleSwitch(QWidget):
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self._checked = not self._checked
|
||||
self.update_colors()
|
||||
self.animate_thumb()
|
||||
self.checked = not self.checked
|
||||
|
||||
def update_colors(self):
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.toggle.toggle import ToggleSwitch
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class ToggleSwitchPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("toggle_on", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("toggle_on")
|
||||
|
||||
def includeFile(self):
|
||||
return "toggle_switch"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.vscode.vscode import VSCodeEditor
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class VSCodeEditorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Developer"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("developer_mode_tv", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("developer_mode_tv")
|
||||
|
||||
def includeFile(self):
|
||||
return "vs_code_editor"
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
|
||||
|
||||
DOM_XML = """
|
||||
@@ -35,9 +34,7 @@ class BECWaveformWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cov
|
||||
return "BEC Plots"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("show_chart", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("show_chart")
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_waveform_widget"
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import os
|
||||
from typing import Literal
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import BaseModel
|
||||
from qtpy.QtCore import QObject, QSize, Slot
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtCore import QObject, Slot
|
||||
from qtpy.QtWidgets import QComboBox, QLineEdit, QPushButton, QSpinBox, QTableWidget, QVBoxLayout
|
||||
|
||||
import bec_widgets
|
||||
@@ -38,10 +38,7 @@ class CurveSettings(SettingWidget):
|
||||
self.ui.normalize_colors_dap.clicked.connect(lambda: self.change_colormap("dap"))
|
||||
|
||||
def _setup_icons(self):
|
||||
add_icon = QIcon()
|
||||
add_icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "toolbar_icons", "add.svg"), size=QSize(20, 20)
|
||||
)
|
||||
add_icon = material_icon(icon_name="add", size=(20, 20), convert_to_pixmap=False)
|
||||
self.ui.add_dap.setIcon(add_icon)
|
||||
self.ui.add_dap.setToolTip("Add DAP Curve")
|
||||
self.ui.add_curve.setIcon(add_icon)
|
||||
@@ -123,10 +120,10 @@ class CurveSettings(SettingWidget):
|
||||
cm = self.ui.color_map_selector_dap.combo.currentText()
|
||||
table = self.ui.dap_table
|
||||
rows = table.rowCount()
|
||||
colors = Colors.golden_angle_color(colormap=cm, num=rows + 1, format="HEX")
|
||||
colors = Colors.golden_angle_color(colormap=cm, num=max(10, rows + 1), format="HEX")
|
||||
color_button_col = 2 if target == "scan" else 3
|
||||
for row, color in zip(range(rows), colors):
|
||||
table.cellWidget(row, color_button_col).setColor(color)
|
||||
for row in range(rows):
|
||||
table.cellWidget(row, color_button_col).setColor(colors[row])
|
||||
|
||||
@Slot()
|
||||
def accept_changes(self):
|
||||
@@ -253,12 +250,12 @@ class DialogRow(QObject):
|
||||
self.width = QSpinBox()
|
||||
self.width.setMinimum(1)
|
||||
self.width.setMaximum(20)
|
||||
self.width.setValue(2)
|
||||
self.width.setValue(4)
|
||||
|
||||
self.symbol_size = QSpinBox()
|
||||
self.symbol_size.setMinimum(1)
|
||||
self.symbol_size.setMaximum(20)
|
||||
self.symbol_size.setValue(5)
|
||||
self.symbol_size.setValue(7)
|
||||
|
||||
self.remove_button.clicked.connect(
|
||||
lambda: self.remove_row()
|
||||
@@ -283,9 +280,10 @@ class DialogRow(QObject):
|
||||
self.width.setValue(self.config.pen_width)
|
||||
self.symbol_size.setValue(self.config.symbol_size)
|
||||
else:
|
||||
default_color = Colors.golden_angle_color(
|
||||
colormap="magma", num=self.row + 1, format="HEX"
|
||||
)[-1]
|
||||
default_colors = Colors.golden_angle_color(
|
||||
colormap="magma", num=max(10, self.row + 1), format="HEX"
|
||||
)
|
||||
default_color = default_colors[self.row]
|
||||
self.color_button.setColor(default_color)
|
||||
|
||||
self.table_widget.setCellWidget(self.row, 0, self.device_line_edit)
|
||||
@@ -306,9 +304,10 @@ class DialogRow(QObject):
|
||||
self.width.setValue(self.config.pen_width)
|
||||
self.symbol_size.setValue(self.config.symbol_size)
|
||||
else:
|
||||
default_color = Colors.golden_angle_color(
|
||||
colormap="magma", num=self.row + 1, format="HEX"
|
||||
)[-1]
|
||||
default_colors = Colors.golden_angle_color(
|
||||
colormap="magma", num=max(10, self.row + 1), format="HEX"
|
||||
)
|
||||
default_color = default_colors[self.row]
|
||||
self.color_button.setColor(default_color)
|
||||
|
||||
self.table_widget.setCellWidget(self.row, 0, self.device_line_edit)
|
||||
@@ -339,5 +338,5 @@ class StyleComboBox(QComboBox):
|
||||
class RemoveButton(QPushButton):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
icon_path = os.path.join(MODULE_PATH, "assets", "toolbar_icons", "remove.svg")
|
||||
self.setIcon(QIcon(icon_path))
|
||||
icon = material_icon("disabled_by_default", size=(20, 20), convert_to_pixmap=False)
|
||||
self.setIcon(icon)
|
||||
|
||||
@@ -5,11 +5,12 @@ from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.qt_utils.toolbar import IconAction, ModularToolBar, SeparatorAction
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, SeparatorAction
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
|
||||
@@ -51,6 +52,20 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"export",
|
||||
"export_to_matplotlib",
|
||||
]
|
||||
scan_signal_update = Signal()
|
||||
async_signal_update = Signal()
|
||||
dap_params_update = Signal(dict)
|
||||
dap_summary_update = Signal(dict)
|
||||
autorange_signal = Signal()
|
||||
new_scan = Signal()
|
||||
crosshair_position_changed = Signal(tuple)
|
||||
crosshair_position_changed_string = Signal(str)
|
||||
crosshair_position_clicked = Signal(tuple)
|
||||
crosshair_position_clicked_string = Signal(str)
|
||||
crosshair_coordinates_changed = Signal(tuple)
|
||||
crosshair_coordinates_changed_string = Signal(str)
|
||||
crosshair_coordinates_clicked = Signal(tuple)
|
||||
crosshair_coordinates_clicked_string = Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -74,27 +89,32 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.fig = BECFigure()
|
||||
self.toolbar = ModularToolBar(
|
||||
actions={
|
||||
"save": IconAction(icon_path="save.svg", tooltip="Open Export Dialog"),
|
||||
"matplotlib": IconAction(
|
||||
icon_path="photo_library.svg", tooltip="Open Matplotlib Plot"
|
||||
"save": MaterialIconAction(icon_name="save", tooltip="Open Export Dialog"),
|
||||
"matplotlib": MaterialIconAction(
|
||||
icon_name="photo_library", tooltip="Open Matplotlib Plot"
|
||||
),
|
||||
"separator_1": SeparatorAction(),
|
||||
"drag_mode": IconAction(
|
||||
icon_path="drag_pan_mode.svg", tooltip="Drag Mouse Mode", checkable=True
|
||||
"drag_mode": MaterialIconAction(
|
||||
icon_name="drag_pan", tooltip="Drag Mouse Mode", checkable=True
|
||||
),
|
||||
"rectangle_mode": IconAction(
|
||||
icon_path="rectangle_mode.svg", tooltip="Rectangle Zoom Mode", checkable=True
|
||||
"rectangle_mode": MaterialIconAction(
|
||||
icon_name="frame_inspect", tooltip="Rectangle Zoom Mode", checkable=True
|
||||
),
|
||||
"auto_range": MaterialIconAction(
|
||||
icon_name="open_in_full", tooltip="Autorange Plot"
|
||||
),
|
||||
"auto_range": IconAction(icon_path="auto_range.svg", tooltip="Autorange Plot"),
|
||||
"separator_2": SeparatorAction(),
|
||||
"curves": IconAction(
|
||||
icon_path="line_axis.svg", tooltip="Open Curves Configuration"
|
||||
"curves": MaterialIconAction(
|
||||
icon_name="stacked_line_chart", tooltip="Open Curves Configuration"
|
||||
),
|
||||
"fit_params": IconAction(
|
||||
icon_path="fitting_parameters.svg", tooltip="Open Fitting Parameters"
|
||||
"fit_params": MaterialIconAction(
|
||||
icon_name="monitoring", tooltip="Open Fitting Parameters"
|
||||
),
|
||||
"axis_settings": IconAction(
|
||||
icon_path="settings.svg", tooltip="Open Configuration Dialog"
|
||||
"axis_settings": MaterialIconAction(
|
||||
icon_name="settings", tooltip="Open Configuration Dialog"
|
||||
),
|
||||
"crosshair": MaterialIconAction(
|
||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
@@ -110,8 +130,33 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
|
||||
self.config = config
|
||||
|
||||
self.hook_waveform_signals()
|
||||
self._hook_actions()
|
||||
|
||||
def hook_waveform_signals(self):
|
||||
self.waveform.scan_signal_update.connect(self.scan_signal_update)
|
||||
self.waveform.async_signal_update.connect(self.async_signal_update)
|
||||
self.waveform.dap_params_update.connect(self.dap_params_update)
|
||||
self.waveform.dap_summary_update.connect(self.dap_summary_update)
|
||||
self.waveform.autorange_signal.connect(self.autorange_signal)
|
||||
self.waveform.new_scan.connect(self.new_scan)
|
||||
self.waveform.crosshair_coordinates_changed.connect(self.crosshair_coordinates_changed)
|
||||
self.waveform.crosshair_coordinates_clicked.connect(self.crosshair_coordinates_clicked)
|
||||
self.waveform.crosshair_coordinates_changed.connect(
|
||||
self._emit_crosshair_coordinates_changed_string
|
||||
)
|
||||
self.waveform.crosshair_coordinates_clicked.connect(
|
||||
self._emit_crosshair_coordinates_clicked_string
|
||||
)
|
||||
self.waveform.crosshair_position_changed.connect(self.crosshair_position_changed)
|
||||
self.waveform.crosshair_position_clicked.connect(self.crosshair_position_clicked)
|
||||
self.waveform.crosshair_position_changed.connect(
|
||||
self._emit_crosshair_position_changed_string
|
||||
)
|
||||
self.waveform.crosshair_position_clicked.connect(
|
||||
self._emit_crosshair_position_clicked_string
|
||||
)
|
||||
|
||||
def _hook_actions(self):
|
||||
self.toolbar.widgets["save"].action.triggered.connect(self.export)
|
||||
self.toolbar.widgets["matplotlib"].action.triggered.connect(self.export_to_matplotlib)
|
||||
@@ -123,6 +168,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["curves"].action.triggered.connect(self.show_curve_settings)
|
||||
self.toolbar.widgets["fit_params"].action.triggered.connect(self.show_fit_summary_dialog)
|
||||
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
|
||||
self.toolbar.widgets["crosshair"].action.triggered.connect(self.waveform.toggle_crosshair)
|
||||
# self.toolbar.widgets["import"].action.triggered.connect(
|
||||
# lambda: self.load_config(path=None, gui=True)
|
||||
# )
|
||||
@@ -130,6 +176,22 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
# lambda: self.save_config(path=None, gui=True)
|
||||
# )
|
||||
|
||||
@SafeSlot(tuple)
|
||||
def _emit_crosshair_coordinates_changed_string(self, coordinates):
|
||||
self.crosshair_coordinates_changed_string.emit(str(coordinates))
|
||||
|
||||
@SafeSlot(tuple)
|
||||
def _emit_crosshair_coordinates_clicked_string(self, coordinates):
|
||||
self.crosshair_coordinates_clicked_string.emit(str(coordinates))
|
||||
|
||||
@SafeSlot(tuple)
|
||||
def _emit_crosshair_position_changed_string(self, position):
|
||||
self.crosshair_position_changed_string.emit(str(position))
|
||||
|
||||
@SafeSlot(tuple)
|
||||
def _emit_crosshair_position_clicked_string(self, position):
|
||||
self.crosshair_position_clicked_string.emit(str(position))
|
||||
|
||||
###################################
|
||||
# Dialog Windows
|
||||
###################################
|
||||
@@ -222,7 +284,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
color_map_z: str | None = "magma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
dap: str | None = None, # TODO add dap custom curve wrapper
|
||||
@@ -565,10 +627,12 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("auto")
|
||||
widget = BECWaveformWidget()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QGuiApplication, QIcon
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.website.website import WebsiteWidget
|
||||
|
||||
DOM_XML = """
|
||||
@@ -34,9 +33,7 @@ class WebsiteWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
palette = QGuiApplication.palette()
|
||||
pixmap = material_icon("travel_explore", color=palette.text().color(), filled=True)
|
||||
return QIcon(pixmap)
|
||||
return designer_material_icon("travel_explore")
|
||||
|
||||
def includeFile(self):
|
||||
return "website_widget"
|
||||
|
||||
@@ -65,7 +65,7 @@ add_module_names = False # Remove namespaces from class/method signatures
|
||||
autodoc_inherit_docstrings = True # If no docstring, inherit from base class
|
||||
set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints
|
||||
autoclass_content = "both" # Include both class docstring and __init__
|
||||
autodoc_mock_imports = ["pyqtgraph"]
|
||||
autodoc_mock_imports = ["pyqtgraph", "qtpy", "PySide6"]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
143
docs/developer/widget_development/bec_dispatcher.md
Normal file
143
docs/developer/widget_development/bec_dispatcher.md
Normal file
@@ -0,0 +1,143 @@
|
||||
(developer.bec_dispatcher)=
|
||||
|
||||
# BECDispatcher
|
||||
|
||||
## Overview
|
||||
|
||||
The [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher)
|
||||
is a powerful tool that
|
||||
simplifies the process of connecting Qt slots to message updates from the BEC server. It enables real-time communication
|
||||
between your widget and the BEC server by listening to specific message channels and triggering callbacks when new data
|
||||
is received.
|
||||
|
||||
This tool is especially useful for creating widgets that need to respond to dynamic data, such as device readbacks or
|
||||
scan updates. By
|
||||
using [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher),
|
||||
you
|
||||
can create callback functions that react to incoming messages and update your widget's state or perform other tasks
|
||||
based on the data received.
|
||||
|
||||
## How It Works
|
||||
|
||||
When you create a widget that needs to respond to updates from the BEC server, you can use
|
||||
the [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher)
|
||||
to
|
||||
connect specific Qt slots (callback functions) to message endpoints. These endpoints are defined within the BEC system
|
||||
and represent specific channels of information (
|
||||
e.g., [`device readback`](https://beamline-experiment-control.readthedocs.io/en/latest/api_reference/_autosummary/bec_lib.endpoints.MessageEndpoints.html#bec_lib.endpoints.MessageEndpoints.device_readback),
|
||||
[`scan_segment`](https://beamline-experiment-control.readthedocs.io/en/latest/api_reference/_autosummary/bec_lib.endpoints.MessageEndpoints.html#bec_lib.endpoints.MessageEndpoints.scan_segment),
|
||||
etc.).
|
||||
|
||||
### Step-by-Step Guide
|
||||
|
||||
1. **Create a Callback Function**: Define a function within your widget that will handle the data received from the BEC
|
||||
server. This function should usually accept two parameters: `msg_content` (the message content) and `metadata` (
|
||||
additional
|
||||
information about the message).
|
||||
|
||||
```python
|
||||
# Example for a callback function that updates a widget display based on motor readback data
|
||||
from qtpy.QtCore import Slot
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_device_readback(self, msg_content, metadata):
|
||||
# Process the incoming data
|
||||
new_value = msg_content["signals"]['motor_x']["value"]
|
||||
# Update the widget's display or perform another action
|
||||
self.update_display(new_value)
|
||||
```
|
||||
|
||||
2. **Connect the Slot to an Endpoint**: Use
|
||||
the [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher)
|
||||
to connect your callback function to a specific
|
||||
endpoint. The endpoint represents the type of data or message you're interested in.
|
||||
|
||||
```python
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, MessageEndpoints.device_readback("motor_x"))
|
||||
```
|
||||
|
||||
3. **Handle Incoming Data**: Your callback function will be triggered automatically whenever a new message is received
|
||||
on the connected endpoint. Use the data in `msg_content` to update your widget or perform other actions.
|
||||
|
||||
4. **Clean Up Connections**: If your widget is being destroyed or you no longer need to listen for updates, make sure to
|
||||
disconnect your slots from
|
||||
the [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher)
|
||||
to avoid memory or thread leaks.
|
||||
|
||||
```python
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_readback, MessageEndpoints.device_readback("motor_x"))
|
||||
```
|
||||
|
||||
### Example: Motor Map Widget
|
||||
|
||||
The [`BECMotorMap`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.widgets.figure.plots.motor_map.motor_map.BECMotorMap.html#bec-widgets-widgets-figure-plots-motor-map-motor-map-becmotormap)
|
||||
widget is a great example of
|
||||
how [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher)
|
||||
can be used to handle real-time data updates. This
|
||||
widget listens for updates on specific motor positions and dynamically updates the motor map display.
|
||||
|
||||
Here's a breakdown of
|
||||
how [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher)
|
||||
is used in
|
||||
the [`BECMotorMap`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.widgets.figure.plots.motor_map.motor_map.BECMotorMap.html#bec-widgets-widgets-figure-plots-motor-map-motor-map-becmotormap)
|
||||
widget:
|
||||
|
||||
1. **Connecting to Motor Readbacks**:
|
||||
The widget connects to
|
||||
the [`device readback`](https://beamline-experiment-control.readthedocs.io/en/latest/api_reference/_autosummary/bec_lib.endpoints.MessageEndpoints.html#bec_lib.endpoints.MessageEndpoints.device_readback)
|
||||
endpoints using
|
||||
the [`connect_slot`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher.connect_slot)
|
||||
method
|
||||
of [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher).
|
||||
This allows
|
||||
the widget to receive real-time updates about the motor positions.
|
||||
|
||||
```{literalinclude} ../../../bec_widgets/widgets/figure/plots/motor_map/motor_map.py
|
||||
:language: python
|
||||
:pyobject: BECMotorMap._connect_motor_to_slots
|
||||
:dedent: 4
|
||||
```
|
||||
|
||||
2. **Handling Readback Data**:
|
||||
The `on_device_readback` slot is called whenever new data is received from the motor readback. This slot processes
|
||||
the data and updates the motor map plot accordingly.
|
||||
|
||||
```{literalinclude} ../../../bec_widgets/widgets/figure/plots/motor_map/motor_map.py
|
||||
:language: python
|
||||
:pyobject: BECMotorMap.on_device_readback
|
||||
:dedent: 4
|
||||
```
|
||||
|
||||
3. **Updating the Plot**:
|
||||
The motor map plot is updated in response to the new data, providing a real-time visualization of the motor's
|
||||
position.
|
||||
|
||||
```{literalinclude} ../../../bec_widgets/widgets/figure/plots/motor_map/motor_map.py
|
||||
:language: python
|
||||
:pyobject: BECMotorMap._update_plot
|
||||
:dedent: 4
|
||||
```
|
||||
|
||||
4. **Disconnecting When No Longer Needed**:
|
||||
The widget ensures that connections are properly cleaned up when no longer needed.
|
||||
|
||||
```{literalinclude} ../../../bec_widgets/widgets/figure/plots/motor_map/motor_map.py
|
||||
:language: python
|
||||
:pyobject: BECMotorMap._update_plot
|
||||
:dedent: 4
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher)
|
||||
is a key tool for developing interactive and responsive widgets within the BEC framework. By
|
||||
leveraging this tool, you can create widgets that automatically respond to real-time data updates from the BEC server,
|
||||
enhancing the interactivity and functionality of your user interface.
|
||||
|
||||
In next tutorials we will cover how to create a custom widget using the BECDispatcher and BECWidget base class.
|
||||
|
||||
```{note}
|
||||
For more details on specific messages and endpoints, please refer to the [Message Endpoints Documentation](https://beamline-experiment-control.readthedocs.io/en/latest/api_reference/_autosummary/bec_lib.endpoints.MessageEndpoints.html#bec-lib-endpoints-messageendpoints).
|
||||
```
|
||||
@@ -8,4 +8,6 @@ maxdepth: 2
|
||||
hidden: false
|
||||
---
|
||||
|
||||
bec_dispatcher
|
||||
|
||||
```
|
||||
@@ -14,6 +14,18 @@ The `Stop Button` is a specialized control that provides an immediate interface
|
||||
- **Immediate Termination**: Instantly halts the execution of the current script or process.
|
||||
- **Queue Management**: Clears any pending operations in the scan queue, ensuring the system is reset and ready for new tasks.
|
||||
|
||||
## Dark Mode Button
|
||||
|
||||
The `Dark Mode Button` is a toggle control that allows users to switch between light and dark themes in the BEC GUI. It provides a convenient way to adjust the interface's appearance based on user preferences or environmental conditions.
|
||||
|
||||
```{figure} ./dark_mode_enabled.png
|
||||
```
|
||||
```{figure} ./dark_mode_disabled.png
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- **Theme Switching**: Enables users to switch between light and dark themes with a single click.
|
||||
- **Configurable from BECDesigner**: The defaults for the dark mode can be set in the BECDesigner, allowing users to customize the startup appearance of the GUI.
|
||||
````
|
||||
|
||||
````{tab} Examples
|
||||
@@ -46,5 +58,6 @@ my_gui.show()
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.StopButton.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.DarkModeButton.rst
|
||||
```
|
||||
````
|
||||
BIN
docs/user/widgets/buttons/dark_mode_disabled.png
Normal file
BIN
docs/user/widgets/buttons/dark_mode_disabled.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
docs/user/widgets/buttons/dark_mode_enabled.png
Normal file
BIN
docs/user/widgets/buttons/dark_mode_enabled.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
docs/user/widgets/scan_control/hide_scan_control.png
Normal file
BIN
docs/user/widgets/scan_control/hide_scan_control.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -23,11 +23,19 @@ By default, this widget supports scans that are derived from the following base
|
||||
The full procedure how to design `gui_config` for your custom scan class is described in the [Scan GUI Configuration](https://bec.readthedocs.io/en/latest/developer/scans/scan_gui_config.html) tutorial.
|
||||
```
|
||||
|
||||
## BECDesigner Customization
|
||||
Within the BECDesigner's [property editor](https://doc.qt.io/qt-6/designer-widget-mode.html#the-property-editor/), the `ScanControl` widget can be customized to suit your application's requirements. The widget provides the following customization options:
|
||||
- **Hide Scan Control**: Allows you to hide the scan control buttons from the widget interface. This is useful when you want to place the control buttons in a different location.
|
||||
- **Hide Scan Selection**: Allows you to hide the scan selection combobox from the widget interface. This is useful when you want to restrict the user to a specific scan type or implement a custom scan selection mechanism.
|
||||
|
||||
```{figure} ./hide_scan_control.png
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} Examples
|
||||
|
||||
The `ScanControl` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `QtDesigner`. Below are examples demonstrating how to create and use the `ScanControl` widget.
|
||||
The `ScanControl` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `ScanControl` widget.
|
||||
|
||||
## Example 1 - Adding Scan Control Widget to BECDockArea
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "0.95.1"
|
||||
version = "0.99.3"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
@@ -19,7 +19,7 @@ dependencies = [
|
||||
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
||||
"pydantic~=2.0",
|
||||
"pyqtgraph~=0.13",
|
||||
"bec_qthemes~=0.0",
|
||||
"bec_qthemes~=0.4, >=0.4.2",
|
||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||
"qtpy~=2.4",
|
||||
"pyte", # needed for vt100 console
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plot_settings:
|
||||
background_color: "black"
|
||||
num_columns: 1
|
||||
colormap: "plasma"
|
||||
colormap: "magma"
|
||||
scan_types: false
|
||||
plot_data:
|
||||
- plot_name: "BPM4i plots vs samx"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plot_settings:
|
||||
background_color: "black"
|
||||
num_columns: 1
|
||||
colormap: "plasma"
|
||||
colormap: "magma"
|
||||
scan_types: false
|
||||
plot_data:
|
||||
- plot_name: "BPM4i plots vs samx"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plot_settings:
|
||||
background_color: "white"
|
||||
num_columns: 3
|
||||
colormap: "plasma"
|
||||
colormap: "magma"
|
||||
scan_types: true
|
||||
plot_data:
|
||||
grid_scan:
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import pytest
|
||||
from qtpy.QtCore import QPointF
|
||||
|
||||
from bec_widgets.utils import Crosshair
|
||||
from bec_widgets.widgets.image.image_widget import BECImageWidget
|
||||
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
# pylint: disable = redefined-outer-name
|
||||
|
||||
|
||||
def test_mouse_moved_lines(qtbot):
|
||||
# Create a PlotWidget and add a PlotItem
|
||||
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
||||
plot_item = plot_widget.getPlotItem()
|
||||
plot_item.plot([1, 2, 3], [4, 5, 6])
|
||||
@pytest.fixture
|
||||
def plot_widget_with_crosshair(qtbot, mocked_client):
|
||||
widget = BECWaveformWidget(client=mocked_client())
|
||||
widget.plot(x=[1, 2, 3], y=[4, 5, 6])
|
||||
widget.waveform.hook_crosshair()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
|
||||
# Create a Crosshair instance
|
||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
||||
yield widget.waveform.crosshair, widget.waveform.plot_item
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def image_widget_with_crosshair(qtbot, mocked_client):
|
||||
widget = BECImageWidget(client=mocked_client())
|
||||
widget._image.add_custom_image(name="test", data=np.random.random((100, 200)))
|
||||
widget._image.hook_crosshair()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
|
||||
yield widget._image.crosshair, widget._image.plot_item
|
||||
|
||||
|
||||
def test_mouse_moved_lines(plot_widget_with_crosshair):
|
||||
crosshair, plot_item = plot_widget_with_crosshair
|
||||
|
||||
# Connect the signals to slots that will store the emitted values
|
||||
emitted_values_1D = []
|
||||
@@ -28,18 +49,12 @@ def test_mouse_moved_lines(qtbot):
|
||||
crosshair.mouse_moved(event_mock)
|
||||
|
||||
# Assert the expected behavior
|
||||
assert crosshair.v_line.pos().x() == 2
|
||||
assert crosshair.h_line.pos().y() == 5
|
||||
assert np.isclose(crosshair.v_line.pos().x(), 2)
|
||||
assert np.isclose(crosshair.h_line.pos().y(), 5)
|
||||
|
||||
|
||||
def test_mouse_moved_signals(qtbot):
|
||||
# Create a PlotWidget and add a PlotItem
|
||||
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
||||
plot_item = plot_widget.getPlotItem()
|
||||
plot_item.plot([1, 2, 3], [4, 5, 6])
|
||||
|
||||
# Create a Crosshair instance
|
||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
||||
def test_mouse_moved_signals(plot_widget_with_crosshair):
|
||||
crosshair, plot_item = plot_widget_with_crosshair
|
||||
|
||||
# Create a slot that will store the emitted values as tuples
|
||||
emitted_values_1D = []
|
||||
@@ -59,17 +74,11 @@ def test_mouse_moved_signals(qtbot):
|
||||
crosshair.mouse_moved(event_mock)
|
||||
|
||||
# Assert the expected behavior
|
||||
assert emitted_values_1D == [(2, [5])]
|
||||
assert emitted_values_1D == [("Curve 1", 2, 5)]
|
||||
|
||||
|
||||
def test_mouse_moved_signals_outside(qtbot):
|
||||
# Create a PlotWidget and add a PlotItem
|
||||
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
||||
plot_item = plot_widget.getPlotItem()
|
||||
plot_item.plot([1, 2, 3], [4, 5, 6])
|
||||
|
||||
# Create a Crosshair instance
|
||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
||||
def test_mouse_moved_signals_outside(plot_widget_with_crosshair):
|
||||
crosshair, plot_item = plot_widget_with_crosshair
|
||||
|
||||
# Create a slot that will store the emitted values as tuples
|
||||
emitted_values_1D = []
|
||||
@@ -92,17 +101,9 @@ def test_mouse_moved_signals_outside(qtbot):
|
||||
assert emitted_values_1D == []
|
||||
|
||||
|
||||
def test_mouse_moved_signals_2D(qtbot):
|
||||
# write similar test for 2D plot
|
||||
def test_mouse_moved_signals_2D(image_widget_with_crosshair):
|
||||
crosshair, plot_item = image_widget_with_crosshair
|
||||
|
||||
# Create a PlotWidget and add a PlotItem
|
||||
plot_widget = pg.PlotWidget(title="2D plot with crosshair and ROI square")
|
||||
data_2D = np.random.random((100, 200))
|
||||
plot_item = plot_widget.getPlotItem()
|
||||
image_item = pg.ImageItem(data_2D)
|
||||
plot_item.addItem(image_item)
|
||||
# Create a Crosshair instance
|
||||
crosshair = Crosshair(plot_item=plot_item)
|
||||
# Create a slot that will store the emitted values as tuples
|
||||
emitted_values_2D = []
|
||||
|
||||
@@ -118,20 +119,12 @@ def test_mouse_moved_signals_2D(qtbot):
|
||||
# Call the mouse_moved method
|
||||
crosshair.mouse_moved(event_mock)
|
||||
# Assert the expected behavior
|
||||
assert emitted_values_2D == [(22.0, 55.0)]
|
||||
assert emitted_values_2D == [("test", 22.0, 55.0)]
|
||||
|
||||
|
||||
def test_mouse_moved_signals_2D_outside(qtbot):
|
||||
# write similar test for 2D plot
|
||||
def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair):
|
||||
crosshair, plot_item = image_widget_with_crosshair
|
||||
|
||||
# Create a PlotWidget and add a PlotItem
|
||||
plot_widget = pg.PlotWidget(title="2D plot with crosshair and ROI square")
|
||||
data_2D = np.random.random((100, 200))
|
||||
plot_item = plot_widget.getPlotItem()
|
||||
image_item = pg.ImageItem(data_2D)
|
||||
plot_item.addItem(image_item)
|
||||
# Create a Crosshair instance
|
||||
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
||||
# Create a slot that will store the emitted values as tuples
|
||||
emitted_values_2D = []
|
||||
|
||||
|
||||
70
tests/unit_tests/test_dark_mode_button.py
Normal file
70
tests/unit_tests/test_dark_mode_button.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
from bec_widgets.widgets.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dark_mode_button(qtbot, mocked_client):
|
||||
"""
|
||||
Fixture for the dark mode button.
|
||||
"""
|
||||
button = DarkModeButton(client=mocked_client)
|
||||
qtbot.addWidget(button)
|
||||
qtbot.waitExposed(button)
|
||||
yield button
|
||||
|
||||
|
||||
def test_dark_mode_button_init(dark_mode_button):
|
||||
"""
|
||||
Test that the dark mode button is initialized correctly.
|
||||
"""
|
||||
assert dark_mode_button.dark_mode_enabled is False
|
||||
assert dark_mode_button.mode_button.toolTip() == "Set Dark Mode"
|
||||
|
||||
|
||||
def test_dark_mode_button_toggle(dark_mode_button):
|
||||
"""
|
||||
Test that the dark mode button toggles correctly.
|
||||
"""
|
||||
dark_mode_button.toggle_dark_mode()
|
||||
assert dark_mode_button.dark_mode_enabled is True
|
||||
assert dark_mode_button.mode_button.toolTip() == "Set Light Mode"
|
||||
|
||||
dark_mode_button.toggle_dark_mode()
|
||||
assert dark_mode_button.dark_mode_enabled == False
|
||||
assert dark_mode_button.mode_button.toolTip() == "Set Dark Mode"
|
||||
|
||||
|
||||
def test_dark_mode_button_toggles_on_click(dark_mode_button, qtbot):
|
||||
"""
|
||||
Test that the dark mode button toggles correctly when clicked.
|
||||
"""
|
||||
qtbot.mouseClick(dark_mode_button.mode_button, Qt.MouseButton.LeftButton)
|
||||
assert dark_mode_button.dark_mode_enabled is True
|
||||
assert dark_mode_button.mode_button.toolTip() == "Set Light Mode"
|
||||
|
||||
qtbot.mouseClick(dark_mode_button.mode_button, Qt.MouseButton.LeftButton)
|
||||
assert dark_mode_button.dark_mode_enabled is False
|
||||
assert dark_mode_button.mode_button.toolTip() == "Set Dark Mode"
|
||||
|
||||
|
||||
def test_dark_mode_button_changes_theme(dark_mode_button):
|
||||
"""
|
||||
Test that the dark mode button changes the theme correctly.
|
||||
"""
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.dark_mode_button.dark_mode_button.set_theme"
|
||||
) as mocked_set_theme:
|
||||
dark_mode_button.toggle_dark_mode()
|
||||
mocked_set_theme.assert_called_with("dark")
|
||||
|
||||
dark_mode_button.toggle_dark_mode()
|
||||
mocked_set_theme.assert_called_with("light")
|
||||
@@ -73,7 +73,7 @@ def test_create_waveform1D_by_config(qtbot, mocked_client):
|
||||
"x_grid": False,
|
||||
"y_grid": False,
|
||||
},
|
||||
"color_palette": "plasma",
|
||||
"color_palette": "magma",
|
||||
"curves": {
|
||||
"bpm4i-bpm4i": {
|
||||
"widget_class": "BECCurve",
|
||||
@@ -81,11 +81,11 @@ def test_create_waveform1D_by_config(qtbot, mocked_client):
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"color_map_z": "plasma",
|
||||
"color_map_z": "magma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
"pen_width": 2,
|
||||
"symbol_size": 7,
|
||||
"pen_width": 4,
|
||||
"pen_style": "dash",
|
||||
"source": "scan_segment",
|
||||
"signals": {
|
||||
@@ -114,11 +114,11 @@ def test_create_waveform1D_by_config(qtbot, mocked_client):
|
||||
"parent_id": "widget_1",
|
||||
"label": "curve-custom",
|
||||
"color": "blue",
|
||||
"color_map_z": "plasma",
|
||||
"color_map_z": "magma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
"pen_width": 2,
|
||||
"symbol_size": 7,
|
||||
"pen_width": 5,
|
||||
"pen_style": "dashdot",
|
||||
"source": "custom",
|
||||
"signals": None,
|
||||
@@ -155,11 +155,11 @@ def test_getting_curve(qtbot, mocked_client):
|
||||
gui_id="test_curve",
|
||||
parent_id=w1.gui_id,
|
||||
label="bpm4i-bpm4i",
|
||||
color="#cc4778",
|
||||
color="#b73779",
|
||||
symbol="o",
|
||||
symbol_color=None,
|
||||
symbol_size=5,
|
||||
pen_width=2,
|
||||
symbol_size=7,
|
||||
pen_width=4,
|
||||
pen_style="solid",
|
||||
source="scan_segment",
|
||||
signals=Signal(
|
||||
@@ -398,11 +398,11 @@ def test_curve_add_by_config(qtbot, mocked_client):
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"color_map_z": "plasma",
|
||||
"color_map_z": "magma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
"pen_width": 2,
|
||||
"symbol_size": 7,
|
||||
"pen_width": 4,
|
||||
"pen_style": "dash",
|
||||
"source": "scan_segment",
|
||||
"signals": {
|
||||
@@ -522,7 +522,7 @@ def test_scatter_2d_update(qtbot, mocked_client):
|
||||
|
||||
data = c1.get_data()
|
||||
expected_x_y_data = ([1, 2, 3], [1, 2, 3])
|
||||
expected_z_colors = w1._make_z_gradient([1, 3, 2], "plasma")
|
||||
expected_z_colors = w1._make_z_gradient([1, 3, 2], "magma")
|
||||
|
||||
scatter_points = c1.scatter.points()
|
||||
colors = [point.brush().color() for point in scatter_points]
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_waveform_plot_data(waveform_widget, mock_waveform):
|
||||
y_entry=None,
|
||||
z_entry=None,
|
||||
color=None,
|
||||
color_map_z="plasma",
|
||||
color_map_z="magma",
|
||||
label=None,
|
||||
validate=True,
|
||||
dap=None,
|
||||
@@ -86,7 +86,7 @@ def test_waveform_plot_scan_curves(waveform_widget, mock_waveform):
|
||||
y_entry=None,
|
||||
z_entry=None,
|
||||
color=None,
|
||||
color_map_z="plasma",
|
||||
color_map_z="magma",
|
||||
label=None,
|
||||
validate=True,
|
||||
dap="GaussianModel",
|
||||
|
||||
Reference in New Issue
Block a user