0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

WIP Demo works as expected with dialog for setting in designer

This commit is contained in:
2025-01-13 13:59:22 +01:00
parent f4202427ad
commit b4836eeabe
9 changed files with 372 additions and 210 deletions

View File

@ -6,7 +6,9 @@ def main(): # pragma: no cover
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.plots_next_gen.waveform.waveform_plot_plugin import WaveformPlotPlugin
from bec_widgets.widgets.plots_next_gen.waveform.demo.waveform_plot_plugin import (
WaveformPlotPlugin,
)
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlotPlugin())

View File

@ -0,0 +1,93 @@
"""
waveform_plot.py
Minimal WaveformPlot widget that has:
- a 'curvesJson' property
- a pyqtgraph PlotItem
- dummy data for each curve
"""
import json
import pyqtgraph as pg
from qtpy.QtCore import Property
from qtpy.QtWidgets import QWidget, QVBoxLayout
class WaveformPlot(QWidget):
"""
Minimal demonstration widget with a 'curvesJson' property.
In your real code, you'd subclass your PlotBase, but let's keep it plain.
"""
ICON_NAME = "multiline_chart" # If you want to set an icon in the plugin
def __init__(self, parent=None):
super().__init__(parent)
self._curves_json = "[]" # Start with empty array
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# A PyQtGraph PlotItem inside a PlotWidget
self.plot_item = pg.PlotItem()
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
layout.addWidget(self.plot_widget)
self.plot_item.addLegend()
# Keep track of the actual PlotDataItems
self._plot_curves = []
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)
def _rebuild_curves(self):
"""
Parse the JSON, remove old plot items, create new ones with dummy data.
"""
# Remove old
for c in self._plot_curves:
self.plot_item.removeItem(c)
self._plot_curves.clear()
# Parse the JSON
try:
data = json.loads(self._curves_json)
if not isinstance(data, list):
raise ValueError("curvesJson must be a JSON list.")
except Exception:
# If parse fails, just do nothing
return
# Create new PlotDataItems
for idx, cdef in enumerate(data):
label = cdef.get("label", f"Curve{idx + 1}")
color = cdef.get("color", "blue")
# Dummy data
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)
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
import json
app = QApplication(sys.argv)
w = WaveformPlot()
w.curvesJson = json.dumps(
[{"label": "FirstCurve", "color": "red"}, {"label": "SecondCurve", "color": "green"}]
)
w.show()
sys.exit(app.exec_())

View File

