1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-09 18:20:55 +02:00

Compare commits

...

54 Commits

Author SHA1 Message Date
f25133fa71 WIP: waveform isort 2025-02-10 12:31:15 +01:00
750d95de4c WIP: DAP combo isort 2025-02-10 12:31:06 +01:00
1cefa1389b WIP: Curve prototype isort 2025-02-10 12:30:54 +01:00
7df40ae8c1 WIP: Expansion isort 2025-02-10 12:30:40 +01:00
12e7131ced WIP: Jupyter isort 2025-02-10 12:30:26 +01:00
d43ac980ad WIP: curve setting prototyping 2025-02-10 12:29:24 +01:00
5c3516cefa WIP: Expansion Panel demo 2025-02-10 12:28:38 +01:00
384beba521 WIP Jupyter Console Window 2025-02-10 12:27:51 +01:00
99b8b82fbc WIP DAP COMBO BOX: inheritance is changed to QWidget 2025-02-04 14:47:47 +01:00
d4397f0b1a WIP EXPANSION PANEL: Color works correct with qcolor and hex 2025-01-31 20:46:31 +01:00
4fb5922b4a WIP EXPANSION PANEL: Plugin accept other widgets 2025-01-31 20:27:30 +01:00
6d8bc4a750 WIP EXPANSION PANEL: Basic expantion panel demo 2025-01-31 20:15:32 +01:00
b6ef1fd625 WIP Waveform changed the x_mode for suffix correct propagation 2025-01-31 13:26:45 +01:00
b99bef799f WIP Plot base added suffix option 2025-01-31 13:26:04 +01:00
40a616fa35 WIP Waveform fix roi unhook where there are no roi curves 2025-01-31 12:10:53 +01:00
8b72af7322 WIP Waveform fix roi unhook to fit whole region after unhook 2025-01-31 11:54:10 +01:00
8828c81ff1 WIP Waveform RPC added 2025-01-31 11:51:54 +01:00
7965ea5014 WIP Curve setting prototype 2025-01-31 11:51:54 +01:00
328681017d WIP Waveform added to the dock area 2025-01-31 11:51:54 +01:00
47185117a3 WIP Waveform Plugin cathergory for next gen 2025-01-31 11:51:54 +01:00
ee9fbdb178 WIP Waveform designable property False 2025-01-31 11:51:54 +01:00
88cd84a086 WIP ROI manager isort 2025-01-31 11:51:54 +01:00
3046fda738 WIP Waveform when dap curve is removed, it is also removed from the dap summary 2025-01-31 11:51:54 +01:00
90a27f2b07 WIP Waveform signal for async and sync update adjusted 2025-01-31 11:51:54 +01:00
3cffe81d6b WIP roi fixed for dap 2025-01-31 11:51:54 +01:00
e469260b32 WIP Waveform lmfit dialog added 2025-01-31 11:51:54 +01:00
660db95d9f WIP fix(lmfit_dialog_vertical): vertical sizePolicy fixed 2025-01-31 11:51:54 +01:00
df9f76020c WIP Curve clean up TODOs 2025-01-31 11:51:54 +01:00
ac8a51e821 WIP ROI manager for Waveform, however DAP do not update correctly, DO NOT PUT INTO PLOTBASE MR 2025-01-31 11:51:54 +01:00
995960e590 WIP minor cleanup 2025-01-31 11:51:54 +01:00
4b454ecdff WIP dap is updated after adding curve with plot 2025-01-31 11:51:54 +01:00
9e9399db23 WIP curve json setter done 2025-01-31 11:51:54 +01:00
a5cf9319d7 WIP Dap block temporary disabled, timeout has to be implemented to BECProxy 2025-01-31 11:51:54 +01:00
27308547c0 WIP plot logic based on cinfig creation 2025-01-31 11:51:54 +01:00
1686c2a399 WIP pre changing to curve config genration instead of mixed logic 2025-01-31 11:51:54 +01:00
f7d54b1068 WIP dap works, however no timestamps fitting, dap adding logic can be improved 2025-01-31 11:51:54 +01:00
a990ad4bf4 WIP Dap implemnetation work in progress 2025-01-31 11:51:54 +01:00
627ac91f55 WIP Waveform sync and async operation working 2025-01-31 11:51:54 +01:00
03d0dbb7f5 WIP async readback works 2025-01-31 11:51:54 +01:00
4eda839948 WIP X mode works for sync curves 2025-01-31 11:51:54 +01:00
1139eefb66 WIP adjustments of curve pydantic 2025-01-31 11:51:54 +01:00
52dfbf357a WIP sync curves updates 2025-01-31 11:51:54 +01:00
3f758e4b08 WIP Waveform basic custom curve management added and tested in designer 2025-01-31 11:51:54 +01:00
f7763afc7e WIP Jupiter window added debug area for waveform 2025-01-31 11:51:54 +01:00
7c46e1075a WIP Curve config reviewed and commented 2025-01-31 11:51:54 +01:00
ca4ab59361 WIP Waveform plugin added 2025-01-31 11:51:53 +01:00
b3521d9551 WIP waveform template extend 2025-01-31 11:51:53 +01:00
cec8c115b6 WIP demo waveforms isorted 2025-01-31 11:51:53 +01:00
00a0335885 WIP Curve copied 1-1 2025-01-31 11:51:53 +01:00
c2bb6ad21d WIP Waveform Demo with curve object 2025-01-31 11:51:53 +01:00
ce8c7d7a96 WIP Real Waveform started template 2025-01-31 11:51:53 +01:00
34b309e46d WIP Dialog is made more general and Dialog widget is reusable 2025-01-31 11:51:53 +01:00
b4836eeabe WIP Demo works as expected with dialog for setting in designer 2025-01-31 11:51:53 +01:00
f4202427ad WIP Demo waveform plugin with json curves 2025-01-31 11:51:53 +01:00
41 changed files with 3736 additions and 273 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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:

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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