mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-09 18:20:55 +02:00
Compare commits
54 Commits
feature/sc
...
feat/next-
| Author | SHA1 | Date | |
|---|---|---|---|
| f25133fa71 | |||
| 750d95de4c | |||
| 1cefa1389b | |||
| 7df40ae8c1 | |||
| 12e7131ced | |||
| d43ac980ad | |||
| 5c3516cefa | |||
| 384beba521 | |||
| 99b8b82fbc | |||
| d4397f0b1a | |||
| 4fb5922b4a | |||
| 6d8bc4a750 | |||
| b6ef1fd625 | |||
| b99bef799f | |||
| 40a616fa35 | |||
| 8b72af7322 | |||
| 8828c81ff1 | |||
| 7965ea5014 | |||
| 328681017d | |||
| 47185117a3 | |||
| ee9fbdb178 | |||
| 88cd84a086 | |||
| 3046fda738 | |||
| 90a27f2b07 | |||
| 3cffe81d6b | |||
| e469260b32 | |||
| 660db95d9f | |||
| df9f76020c | |||
| ac8a51e821 | |||
| 995960e590 | |||
| 4b454ecdff | |||
| 9e9399db23 | |||
| a5cf9319d7 | |||
| 27308547c0 | |||
| 1686c2a399 | |||
| f7d54b1068 | |||
| a990ad4bf4 | |||
| 627ac91f55 | |||
| 03d0dbb7f5 | |||
| 4eda839948 | |||
| 1139eefb66 | |||
| 52dfbf357a | |||
| 3f758e4b08 | |||
| f7763afc7e | |||
| 7c46e1075a | |||
| ca4ab59361 | |||
| b3521d9551 | |||
| cec8c115b6 | |||
| 00a0335885 | |||
| c2bb6ad21d | |||
| ce8c7d7a96 | |||
| 34b309e46d | |||
| b4836eeabe | |||
| f4202427ad |
@@ -2996,6 +2996,140 @@ class BECWaveformWidget(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class Curve(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the curve from the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def dap_params(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the curve.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- color: str
|
||||
- symbol: str
|
||||
- symbol_color: str
|
||||
- symbol_size: int
|
||||
- pen_width: int
|
||||
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_data(self, x, y):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_color(self, color: "str", symbol_color: "Optional[str]" = None):
|
||||
"""
|
||||
Change the color of the curve.
|
||||
|
||||
Args:
|
||||
color(str): Color of the curve.
|
||||
symbol_color(str, optional): Color of the symbol. Defaults to None.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_color_map_z(self, colormap: "str"):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol(self, symbol: "str"):
|
||||
"""
|
||||
Change the symbol of the curve.
|
||||
|
||||
Args:
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol_color(self, symbol_color: "str"):
|
||||
"""
|
||||
Change the symbol color of the curve.
|
||||
|
||||
Args:
|
||||
symbol_color(str): Color of the symbol.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol_size(self, symbol_size: "int"):
|
||||
"""
|
||||
Change the symbol size of the curve.
|
||||
|
||||
Args:
|
||||
symbol_size(int): Size of the symbol.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_pen_width(self, pen_width: "int"):
|
||||
"""
|
||||
Change the pen width of the curve.
|
||||
|
||||
Args:
|
||||
pen_width(int): Width of the pen.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_pen_style(self, pen_style: "Literal['solid', 'dash', 'dot', 'dashdot']"):
|
||||
"""
|
||||
Change the pen style of the curve.
|
||||
|
||||
Args:
|
||||
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_data(self) -> "tuple[np.ndarray, np.ndarray]":
|
||||
"""
|
||||
Get the data of the curve.
|
||||
Returns:
|
||||
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def dap_params(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
class DapComboBox(RPCBase):
|
||||
@rpc_call
|
||||
def select_y_axis(self, y_axis: str):
|
||||
|
||||
@@ -17,10 +17,12 @@ from qtpy.QtWidgets import (
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.widgets.containers.expantion_panel.expansion_panel import ExpansionPanel
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
||||
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
|
||||
|
||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
@@ -65,6 +67,8 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"btn6": self.btn6,
|
||||
"pb": self.pb,
|
||||
"pi": self.pi,
|
||||
"wfng": self.wfng,
|
||||
"ep": self.ep,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -100,7 +104,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.pb = PlotBase()
|
||||
self.pi = self.pb.plot_item
|
||||
fourth_tab_layout.addWidget(self.pb)
|
||||
tab_widget.addTab(fourth_tab, "PltoBase")
|
||||
tab_widget.addTab(fourth_tab, "PlotBase")
|
||||
|
||||
tab_widget.setCurrentIndex(3)
|
||||
|
||||
@@ -117,6 +121,26 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.btn5 = QPushButton("Button 5")
|
||||
self.btn6 = QPushButton("Button 6")
|
||||
|
||||
fifth_tab = QWidget()
|
||||
fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
self.wfng = Waveform()
|
||||
fifth_tab_layout.addWidget(self.wfng)
|
||||
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
tab_widget.setCurrentIndex(4)
|
||||
# add stuff to the new Waveform widget
|
||||
self._init_waveform()
|
||||
|
||||
six_tab = QWidget()
|
||||
six_tab_layout = QVBoxLayout(six_tab)
|
||||
self.ep = ExpansionPanel()
|
||||
self.ep.content_layout.addWidget(self.btn1)
|
||||
self.ep.content_layout.addWidget(self.btn2)
|
||||
self.ep.content_layout.addWidget(self.btn3)
|
||||
self.ep.content_layout.addWidget(self.btn4)
|
||||
six_tab_layout.addWidget(self.ep)
|
||||
tab_widget.addTab(six_tab, "Exp Panel")
|
||||
tab_widget.setCurrentIndex(5)
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
|
||||
@@ -125,6 +149,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
|
||||
self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
def _init_waveform(self):
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve1")
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve2")
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve3")
|
||||
self.wfng.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
|
||||
self.wfng.plot(y_name="bpm3a", y_entry="bpm3a", dap="GaussianModel")
|
||||
|
||||
def _init_figure(self):
|
||||
self.w1 = self.figure.plot(x_name="samx", y_name="bpm4i", row=0, col=0)
|
||||
self.w1.set(
|
||||
@@ -191,9 +222,11 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.im.image("waveform", "1d")
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.wf = self.d2.add_widget("BECFigure", row=0, col=0)
|
||||
self.wf = self.d2.add_widget("BECWaveformWidget", row=0, col=0)
|
||||
self.wf.plot("bpm4i")
|
||||
self.wf.plot("bpm3a")
|
||||
|
||||
self.mw = self.wf.multi_waveform(monitor="waveform") # , config=config)
|
||||
self.mw = None # self.wf.multi_waveform(monitor="waveform") # , config=config)
|
||||
|
||||
self.dock.save_state()
|
||||
|
||||
@@ -219,7 +252,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
app.setApplicationName("Jupyter Console")
|
||||
app.setApplicationDisplayName("Jupyter Console")
|
||||
apply_theme("dark")
|
||||
icon = material_icon("terminal", color="#434343", filled=True)
|
||||
icon = material_icon("terminal", color=(255, 255, 255, 255), filled=True)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import Field
|
||||
from pyqtgraph.dockarea.DockArea import DockArea
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QPainter, QPaintEvent
|
||||
from qtpy.QtWidgets import QApplication, QSizePolicy, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.toolbar import (
|
||||
@@ -27,6 +27,7 @@ from bec_widgets.widgets.plots.image.image_widget import BECImageWidget
|
||||
from bec_widgets.widgets.plots.motor_map.motor_map_widget import BECMotorMapWidget
|
||||
from bec_widgets.widgets.plots.multi_waveform.multi_waveform_widget import BECMultiWaveformWidget
|
||||
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
||||
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
|
||||
@@ -106,6 +107,13 @@ class BECDockArea(BECWidget, QWidget):
|
||||
tooltip="Add Motor Map",
|
||||
filled=True,
|
||||
),
|
||||
"separator_next_gen": SeparatorAction(),
|
||||
"waveform_ng": MaterialIconAction(
|
||||
icon_name=Waveform.ICON_NAME,
|
||||
color="#FFD700",
|
||||
tooltip="Add Waveform Next Gen",
|
||||
filled=True,
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_0": SeparatorAction(),
|
||||
@@ -182,6 +190,9 @@ class BECDockArea(BECWidget, QWidget):
|
||||
self.toolbar.widgets["menu_plots"].widgets["motor_map"].triggered.connect(
|
||||
lambda: self.add_dock(widget="BECMotorMapWidget", prefix="motor_map")
|
||||
)
|
||||
self.toolbar.widgets["menu_plots"].widgets["waveform_ng"].triggered.connect(
|
||||
lambda: self.add_dock(widget="Waveform", prefix="waveform_ng")
|
||||
)
|
||||
|
||||
# Menu Devices
|
||||
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect(
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QEvent, Qt
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
|
||||
class ExpansionPanel(BECWidget, QWidget):
|
||||
"""
|
||||
A collapsible container widget for Qt Designer.
|
||||
|
||||
Key improvements in this version:
|
||||
- A "title_color" property (type "QColor") that accepts either
|
||||
a QColor or a string (hex/rgb/named color).
|
||||
- The label text color is updated accordingly.
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
config: ConnectionConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
title="Panel",
|
||||
expanded=False,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
self.setObjectName("ExpansionPanel")
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
|
||||
# Properties
|
||||
self._expanded = expanded
|
||||
self._title_color = QColor("black") # Default text color
|
||||
self.header_frame = None
|
||||
self.content_frame = None
|
||||
|
||||
# Main layout
|
||||
self._main_layout = QVBoxLayout(self)
|
||||
self._main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._main_layout.setSpacing(0)
|
||||
|
||||
# Create header / content
|
||||
self._init_header(title)
|
||||
self._init_content()
|
||||
|
||||
# Visible or hidden
|
||||
self.content_frame.setVisible(expanded)
|
||||
|
||||
# Defer event filter after constructing frames
|
||||
self.installEventFilter(self)
|
||||
|
||||
def _init_header(self, title):
|
||||
"""
|
||||
Create the header frame with arrow button and label.
|
||||
"""
|
||||
self.header_frame = QFrame(self)
|
||||
self.header_frame.setObjectName("headerFrame")
|
||||
self.header_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.header_frame.setStyleSheet(
|
||||
"""
|
||||
#headerFrame {
|
||||
background-color: "#141414";
|
||||
border-radius: 4px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
self._header_layout = QHBoxLayout(self.header_frame)
|
||||
self._header_layout.setContentsMargins(3, 2, 3, 2)
|
||||
self._header_layout.setSpacing(5)
|
||||
|
||||
self.btn_toggle = QPushButton("▼" if self._expanded else "►", self.header_frame)
|
||||
self.btn_toggle.setFixedSize(25, 25)
|
||||
self.btn_toggle.setStyleSheet("border: none; font-weight: bold;")
|
||||
self.btn_toggle.clicked.connect(self.toggle)
|
||||
self._header_layout.addWidget(self.btn_toggle, alignment=Qt.AlignVCenter)
|
||||
|
||||
self.label_title = QLabel(title, self.header_frame)
|
||||
self.label_title.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self._header_layout.addWidget(self.label_title, alignment=Qt.AlignVCenter | Qt.AlignLeft)
|
||||
self._header_layout.addStretch()
|
||||
|
||||
self._main_layout.addWidget(self.header_frame)
|
||||
|
||||
def _init_content(self):
|
||||
"""Create the collapsible content frame (with its own layout)."""
|
||||
self.content_frame = QFrame(self)
|
||||
self.content_frame.setObjectName("ContentFrame")
|
||||
self.content_frame.setStyleSheet(
|
||||
"""
|
||||
#ContentFrame {
|
||||
border-radius: 4px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.content_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self._content_layout = QVBoxLayout(self.content_frame)
|
||||
self._content_layout.setContentsMargins(5, 5, 5, 5)
|
||||
self._content_layout.setSpacing(5)
|
||||
|
||||
self._main_layout.addWidget(self.content_frame)
|
||||
|
||||
@SafeSlot()
|
||||
def toggle(self):
|
||||
"""Collapse or expand the content area."""
|
||||
self._expanded = not self._expanded
|
||||
if self.content_frame:
|
||||
self.content_frame.setVisible(self._expanded)
|
||||
self.btn_toggle.setText("▼" if self._expanded else "►")
|
||||
|
||||
@SafeProperty(bool)
|
||||
def expanded(self) -> bool:
|
||||
return self._expanded
|
||||
|
||||
@expanded.setter
|
||||
def expanded(self, value: bool):
|
||||
if value != self._expanded:
|
||||
self.toggle()
|
||||
|
||||
@SafeProperty(str)
|
||||
def title(self):
|
||||
"""Property for the header text."""
|
||||
if self.label_title:
|
||||
return self.label_title.text()
|
||||
return ""
|
||||
|
||||
@title.setter
|
||||
def title(self, value: str):
|
||||
if self.label_title:
|
||||
self.label_title.setText(value)
|
||||
|
||||
@SafeProperty("QColor")
|
||||
def title_color(self):
|
||||
"""
|
||||
A 'QColor' property recognized by Designer. We also accept
|
||||
strings for convenience (e.g. "#FF0000", "rgb(0,255,0)", "blue").
|
||||
"""
|
||||
return self._title_color
|
||||
|
||||
@title_color.setter
|
||||
def title_color(self, color):
|
||||
# TODO set bec widget color validator here
|
||||
|
||||
# If user passes a string instead of a QColor, parse it
|
||||
if isinstance(color, str):
|
||||
new_col = QColor(color)
|
||||
if not new_col.isValid():
|
||||
return # ignore invalid color string
|
||||
self._title_color = new_col
|
||||
elif isinstance(color, QColor):
|
||||
self._title_color = color
|
||||
else:
|
||||
# unknown type, ignore
|
||||
return
|
||||
|
||||
# Update label's style
|
||||
color_hex = self._title_color.name() # e.g. "#RRGGBB"
|
||||
self.label_title.setStyleSheet(f"color: {color_hex};")
|
||||
|
||||
@property
|
||||
def content_layout(self) -> QVBoxLayout:
|
||||
"""Layout of the content frame for programmatic additions."""
|
||||
return self._content_layout
|
||||
|
||||
@property
|
||||
def header_layout(self) -> QHBoxLayout:
|
||||
"""Layout of the content frame for programmatic additions."""
|
||||
return self._header_layout
|
||||
|
||||
def event(self, e):
|
||||
"""
|
||||
If Designer adds child widgets, re-parent them into _content_layout
|
||||
unless they're our known frames.
|
||||
"""
|
||||
if e.type() == QEvent.ChildAdded:
|
||||
child_obj = e.child()
|
||||
if child_obj is not None and isinstance(child_obj, QWidget):
|
||||
if self.header_frame is not None and self.content_frame is not None:
|
||||
if child_obj not in (self.header_frame, self.content_frame):
|
||||
self._content_layout.addWidget(child_obj)
|
||||
|
||||
return super().event(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test if not using Designer
|
||||
from qtpy.QtWidgets import QApplication, QPushButton, QVBoxLayout
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
panel = ExpansionPanel(title="Test Panel", expanded=False)
|
||||
panel2 = ExpansionPanel(title="Test Panel 2", expanded=False)
|
||||
|
||||
# You can set the color via hex string
|
||||
panel.title_color = "#FF00FF"
|
||||
|
||||
panel.content_layout.addWidget(QPushButton("Test Button 1"))
|
||||
panel.content_layout.addWidget(QPushButton("Test Button 2"))
|
||||
|
||||
panel2.content_layout.addWidget(QPushButton("Test Button 1"))
|
||||
panel2.content_layout.addWidget(QPushButton("Test Button 2"))
|
||||
|
||||
container = QWidget()
|
||||
lay = QVBoxLayout(container)
|
||||
lay.addWidget(panel)
|
||||
lay.addWidget(panel2)
|
||||
container.resize(400, 300)
|
||||
container.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['expansion_panel.py']}
|
||||
@@ -0,0 +1,53 @@
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
|
||||
# Make sure the path below is correct
|
||||
from bec_widgets.widgets.containers.expantion_panel.expansion_panel import ExpansionPanel
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='ExpansionPanel' name='expansion_panel'/>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class ExpansionPanelPlugin(QDesignerCustomWidgetInterface):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.initialized = False
|
||||
|
||||
def createWidget(self, parent):
|
||||
return ExpansionPanel(parent=parent)
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(ExpansionPanel.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_widgets.widgets.containers.expantion_panel.expansion_panel"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
if self.initialized:
|
||||
return
|
||||
self.initialized = True
|
||||
|
||||
def isContainer(self):
|
||||
return True # crucial for Designer to allow dropping child widgets
|
||||
|
||||
def isInitialized(self):
|
||||
return self.initialized
|
||||
|
||||
def name(self):
|
||||
return "ExpansionPanel"
|
||||
|
||||
def toolTip(self):
|
||||
return "A collapsible panel container widget"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,17 @@
|
||||
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.containers.expantion_panel.expansion_panel_plugin import (
|
||||
ExpansionPanelPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(ExpansionPanelPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,15 +1,16 @@
|
||||
""" Module for DapComboBox widget class to select a DAP model from a combobox. """
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from PySide6.QtWidgets import QSizePolicy
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DapComboBox(BECWidget, QWidget):
|
||||
class DapComboBox(BECWidget, QComboBox):
|
||||
"""
|
||||
The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC.
|
||||
|
||||
@@ -39,17 +40,13 @@ class DapComboBox(BECWidget, QWidget):
|
||||
def __init__(
|
||||
self, parent=None, client=None, gui_id: str | None = None, default_fit: str | None = None
|
||||
):
|
||||
super().__init__(client=client, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.fit_model_combobox = QComboBox(self)
|
||||
self.layout.addWidget(self.fit_model_combobox)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
BECWidget.__init__(self, client=client, gui_id=gui_id)
|
||||
QComboBox.__init__(self, parent=parent)
|
||||
self._available_models = None
|
||||
self._x_axis = None
|
||||
self._y_axis = None
|
||||
self.populate_fit_model_combobox()
|
||||
self.fit_model_combobox.currentTextChanged.connect(self._update_current_fit)
|
||||
self.currentTextChanged.connect(self._update_current_fit)
|
||||
# Set default fit model
|
||||
self.select_default_fit(default_fit)
|
||||
|
||||
@@ -124,7 +121,7 @@ class DapComboBox(BECWidget, QWidget):
|
||||
x_axis(str): X axis.
|
||||
"""
|
||||
self.x_axis = x_axis
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
self._update_current_fit(self.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_y_axis(self, y_axis: str):
|
||||
@@ -134,7 +131,7 @@ class DapComboBox(BECWidget, QWidget):
|
||||
y_axis(str): Y axis.
|
||||
"""
|
||||
self.y_axis = y_axis
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
self._update_current_fit(self.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_fit_model(self, fit_name: str | None):
|
||||
@@ -145,14 +142,14 @@ class DapComboBox(BECWidget, QWidget):
|
||||
"""
|
||||
if not self._validate_dap_model(fit_name):
|
||||
raise ValueError(f"Fit {fit_name} is not valid.")
|
||||
self.fit_model_combobox.setCurrentText(fit_name)
|
||||
self.setCurrentText(fit_name)
|
||||
|
||||
def populate_fit_model_combobox(self):
|
||||
"""Populate the fit_model_combobox with the devices."""
|
||||
# pylint: disable=protected-access
|
||||
self.available_models = [model for model in self.client.dap._available_dap_plugins.keys()]
|
||||
self.fit_model_combobox.clear()
|
||||
self.fit_model_combobox.addItems(self.available_models)
|
||||
self.clear()
|
||||
self.addItems(self.available_models)
|
||||
|
||||
def _validate_dap_model(self, model: str | None) -> bool:
|
||||
"""Validate the DAP model.
|
||||
@@ -169,14 +166,14 @@ class DapComboBox(BECWidget, QWidget):
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
# widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(DapComboBox())
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>303</width>
|
||||
<height>457</height>
|
||||
<width>337</width>
|
||||
<height>552</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -35,11 +35,17 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_curve_selection">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>100</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Select Curve</string>
|
||||
</property>
|
||||
@@ -60,7 +66,7 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_summary">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -68,7 +74,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
@@ -113,7 +119,7 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_parameters">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -121,7 +127,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
|
||||
0
bec_widgets/widgets/plots_next_gen/__init__.py
Normal file
0
bec_widgets/widgets/plots_next_gen/__init__.py
Normal file
@@ -20,6 +20,7 @@ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions impor
|
||||
MouseInteractionToolbarBundle,
|
||||
)
|
||||
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.plot_export import PlotExportBundle
|
||||
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.roi_bundle import ROIBundle
|
||||
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.save_state import SaveStateBundle
|
||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
@@ -94,6 +95,8 @@ class PlotBase(BECWidget, QWidget):
|
||||
self.crosshair = None
|
||||
self.fps_monitor = None
|
||||
self.fps_label = QLabel(alignment=Qt.AlignmentFlag.AlignRight)
|
||||
self._user_x_label = ""
|
||||
self._x_label_suffix = ""
|
||||
|
||||
self._init_ui()
|
||||
|
||||
@@ -118,18 +121,14 @@ class PlotBase(BECWidget, QWidget):
|
||||
self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
|
||||
self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
|
||||
self.state_export_bundle = SaveStateBundle("state_export", target_widget=self)
|
||||
self.roi_bundle = ROIBundle("roi", target_widget=self)
|
||||
|
||||
# Add elements to toolbar
|
||||
self.toolbar.add_bundle(self.plot_export_bundle, target_widget=self)
|
||||
self.toolbar.add_bundle(self.state_export_bundle, target_widget=self)
|
||||
self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
|
||||
self.toolbar.add_bundle(self.roi_bundle, target_widget=self)
|
||||
|
||||
self.toolbar.add_action("separator_0", SeparatorAction(), target_widget=self)
|
||||
self.toolbar.add_action(
|
||||
"crosshair",
|
||||
MaterialIconAction(icon_name="point_scan", tooltip="Show Crosshair", checkable=True),
|
||||
target_widget=self,
|
||||
)
|
||||
self.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self)
|
||||
self.toolbar.add_action(
|
||||
"fps_monitor",
|
||||
@@ -141,7 +140,6 @@ class PlotBase(BECWidget, QWidget):
|
||||
self.toolbar.widgets["fps_monitor"].action.toggled.connect(
|
||||
lambda checked: setattr(self, "enable_fps_monitor", checked)
|
||||
)
|
||||
self.toolbar.widgets["crosshair"].action.toggled.connect(self.toggle_crosshair)
|
||||
|
||||
def add_side_menus(self):
|
||||
"""Adds multiple menus to the side panel."""
|
||||
@@ -256,12 +254,45 @@ class PlotBase(BECWidget, QWidget):
|
||||
|
||||
@SafeProperty(str, doc="The text of the x label")
|
||||
def x_label(self) -> str:
|
||||
return self.plot_item.getAxis("bottom").labelText
|
||||
return self._user_x_label
|
||||
|
||||
@x_label.setter
|
||||
def x_label(self, value: str):
|
||||
self.plot_item.setLabel("bottom", text=value)
|
||||
self.property_changed.emit("x_label", value)
|
||||
self._user_x_label = value
|
||||
self._apply_x_label()
|
||||
self.property_changed.emit("x_label", self._user_x_label)
|
||||
|
||||
@property
|
||||
def x_label_suffix(self) -> str:
|
||||
"""
|
||||
A read-only (or internal) suffix automatically appended to the user label.
|
||||
Not settable by the user directly from the UI.
|
||||
"""
|
||||
return self._x_label_suffix
|
||||
|
||||
def set_x_label_suffix(self, suffix: str):
|
||||
"""
|
||||
Public or protected method to update the suffix.
|
||||
The user code or subclass (Waveform) can call this
|
||||
when x_mode changes, but the AxisSettings won't show it.
|
||||
"""
|
||||
self._x_label_suffix = suffix
|
||||
self._apply_x_label()
|
||||
|
||||
@property
|
||||
def x_label_combined(self) -> str:
|
||||
"""
|
||||
The final label shown on the axis = user portion + suffix.
|
||||
"""
|
||||
return self._user_x_label + self._x_label_suffix
|
||||
|
||||
def _apply_x_label(self):
|
||||
"""
|
||||
Actually updates the pyqtgraph axis label text to
|
||||
the combined label. Called whenever user label or suffix changes.
|
||||
"""
|
||||
final_label = self.x_label_combined
|
||||
self.plot_item.setLabel("bottom", text=final_label)
|
||||
|
||||
@SafeProperty(str, doc="The text of the y label")
|
||||
def y_label(self) -> str:
|
||||
|
||||
@@ -1,240 +1,245 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>241</width>
|
||||
<height>526</height>
|
||||
</rect>
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>218</width>
|
||||
<height>561</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>General</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="title"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Inner Axes</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="x_axis_box">
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="x_scale_label">
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="x_label"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="x_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="ToggleSwitch" name="x_log">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="x_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="x_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="x_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="ToggleSwitch" name="x_grid">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="title"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_outer_axes">
|
||||
<property name="text">
|
||||
<string>Outer Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="y_axis_box">
|
||||
<property name="title">
|
||||
<string>Y Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="y_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="y_label"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="y_scale_label">
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="y_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="y_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="y_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="ToggleSwitch" name="y_log">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="ToggleSwitch" name="y_grid">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="ToggleSwitch" name="outer_axes">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Inner Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="ToggleSwitch" name="inner_axes"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="ToggleSwitch" name="inner_axes"/>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_outer_axes">
|
||||
<property name="text">
|
||||
<string>Outer Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="ToggleSwitch" name="outer_axes">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="x_axis_box">
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="x_scale_label">
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="x_label"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="x_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="ToggleSwitch" name="x_log">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="x_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="x_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="x_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="ToggleSwitch" name="x_grid">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="y_axis_box">
|
||||
<property name="title">
|
||||
<string>Y Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="y_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="y_label"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="y_scale_label">
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="y_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="y_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="y_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="ToggleSwitch" name="y_log">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="ToggleSwitch" name="y_grid">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||
|
||||
|
||||
class ROIBundle(ToolbarBundle):
|
||||
"""
|
||||
A bundle of actions that are hooked in this constructor itself,
|
||||
so that you can immediately connect the signals and toggle states.
|
||||
|
||||
This bundle is for a toolbar that controls crosshair and ROI interaction.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id="roi", target_widget=None, **kwargs):
|
||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||
self.target_widget = target_widget
|
||||
|
||||
# Create each MaterialIconAction with a parent
|
||||
# so the signals can fire even if the toolbar isn't added yet.
|
||||
crosshair = MaterialIconAction(
|
||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
||||
)
|
||||
roi = MaterialIconAction(
|
||||
icon_name="align_justify_space_between",
|
||||
tooltip="Add ROI region for DAP",
|
||||
checkable=True,
|
||||
)
|
||||
|
||||
# Add them to the bundle
|
||||
self.add_action("crosshair", crosshair)
|
||||
self.add_action("roi_linear", roi)
|
||||
|
||||
# Immediately connect signals
|
||||
crosshair.action.toggled.connect(self.target_widget.toggle_crosshair)
|
||||
268
bec_widgets/widgets/plots_next_gen/waveform/curve.py
Normal file
268
bec_widgets/widgets/plots_next_gen/waveform/curve.py
Normal file
@@ -0,0 +1,268 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy import QtCore
|
||||
|
||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# noinspection PyDataclass
|
||||
class DeviceSignal(BaseModel):
|
||||
"""The configuration of a signal in the 1D waveform widget."""
|
||||
|
||||
name: str
|
||||
entry: str
|
||||
dap: Optional[str] = None # TODO utilize differently than in past
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
# noinspection PyDataclass
|
||||
class CurveConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the curve.")
|
||||
label: Optional[str] = Field(None, description="The label of the curve.")
|
||||
color: Optional[str | tuple] = Field(None, description="The color of the curve.")
|
||||
symbol: Optional[str | None] = Field("o", description="The symbol of the curve.")
|
||||
symbol_color: Optional[str | tuple] = Field(
|
||||
None, description="The color of the symbol 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: Literal["device", "dap", "custom"] = Field(
|
||||
"custom", description="The source of the curve."
|
||||
)
|
||||
signal: Optional[DeviceSignal] = Field(None, description="The signal of the curve.")
|
||||
# TODO do validator for parent_label
|
||||
parent_label: Optional[str] = Field(
|
||||
None, description="The label of the parent plot, only relevant for dap curves."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
_validate_symbol_color = field_validator("symbol_color")(Colors.validate_color)
|
||||
|
||||
|
||||
class Curve(BECConnector, pg.PlotDataItem):
|
||||
USER_ACCESS = [
|
||||
"remove",
|
||||
"dap_params",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
"set_color_map_z",
|
||||
"set_symbol",
|
||||
"set_symbol_color",
|
||||
"set_symbol_size",
|
||||
"set_pen_width",
|
||||
"set_pen_style",
|
||||
"get_data",
|
||||
"dap_params",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
config: Optional[CurveConfig] = None,
|
||||
gui_id: Optional[str] = None,
|
||||
parent_item: Optional[Waveform] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = CurveConfig(label=name, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
# config.widget_class = self.__class__.__name__
|
||||
super().__init__(config=config, gui_id=gui_id)
|
||||
pg.PlotDataItem.__init__(self, name=name)
|
||||
|
||||
self.parent_item = parent_item
|
||||
self.apply_config()
|
||||
self.dap_params = None
|
||||
self.dap_summary = None
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
|
||||
def apply_config(self):
|
||||
pen_style_map = {
|
||||
"solid": QtCore.Qt.SolidLine,
|
||||
"dash": QtCore.Qt.DashLine,
|
||||
"dot": QtCore.Qt.DotLine,
|
||||
"dashdot": QtCore.Qt.DashDotLine,
|
||||
}
|
||||
pen_style = pen_style_map.get(self.config.pen_style, QtCore.Qt.SolidLine)
|
||||
|
||||
pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=pen_style)
|
||||
self.setPen(pen)
|
||||
|
||||
if self.config.symbol:
|
||||
symbol_color = self.config.symbol_color or self.config.color
|
||||
brush = pg.mkBrush(color=symbol_color)
|
||||
|
||||
self.setSymbolBrush(brush)
|
||||
self.setSymbolSize(self.config.symbol_size)
|
||||
self.setSymbol(self.config.symbol)
|
||||
|
||||
@property
|
||||
def dap_params(self):
|
||||
return self._dap_params
|
||||
|
||||
@dap_params.setter
|
||||
def dap_params(self, value):
|
||||
self._dap_params = value
|
||||
|
||||
@property
|
||||
def dap_summary(self):
|
||||
return self._dap_report
|
||||
|
||||
@dap_summary.setter
|
||||
def dap_summary(self, value):
|
||||
self._dap_report = value
|
||||
|
||||
def set_data(self, x, y):
|
||||
if self.config.source == "custom":
|
||||
self.setData(x, y)
|
||||
else:
|
||||
raise ValueError(f"Source {self.config.source} do not allow custom data setting.")
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the curve.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- color: str
|
||||
- symbol: str
|
||||
- symbol_color: str
|
||||
- symbol_size: int
|
||||
- pen_width: int
|
||||
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
|
||||
"""
|
||||
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"color": self.set_color,
|
||||
"color_map_z": self.set_color_map_z,
|
||||
"symbol": self.set_symbol,
|
||||
"symbol_color": self.set_symbol_color,
|
||||
"symbol_size": self.set_symbol_size,
|
||||
"pen_width": self.set_pen_width,
|
||||
"pen_style": self.set_pen_style,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
logger.warning(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def set_color(self, color: str, symbol_color: Optional[str] = None):
|
||||
"""
|
||||
Change the color of the curve.
|
||||
|
||||
Args:
|
||||
color(str): Color of the curve.
|
||||
symbol_color(str, optional): Color of the symbol. Defaults to None.
|
||||
"""
|
||||
self.config.color = color
|
||||
self.config.symbol_color = symbol_color or color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol(self, symbol: str):
|
||||
"""
|
||||
Change the symbol of the curve.
|
||||
|
||||
Args:
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
self.config.symbol = symbol
|
||||
self.setSymbol(symbol)
|
||||
self.updateItems()
|
||||
|
||||
def set_symbol_color(self, symbol_color: str):
|
||||
"""
|
||||
Change the symbol color of the curve.
|
||||
|
||||
Args:
|
||||
symbol_color(str): Color of the symbol.
|
||||
"""
|
||||
self.config.symbol_color = symbol_color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol_size(self, symbol_size: int):
|
||||
"""
|
||||
Change the symbol size of the curve.
|
||||
|
||||
Args:
|
||||
symbol_size(int): Size of the symbol.
|
||||
"""
|
||||
self.config.symbol_size = symbol_size
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_width(self, pen_width: int):
|
||||
"""
|
||||
Change the pen width of the curve.
|
||||
|
||||
Args:
|
||||
pen_width(int): Width of the pen.
|
||||
"""
|
||||
self.config.pen_width = pen_width
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_style(self, pen_style: Literal["solid", "dash", "dot", "dashdot"]):
|
||||
"""
|
||||
Change the pen style of the curve.
|
||||
|
||||
Args:
|
||||
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
|
||||
"""
|
||||
self.config.pen_style = pen_style
|
||||
self.apply_config()
|
||||
|
||||
def set_color_map_z(self, colormap: str):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
self.config.color_map_z = colormap
|
||||
self.apply_config()
|
||||
self.parent_item.scan_history(-1)
|
||||
|
||||
def get_data(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Get the data of the curve.
|
||||
Returns:
|
||||
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
|
||||
"""
|
||||
try:
|
||||
x_data, y_data = self.getData()
|
||||
except TypeError:
|
||||
x_data, y_data = np.array([]), np.array([])
|
||||
return x_data, y_data
|
||||
|
||||
def clear_data(self):
|
||||
self.setData([], [])
|
||||
|
||||
def remove(self):
|
||||
"""Remove the curve from the plot."""
|
||||
# self.parent_item.removeItem(self)
|
||||
self.parent_item.remove_curve(self.name())
|
||||
self.rpc_register.remove_rpc(self)
|
||||
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
|
||||
|
||||
class CurveItem(QWidget):
|
||||
"""
|
||||
#TODO change this nonsense docstring
|
||||
Widget that lets a user set up curves for the Waveform widget.
|
||||
It allows:
|
||||
- Selecting color palette for the entire widget
|
||||
- Choosing x-axis mode
|
||||
- Selecting device and signal
|
||||
- Adding a new curve
|
||||
- Viewing existing curves in a QTreeWidget
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
|
||||
self.setObjectName("CurveSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "curve_settings.ui"), self)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.ui)
|
||||
@@ -0,0 +1,75 @@
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from pyqtgraph import ColorMapWidget
|
||||
from PySide6.QtWidgets import QComboBox, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.containers.expantion_panel.expansion_panel import ExpansionPanel
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit import (
|
||||
SignalLineEdit,
|
||||
)
|
||||
|
||||
|
||||
class CurveSettingWidget(QWidget):
|
||||
"""
|
||||
Widget that lets a user set up curves for the Waveform widget.
|
||||
It allows:
|
||||
- Selecting color palette for the entire widget
|
||||
- Choosing x-axis mode
|
||||
- Selecting device and signal
|
||||
- Adding a new curve
|
||||
- Viewing existing curves in a QTreeWidget
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
|
||||
self.setObjectName("CurveSettings")
|
||||
self.current_path = os.path.dirname(__file__)
|
||||
self.main_setting_ui = UILoader().load_ui(
|
||||
os.path.join(self.current_path, "main_settings.ui"), self
|
||||
)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
# self.layout.addWidget(self.ui)
|
||||
|
||||
self.main_setting = ExpansionPanel(title="Main Settings", expanded=True)
|
||||
self.curve_setting_1 = ExpansionPanel(title="Curve 1", expanded=False)
|
||||
self.curve_setting_2 = ExpansionPanel(title="Curve 2", expanded=False)
|
||||
self.curve_setting_3 = ExpansionPanel(title="Curve 3", expanded=False)
|
||||
|
||||
self.layout.addWidget(self.main_setting)
|
||||
for cs in [self.curve_setting_1, self.curve_setting_2, self.curve_setting_3]:
|
||||
self.layout.addWidget(cs)
|
||||
self._init_curve(cs)
|
||||
self._init_main_settings()
|
||||
# add spacer
|
||||
self.layout.addStretch()
|
||||
|
||||
def _init_main_settings(self):
|
||||
self.main_setting.content_layout.addWidget(self.main_setting_ui)
|
||||
|
||||
def _init_curve(self, curve_setting):
|
||||
icon = material_icon("delete", color=(255, 0, 0, 255))
|
||||
delete_button = QPushButton()
|
||||
delete_button.setIcon(icon)
|
||||
curve_ui = UILoader().load_ui(os.path.join(self.current_path, "curve_3.ui"), self)
|
||||
curve_setting.header_layout.addWidget(delete_button)
|
||||
curve_setting.content_layout.addWidget(curve_ui)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = CurveSettingWidget()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1,17 @@
|
||||
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.plots_next_gen.waveform.demo.waveform_plot_plugin import (
|
||||
WaveformPlotPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlotPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
waveform_plot.py
|
||||
A minimal demonstration widget with multiple properties:
|
||||
- deviceName (str)
|
||||
- curvesJson (str)
|
||||
- someFlag (bool)
|
||||
|
||||
It uses pyqtgraph to show dummy curves from 'curvesJson'.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import Property, QPointF
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class WaveformPlot(QWidget):
|
||||
"""
|
||||
Minimal demonstration of a multi-property widget:
|
||||
- deviceName (string)
|
||||
- curvesJson (string containing JSON)
|
||||
- someFlag (boolean)
|
||||
"""
|
||||
|
||||
ICON_NAME = "multiline_chart" # For a designer icon, if desired
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._device_name = "MyDevice"
|
||||
self._curves_json = "[]"
|
||||
self._some_flag = False
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.plot_item = pg.PlotItem()
|
||||
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
|
||||
layout.addWidget(self.plot_widget)
|
||||
|
||||
self._plot_curves = []
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Property #1: deviceName
|
||||
# ------------------------------------------------------------------------
|
||||
def getDeviceName(self) -> str:
|
||||
return self._device_name
|
||||
|
||||
def setDeviceName(self, val: str):
|
||||
if self._device_name != val:
|
||||
self._device_name = val
|
||||
# You might do something in your real code
|
||||
# e.g. re-subscribe to a device, etc.
|
||||
|
||||
deviceName = Property(str, fget=getDeviceName, fset=setDeviceName)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Property #2: curvesJson
|
||||
# ------------------------------------------------------------------------
|
||||
def getCurvesJson(self) -> str:
|
||||
return self._curves_json
|
||||
|
||||
def setCurvesJson(self, new_json: str):
|
||||
if self._curves_json != new_json:
|
||||
self._curves_json = new_json
|
||||
self._rebuild_curves()
|
||||
|
||||
curvesJson = Property(str, fget=getCurvesJson, fset=setCurvesJson, designable=False)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Property #3: someFlag
|
||||
# ------------------------------------------------------------------------
|
||||
def getSomeFlag(self) -> bool:
|
||||
return self._some_flag
|
||||
|
||||
def setSomeFlag(self, val: bool):
|
||||
if self._some_flag != val:
|
||||
self._some_flag = val
|
||||
# React to the flag in your real code if needed
|
||||
|
||||
someFlag = Property(bool, fget=getSomeFlag, fset=setSomeFlag)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Re-build the curves from the JSON
|
||||
# ------------------------------------------------------------------------
|
||||
def _rebuild_curves(self):
|
||||
# Remove existing PlotDataItems
|
||||
for c in self._plot_curves:
|
||||
self.plot_item.removeItem(c)
|
||||
self._plot_curves.clear()
|
||||
|
||||
# Try parse JSON
|
||||
try:
|
||||
arr = json.loads(self._curves_json)
|
||||
if not isinstance(arr, list):
|
||||
raise ValueError("curvesJson must be a JSON list.")
|
||||
except Exception:
|
||||
# If parsing fails, do nothing
|
||||
return
|
||||
|
||||
# Create new PlotDataItems from the JSON
|
||||
for idx, cdef in enumerate(arr):
|
||||
label = cdef.get("label", f"Curve{idx + 1}")
|
||||
color = cdef.get("color", "blue")
|
||||
|
||||
x = [0, 1, 2, 3, 4]
|
||||
y = [val + idx for val in x]
|
||||
|
||||
item = pg.PlotDataItem(x, y, pen=color, name=label)
|
||||
self.plot_item.addItem(item)
|
||||
self._plot_curves.append(item)
|
||||
|
||||
|
||||
# Optional standalone test
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = WaveformPlot()
|
||||
w.deviceName = "TestDevice"
|
||||
w.curvesJson = json.dumps([{"label": "A", "color": "red"}, {"label": "B", "color": "green"}])
|
||||
w.someFlag = True
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['waveform_demo.py']}
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
waveform_plot_config_dialog.py
|
||||
A single dialog that configures a WaveformPlot's properties:
|
||||
- deviceName
|
||||
- someFlag
|
||||
- curvesJson (with add/remove curve, color picking, etc.)
|
||||
|
||||
You can call this dialog in normal code:
|
||||
dlg = WaveformPlotConfigDialog(myWaveformPlot)
|
||||
if dlg.exec_() == QDialog.Accepted:
|
||||
# properties updated
|
||||
|
||||
Or from a QDesignerTaskMenu (see waveform_plot_taskmenu.py).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QColorDialog,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class WaveformPlotConfigDialog(QDialog):
|
||||
"""
|
||||
Edits three properties of a WaveformPlot:
|
||||
- deviceName (string)
|
||||
- someFlag (bool)
|
||||
- curvesJson (JSON array of {label, color})
|
||||
|
||||
In real usage, you might add more fields (pen widths, device signals, etc.).
|
||||
"""
|
||||
|
||||
def __init__(self, waveform_plot, parent=None):
|
||||
super().__init__(parent, Qt.WindowTitleHint | Qt.WindowSystemMenuHint)
|
||||
self.setWindowTitle("WaveformPlot Configuration")
|
||||
|
||||
self._wp = waveform_plot # We'll read and write properties on this widget
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
# ---------------------------
|
||||
# Row 1: deviceName
|
||||
# ---------------------------
|
||||
row1 = QHBoxLayout()
|
||||
row1.addWidget(QLabel("Device Name:"))
|
||||
self._device_name_edit = QLineEdit(self)
|
||||
self._device_name_edit.setText(self._wp.deviceName)
|
||||
row1.addWidget(self._device_name_edit)
|
||||
main_layout.addLayout(row1)
|
||||
|
||||
# ---------------------------
|
||||
# Row 2: someFlag (bool)
|
||||
# ---------------------------
|
||||
row2 = QHBoxLayout()
|
||||
self._flag_checkbox = QCheckBox("someFlag", self)
|
||||
self._flag_checkbox.setChecked(self._wp.someFlag)
|
||||
row2.addWidget(self._flag_checkbox)
|
||||
row2.addStretch()
|
||||
main_layout.addLayout(row2)
|
||||
|
||||
# ---------------------------
|
||||
# The curves config area
|
||||
# We'll store an internal list of curves
|
||||
# so we can load them from curvesJson
|
||||
# and then re-serialize after changes.
|
||||
# ---------------------------
|
||||
self._curves_data = []
|
||||
try:
|
||||
arr = json.loads(self._wp.curvesJson)
|
||||
if isinstance(arr, list):
|
||||
self._curves_data = arr
|
||||
except:
|
||||
pass
|
||||
|
||||
self._curves_layout = QVBoxLayout()
|
||||
main_layout.addLayout(self._curves_layout)
|
||||
|
||||
add_curve_btn = QPushButton("Add Curve")
|
||||
add_curve_btn.clicked.connect(self._on_add_curve)
|
||||
main_layout.addWidget(add_curve_btn)
|
||||
|
||||
# OK / Cancel
|
||||
box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
|
||||
box.accepted.connect(self.accept)
|
||||
box.rejected.connect(self.reject)
|
||||
main_layout.addWidget(box)
|
||||
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def _refresh_curves_rows(self):
|
||||
# Clear old row widgets
|
||||
while True:
|
||||
item = self._curves_layout.takeAt(0)
|
||||
if not item:
|
||||
break
|
||||
w = item.widget()
|
||||
if w:
|
||||
w.deleteLater()
|
||||
|
||||
# Create row per curve
|
||||
for idx, cinfo in enumerate(self._curves_data):
|
||||
row_widget = self._create_curve_row(idx, cinfo)
|
||||
self._curves_layout.addWidget(row_widget)
|
||||
|
||||
def _create_curve_row(self, idx, cinfo):
|
||||
container = QWidget(self)
|
||||
hl = QHBoxLayout(container)
|
||||
|
||||
label_edit = QLineEdit(cinfo.get("label", ""), container)
|
||||
label_edit.setPlaceholderText("Label")
|
||||
label_edit.textChanged.connect(lambda txt, i=idx: self._on_label_changed(i, txt))
|
||||
hl.addWidget(label_edit)
|
||||
|
||||
color_btn = QPushButton(cinfo.get("color", "Pick Color"), container)
|
||||
color_btn.clicked.connect(lambda _=None, i=idx: self._pick_color(i))
|
||||
hl.addWidget(color_btn)
|
||||
|
||||
rm_btn = QPushButton("X", container)
|
||||
rm_btn.clicked.connect(lambda _=None, i=idx: self._on_remove_curve(i))
|
||||
hl.addWidget(rm_btn)
|
||||
|
||||
return container
|
||||
|
||||
def _on_add_curve(self):
|
||||
self._curves_data.append({"label": "NewCurve", "color": "blue"})
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def _on_remove_curve(self, idx: int):
|
||||
if 0 <= idx < len(self._curves_data):
|
||||
self._curves_data.pop(idx)
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def _on_label_changed(self, idx: int, new_label: str):
|
||||
if 0 <= idx < len(self._curves_data):
|
||||
self._curves_data[idx]["label"] = new_label
|
||||
|
||||
def _pick_color(self, idx: int):
|
||||
if 0 <= idx < len(self._curves_data):
|
||||
dlg = QColorDialog(self)
|
||||
if dlg.exec_() == QDialog.Accepted:
|
||||
c = dlg.selectedColor().name()
|
||||
self._curves_data[idx]["color"] = c
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
If user presses OK, update the widget's properties:
|
||||
deviceName
|
||||
someFlag
|
||||
curvesJson
|
||||
"""
|
||||
# 1) deviceName
|
||||
self._wp.deviceName = self._device_name_edit.text().strip()
|
||||
|
||||
# 2) someFlag
|
||||
self._wp.someFlag = self._flag_checkbox.isChecked()
|
||||
|
||||
# 3) curvesJson
|
||||
new_json = json.dumps(self._curves_data, indent=2)
|
||||
self._wp.curvesJson = new_json
|
||||
|
||||
super().accept()
|
||||
|
||||
# For standalone usage, you can do:
|
||||
# dlg = WaveformPlotConfigDialog(wp)
|
||||
# if dlg.exec_() == QDialog.Accepted:
|
||||
# # properties were updated
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
waveform_plot_plugin.py
|
||||
Registers WaveformPlot with Qt Designer,
|
||||
including the WaveformPlotTaskMenu extension.
|
||||
"""
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from .waveform_demo import WaveformPlot
|
||||
from .waveform_plot_taskmenu import WaveformPlotTaskMenuFactory
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='WaveformPlot' name='waveformPlot'>
|
||||
<property name='geometry'>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>300</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name='deviceName'>
|
||||
<string>MyDevice</string>
|
||||
</property>
|
||||
<property name='someFlag'>
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name='curvesJson'>
|
||||
<string>[{"label": "DefaultCurve", "color": "red"}]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
|
||||
"""
|
||||
Exposes WaveformPlot to Designer, plus sets up the Task Menu extension
|
||||
for "Edit Configuration..." popup.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._initialized = False
|
||||
|
||||
def initialize(self, form_editor):
|
||||
if self._initialized:
|
||||
return
|
||||
self._initialized = True
|
||||
|
||||
# Register the TaskMenu extension
|
||||
manager = form_editor.extensionManager()
|
||||
if manager:
|
||||
factory = WaveformPlotTaskMenuFactory(manager)
|
||||
manager.registerExtensions(factory, "org.qt-project.Qt.Designer.TaskMenu")
|
||||
|
||||
def isInitialized(self):
|
||||
return self._initialized
|
||||
|
||||
def createWidget(self, parent):
|
||||
return WaveformPlot(parent)
|
||||
|
||||
def name(self):
|
||||
return "WaveformPlot"
|
||||
|
||||
def group(self):
|
||||
return "Waveform Widgets"
|
||||
|
||||
def icon(self):
|
||||
# If you have a real icon, load it here
|
||||
return QIcon()
|
||||
|
||||
def toolTip(self):
|
||||
return "A multi-property WaveformPlot example"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def includeFile(self):
|
||||
# The Python import path for your waveforms
|
||||
# E.g. "my_widgets.waveform.waveform_plot"
|
||||
return __name__
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
waveform_plot_taskmenu.py
|
||||
Task Menu extension for Qt Designer.
|
||||
It attaches "Edit Configuration..." to the WaveformPlot,
|
||||
launching WaveformPlotConfigDialog.
|
||||
"""
|
||||
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
|
||||
from qtpy.QtGui import QAction
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo.waveform_demo import WaveformPlot
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo.waveform_plot_config_dialog import (
|
||||
WaveformPlotConfigDialog,
|
||||
)
|
||||
|
||||
|
||||
class WaveformPlotTaskMenu(QPyDesignerTaskMenuExtension):
|
||||
def __init__(self, widget: WaveformPlot, parent=None):
|
||||
super().__init__(parent)
|
||||
self._widget = widget
|
||||
self._edit_action = QAction("Edit Configuration...", self)
|
||||
self._edit_action.triggered.connect(self._on_edit)
|
||||
|
||||
def taskActions(self):
|
||||
return [self._edit_action]
|
||||
|
||||
def preferredEditAction(self):
|
||||
# Double-click in Designer might open this
|
||||
return self._edit_action
|
||||
|
||||
@Slot()
|
||||
def _on_edit(self):
|
||||
# Show the same config dialog we can use in normal code
|
||||
dlg = WaveformPlotConfigDialog(self._widget)
|
||||
dlg.exec_() # If user presses OK, the widget's properties are updated
|
||||
|
||||
|
||||
class WaveformPlotTaskMenuFactory(QExtensionFactory):
|
||||
"""
|
||||
Creates a WaveformPlotTaskMenu if the widget is an instance of WaveformPlot
|
||||
and the requested extension is 'TaskMenu'.
|
||||
"""
|
||||
|
||||
def createExtension(self, obj, iid, parent):
|
||||
if iid == "org.qt-project.Qt.Designer.TaskMenu" and isinstance(obj, WaveformPlot):
|
||||
return WaveformPlotTaskMenu(obj, parent)
|
||||
return None
|
||||
@@ -0,0 +1,55 @@
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
|
||||
class CurveConfig(BaseModel):
|
||||
label: str = Field(..., description="Label/ID of this curve")
|
||||
color: str = Field("blue", description="Curve color")
|
||||
symbol: Optional[str] = Field(None, description="Symbol e.g. 'o', 'x'")
|
||||
pen_width: int = Field(2, description="Pen width in px")
|
||||
pen_style: Literal["solid", "dash", "dot", "dashdot"] = "solid"
|
||||
|
||||
# You can add device/signal if desired:
|
||||
# signals: Optional[Signal] = None
|
||||
# etc.
|
||||
|
||||
class Config:
|
||||
model_config = {"validate_assignment": True}
|
||||
|
||||
|
||||
pen_style_map = {
|
||||
"solid": Qt.SolidLine,
|
||||
"dash": Qt.DashLine,
|
||||
"dot": Qt.DotLine,
|
||||
"dashdot": Qt.DashDotLine,
|
||||
}
|
||||
|
||||
|
||||
class BECCurve(pg.PlotDataItem):
|
||||
"""
|
||||
A custom PlotDataItem that holds a reference to a Pydantic-based CurveConfig.
|
||||
"""
|
||||
|
||||
def __init__(self, config: CurveConfig, parent=None):
|
||||
super().__init__(name=config.label) # set the PlotDataItem name
|
||||
self.config = config
|
||||
self._parent = parent # optional reference to the WaveformPlot
|
||||
# now apply config to actual PlotDataItem
|
||||
self.apply_config()
|
||||
|
||||
def apply_config(self):
|
||||
style = pen_style_map.get(self.config.pen_style, Qt.SolidLine)
|
||||
pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=style)
|
||||
self.setPen(pen)
|
||||
|
||||
if self.config.symbol is not None:
|
||||
self.setSymbol(self.config.symbol)
|
||||
else:
|
||||
self.setSymbol(None)
|
||||
|
||||
def set_data_custom(self, x, y):
|
||||
# If you only want to allow custom data if config.source == "custom", etc.
|
||||
self.setData(x, y)
|
||||
@@ -0,0 +1,17 @@
|
||||
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.plots_next_gen.waveform.demo_2.waveform_demo2_plugin import (
|
||||
WaveformPlotDemo2Plugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlotDemo2Plugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,144 @@
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import Property
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo_2.demo_curve import BECCurve, CurveConfig
|
||||
|
||||
|
||||
class WaveformPlotDemo2(QWidget):
|
||||
"""
|
||||
A Plot widget that stores multiple curves in a single JSON property (`curvesJson`).
|
||||
Internally, we keep a list of (CurveConfig, BECCurve).
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._curves_json = "[]"
|
||||
self._curves: List[BECCurve] = [] # the actual PlotDataItems
|
||||
self._curve_configs: List[CurveConfig] = []
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.plot_item = pg.PlotItem()
|
||||
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
|
||||
layout.addWidget(self.plot_widget)
|
||||
self.plot_item.addLegend()
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# QProperty: curvesJson
|
||||
# ------------------------------------------------------------------------
|
||||
def getCurvesJson(self) -> str:
|
||||
return self._curves_json
|
||||
|
||||
def setCurvesJson(self, val: str):
|
||||
if self._curves_json != val:
|
||||
self._curves_json = val
|
||||
self._rebuild_curves_from_json()
|
||||
|
||||
curvesJson = Property(str, fget=getCurvesJson, fset=setCurvesJson)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Internal method: parse JSON -> create/update BECCurve objects
|
||||
# ------------------------------------------------------------------------
|
||||
def _rebuild_curves_from_json(self):
|
||||
# 1) Remove existing items from the plot
|
||||
for c in self._curves:
|
||||
self.plot_item.removeItem(c)
|
||||
self._curves.clear()
|
||||
self._curve_configs.clear()
|
||||
|
||||
# 2) Parse JSON
|
||||
try:
|
||||
raw_list = json.loads(self._curves_json)
|
||||
if not isinstance(raw_list, list):
|
||||
raise ValueError("curvesJson must be a JSON list.")
|
||||
except Exception:
|
||||
raw_list = []
|
||||
|
||||
# 3) Convert each raw dict -> CurveConfig -> BECCurve
|
||||
for entry in raw_list:
|
||||
try:
|
||||
cfg = CurveConfig(**entry)
|
||||
except Exception:
|
||||
# fallback or skip
|
||||
continue
|
||||
curve_obj = BECCurve(config=cfg, parent=self)
|
||||
# For demonstration, set some dummy data
|
||||
xdata = [0, 1, 2, 3, 4]
|
||||
ydata = [val + hash(cfg.label) % 3 for val in xdata]
|
||||
curve_obj.setData(xdata, ydata)
|
||||
|
||||
self.plot_item.addItem(curve_obj)
|
||||
self._curves.append(curve_obj)
|
||||
self._curve_configs.append(cfg)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# CLI / dynamic methods to add, remove, or modify curves at runtime
|
||||
# ------------------------------------------------------------------------
|
||||
def list_curve_labels(self) -> list[str]:
|
||||
return [cfg.label for cfg in self._curve_configs]
|
||||
|
||||
def get_curve(self, label: str) -> BECCurve:
|
||||
# Return the actual BECCurve object (or a config, or both)
|
||||
for c in self._curves:
|
||||
if c.config.label == label:
|
||||
return c
|
||||
raise ValueError(f"No curve with label='{label}'")
|
||||
|
||||
def add_curve(self, cfg: CurveConfig):
|
||||
"""
|
||||
Add a new curve from code. We just insert the new config
|
||||
into the list, then re-serialize to JSON => triggers rebuild
|
||||
"""
|
||||
# insert new config to the internal list
|
||||
self._curve_configs.append(cfg)
|
||||
self._sync_json_from_configs()
|
||||
|
||||
def remove_curve(self, label: str):
|
||||
for i, c in enumerate(self._curve_configs):
|
||||
if c.label == label:
|
||||
self._curve_configs.pop(i)
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"No curve with label='{label}' found to remove.")
|
||||
|
||||
self._sync_json_from_configs()
|
||||
|
||||
def set_curve_property(self, label: str, **kwargs):
|
||||
"""
|
||||
For example, set_curve_property("Curve1", color="red", pen_width=4)
|
||||
We'll update the pydantic model, then re-sync to JSON, rebuild.
|
||||
"""
|
||||
c = self._find_config(label)
|
||||
for k, v in kwargs.items():
|
||||
setattr(c, k, v) # pydantic assignment
|
||||
self._sync_json_from_configs()
|
||||
|
||||
def _find_config(self, label: str) -> CurveConfig:
|
||||
for cfg in self._curve_configs:
|
||||
if cfg.label == label:
|
||||
return cfg
|
||||
raise ValueError(f"No config with label='{label}' found.")
|
||||
|
||||
def _sync_json_from_configs(self):
|
||||
"""
|
||||
Re-serialize our internal curve configs -> JSON string,
|
||||
call setCurvesJson(...) => triggers the rebuild in the same widget
|
||||
so the user and Designer stay in sync
|
||||
"""
|
||||
raw_list = [cfg.dict() for cfg in self._curve_configs]
|
||||
new_json = json.dumps(raw_list, indent=2)
|
||||
self.setCurvesJson(new_json)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
w = WaveformPlotDemo2()
|
||||
w.show()
|
||||
w.add_curve(CurveConfig(label="Curve1", color="red"))
|
||||
w.add_curve(CurveConfig(label="Curve2", color="blue"))
|
||||
app.exec_()
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['waveform_demo2.py']}
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
waveform_plot_plugin.py
|
||||
Registers WaveformPlotDemo2 with Qt Designer,
|
||||
including the WaveformPlotDemo2TaskMenu extension.
|
||||
"""
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo_2.waveform_demo2 import WaveformPlotDemo2
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='WaveformPlotDemo2' name='WaveformPlotDemo2'>
|
||||
<property name='geometry'>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>300</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name='deviceName'>
|
||||
<string>MyDevice</string>
|
||||
</property>
|
||||
<property name='someFlag'>
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name='curvesJson'>
|
||||
<string>[{"label": "DefaultCurve", "color": "red"}]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class WaveformPlotDemo2Plugin(QDesignerCustomWidgetInterface):
|
||||
"""
|
||||
Exposes WaveformPlotDemo2 to Designer, plus sets up the Task Menu extension
|
||||
for "Edit Configuration..." popup.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._initialized = False
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isInitialized(self):
|
||||
return self._initialized
|
||||
|
||||
def createWidget(self, parent):
|
||||
return WaveformPlotDemo2(parent)
|
||||
|
||||
def name(self):
|
||||
return "WaveformPlotDemo2"
|
||||
|
||||
def group(self):
|
||||
return "Waveform Widgets"
|
||||
|
||||
def icon(self):
|
||||
# If you have a real icon, load it here
|
||||
return QIcon()
|
||||
|
||||
def toolTip(self):
|
||||
return "A multi-property WaveformPlotDemo2 example"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def includeFile(self):
|
||||
# The Python import path for your waveforms
|
||||
# E.g. "my_widgets.waveform.waveform_plot"
|
||||
return __name__
|
||||
@@ -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.plots_next_gen.waveform.waveform_plugin import WaveformPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,183 @@
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QTreeWidgetItem
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class CurveSettingWidgetOld(QWidget):
|
||||
"""
|
||||
Widget that lets a user set up curves for the Waveform widget.
|
||||
It allows:
|
||||
- Selecting color palette for the entire widget
|
||||
- Choosing x-axis mode
|
||||
- Selecting device and signal
|
||||
- Adding a new curve
|
||||
- Viewing existing curves in a QTreeWidget
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
|
||||
self.setObjectName("CurveSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "curve_settings.ui"), self)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.ui)
|
||||
|
||||
self.connect_all_signals()
|
||||
|
||||
self.refresh_tree_from_waveform() # TODO implement
|
||||
|
||||
def connect_all_signals(self):
|
||||
self.ui.pushButton.clicked.connect(self.on_apply_color_palette)
|
||||
self.ui.x_mode.currentTextChanged.connect(self.enable_ui_elements_x_mode)
|
||||
self.ui.x_mode.currentTextChanged.connect(self.on_x_mode_changed)
|
||||
self.enable_ui_elements_x_mode() # Enable or disable the x-axis mode elements based on the x-axis mode
|
||||
|
||||
self.ui.add_curve.clicked.connect(self.add_curve_from_ui)
|
||||
|
||||
# TODO: Implement this method
|
||||
|
||||
# General property forwarding for the target widget
|
||||
# for widget in [self.ui.x_mode]:
|
||||
# WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||
|
||||
def enable_ui_elements_x_mode(self):
|
||||
"""
|
||||
Enable or disable the x-axis mode elements based on the x-axis mode.
|
||||
"""
|
||||
combo_box_mode = self.ui.x_mode.currentText()
|
||||
if combo_box_mode == "device":
|
||||
self.ui.device_line_edit.setEnabled(True)
|
||||
self.ui.signal_line_edit.setEnabled(True)
|
||||
else:
|
||||
self.ui.device_line_edit.setEnabled(False)
|
||||
self.ui.signal_line_edit.setEnabled(False)
|
||||
|
||||
@SafeSlot("QString")
|
||||
def on_x_mode_changed(self, text):
|
||||
"""
|
||||
Update the x-axis mode of the target widget.
|
||||
"""
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
self.target_widget.x_mode = text
|
||||
if text == "device":
|
||||
self.target_widget.device = self.ui.device_line_edit.text()
|
||||
self.target_widget.signal = self.ui.signal_line_edit.text()
|
||||
|
||||
self.refresh_tree_from_waveform() # TODO implement
|
||||
|
||||
@SafeSlot()
|
||||
def on_apply_color_palette(self):
|
||||
"""
|
||||
Apply the selected color palette to the target widget.
|
||||
"""
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
color_map = getattr(self.ui.bec_color_map_widget, "colormap", None)
|
||||
|
||||
self.target_widget.color_palette = color_map
|
||||
|
||||
self.refresh_tree_from_waveform() # TODO implement
|
||||
|
||||
def add_curve_from_ui(self):
|
||||
"""
|
||||
Add a curve to the target widget based on the UI elements.
|
||||
"""
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
def refresh_tree_from_waveform(self):
|
||||
"""
|
||||
Clears the treeWidget and repopulates it with the current curves
|
||||
from the target_widget’s curve_json.
|
||||
"""
|
||||
self.ui.treeWidget.clear()
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
# The Waveform has a SafeProperty 'curve_json' that returns JSON for all device curves
|
||||
# or you can iterate over target_widget.curves and build your own representation.
|
||||
# For a simpler approach, we’ll just iterate curves directly:
|
||||
|
||||
for curve in self.target_widget.curves:
|
||||
# Make a top-level item in the tree for each curve
|
||||
top_item = QTreeWidgetItem(self.ui.treeWidget)
|
||||
top_item.setText(0, "CURVE")
|
||||
top_item.setText(1, curve.name()) # e.g. "myDevice-myEntry"
|
||||
|
||||
# Child: device name
|
||||
dev_item = QTreeWidgetItem(top_item)
|
||||
dev_item.setText(0, "device")
|
||||
if curve.config.signal:
|
||||
dev_item.setText(1, curve.config.signal.name)
|
||||
|
||||
# Child: entry
|
||||
entry_item = QTreeWidgetItem(top_item)
|
||||
entry_item.setText(0, "signal")
|
||||
if curve.config.signal:
|
||||
entry_item.setText(1, curve.config.signal.entry)
|
||||
|
||||
# Child: color
|
||||
color_item = QTreeWidgetItem(top_item)
|
||||
color_item.setText(0, "color")
|
||||
if curve.config.color:
|
||||
color_item.setText(1, str(curve.config.color))
|
||||
|
||||
# Child: source (custom/device/dap)
|
||||
source_item = QTreeWidgetItem(top_item)
|
||||
source_item.setText(0, "source")
|
||||
source_item.setText(1, curve.config.source)
|
||||
|
||||
# Expand the top-level item
|
||||
self.ui.treeWidget.addTopLevelItem(top_item)
|
||||
top_item.setExpanded(True)
|
||||
|
||||
# Optionally, resize columns
|
||||
# self.ui.treeWidget.header().resizeSections(Qt.ResizeToContents)
|
||||
|
||||
@SafeSlot()
|
||||
def set_property(self, widget: QWidget, value):
|
||||
"""
|
||||
Set property of the target widget based on the widget that emitted the signal.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget that emitted the signal.
|
||||
value(): The value to set the property to.
|
||||
"""
|
||||
|
||||
property_name = widget.objectName()
|
||||
setattr(self.target_widget, property_name, value)
|
||||
|
||||
@SafeSlot()
|
||||
def update_property(self, property_name: str, value):
|
||||
"""
|
||||
Update the value of the widget based on the property name and value.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
property_name(str): The name of the property to update.
|
||||
value: The value to set the property to.
|
||||
"""
|
||||
try: # to avoid crashing when the widget is not found in Designer
|
||||
widget_to_set = self.ui.findChild(QWidget, property_name)
|
||||
except RuntimeError:
|
||||
return
|
||||
# Block signals to avoid triggering set_property again
|
||||
was_blocked = widget_to_set.blockSignals(True)
|
||||
WidgetIO.set_value(widget_to_set, value)
|
||||
widget_to_set.blockSignals(was_blocked)
|
||||
@@ -0,0 +1,189 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>256</width>
|
||||
<height>563</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Color Palette</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton">
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECColorMapWidget" name="bec_color_map_widget"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>151</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Device</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="DeviceLineEdit" name="device_line_edit"/>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="x_mode">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>auto</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>index</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>timestamp</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>device</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="SignalLineEdit" name="signal_line_edit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_curve">
|
||||
<property name="text">
|
||||
<string>Add Curve</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="treeWidget">
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Property</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Value</string>
|
||||
</property>
|
||||
</column>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>DEVICE</string>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>device</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>name</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>color</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>style</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</item>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DeviceLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>device_line_edit</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SignalLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>signal_line_edit</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECColorMapWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_color_map_widget</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>device_line_edit</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>signal_line_edit</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>158</x>
|
||||
<y>174</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>165</x>
|
||||
<y>222</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -0,0 +1,84 @@
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
|
||||
|
||||
|
||||
class WaveformROIManager(QObject):
|
||||
"""
|
||||
A reusable helper class that manages a single linear ROI region on a given plot item.
|
||||
It provides signals to notify about region changes and active state.
|
||||
"""
|
||||
|
||||
roi_changed = Signal(tuple) # Emitted when the ROI (left, right) changes
|
||||
roi_active = Signal(bool) # Emitted when ROI is enabled or disabled
|
||||
|
||||
def __init__(self, plot_item: pg.PlotItem, parent=None):
|
||||
super().__init__(parent)
|
||||
self._plot_item = plot_item
|
||||
self._roi_wrapper: LinearRegionWrapper | None = None
|
||||
self._roi_region: tuple[float, float] | None = None
|
||||
self._accent_colors = get_accent_colors()
|
||||
|
||||
@property
|
||||
def roi_region(self) -> tuple[float, float] | None:
|
||||
return self._roi_region
|
||||
|
||||
@roi_region.setter
|
||||
def roi_region(self, value: tuple[float, float] | None):
|
||||
self._roi_region = value
|
||||
if self._roi_wrapper is not None and value is not None:
|
||||
self._roi_wrapper.linear_region_selector.setRegion(value)
|
||||
|
||||
@Slot(bool)
|
||||
def toggle_roi(self, enabled: bool) -> None:
|
||||
if enabled:
|
||||
self._enable_roi()
|
||||
else:
|
||||
self._disable_roi()
|
||||
|
||||
@Slot(tuple)
|
||||
def select_roi(self, region: tuple[float, float]):
|
||||
# If ROI not present, enabling it
|
||||
if self._roi_wrapper is None:
|
||||
self.toggle_roi(True)
|
||||
self.roi_region = region
|
||||
|
||||
def _enable_roi(self):
|
||||
if self._roi_wrapper is not None:
|
||||
# Already enabled
|
||||
return
|
||||
color = self._accent_colors.default
|
||||
color.setAlpha(int(0.2 * 255))
|
||||
hover_color = self._accent_colors.default
|
||||
hover_color.setAlpha(int(0.35 * 255))
|
||||
|
||||
self._roi_wrapper = LinearRegionWrapper(
|
||||
self._plot_item, color=color, hover_color=hover_color, parent=self
|
||||
)
|
||||
self._roi_wrapper.add_region_selector()
|
||||
self._roi_wrapper.region_changed.connect(self._on_region_changed)
|
||||
|
||||
# If we already had a region, apply it
|
||||
if self._roi_region is not None:
|
||||
self._roi_wrapper.linear_region_selector.setRegion(self._roi_region)
|
||||
else:
|
||||
self._roi_region = self._roi_wrapper.linear_region_selector.getRegion()
|
||||
|
||||
self.roi_active.emit(True)
|
||||
|
||||
def _disable_roi(self):
|
||||
if self._roi_wrapper is not None:
|
||||
self._roi_wrapper.region_changed.disconnect(self._on_region_changed)
|
||||
self._roi_wrapper.cleanup()
|
||||
self._roi_wrapper.deleteLater()
|
||||
self._roi_wrapper = None
|
||||
|
||||
self._roi_region = None
|
||||
self.roi_active.emit(False)
|
||||
|
||||
@Slot(tuple)
|
||||
def _on_region_changed(self, region: tuple[float, float]):
|
||||
self._roi_region = region
|
||||
self.roi_changed.emit(region)
|
||||
1256
bec_widgets/widgets/plots_next_gen/waveform/waveform.py
Normal file
1256
bec_widgets/widgets/plots_next_gen/waveform/waveform.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{'files': ['waveform.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='Waveform' name='waveform'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class WaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = Waveform(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Plot Widgets Next Gen"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(Waveform.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "waveform"
|
||||
|
||||
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 "Waveform"
|
||||
|
||||
def toolTip(self):
|
||||
return "Waveform"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
Reference in New Issue
Block a user