@ -0,0 +1,92 @@
"""
waveform_plot_plugin.py
Registers WaveformPlot with Qt Designer and installs the task-menu extension factory.
"""
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from .waveform_demo import WaveformPlot
# Import your classes
from .waveform_plot_taskmenu import WaveformPlotTaskMenuFactory
# If you have an icon in resources or a function:
# from .some_icon_provider import get_designer_icon
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='curvesJson'>
<stringlist>
<string>[{"label":"DefaultCurve","color":"red"}]</string>
</stringlist>
</property>
</widget>
</ui>
"""
class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
"""
Minimal plugin that exposes WaveformPlot to Qt Designer,
plus sets up the WaveformPlotTaskMenu extension.
"""
def __init__(self):
super().__init__()
self._initialized = False
def initialize(self, form_editor):
if self._initialized:
return
self._initialized = True
# Register the Task Menu extension factory
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 toolTip(self):
return "WaveformPlot with multiple curves"
def whatsThis(self):
return self.toolTip()
def isContainer(self):
return False
def domXml(self):
return DOM_XML
def includeFile(self):
# The module path that Qt Designer uses in generated .ui -> .py code
# e.g. "my_package.waveform_plot"
return __name__
def icon(self):
# If you have an icon, return QIcon(":/myicon.png") or similar
return QIcon()

View File

@ -0,0 +1,184 @@
"""
waveform_plot_taskmenu.py
Implements the dialog to edit WaveformPlot's 'curvesJson' property,
and the QPyDesignerTaskMenuExtension to integrate into Qt Designer.
"""
import json
from qtpy.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
from qtpy.QtGui import QAction
from qtpy.QtWidgets import (
QDialog,
QDialogButtonBox,
QVBoxLayout,
QHBoxLayout,
QWidget,
QPushButton,
QLineEdit,
QColorDialog,
)
from qtpy.QtCore import Qt, Slot
from bec_widgets.widgets.plots_next_gen.waveform.demo.waveform_demo import WaveformPlot
# We'll import the widget class name for type-checking
class WaveformCurvesDialog(QDialog):
"""
A dialog allowing the user to edit the JSON for curves.
We store an internal list of dicts with "label", "color", etc.
"""
def __init__(self, curves_json: str, parent=None):
super().__init__(parent, Qt.WindowTitleHint | Qt.WindowSystemMenuHint)
self.setWindowTitle("Edit Curves")
self.resize(400, 300)
self._data = []
# Try to parse incoming JSON
try:
arr = json.loads(curves_json)
if isinstance(arr, list):
self._data = arr
except:
pass
main_layout = QVBoxLayout(self)
self.setLayout(main_layout)
# Layout for the "list" of curve rows
self.curves_layout = QVBoxLayout()
main_layout.addLayout(self.curves_layout)
# "Add curve" button
add_btn = QPushButton("Add Curve")
add_btn.clicked.connect(self._on_add_curve)
main_layout.addWidget(add_btn)
# OK/Cancel
box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
box.accepted.connect(self.accept)
box.rejected.connect(self.reject)
main_layout.addWidget(box)
# Build row widgets
self._refresh_rows()
def _refresh_rows(self):
# Clear existing row widgets
while True:
item = self.curves_layout.takeAt(0)
if not item:
break
w = item.widget()
if w:
w.deleteLater()
# Rebuild a row for each entry
for idx, cinfo in enumerate(self._data):
row_widget = self._create_curve_row(idx, cinfo)
self.curves_layout.addWidget(row_widget)
def _create_curve_row(self, idx: int, info: dict):
container = QWidget(self)
hl = QHBoxLayout(container)
# label text
label_edit = QLineEdit(info.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 button
color_btn = QPushButton(info.get("color", "Pick Color"), container)
color_btn.clicked.connect(lambda _=None, i=idx: self._pick_color(i))
hl.addWidget(color_btn)
# remove button
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._data.append({"label": "NewCurve", "color": "blue"})
self._refresh_rows()
def _on_remove_curve(self, idx: int):
if 0 <= idx < len(self._data):
self._data.pop(idx)
self._refresh_rows()
def _on_label_changed(self, idx: int, text: str):
if 0 <= idx < len(self._data):
self._data[idx]["label"] = text
def _pick_color(self, idx: int):
if 0 <= idx < len(self._data):
old_col = self._data[idx].get("color", "#ff0000")
dlg = QColorDialog(self)
dlg.setCurrentColor(dlg.currentColor()) # Or parse old_col if you wish
if dlg.exec_() == QDialog.Accepted:
c = dlg.selectedColor().name()
self._data[idx]["color"] = c
self._refresh_rows()
def curves_json(self) -> str:
"""Return the final JSON after user edits."""
return json.dumps(self._data, indent=2)
class WaveformPlotTaskMenu(QPyDesignerTaskMenuExtension):
"""
Implements a "Task Menu" action for WaveformPlot in Qt Designer:
'Edit Curves...' which opens WaveformCurvesDialog.
"""
def __init__(self, widget: WaveformPlot, parent=None):
super().__init__(parent)
self._widget = widget
self._edit_action = QAction("Edit Curves...", self)
self._edit_action.triggered.connect(self._edit_curves)
def taskActions(self):
return [self._edit_action]
def preferredEditAction(self):
return self._edit_action
@Slot()
def _edit_curves(self):
# read current property
old_json = self._widget.curvesJson
# pop up the dialog
dlg = WaveformCurvesDialog(old_json, parent=self._widget)
if dlg.exec_() == QDialog.Accepted:
# get new JSON
new_json = dlg.curves_json()
# set property so Designer picks it up
self._widget.setProperty("curvesJson", new_json)
class WaveformPlotTaskMenuFactory(QExtensionFactory):
"""
The factory that creates a WaveformPlotTaskMenu if the widget is a WaveformPlot.
"""
def __init__(self, parent=None):
super().__init__(parent)
@staticmethod
def task_menu_iid():
return "org.qt-project.Qt.Designer.TaskMenu"
def createExtension(self, obj, iid, parent):
# Check we are asked for a TaskMenu extension and the widget is our WaveformPlot
if iid == self.task_menu_iid() and isinstance(obj, WaveformPlot):
return WaveformPlotTaskMenu(obj, parent)
return None

View File

@ -1,138 +0,0 @@
from __future__ import annotations
import json
from typing import Optional
import pyqtgraph as pg
from qtpy.QtCore import Property, Signal
from qtpy.QtWidgets import QApplication, QWidget, QVBoxLayout
from bec_widgets.utils.bec_widget import BECWidget
##############################################################################
# MinimalPlotBase (a stand-in for your real PlotBase)
##############################################################################
class MinimalPlotBase(QWidget):
"""
A trivial container that just holds a single pyqtgraph PlotWidget.
In your actual code, replace this with your real 'PlotBase' class.
"""
def __init__(self, parent=None):
super().__init__(parent)
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_item.addLegend()
##############################################################################
# WaveformPlot subclass that uses a QProperty for multiple curve definitions
##############################################################################
class WaveformPlot(BECWidget, MinimalPlotBase):
"""
Demonstrates a 'curvesJson' QProperty that holds an array of curve definitions.
Each array entry might look like:
{
"label": "MyCurve",
"color": "#ff0000"
// optionally: "device": "devA", "signal": "sigB", ...
}
On setting 'curvesJson', the widget parses the JSON, clears old curves,
and creates new PlotDataItems with dummy data (for demonstration).
"""
PLUGIN = True
# Signal to notify when any property changes (optional convenience signal)
property_changed = Signal(str, object)
# We'll store our JSON string in this private attribute
_curves_json: str = ""
def __init__(self, parent: Optional[QWidget] = None):
super().__init__()
MinimalPlotBase.__init__(self, parent=parent)
# Keep track of the PlotDataItem objects so we can remove them on update
self._waveform_curves = []
# ------------------------------------------------------------------------
# QProperty: curvesJson
# ------------------------------------------------------------------------
def getCurvesJson(self) -> str:
"""Return the JSON string describing all curves."""
return self._curves_json
def setCurvesJson(self, new_json: str):
"""Set a new JSON definition for the curves; parse and rebuild them."""
if self._curves_json != new_json:
self._curves_json = new_json
# Emit a signal if you like
self.property_changed.emit("curvesJson", new_json)
# Rebuild the curves
self._build_curves_from_json(new_json)
# The actual QProperty for Designer (or QSettings) to see
curvesJson = Property(str, fget=getCurvesJson, fset=setCurvesJson)
# ------------------------------------------------------------------------
# Build or rebuild the curves from the JSON definition
# ------------------------------------------------------------------------
def _build_curves_from_json(self, json_str: str):
"""
Clears out any existing PlotDataItems,
then parses the JSON and creates new items.
Here we just do dummy data to show them visually.
"""
# 1. Remove old items
for c in self._waveform_curves:
self.plot_item.removeItem(c)
self._waveform_curves.clear()
# 2. Parse the JSON
try:
curve_defs = json.loads(json_str)
if not isinstance(curve_defs, list):
raise ValueError("curvesJson must be a JSON list of objects.")
except Exception as e:
print(f"[WaveformPlot] Error parsing curvesJson: {e}")
return
# 3. Create new PlotDataItems for each definition
for idx, cdef in enumerate(curve_defs):
label = cdef.get("label", f"Curve{idx + 1}")
color = cdef.get("color", "blue")
# In your real code, you might also parse "device", "signal", etc.
# Dummy data (just to show distinct lines)
xdata = [0, 1, 2, 3, 4]
ybase = idx * 3
ydata = [ybase + 0, ybase + 1, ybase + 2, ybase + 1, ybase + 0]
curve_item = pg.PlotDataItem(xdata, ydata, pen=color, name=label)
self.plot_item.addItem(curve_item)
self._waveform_curves.append(curve_item)
##############################################################################
# Standalone test
##############################################################################
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
widget = WaveformPlot()
widget.setWindowTitle("Minimal multiple-curves example with QProperty + PlotBase-like class")
# Example JSON: two curves with different color/labels
example_json = json.dumps(
[{"label": "Alpha", "color": "red"}, {"label": "Beta", "color": "#00ff00"}]
)
widget.curvesJson = example_json
widget.show()
sys.exit(app.exec_())

View File

@ -1,71 +0,0 @@
# 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_demo import WaveformPlot
class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
"""
Minimal custom widget plugin for Qt Designer.
- Creates WaveformPlot
- Provides DOM XML
- Installs our TaskMenu extension factory
"""
def __init__(self):
super().__init__()
self._initialized = False
self._extension_factory = None
def initialize(self, form_editor):
if self._initialized:
return
self._initialized = True
# Register our task menu extension factory with the form editor
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 "MyPlotWidgets"
def toolTip(self):
return "WaveformPlot with multiple curves"
def whatsThis(self):
return self.toolTip()
def includeFile(self):
# Typically the python module name where WaveformPlot is defined
# (used in the generated ui code)
return __name__
def icon(self):
# Provide an icon if desired
# e.g. QIcon(":/icons/waveform.png")
return None
def isContainer(self):
return False
def domXml(self):
return """
<ui language='c++'>
<widget class='WaveformPlot' name='waveformPlot'>
</widget>
</ui>
"""