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:
0
bec_widgets/widgets/plots_next_gen/__init__.py
Normal file
0
bec_widgets/widgets/plots_next_gen/__init__.py
Normal 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())
|
||||
|
@ -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_())
|
@ -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()
|
@ -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
|
@ -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_())
|
@ -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>
|
||||
"""
|
Reference in New Issue
Block a user