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

WIP Dialog is made more general and Dialog widget is reusable

This commit is contained in:
2025-01-13 15:55:41 +01:00
parent b4836eeabe
commit 34b309e46d
4 changed files with 273 additions and 200 deletions

View File

@ -1,42 +1,62 @@
"""
waveform_plot.py
A minimal demonstration widget with multiple properties:
- deviceName (str)
- curvesJson (str)
- someFlag (bool)
Minimal WaveformPlot widget that has:
- a 'curvesJson' property
- a pyqtgraph PlotItem
- dummy data for each curve
It uses pyqtgraph to show dummy curves from 'curvesJson'.
"""
import json
import pyqtgraph as pg
from qtpy.QtCore import Property
from qtpy.QtCore import Property, QPointF
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.
Minimal demonstration of a multi-property widget:
- deviceName (string)
- curvesJson (string containing JSON)
- someFlag (boolean)
"""
ICON_NAME = "multiline_chart" # If you want to set an icon in the plugin
ICON_NAME = "multiline_chart" # For a designer icon, if desired
def __init__(self, parent=None):
super().__init__(parent)
self._curves_json = "[]" # Start with empty array
self._device_name = "MyDevice"
self._curves_json = "[]"
self._some_flag = False
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 = []
# ------------------------------------------------------------------------
# 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
@ -47,30 +67,42 @@ class WaveformPlot(QWidget):
curvesJson = Property(str, fget=getCurvesJson, fset=setCurvesJson)
# ------------------------------------------------------------------------
# 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):
"""
Parse the JSON, remove old plot items, create new ones with dummy data.
"""
# Remove old
# Remove existing PlotDataItems
for c in self._plot_curves:
self.plot_item.removeItem(c)
self._plot_curves.clear()
# Parse the JSON
# Try parse JSON
try:
data = json.loads(self._curves_json)
if not isinstance(data, list):
arr = json.loads(self._curves_json)
if not isinstance(arr, list):
raise ValueError("curvesJson must be a JSON list.")
except Exception:
# If parse fails, just do nothing
# If parsing fails, do nothing
return
# Create new PlotDataItems
for idx, cdef in enumerate(data):
# 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")
# Dummy data
x = [0, 1, 2, 3, 4]
y = [val + idx for val in x]
@ -79,6 +111,7 @@ class WaveformPlot(QWidget):
self._plot_curves.append(item)
# Optional standalone test
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
@ -86,8 +119,8 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
w = WaveformPlot()
w.curvesJson = json.dumps(
[{"label": "FirstCurve", "color": "red"}, {"label": "SecondCurve", "color": "green"}]
)
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,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 (
QDialog,
QVBoxLayout,
QHBoxLayout,
QWidget,
QDialogButtonBox,
QLabel,
QLineEdit,
QPushButton,
QCheckBox,
QColorDialog,
)
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

@ -1,21 +1,15 @@
"""
waveform_plot_plugin.py
Registers WaveformPlot with Qt Designer and installs the task-menu extension factory.
Registers WaveformPlot with Qt Designer,
including the WaveformPlotTaskMenu extension.
"""
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'>
@ -27,10 +21,14 @@ DOM_XML = """
<height>200</height>
</rect>
</property>
<property name='deviceName'>
<string>MyDevice</string>
</property>
<property name='someFlag'>
<bool>false</bool>
</property>
<property name='curvesJson'>
<stringlist>
<string>[{"label":"DefaultCurve","color":"red"}]</string>
</stringlist>
<string>[{"label": "DefaultCurve", "color": "red"}]</string>
</property>
</widget>
</ui>
@ -39,8 +37,8 @@ DOM_XML = """
class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
"""
Minimal plugin that exposes WaveformPlot to Qt Designer,
plus sets up the WaveformPlotTaskMenu extension.
Exposes WaveformPlot to Designer, plus sets up the Task Menu extension
for "Edit Configuration..." popup.
"""
def __init__(self):
@ -52,7 +50,7 @@ class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
return
self._initialized = True
# Register the Task Menu extension factory
# Register the TaskMenu extension
manager = form_editor.extensionManager()
if manager:
factory = WaveformPlotTaskMenuFactory(manager)
@ -70,8 +68,12 @@ class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
def group(self):
return "Waveform Widgets"
def icon(self):
# If you have a real icon, load it here
return QIcon()
def toolTip(self):
return "WaveformPlot with multiple curves"
return "A multi-property WaveformPlot example"
def whatsThis(self):
return self.toolTip()
@ -83,10 +85,6 @@ class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
return DOM_XML
def includeFile(self):
# The module path that Qt Designer uses in generated .ui -> .py code
# e.g. "my_package.waveform_plot"
# The Python import path for your waveforms
# E.g. "my_widgets.waveform.waveform_plot"
return __name__
def icon(self):
# If you have an icon, return QIcon(":/myicon.png") or similar
return QIcon()

View File

@ -1,184 +1,48 @@
"""
waveform_plot_taskmenu.py
Implements the dialog to edit WaveformPlot's 'curvesJson' property,
and the QPyDesignerTaskMenuExtension to integrate into Qt Designer.
Task Menu extension for Qt Designer.
It attaches "Edit Configuration..." to the WaveformPlot,
launching WaveformPlotConfigDialog.
"""
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 qtpy.QtCore import 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)
from bec_widgets.widgets.plots_next_gen.waveform.demo.waveform_plot_config_dialog import (
WaveformPlotConfigDialog,
)
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)
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 _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)
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):
"""
The factory that creates a WaveformPlotTaskMenu if the widget is a WaveformPlot.
Creates a WaveformPlotTaskMenu if the widget is an instance of WaveformPlot
and the requested extension is 'TaskMenu'.
"""
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):
if iid == "org.qt-project.Qt.Designer.TaskMenu" and isinstance(obj, WaveformPlot):
return WaveformPlotTaskMenu(obj, parent)
return None