mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-11 03:00:54 +02:00
Compare commits
11 Commits
docs/add_t
...
v0.64.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17133771bb | ||
| e5a7d47b21 | |||
|
|
71ec61e27b | ||
| b3575eb068 | |||
| 216511b951 | |||
| 6dabbf874f | |||
|
|
d5aad06c88 | ||
| 5d6672069e | |||
| 140ad83380 | |||
| ea805d1362 | |||
| 9e16f2faf9 |
92
CHANGELOG.md
92
CHANGELOG.md
@@ -1,6 +1,48 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.64.2 (2024-06-19)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(client_utils): added close rpc command to shutdown of gui from bec_ipython_client ([`e5a7d47`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e5a7d47b21cbf066f740f1d11d7c9ea7c70f3080))
|
||||
|
||||
## v0.64.1 (2024-06-19)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(widgets): removed widget module import of sub widgets ([`216511b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/216511b951ff0e15b6d7c70133095f3ac45c23f4))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(utils): moved get_rpc_widgets to plugin_utils ([`6dabbf8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6dabbf874fbbdde89c34a7885bf95aa9c895a28b))
|
||||
|
||||
### Test
|
||||
|
||||
* test: moved rpc_classes test ([`b3575eb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b3575eb06852b456cde915dfda281a3e778e3aeb))
|
||||
|
||||
## v0.64.0 (2024-06-19)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: add job optional dependency check ([`27426ce`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/27426ce7a52b4cbad7f3bef114d6efe6ad73bd7f))
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: fix links in developer section ([`9e16f2f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9e16f2faf9c59a5d36ae878512c5a910cca31e69))
|
||||
|
||||
* docs: refactor developer section, add widget tutorial ([`2a36d93`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2a36d9364f242bf42e4cda4b50e6f46aa3833bbd))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: add option to change size of the fonts ([`ea805d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ea805d1362fc084d3b703b6f81b0180072f0825d))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(plot_base): font size is set with setScale which is scaling the whole legend window ([`5d66720`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d6672069ea1cbceb62104f66c127e4e3c23e4a4))
|
||||
|
||||
### Test
|
||||
|
||||
* test: add tests ([`140ad83`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/140ad83380808928edf7953e23c762ab72a0a1e9))
|
||||
|
||||
## v0.63.2 (2024-06-14)
|
||||
|
||||
@@ -16,7 +58,6 @@ Like with QtWebEngine ([`6f96498`](https://gitlab.psi.ch/bec/bec_widgets/-/commi
|
||||
|
||||
This reverts commit fe04dd80e59a0e74f7fdea603e0642707ecc7c2a. ([`836b6e6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/836b6e64f694916d6b6f909dedf11a4a6d2c86a4))
|
||||
|
||||
|
||||
## v0.63.1 (2024-06-13)
|
||||
|
||||
### Fix
|
||||
@@ -26,7 +67,6 @@ This reverts commit fe04dd80e59a0e74f7fdea603e0642707ecc7c2a. ([`836b6e6`](https
|
||||
The proper finalization sequence will be executed by the remote process
|
||||
on SIGTERM ([`9263f8e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9263f8ef5c17ae7a007a1a564baf787b39061756))
|
||||
|
||||
|
||||
## v0.63.0 (2024-06-13)
|
||||
|
||||
### Documentation
|
||||
@@ -51,7 +91,6 @@ on SIGTERM ([`9263f8e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9263f8ef5
|
||||
|
||||
This reverts commit abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3 ([`fe04dd8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fe04dd80e59a0e74f7fdea603e0642707ecc7c2a))
|
||||
|
||||
|
||||
## v0.62.0 (2024-06-12)
|
||||
|
||||
### Feature
|
||||
@@ -62,7 +101,6 @@ This reverts commit abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3 ([`fe04dd8`](https:
|
||||
|
||||
* doc: add documentation about creating custom GUI applications embedding BEC Widgets ([`17a0068`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17a00687579f5efab1990cd83862ec0e78198633))
|
||||
|
||||
|
||||
## v0.61.0 (2024-06-12)
|
||||
|
||||
### Feature
|
||||
@@ -73,7 +111,6 @@ This reverts commit abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3 ([`fe04dd8`](https:
|
||||
|
||||
* refactor: improve labe of auto_update script ([`40b5688`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40b568815893cd41af3531bb2e647ca1e2e315f4))
|
||||
|
||||
|
||||
## v0.60.0 (2024-06-08)
|
||||
|
||||
### Ci
|
||||
@@ -116,61 +153,16 @@ This reverts commit abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3 ([`fe04dd8`](https:
|
||||
|
||||
* test: added missing pylint statement to header ([`f662985`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6629852ebc2b4ee239fa560cc310a5ae2627cf7))
|
||||
|
||||
|
||||
## v0.59.1 (2024-06-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(curve): set_color_map_z typo fixed in user access ([`e7838b0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e7838b0f2fc23b0a232ed7d68fbd7f3493a91b9e))
|
||||
|
||||
|
||||
## v0.59.0 (2024-06-07)
|
||||
|
||||
### Build
|
||||
|
||||
* build: added webengine dependency ([`d56c549`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d56c5493cd28f379d04a79d90b01c73b0760da1b))
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: merged additional tests to parallel matrix job ([`178fe4d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/178fe4d2da3a959f7cd90e7ea0f47314dc1ef4ed))
|
||||
|
||||
* ci: added webengine dependencies ([`2d79ef8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2d79ef8fe5e52c61f4a78782770377cd6b41958b))
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: added website docs ([`cf6e5a4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf6e5a40fc8320e9898a446a5bf14b77e94ef013))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(widget): added simple website widget with rpc ([`64abd67`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/64abd67b9b416bff9c89880b248d6e8639aa1e70))
|
||||
|
||||
|
||||
## v0.58.1 (2024-06-07)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(dock): new dock can be detached upon creation ([`02a2608`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/02a26086c4540127a11c235cba30afc4fd712007))
|
||||
|
||||
|
||||
## v0.58.0 (2024-06-07)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(utils.colors): general color validators ([`3094632`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30946321348abc349fb4003dc39d0232dc19606c))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: bar colormap dynamic setting ([`67fd5e8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/67fd5e8581f60fe64027ac57f1f12cefa4d28343))
|
||||
|
||||
* fix: formatting isort ([`bf699ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bf699ec1fbe2aacd31854e84fb0438c336840fcf))
|
||||
|
||||
* fix(curve): 2D scatter updated if color_map_z is changed ([`6985ff0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6985ff0fcef9791b53198206ec8cbccd1d65ef99))
|
||||
|
||||
* fix(curve): color_map_z setting works ([`33f7be4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33f7be42c512402dab3fdd9781a8234e3ec5f4ba))
|
||||
|
||||
### Test
|
||||
|
||||
* test(color): validation tests added ([`c0ddece`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c0ddeceeeabacbf33019a8f24b18821926dc17ac))
|
||||
|
||||
|
||||
## v0.57.7 (2024-06-07)
|
||||
|
||||
@@ -1044,33 +1044,37 @@ class BECImageShow(RPCBase):
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_title(self, title: "str"):
|
||||
def set_title(self, title: "str", size: "int" = None):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
size(int): Font size of the title.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_label(self, label: "str"):
|
||||
def set_x_label(self, label: "str", size: "int" = None):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_label(self, label: "str"):
|
||||
def set_y_label(self, label: "str", size: "int" = None):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -1268,33 +1272,37 @@ class BECPlotBase(RPCBase):
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_title(self, title: "str"):
|
||||
def set_title(self, title: "str", size: "int" = None):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
size(int): Font size of the title.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_label(self, label: "str"):
|
||||
def set_x_label(self, label: "str", size: "int" = None):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_label(self, label: "str"):
|
||||
def set_y_label(self, label: "str", size: "int" = None):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -1370,6 +1378,15 @@ class BECPlotBase(RPCBase):
|
||||
Remove the plot widget from the figure.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_legend_label_size(self, size: "int" = None):
|
||||
"""
|
||||
Set the font size of the legend.
|
||||
|
||||
Args:
|
||||
size(int): Font size of the legend.
|
||||
"""
|
||||
|
||||
|
||||
class BECWaveform(RPCBase):
|
||||
@property
|
||||
@@ -1516,33 +1533,37 @@ class BECWaveform(RPCBase):
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_title(self, title: "str"):
|
||||
def set_title(self, title: "str", size: "int" = None):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
size(int): Font size of the title.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_x_label(self, label: "str"):
|
||||
def set_x_label(self, label: "str", size: "int" = None):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_y_label(self, label: "str"):
|
||||
def set_y_label(self, label: "str", size: "int" = None):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -1618,6 +1639,15 @@ class BECWaveform(RPCBase):
|
||||
Remove the plot widget from the figure.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_legend_label_size(self, size: "int" = None):
|
||||
"""
|
||||
Set the font size of the legend.
|
||||
|
||||
Args:
|
||||
size(int): Font size of the legend.
|
||||
"""
|
||||
|
||||
|
||||
class Ring(RPCBase):
|
||||
@rpc_call
|
||||
|
||||
@@ -173,8 +173,15 @@ class BECGuiClientMixin:
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close the figure.
|
||||
Close the gui window.
|
||||
"""
|
||||
if self._process is None:
|
||||
return
|
||||
|
||||
self._run_rpc("close", (), wait_for_rpc_response=False)
|
||||
while self.gui_is_alive():
|
||||
time.sleep(0.2)
|
||||
|
||||
self._client.shutdown()
|
||||
if self._process:
|
||||
self._process.terminate()
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
@@ -10,9 +9,8 @@ from typing import Literal
|
||||
|
||||
import black
|
||||
import isort
|
||||
from qtpy.QtWidgets import QGraphicsWidget, QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.utils.plugin_utils import get_rpc_classes
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import get_overloads
|
||||
@@ -138,50 +136,6 @@ class {class_name}(RPCBase):"""
|
||||
with open(file_name, "w", encoding="utf-8") as file:
|
||||
file.write(formatted_content)
|
||||
|
||||
@staticmethod
|
||||
def get_rpc_classes(
|
||||
repo_name: str,
|
||||
) -> dict[Literal["connector_classes", "top_level_classes"], list[type]]:
|
||||
"""
|
||||
Get all RPC-enabled classes in the specified repository.
|
||||
|
||||
Args:
|
||||
repo_name(str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
|
||||
"""
|
||||
connector_classes = []
|
||||
top_level_classes = []
|
||||
anchor_module = importlib.import_module(f"{repo_name}.widgets")
|
||||
directory = os.path.dirname(anchor_module.__file__)
|
||||
for root, _, files in sorted(os.walk(directory)):
|
||||
for file in files:
|
||||
if not file.endswith(".py") or file.startswith("__"):
|
||||
continue
|
||||
|
||||
path = os.path.join(root, file)
|
||||
subs = os.path.dirname(os.path.relpath(path, directory)).split("/")
|
||||
if len(subs) == 1 and not subs[0]:
|
||||
module_name = file.split(".")[0]
|
||||
else:
|
||||
module_name = ".".join(subs + [file.split(".")[0]])
|
||||
|
||||
module = importlib.import_module(f"{repo_name}.widgets.{module_name}")
|
||||
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name)
|
||||
if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
|
||||
continue
|
||||
if isinstance(obj, type) and issubclass(obj, BECConnector):
|
||||
connector_classes.append(obj)
|
||||
if len(subs) == 1 and (
|
||||
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
|
||||
):
|
||||
top_level_classes.append(obj)
|
||||
|
||||
return {"connector_classes": connector_classes, "top_level_classes": top_level_classes}
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
@@ -197,7 +151,7 @@ def main():
|
||||
current_path = os.path.dirname(__file__)
|
||||
client_path = os.path.join(current_path, "client.py")
|
||||
|
||||
rpc_classes = ClientGenerator.get_rpc_classes("bec_widgets")
|
||||
rpc_classes = get_rpc_classes("bec_widgets")
|
||||
rpc_classes["connector_classes"].sort(key=lambda x: x.__name__)
|
||||
|
||||
generator = ClientGenerator()
|
||||
|
||||
@@ -40,7 +40,7 @@ class BECWidgetsCLIServer:
|
||||
self._shutdown_event = False
|
||||
self._heartbeat_timer = QTimer()
|
||||
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
||||
self._heartbeat_timer.start(200) # Emit heartbeat every 1 seconds
|
||||
self._heartbeat_timer.start(200)
|
||||
|
||||
def on_rpc_update(self, msg: dict, metadata: dict):
|
||||
request_id = metadata.get("request_id")
|
||||
@@ -105,7 +105,7 @@ class BECWidgetsCLIServer:
|
||||
self.client.connector.set(
|
||||
MessageEndpoints.gui_heartbeat(self.gui_id),
|
||||
messages.StatusMessage(name=self.gui_id, status=1, info={}),
|
||||
expire=10,
|
||||
expire=1,
|
||||
)
|
||||
|
||||
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
||||
|
||||
@@ -10,8 +10,8 @@ from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import BECDispatcher, UILoader
|
||||
from bec_widgets.widgets import BECFigure
|
||||
from bec_widgets.widgets.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
|
||||
# class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib.plugin_helper import _get_available_plugins
|
||||
from qtpy.QtWidgets import QGraphicsWidget, QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
@@ -38,3 +42,47 @@ def get_plugin_widgets() -> dict[str, BECConnector]:
|
||||
|
||||
def _filter_plugins(obj):
|
||||
return inspect.isclass(obj) and issubclass(obj, BECConnector)
|
||||
|
||||
|
||||
def get_rpc_classes(
|
||||
repo_name: str,
|
||||
) -> dict[Literal["connector_classes", "top_level_classes"], list[type]]:
|
||||
"""
|
||||
Get all RPC-enabled classes in the specified repository.
|
||||
|
||||
Args:
|
||||
repo_name(str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
|
||||
"""
|
||||
connector_classes = []
|
||||
top_level_classes = []
|
||||
anchor_module = importlib.import_module(f"{repo_name}.widgets")
|
||||
directory = os.path.dirname(anchor_module.__file__)
|
||||
for root, _, files in sorted(os.walk(directory)):
|
||||
for file in files:
|
||||
if not file.endswith(".py") or file.startswith("__"):
|
||||
continue
|
||||
|
||||
path = os.path.join(root, file)
|
||||
subs = os.path.dirname(os.path.relpath(path, directory)).split("/")
|
||||
if len(subs) == 1 and not subs[0]:
|
||||
module_name = file.split(".")[0]
|
||||
else:
|
||||
module_name = ".".join(subs + [file.split(".")[0]])
|
||||
|
||||
module = importlib.import_module(f"{repo_name}.widgets.{module_name}")
|
||||
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name)
|
||||
if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
|
||||
continue
|
||||
if isinstance(obj, type) and issubclass(obj, BECConnector):
|
||||
connector_classes.append(obj)
|
||||
if len(subs) == 1 and (
|
||||
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
|
||||
):
|
||||
top_level_classes.append(obj)
|
||||
|
||||
return {"connector_classes": connector_classes, "top_level_classes": top_level_classes}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .buttons import StopButton
|
||||
from .dock import BECDock, BECDockArea
|
||||
from .figure import BECFigure, FigureConfig
|
||||
from .scan_control import ScanControl
|
||||
from .spiral_progress_bar import SpiralProgressBar
|
||||
# from .buttons import StopButton
|
||||
# from .dock import BECDock, BECDockArea
|
||||
# from .figure import BECFigure, FigureConfig
|
||||
# from .scan_control import ScanControl
|
||||
# from .spiral_progress_bar import SpiralProgressBar
|
||||
|
||||
@@ -11,7 +11,7 @@ from bec_widgets.utils import BECConnector, ConnectionConfig, GridLayoutManager
|
||||
if TYPE_CHECKING:
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.widgets import BECDockArea
|
||||
from bec_widgets.widgets.dock import BECDockArea
|
||||
|
||||
|
||||
class DockConfig(ConnectionConfig):
|
||||
|
||||
@@ -711,6 +711,12 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
qdarktheme.setup_theme(theme)
|
||||
self.setBackground("k" if theme == "dark" else "w")
|
||||
self.config.theme = theme
|
||||
for plot in self.widget_list:
|
||||
plot.set_x_label(plot.plot_item.getAxis("bottom").label.toPlainText())
|
||||
plot.set_y_label(plot.plot_item.getAxis("left").label.toPlainText())
|
||||
if plot.plot_item.titleLabel.text:
|
||||
plot.set_title(plot.plot_item.titleLabel.text)
|
||||
plot.set_legend_label_size()
|
||||
|
||||
def _remove_by_coordinates(self, row: int, col: int) -> None:
|
||||
"""
|
||||
|
||||
@@ -5,6 +5,8 @@ from typing import Literal, Optional
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtGui import QFont, QFontDatabase, QFontInfo
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
@@ -12,8 +14,14 @@ from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
class AxisConfig(BaseModel):
|
||||
title: Optional[str] = Field(None, description="The title of the axes.")
|
||||
title_size: Optional[int] = Field(None, description="The font size of the title.")
|
||||
x_label: Optional[str] = Field(None, description="The label for the x-axis.")
|
||||
x_label_size: Optional[int] = Field(None, description="The font size of the x-axis label.")
|
||||
y_label: Optional[str] = Field(None, description="The label for the y-axis.")
|
||||
y_label_size: Optional[int] = Field(None, description="The font size of the y-axis label.")
|
||||
legend_label_size: Optional[int] = Field(
|
||||
None, description="The font size of the legend labels."
|
||||
)
|
||||
x_scale: Literal["linear", "log"] = Field("linear", description="The scale of the x-axis.")
|
||||
y_scale: Literal["linear", "log"] = Field("linear", description="The scale of the y-axis.")
|
||||
x_lim: Optional[tuple] = Field(None, description="The limits of the x-axis.")
|
||||
@@ -50,6 +58,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"remove",
|
||||
"set_legend_label_size",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -85,6 +94,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
@@ -95,6 +105,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"y_scale": self.set_y_scale,
|
||||
"x_lim": self.set_x_lim,
|
||||
"y_lim": self.set_y_lim,
|
||||
"legend_label_size": self.set_legend_label_size,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
@@ -116,34 +127,79 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
|
||||
self.set(**{k: v for k, v in config_mappings.items() if v is not None})
|
||||
|
||||
def set_title(self, title: str):
|
||||
def set_legend_label_size(self, size: int = None):
|
||||
"""
|
||||
Set the font size of the legend.
|
||||
|
||||
Args:
|
||||
size(int): Font size of the legend.
|
||||
"""
|
||||
if not self.plot_item.legend:
|
||||
return
|
||||
if self.config.axis.legend_label_size or size:
|
||||
if size:
|
||||
self.config.axis.legend_label_size = size
|
||||
scale = (
|
||||
size / 9
|
||||
) # 9 is the default font size of the legend, so we always scale it against 9
|
||||
self.plot_item.legend.setScale(scale)
|
||||
|
||||
def get_text_color(self):
|
||||
return "#FFF" if self.figure.config.theme == "dark" else "#000"
|
||||
|
||||
def set_title(self, title: str, size: int = None):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
size(int): Font size of the title.
|
||||
"""
|
||||
self.plot_item.setTitle(title)
|
||||
if self.config.axis.title_size or size:
|
||||
if size:
|
||||
self.config.axis.title_size = size
|
||||
style = {"color": self.get_text_color(), "size": f"{self.config.axis.title_size}pt"}
|
||||
else:
|
||||
style = {}
|
||||
self.plot_item.setTitle(title, **style)
|
||||
self.config.axis.title = title
|
||||
|
||||
def set_x_label(self, label: str):
|
||||
def set_x_label(self, label: str, size: int = None):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
self.plot_item.setLabel("bottom", label)
|
||||
if self.config.axis.x_label_size or size:
|
||||
if size:
|
||||
self.config.axis.x_label_size = size
|
||||
style = {
|
||||
"color": self.get_text_color(),
|
||||
"font-size": f"{self.config.axis.x_label_size}pt",
|
||||
}
|
||||
else:
|
||||
style = {}
|
||||
self.plot_item.setLabel("bottom", label, **style)
|
||||
self.config.axis.x_label = label
|
||||
|
||||
def set_y_label(self, label: str):
|
||||
def set_y_label(self, label: str, size: int = None):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
size(int): Font size of the label.
|
||||
"""
|
||||
self.plot_item.setLabel("left", label)
|
||||
if self.config.axis.y_label_size or size:
|
||||
if size:
|
||||
self.config.axis.y_label_size = size
|
||||
color = self.get_text_color()
|
||||
style = {"color": color, "font-size": f"{self.config.axis.y_label_size}pt"}
|
||||
else:
|
||||
style = {}
|
||||
self.plot_item.setLabel("left", label, **style)
|
||||
self.config.axis.y_label = label
|
||||
|
||||
def set_x_scale(self, scale: Literal["linear", "log"] = "linear"):
|
||||
|
||||
@@ -54,6 +54,7 @@ class BECWaveform(BECPlotBase):
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"remove",
|
||||
"set_legend_label_size",
|
||||
]
|
||||
scan_signal_update = pyqtSignal()
|
||||
|
||||
@@ -401,6 +402,7 @@ class BECWaveform(BECPlotBase):
|
||||
self.config.curves[name] = curve.config
|
||||
if data is not None:
|
||||
curve.setData(data[0], data[1])
|
||||
self.set_legend_label_size()
|
||||
return curve
|
||||
|
||||
def _validate_signal_entries(
|
||||
|
||||
@@ -21,7 +21,7 @@ api_reference/api_reference.md
|
||||
:gutter: 5
|
||||
|
||||
```{grid-item-card}
|
||||
:link: user.getting_started
|
||||
:link: developer.getting_started
|
||||
:link-type: ref
|
||||
:img-top: /assets/rocket_launch_48dp.svg
|
||||
:text-align: center
|
||||
@@ -32,7 +32,7 @@ Learn how to install BEC Widgets and get started with the framework.
|
||||
```
|
||||
|
||||
```{grid-item-card}
|
||||
:link: user.widgets
|
||||
:link: developer.widgets
|
||||
:link-type: ref
|
||||
:img-top: /assets/apps_48dp.svg
|
||||
:text-align: center
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
(developer.widgets.how_to_develop_a_widget)=
|
||||
# How to Develop a Widget
|
||||
This section provides a step-by-step guide on how to develop a new widget for BEC Widgets. We will develop a simple widget that allows you to press a button and specify a user-defined action. The general widget will be based on a [QPushButton](https://doc.qt.io/qt-6/qpushbutton.html) which we will extend to be capable of communicating with BEC through the interface provided by BEC Widgets.
|
||||
|
||||
## Button to start a scan
|
||||
Developing a new widget in BEC Widgets is straightforward. Let's create a widget that allows a user to press a button and execute a `line_scan` in BEC. The proper location to create a new widget is either in the `bec_widgets/widgets` directory, or the beamline plugin widget direction, i.e. `csaxs_bec/bec_widgets`, depending on where your development takes place.
|
||||
|
||||
### Step 1: Create a new widget class
|
||||
|
||||
We first create a simple class that inherits from the `QPushButton` class.
|
||||
The following code snippet demonstrates how to create a new widget:
|
||||
|
||||
``` python
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
|
||||
class StartScanButton(QPushButton):
|
||||
def __init__(self, parent=None):
|
||||
QPushButton.__init__(self, parent=parent)
|
||||
# Connect the button to the on_click method
|
||||
self.clicked.connect(self.on_click)
|
||||
|
||||
def on_click(self):
|
||||
pass
|
||||
```
|
||||
So far we have created the button, but we have not yet put any logic to the `on_click` event of the button.
|
||||
Adding the functionality to be able to execute a scans will be tackled in the next step.
|
||||
|
||||
````{note}
|
||||
To make the button work as a standalone application, you can simply add the following lines at the end.
|
||||
``` python
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = StartScanButton()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
### Step 2: Connect with BEC, implement *on_click* functionality
|
||||
To be able to start a scan, we need to communicate with BEC. This can be facilitated easily by inheriting additionally from [`BECConnector`](../../api_reference/_autosummary/bec_widgets.utils.bec_connector.BECConnector).
|
||||
With the *BECConnector*, we will also have to pass the *client* ([BECClient](https://bec.readthedocs.io/en/latest/api_reference/_autosummary/bec_lib.client.BECClient.html)) and the *gui_id* (str) to init function of both, our *StartScanButton* widget and the `super().__init__(client=client, gui_id=gui_id)` call.
|
||||
In the init of *BECConnector*, the client will be initialised and stored in `self.client`, which gives us access to the available scan objects via `self.client.scans`.
|
||||
|
||||
``` python
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
class StartScanButton(BECConnector, QPushButton):
|
||||
def __init__(self, parent=None, client:=None, gui_id=None):
|
||||
super().__init__(client=client, gui_id=gui_id)
|
||||
QPushButton.__init__(self, parent=parent)
|
||||
|
||||
# Set a default scan command, args and kwargs
|
||||
self.scan_name = "line_scan"
|
||||
self.scan_args = (dev.samx, -5, 5)
|
||||
self.scan_kwargs = {"steps": 50, "exp_time": 0.1, "relative": True}
|
||||
# Set the text of the button to display the current scan name
|
||||
self.set_button_text()
|
||||
# Connect the button to the on_click method
|
||||
self.clicked.connect(self.on_click)
|
||||
|
||||
def set_button_text(self):
|
||||
"""Set the text of the button"""
|
||||
self.setText(f"Start {self.scan_name}")
|
||||
|
||||
def run_command(self):
|
||||
"""Run the scan command."""
|
||||
# Get the scan command from the scans library
|
||||
scan_command = getattr(self.client.scans, self.scan_name)
|
||||
# Run the scan command
|
||||
scan_report = scan_command(*self.scan_args, **self.scan_kwargs)
|
||||
# Wait for the scan to finish
|
||||
scan_report.wait()
|
||||
|
||||
def on_click(self):
|
||||
"""Start a line scan"""
|
||||
self.run_command()
|
||||
```
|
||||
|
||||
```{note}
|
||||
For the args and kwargs of the scan command, we are using the same syntax as in the client: `dev.samx` is not a string but the same object as in the client.
|
||||
```
|
||||
In the *run_command* method, we retrieve the scan object from the client by its name, and execute the method with all *args* and *kwargs* that we have set.
|
||||
The current implementation of *run_command* is a blocking call due to `scan_report.wait()`, which is not ideal for a GUI application since it freezes the GUI. We will adress this in the next step.
|
||||
|
||||
### Step 3: Improving the widget interactivity
|
||||
To not freeze the GUI, we need to run the scan command in a separate thread. We can either use [QThreads](https://doc.qt.io/qtforpython-6/PySide6/QtCore/QThread.html) or the Python [threading module](https://docs.python.org/3/library/threading.html#thread-objects). In this example, we will use the Python threading module. In addition, we add a method `update_style` to change the style of the button to indicate to the user that the scan is running. We also extend the cleanup procedure of `BECConnector` to ensure that the thread is stopped when the widget is closed. This is good practice to avoid having threads running in the background when the widget is closed.
|
||||
|
||||
``` python
|
||||
|
||||
def update_style(self, mode: Literal["ready", "running"]):
|
||||
"""Update the style of the button based on the mode.
|
||||
|
||||
Args:
|
||||
mode (Literal["ready", "running"): The mode of the button.
|
||||
"""
|
||||
if mode == "ready":
|
||||
self.setStyleSheet(
|
||||
"background-color: #4CAF50; color: white; font-size: 16px; padding: 10px 24px;"
|
||||
)
|
||||
elif mode == "running":
|
||||
self.setStyleSheet(
|
||||
"background-color: #808080; color: white; font-size: 16px; padding: 10px 24px;"
|
||||
)
|
||||
|
||||
def run_command(self):
|
||||
"""Run the scan command."""
|
||||
# Switch the style of the button
|
||||
self.update_style("running")
|
||||
# Disable the buttom while the scan is running
|
||||
self.setEnabled(False)
|
||||
# Get the scan command from the scans library
|
||||
scan_command = getattr(self.scans, self.scan_name)
|
||||
# Run the scan command
|
||||
scan_report = scan_command(*self.scan_args, **self.scan_kwargs)
|
||||
# Wait for the scan to finish
|
||||
scan_report.wait()
|
||||
# Reactivate the button
|
||||
self.setEnabled(True)
|
||||
# Switch the style of the button back to ready
|
||||
self.update_style("ready")
|
||||
|
||||
def on_click(self):
|
||||
"""Start a line scan"""
|
||||
thread = threading.Thread(target=self.run_command)
|
||||
thread.start()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget"""
|
||||
# stop thread
|
||||
# stop the thread or if this is implemented via QThread, ensure stopping of QThread.
|
||||
# Ideally, the BECConnector should take care of this automatically.
|
||||
# Important to call super().cleanup() to ensure that the cleanup of the BECConnector is also called
|
||||
super().cleanup()
|
||||
```
|
||||
We now added started the scan in a separate thread, which allows the GUI to remain responsive. We also added a method to change the style of the button to indicate to the user that the scan is running. The cleanup method ensures that the thread is stopped when the widget is closed. In a last step, we know like to make the scan command configurable.
|
||||
|
||||
### Step 4: Make the scan command configurable
|
||||
In order to make the scan comman configurable, we implement a method `set_scan_command` which allows the user to set the scan command, arguments and keyword arguments.
|
||||
This method should also become available through the RPC interface of BEC Widgets, so we add the class attribute `USER_ACCESS` which is a list of strings with functions that should become available for the CLI.
|
||||
|
||||
``` python
|
||||
def set_scan_command(
|
||||
self, scan_name: str, args: tuple, kwargs: dict
|
||||
):
|
||||
"""Set the scan command to run.
|
||||
|
||||
Args:
|
||||
scan_name (str): The name of the scan command.
|
||||
args (tuple): The arguments for the scan command.
|
||||
kwargs (dict): The keyword arguments for the scan command.
|
||||
"""
|
||||
# check if scan_command starts with scans.
|
||||
if not getattr(self.client.scans, scan_name):
|
||||
raise ValueError(
|
||||
f"The scan type must be implemented in the scan library of BEC, received {scan_name}"
|
||||
)
|
||||
self.scan_name = scan_name
|
||||
self.scan_args = args
|
||||
self.scan_kwargs = kwargs
|
||||
self.set_button_text()
|
||||
```
|
||||
|
||||
### Step 5: Generate client interface for RPC
|
||||
We have now prepared the widget which is fully functional as a standalone widget. But we also want to make it available to the BEC command-line-interface (CLI), for which we prepared the **USER_ACCESS** class attribute.
|
||||
The communication between the BEC IPythonClient and the widget is done vie the RPC interface of BEC Widgets.
|
||||
For this, we need to run the `bec_widgets.cli.generate_cli` script to generate the CLI interface.
|
||||
|
||||
``` bash
|
||||
python bec_widgets.cli.generate_cli --core
|
||||
# alternatively use the entry point from BEC Widgets
|
||||
bw-generate-cli
|
||||
```
|
||||
|
||||
This will generate a new client with all relevant methods in [`bec_widgets.cli.client.py`](../../api_reference/_autosummary/bec_widgets.bec_widgets.cli.client.rst).
|
||||
The last step is to make the RPCWidgetHandler class aware of the widget, which means to add the name of the widget to the widgets list in the [`RPCWidgetHandler`](../../api_reference/_autosummary/bec_widgets.bec_widgets.cli.rpc_widget_handler.RPCWidgetHandler.rst) class.
|
||||
|
||||
````{dropdown} View code: RPCWidgetHandler class
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
```{literalinclude} ../../../bec_widgets/cli/rpc_widget_handler.py
|
||||
:language: python
|
||||
:pyobject: RPCWidgetHandler
|
||||
```
|
||||
````
|
||||
|
||||
With this, we have a fully functional widget that allows the user to start a scan with a button. The scan command, arguments and keyword arguments can be set by the user.
|
||||
The full code is shown once again below:
|
||||
|
||||
````{dropdown} View code: Full code of the StartScanButton widget
|
||||
:icon: code-square
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
```
|
||||
import threading
|
||||
from typing import Literal
|
||||
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
|
||||
class StartScanButton(BECConnector, QPushButton):
|
||||
"""A button to start a line scan.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
client (BECClient): The BEC client.
|
||||
gui_id (str): The unique ID of the widget.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["set_scan_command"]
|
||||
|
||||
def __init__(self, parent=None, client=None, gui_id=None):
|
||||
super().__init__(client=client, gui_id=gui_id)
|
||||
QPushButton.__init__(self, parent=parent)
|
||||
|
||||
# Set the scan command to None
|
||||
self.scan_command = None
|
||||
# Set default scan command
|
||||
self.scan_name = "line_scan"
|
||||
self.scan_args = (dev.samx, -5, 5)
|
||||
self.scan_kwargs = {"steps": 50, "exp_time": 0.1, "relative": True}
|
||||
# Set the text of the button
|
||||
self.set_button_text()
|
||||
# Set the style of the button
|
||||
self.update_style("ready")
|
||||
# Connect the button to the on_click method
|
||||
self.clicked.connect(self.on_click)
|
||||
|
||||
def update_style(self, mode: Literal["ready", "running"]):
|
||||
"""Update the style of the button based on the mode.
|
||||
|
||||
Args:
|
||||
mode (Literal["ready", "running"): The mode of the button.
|
||||
"""
|
||||
if mode == "ready":
|
||||
self.setStyleSheet(
|
||||
"background-color: #4CAF50; color: white; font-size: 16px; padding: 10px 24px;"
|
||||
)
|
||||
elif mode == "running":
|
||||
self.setStyleSheet(
|
||||
"background-color: #808080; color: white; font-size: 16px; padding: 10px 24px;"
|
||||
)
|
||||
|
||||
def set_button_text(self):
|
||||
"""Set the text of the button."""
|
||||
self.setText(f"Start {self.scan_name}")
|
||||
|
||||
def set_scan_command(self, scan_name: str, args: tuple, kwargs: dict):
|
||||
"""Set the scan command to run.
|
||||
|
||||
Args:
|
||||
scan_name (str): The name of the scan command.
|
||||
args (tuple): The arguments for the scan command.
|
||||
kwargs (dict): The keyword arguments for the scan command.
|
||||
"""
|
||||
# check if scan_command starts with scans.
|
||||
if not getattr(self.client.scans, scan_name):
|
||||
raise ValueError(
|
||||
f"The scan type must be implemented in the scan library of BEC, received {scan_name}"
|
||||
)
|
||||
self.scan_name = scan_name
|
||||
self.scan_args = args
|
||||
self.scan_kwargs = kwargs
|
||||
self.set_button_text()
|
||||
|
||||
def run_command(self):
|
||||
"""Run the scan command."""
|
||||
# Switch the style of the button
|
||||
self.update_style("running")
|
||||
# Disable the buttom while the scan is running
|
||||
self.setEnabled(False)
|
||||
# Get the scan command from the scans library
|
||||
scan_command = getattr(self.scans, self.scan_name)
|
||||
# Run the scan command
|
||||
scan_report = scan_command(*self.scan_args, **self.scan_kwargs)
|
||||
# Wait for the scan to finish
|
||||
scan_report.wait()
|
||||
# Reactivate the button
|
||||
self.setEnabled(True)
|
||||
# Switch the style of the button back to ready
|
||||
self.update_style("ready")
|
||||
|
||||
def on_click(self):
|
||||
"""Start a line scan"""
|
||||
thread = threading.Thread(target=self.run_command)
|
||||
thread.start()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget"""
|
||||
# stop thread
|
||||
# stop the thread or if this is implemented via QThread, ensure stopping of QThread.
|
||||
# Ideally, the BECConnector should take care of this automatically.
|
||||
# Important to call super().cleanup() to ensure that the cleanup of the BECConnector is also called
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = StartScanButton()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
### Step 6: Write a test for the widget
|
||||
We highly recommend writing tests for the widget to ensure that they work as expected. This allows to run the tests automatically in a CI/CD pipeline and to ensure that the widget works as expected not only now but als in the future.
|
||||
The following code snippet shows an example to test the set_scan_command from the `StartScanButton` widget.
|
||||
``` python
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.start_scan_button import StartScanButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_scan_button(qtbot, mocked_client):
|
||||
widget = StartScanButton(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def test_set_scan_command(test_scan_button):
|
||||
"""Test the set_scan_command function."""
|
||||
test_scan_button.set_scan_command(
|
||||
scan_name="grid_scan",
|
||||
args=(dev.samx, -5, 5, 10, dev.samy, -5, 5, 20),
|
||||
kwargs={"exp_time": 0.1, "relative": True},
|
||||
)
|
||||
# Check first if all parameter have been properly set
|
||||
assert test_scan_button.scan_name == "grid_scan"
|
||||
assert test_scan_button.scan_args == (dev.samx, -5, 5, 10, dev.samy, -5, 5, 20)
|
||||
assert test_scan_button.scan_kwargs == {"exp_time": 0.1, "relative": True}
|
||||
# Next, we check if the displayed text of the button has been updated
|
||||
# We use the .text() method from the QPushButton class to retrieve the text displayed
|
||||
assert test_scan_button.text() == "Start grid_scan"
|
||||
```
|
||||
@@ -48,7 +48,7 @@ users to interact. BEC Widgets must be placed in the window:
|
||||
|
||||
```
|
||||
from qtpy.QWidgets import QMainWindow
|
||||
from bec_widgets.widgets import BECFigure
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
|
||||
window = QMainWindow()
|
||||
bec_figure = BECFigure(gui_id="my_gui_app_id")
|
||||
@@ -78,7 +78,7 @@ Final example:
|
||||
```
|
||||
import sys
|
||||
from qtpy.QtWidgets import QMainWindow, QApplication
|
||||
from bec_widgets.widgets import BECFigure
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
# creation of the Qt application
|
||||
|
||||
@@ -24,7 +24,7 @@ a `StopButton` within a GUI layout:
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QWidget, QVBoxLayout
|
||||
from bec_widgets.widgets import StopButton
|
||||
from bec_widgets.widgets.buttons import StopButton
|
||||
|
||||
|
||||
class MyGui(QWidget):
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "0.63.2"
|
||||
version = "0.64.2"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
|
||||
@@ -8,7 +8,8 @@ from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_widgets.cli.client_utils import _start_plot_process
|
||||
from bec_widgets.cli.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.widgets import BECDockArea, BECFigure
|
||||
from bec_widgets.widgets.dock import BECDockArea
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
|
||||
|
||||
# make threads check in autouse, **will be executed at the end**; better than
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets import BECDock, BECDockArea
|
||||
from bec_widgets.widgets.dock import BECDock, BECDockArea
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets import BECFigure
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
|
||||
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform import BECWaveform
|
||||
|
||||
@@ -97,17 +97,3 @@ def test_client_generator_with_black_formatting():
|
||||
generated_output_formatted = isort.code(generated_output_formatted)
|
||||
|
||||
assert expected_output_formatted == generated_output_formatted
|
||||
|
||||
|
||||
def test_client_generator_classes():
|
||||
generator = ClientGenerator()
|
||||
out = generator.get_rpc_classes("bec_widgets")
|
||||
assert list(out.keys()) == ["connector_classes", "top_level_classes"]
|
||||
connector_cls_names = [cls.__name__ for cls in out["connector_classes"]]
|
||||
top_level_cls_names = [cls.__name__ for cls in out["top_level_classes"]]
|
||||
|
||||
assert "BECFigure" in connector_cls_names
|
||||
assert "BECWaveform" in connector_cls_names
|
||||
assert "BECDockArea" in top_level_cls_names
|
||||
assert "BECFigure" in top_level_cls_names
|
||||
assert "BECWaveform" not in top_level_cls_names
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from qtpy.QtGui import QFontInfo
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .test_bec_figure import bec_figure
|
||||
@@ -37,6 +40,30 @@ def test_plot_base_axes_by_separate_methods(bec_figure):
|
||||
assert plot_base.plot_item.ctrl.logXCheck.isChecked() == True
|
||||
assert plot_base.plot_item.ctrl.logYCheck.isChecked() == True
|
||||
|
||||
# Check the font size by mocking the set functions
|
||||
# I struggled retrieving it from the QFont object directly
|
||||
# thus I mocked the set functions to check internally the functionality
|
||||
with (
|
||||
mock.patch.object(plot_base.plot_item, "setLabel") as mock_set_label,
|
||||
mock.patch.object(plot_base.plot_item, "setTitle") as mock_set_title,
|
||||
):
|
||||
plot_base.set_x_label("Test x Label", 20)
|
||||
plot_base.set_y_label("Test y Label", 16)
|
||||
assert mock_set_label.call_count == 2
|
||||
assert plot_base.config.axis.x_label_size == 20
|
||||
assert plot_base.config.axis.y_label_size == 16
|
||||
col = plot_base.get_text_color()
|
||||
calls = []
|
||||
style = {"color": col, "font-size": "20pt"}
|
||||
calls.append(mock.call("bottom", "Test x Label", **style))
|
||||
style = {"color": col, "font-size": "16pt"}
|
||||
calls.append(mock.call("left", "Test y Label", **style))
|
||||
assert mock_set_label.call_args_list == calls
|
||||
plot_base.set_title("Test Title", 16)
|
||||
style = {"color": col, "size": "16pt"}
|
||||
call = mock.call("Test Title", **style)
|
||||
assert mock_set_title.call_args == call
|
||||
|
||||
|
||||
def test_plot_base_axes_added_by_kwargs(bec_figure):
|
||||
plot_base = bec_figure.add_widget(widget_type="PlotBase", widget_id="test_plot")
|
||||
|
||||
14
tests/unit_tests/test_plugin_utils.py
Normal file
14
tests/unit_tests/test_plugin_utils.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from bec_widgets.utils.plugin_utils import get_rpc_classes
|
||||
|
||||
|
||||
def test_client_generator_classes():
|
||||
out = get_rpc_classes("bec_widgets")
|
||||
assert list(out.keys()) == ["connector_classes", "top_level_classes"]
|
||||
connector_cls_names = [cls.__name__ for cls in out["connector_classes"]]
|
||||
top_level_cls_names = [cls.__name__ for cls in out["top_level_classes"]]
|
||||
|
||||
assert "BECFigure" in connector_cls_names
|
||||
assert "BECWaveform" in connector_cls_names
|
||||
assert "BECDockArea" in top_level_cls_names
|
||||
assert "BECFigure" in top_level_cls_names
|
||||
assert "BECWaveform" not in top_level_cls_names
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from qtpy.QtWidgets import QLineEdit
|
||||
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
from bec_widgets.widgets import ScanControl
|
||||
from bec_widgets.widgets.scan_control import ScanControl
|
||||
from tests.unit_tests.test_msgs.available_scans_message import available_scans_message
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import ValidationError
|
||||
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.widgets import SpiralProgressBar
|
||||
from bec_widgets.widgets.spiral_progress_bar import SpiralProgressBar
|
||||
from bec_widgets.widgets.spiral_progress_bar.ring import RingConfig, RingConnections
|
||||
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBarConfig
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets import StopButton
|
||||
from bec_widgets.widgets.buttons import StopButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
from unittest.mock import MagicMock
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -56,8 +56,12 @@ def test_create_waveform1D_by_config(bec_figure):
|
||||
"col": 0,
|
||||
"axis": {
|
||||
"title": "Widget 1",
|
||||
"title_size": None,
|
||||
"x_label": None,
|
||||
"x_label_size": None,
|
||||
"y_label": None,
|
||||
"y_label_size": None,
|
||||
"legend_label_size": None,
|
||||
"x_scale": "linear",
|
||||
"y_scale": "linear",
|
||||
"x_lim": (1, 10),
|
||||
@@ -193,6 +197,18 @@ def test_add_curve(bec_figure):
|
||||
assert c1.config.source == "scan_segment"
|
||||
|
||||
|
||||
def test_change_legend_font_size(bec_figure):
|
||||
plot = bec_figure.add_plot()
|
||||
|
||||
w1 = plot.add_curve_scan(x_name="samx", y_name="bpm4i")
|
||||
my_func = plot.plot_item.legend
|
||||
with mock.patch.object(my_func, "setScale") as mock_set_scale:
|
||||
plot.set_legend_label_size(18)
|
||||
assert plot.config.axis.legend_label_size == 18
|
||||
assert mock_set_scale.call_count == 1
|
||||
assert mock_set_scale.call_args == mock.call(2)
|
||||
|
||||
|
||||
def test_remove_curve(bec_figure):
|
||||
w1 = bec_figure.add_plot()
|
||||
|
||||
@@ -406,10 +422,10 @@ def test_scan_update(bec_figure, qtbot):
|
||||
"scan_id": 1,
|
||||
}
|
||||
# Mock scan_storage.find_scan_by_ID
|
||||
mock_scan_data_waveform = MagicMock()
|
||||
mock_scan_data_waveform = mock.MagicMock()
|
||||
mock_scan_data_waveform.data = {
|
||||
device_name: {
|
||||
entry: MagicMock(val=[msg_waveform["data"][device_name][entry]["value"]])
|
||||
entry: mock.MagicMock(val=[msg_waveform["data"][device_name][entry]["value"]])
|
||||
for entry in msg_waveform["data"][device_name]
|
||||
}
|
||||
for device_name in msg_waveform["data"]
|
||||
@@ -430,12 +446,12 @@ def test_scan_history_with_val_access(bec_figure, qtbot):
|
||||
c1 = w1.add_curve_scan(x_name="samx", y_name="bpm4i")
|
||||
|
||||
mock_scan_data = {
|
||||
"samx": {"samx": MagicMock(val=np.array([1, 2, 3]))}, # Use MagicMock for .val
|
||||
"bpm4i": {"bpm4i": MagicMock(val=np.array([4, 5, 6]))}, # Use MagicMock for .val
|
||||
"samx": {"samx": mock.MagicMock(val=np.array([1, 2, 3]))}, # Use mock.MagicMock for .val
|
||||
"bpm4i": {"bpm4i": mock.MagicMock(val=np.array([4, 5, 6]))}, # Use mock.MagicMock for .val
|
||||
}
|
||||
|
||||
mock_scan_storage = MagicMock()
|
||||
mock_scan_storage.find_scan_by_ID.return_value = MagicMock(data=mock_scan_data)
|
||||
mock_scan_storage = mock.MagicMock()
|
||||
mock_scan_storage.find_scan_by_ID.return_value = mock.MagicMock(data=mock_scan_data)
|
||||
w1.queue.scan_storage = mock_scan_storage
|
||||
|
||||
fake_scan_id = "fake_scan_id"
|
||||
@@ -464,10 +480,10 @@ def test_scatter_2d_update(bec_figure, qtbot):
|
||||
}
|
||||
msg_metadata = {"scan_name": "line_scan"}
|
||||
|
||||
mock_scan_data = MagicMock()
|
||||
mock_scan_data = mock.MagicMock()
|
||||
mock_scan_data.data = {
|
||||
device_name: {
|
||||
entry: MagicMock(val=msg["data"][device_name][entry]["value"])
|
||||
entry: mock.MagicMock(val=msg["data"][device_name][entry]["value"])
|
||||
for entry in msg["data"][device_name]
|
||||
}
|
||||
for device_name in msg["data"]
|
||||
|
||||
Reference in New Issue
Block a user