1
0
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 Message Date
semantic-release
65345187b3 0.99.3
Automatically generated by python-semantic-release
2024-08-27 13:49:04 +00:00
d48243483e build: updated min version of bec qthemes 2024-08-27 13:09:21 +02:00
1ca9499edd fix(cmaps): unified all defaults to magma cmap 2024-08-27 13:09:21 +02:00
060935ffc5 fix(color maps): color maps should take the background color into account; fixed min colors to 10 2024-08-27 12:36:02 +02:00
semantic-release
50dbef52c0 0.99.2
Automatically generated by python-semantic-release
2024-08-27 09:03:23 +00:00
bb385f07ca ci: additional tests are not allowed to fail 2024-08-27 10:54:46 +02:00
cf28730515 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"
2024-08-27 10:54:46 +02:00
semantic-release
13ae383455 0.99.1
Automatically generated by python-semantic-release
2024-08-27 07:28:47 +00:00
2265458dcc fix(crosshair): emit all crosshair events, not just line coordinates 2024-08-26 14:10:46 +02:00
semantic-release
0a59f08fcc 0.99.0
Automatically generated by python-semantic-release
2024-08-25 11:49:52 +00:00
c70724a456 refactor(darkmodebutton): renamed set_dark_mode_enabled to toggle_dark_mode 2024-08-25 13:45:56 +02:00
406c263746 docs(darkmodebutton): added dark mode button docs 2024-08-25 13:45:56 +02:00
df35aabff3 test(dark_mode_button): added tests for dark mode button 2024-08-25 13:45:56 +02:00
cc8c166b5c feat(darkmodebutton): added button to toggle between dark and light mode 2024-08-25 13:45:56 +02:00
c4f3308dc0 fix(toggle): emit state change 2024-08-25 13:45:56 +02:00
semantic-release
8f3824c0e7 0.98.0
Automatically generated by python-semantic-release
2024-08-25 11:45:36 +00:00
afdf4e8782 fix(toolbar): removed hardcoded color values 2024-08-23 23:00:49 +02:00
2a82032644 fix: transitioning to material icons 2024-08-23 22:40:21 +02:00
88a2f66758 fix(dock_area): transitioned to MaterialIconAction 2024-08-23 22:05:56 +02:00
3f3b207295 fix: fix color palette if qtheme was not called 2024-08-23 20:14:53 +02:00
44cfda1c07 refactor(waveform): use set theme for demo 2024-08-23 20:04:44 +02:00
e42b84c636 fix(figure): removed theme from figure init 2024-08-23 20:04:44 +02:00
77c5aa741c fix: use globally set theme instead of the internal bec widgets theme 2024-08-23 20:04:44 +02:00
2b4449afeb feat(themes): added set_theme method 2024-08-23 20:04:44 +02:00
36ad464159 fix(waveform): fixed icon appearance 2024-08-23 20:04:44 +02:00
semantic-release
e8ae6f2e43 0.97.0
Automatically generated by python-semantic-release
2024-08-23 13:06:31 +00:00
3ecbd60627 fix(toolbar icon): fixed material icon toolbar for theme changes 2024-08-23 14:14:40 +02:00
82a55ddf3e feat(designer): added designer icon factory 2024-08-23 14:12:33 +02:00
semantic-release
7d190719b1 0.96.3
Automatically generated by python-semantic-release
2024-08-23 07:47:59 +00:00
8c2e7c8259 fix: minor fixes for type annotations 2024-08-22 20:44:28 +02:00
dd7c71bb1e docs(dispatcher): docs added 2024-08-22 14:52:52 +02:00
semantic-release
7b5b7a8cbb 0.96.2
Automatically generated by python-semantic-release
2024-08-22 09:49:04 +00:00
af28574bd5 fix(waveform): validation of custom curves removed 2024-08-22 11:35:27 +02:00
617db36ed4 fix(waveform): skip validation for curves that are not BECCurve instances 2024-08-22 10:55:49 +02:00
semantic-release
ebc2e44c7c 0.96.1
Automatically generated by python-semantic-release
2024-08-22 08:41:33 +00:00
44738057a3 fix(crosshair): update markers if necessary 2024-08-22 10:32:35 +02:00
f98a9f9771 fix(waveform_widget): fixed icon appearance 2024-08-22 10:32:35 +02:00
2fe72c9ccb fix: bubble-up signals 2024-08-22 10:32:35 +02:00
f0203d9bf6 ci: fail pytest after 2 failed tests 2024-08-22 10:32:35 +02:00
37835cbf76 fix(crosshair): fixed crosshair for image and waveforms 2024-08-22 10:32:35 +02:00
semantic-release
e005be33d1 0.96.0
Automatically generated by python-semantic-release
2024-08-22 07:50:58 +00:00
9d7718c3d9 docs(scan_control): added designer options 2024-08-22 09:42:00 +02:00
9d8fb0b761 feat(scan_control): added the ability to configure the scan control widget from designer 2024-08-22 09:42:00 +02:00
67 changed files with 1251 additions and 574 deletions

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import itertools
from typing import Type

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View 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_()

View File

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

View File

@@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from 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()

View File

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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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_()

View File

@@ -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"

View File

@@ -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,

View File

@@ -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()

View File

@@ -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

View File

@@ -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}

View File

@@ -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"

View File

@@ -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,

View File

@@ -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"

View File

@@ -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,
)

View File

@@ -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"

View File

@@ -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()

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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):

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)

View File

@@ -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_())

View File

@@ -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"

View File

@@ -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"]

View 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).
```

View File

@@ -8,4 +8,6 @@ maxdepth: 2
hidden: false
---
bec_dispatcher
```

View File

@@ -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
```
````

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
plot_settings:
background_color: "white"
num_columns: 3
colormap: "plasma"
colormap: "magma"
scan_types: true
plot_data:
grid_scan:

View File

@@ -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 = []

View 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")

View File

@@ -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]

View File

@@ -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",