From 74b2ebb0727fcd38b71196e3d79a119ce85d6833 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 3 Jun 2026 12:27:27 -0600 Subject: Remove dead signal_builder.py Superseded by ui/windows/signals_window.py; no longer imported anywhere. Co-Authored-By: Claude Sonnet 4.6 --- ui/signal_builder.py | 605 --------------------------------------------------- 1 file changed, 605 deletions(-) delete mode 100644 ui/signal_builder.py (limited to 'ui/signal_builder.py') diff --git a/ui/signal_builder.py b/ui/signal_builder.py deleted file mode 100644 index a84d861..0000000 --- a/ui/signal_builder.py +++ /dev/null @@ -1,605 +0,0 @@ -""" -ui/signal_builder.py - -Signal Builder — operator-facing window for: - - Tab 1: FILTERS — add/remove/reorder filter stages per channel - Tab 2: DERIVED — create virtual channels (velocity, acceleration, - power, RMS, expression, custom Python) - Tab 3: CHANNELS — show/hide raw channels (replaces readout_panel settings) - -All changes are applied immediately to the SignalProcessor. -""" - -from __future__ import annotations -import traceback -from copy import deepcopy -from typing import List, Optional, Tuple - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QScrollArea, QFrame, QTabWidget, QComboBox, QLineEdit, - QDoubleSpinBox, QSpinBox, QCheckBox, QTextEdit, QGroupBox, - QFormLayout, QToolButton, QSizePolicy, QMessageBox, - QSplitter, QListWidget, QListWidgetItem, -) -from PyQt6.QtCore import Qt, pyqtSignal, QSize -from PyQt6.QtGui import QFont, QColor, QCloseEvent - -from devices.device_registry import DeviceRegistry -from core.signal_processor import ( - SignalProcessor, ChannelPipeline, DerivedChannel, - FilterBase, MovingAverageFilter, MedianFilter, LowPassFilter, - HighPassFilter, ScaleOffsetFilter, DerivativeFilter, IntegralFilter, - FILTER_CLASSES, -) - -# ── Color palette for new derived channels ───────────────────────────────── -_DERIVED_COLORS = [ - "#f72585", "#7209b7", "#3a0ca3", "#4361ee", - "#4cc9f0", "#f77f00", "#d62828", "#588157", -] - -# ══════════════════════════════════════════════════════════════════════════════ -# Helpers -# ══════════════════════════════════════════════════════════════════════════════ - -def _make_channel_combo(registry: DeviceRegistry, - include_derived: bool = False, - processor: Optional[SignalProcessor] = None) -> QComboBox: - cb = QComboBox() - for dev in registry.all_instances(): - for ch in dev.info.channels: - cb.addItem(f"{dev.info.device_id} / {ch.channel_id} ({ch.name})", - userData=(dev.info.device_id, ch.channel_id)) - if include_derived and processor: - for dc in processor.get_derived(): - cb.addItem(f"[derived] {dc.channel_id} ({dc.name})", - userData=("derived", dc.channel_id)) - return cb - - -# ══════════════════════════════════════════════════════════════════════════════ -# Filter stage row -# ══════════════════════════════════════════════════════════════════════════════ - -class FilterRow(QFrame): - removed = pyqtSignal(object) - changed = pyqtSignal() - - _PARAM_SPECS = { - "moving_average": [("window", "int", 10)], - "median": [("window", "int", 5)], - "low_pass": [("alpha", "float", 0.1)], - "high_pass": [("alpha", "float", 0.9)], - "scale_offset": [("scale", "float", 1.0), ("offset", "float", 0.0)], - "derivative": [], - "integral": [], - } - - def __init__(self, filt: FilterBase): - super().__init__() - self.filt = filt - self.setObjectName("traceRow") - self._build() - - def _build(self): - lay = QHBoxLayout(self) - lay.setContentsMargins(6, 4, 6, 4) - lay.setSpacing(8) - - type_lbl = QLabel(self.filt.name.replace("_", " ").title()) - type_lbl.setObjectName("traceSource") - type_lbl.setMinimumWidth(110) - lay.addWidget(type_lbl) - - self._param_widgets = {} - for pname, ptype, default in self._PARAM_SPECS.get(self.filt.name, []): - lbl = QLabel(f"{pname}:") - lbl.setObjectName("pwmLabel") - lay.addWidget(lbl) - if ptype == "int": - w = QSpinBox(); w.setRange(1, 10000); w.setValue(int(self.filt.params.get(pname, default))) - w.setFixedWidth(64); w.setObjectName("traceWidthSpin") - w.valueChanged.connect(lambda v, p=pname: self._update_param(p, v)) - else: - w = QDoubleSpinBox(); w.setRange(-1e9, 1e9); w.setDecimals(4) - w.setValue(float(self.filt.params.get(pname, default))) - w.setFixedWidth(80); w.setObjectName("traceWidthSpin") - w.valueChanged.connect(lambda v, p=pname: self._update_param(p, v)) - lay.addWidget(w) - self._param_widgets[pname] = w - - lay.addStretch() - rm = QToolButton(); rm.setText("✕"); rm.setObjectName("traceRemoveBtn") - rm.setFixedSize(22, 22); rm.clicked.connect(lambda: self.removed.emit(self)) - lay.addWidget(rm) - - def _update_param(self, name: str, value): - self.filt.params[name] = value - # Re-instantiate filter with new params - cls = FILTER_CLASSES.get(self.filt.name) - if cls: - new_filt = cls(**self.filt.params) - self.filt.__dict__.update(new_filt.__dict__) - self.changed.emit() - - -# ══════════════════════════════════════════════════════════════════════════════ -# Filter pipeline editor for one channel -# ══════════════════════════════════════════════════════════════════════════════ - -class PipelineEditor(QWidget): - pipeline_changed = pyqtSignal() - - def __init__(self, pipeline: ChannelPipeline, processor: SignalProcessor): - super().__init__() - self.pipeline = pipeline - self.processor = processor - self._rows: List[FilterRow] = [] - self._build() - - def _build(self): - root = QVBoxLayout(self); root.setContentsMargins(0, 0, 0, 6) - root.setSpacing(4) - - # Enable toggle - top = QHBoxLayout() - self._en_chk = QCheckBox("Pipeline enabled") - self._en_chk.setChecked(self.pipeline.enabled) - self._en_chk.toggled.connect(self._on_enable) - top.addWidget(self._en_chk) - top.addStretch() - root.addLayout(top) - - # Filter rows - self._rows_widget = QWidget() - self._rows_lay = QVBoxLayout(self._rows_widget) - self._rows_lay.setContentsMargins(0, 0, 0, 0); self._rows_lay.setSpacing(3) - for f in self.pipeline.filters: - self._add_row(f) - root.addWidget(self._rows_widget) - - # Add filter bar - add_bar = QHBoxLayout() - self._filter_cb = QComboBox() - self._filter_cb.addItems([k.replace("_"," ").title() for k in FILTER_CLASSES]) - self._filter_cb.setObjectName("channelPickerCb") - add_bar.addWidget(self._filter_cb, 1) - add_btn = QPushButton("+ Add Filter") - add_btn.setObjectName("addTraceBtn") - add_btn.clicked.connect(self._on_add_filter) - add_bar.addWidget(add_btn) - root.addLayout(add_bar) - - def _add_row(self, filt: FilterBase): - row = FilterRow(filt) - row.removed.connect(self._remove_row) - row.changed.connect(self._push) - self._rows.append(row) - self._rows_lay.addWidget(row) - - def _remove_row(self, row: FilterRow): - self.pipeline.filters.remove(row.filt) - self._rows.remove(row) - self._rows_lay.removeWidget(row); row.deleteLater() - self._push() - - def _on_add_filter(self): - key = list(FILTER_CLASSES.keys())[self._filter_cb.currentIndex()] - filt = FILTER_CLASSES[key]() - self.pipeline.filters.append(filt) - self._add_row(filt); self._push() - - def _on_enable(self, v: bool): - self.pipeline.enabled = v; self._push() - - def _push(self): - self.processor.set_pipeline(self.pipeline) - self.pipeline_changed.emit() - - -# ══════════════════════════════════════════════════════════════════════════════ -# Derived channel editor row -# ══════════════════════════════════════════════════════════════════════════════ - -class DerivedEditor(QFrame): - removed = pyqtSignal(object) - changed = pyqtSignal(object) # DerivedChannel - - _BUILTIN_KINDS = ["velocity", "acceleration", "power", "rms", "difference", "sum"] - _SCRIPT_KINDS = ["expression", "function", "custom_script"] - _ALL_KINDS = _BUILTIN_KINDS + _SCRIPT_KINDS - - _KIND_HELP = { - "velocity": "1 source: displacement channel → dy/dt", - "acceleration": "1 source: displacement channel → d²y/dt²", - "power": "2 sources: voltage, current → V×I", - "rms": "1 source → rolling RMS (set window in params)", - "difference": "2 sources → source[0] − source[1]", - "sum": "N sources → Σ sources", - "expression": 'Single line. x = list of source values, t = time.\nExample: x[0] * 2 + x[1]', - "function": "def compute(x, t):\n # x = list of source values\n return x[0] * 2", - "custom_script": "Full script. Must define:\ndef compute(x, t):\n ...\n return value", - } - - def __init__(self, dc: DerivedChannel, registry: DeviceRegistry, - processor: SignalProcessor, color_idx: int = 0): - super().__init__() - self.dc = dc - self.registry = registry - self.processor = processor - self._color_idx = color_idx - self.setObjectName("plotBlock") - self._build() - - def _build(self): - outer = QVBoxLayout(self); outer.setContentsMargins(0, 0, 0, 0); outer.setSpacing(0) - - # ── Header ───────────────────────────────────────────────────── - hdr = QWidget(); hdr.setObjectName("plotBlockHeader"); hdr.setFixedHeight(32) - hl = QHBoxLayout(hdr); hl.setContentsMargins(8, 0, 6, 0) - - self._name_edit = QLineEdit(self.dc.name) - self._name_edit.setObjectName("plotBlockTitle") - self._name_edit.textChanged.connect(lambda t: setattr(self.dc, "name", t)) - hl.addWidget(self._name_edit, 1) - - self._en_chk = QCheckBox() - self._en_chk.setChecked(self.dc.enabled) - self._en_chk.setToolTip("Enable/disable") - self._en_chk.toggled.connect(self._on_enable) - hl.addWidget(self._en_chk) - - rm = QToolButton(); rm.setText("✕"); rm.setObjectName("plotRemoveBtn") - rm.setFixedSize(22, 22); rm.clicked.connect(lambda: self.removed.emit(self)) - hl.addWidget(rm) - outer.addWidget(hdr) - - # ── Body ─────────────────────────────────────────────────────── - body = QWidget(); body.setObjectName("plotBlockSettings") - bl = QVBoxLayout(body); bl.setContentsMargins(10, 8, 10, 10); bl.setSpacing(6) - - # Meta row: id, unit, color - meta = QHBoxLayout() - meta.addWidget(QLabel("ID:")) - self._id_edit = QLineEdit(self.dc.channel_id) - self._id_edit.setObjectName("traceLabel"); self._id_edit.setFixedWidth(90) - self._id_edit.textChanged.connect(lambda t: setattr(self.dc, "channel_id", t)) - meta.addWidget(self._id_edit) - meta.addWidget(QLabel("Unit:")) - self._unit_edit = QLineEdit(self.dc.unit) - self._unit_edit.setObjectName("traceLabel"); self._unit_edit.setFixedWidth(60) - self._unit_edit.textChanged.connect(lambda t: setattr(self.dc, "unit", t)) - meta.addWidget(self._unit_edit) - meta.addStretch() - bl.addLayout(meta) - - # Kind selector + help - kind_row = QHBoxLayout() - kind_row.addWidget(QLabel("Kind:")) - self._kind_cb = QComboBox(); self._kind_cb.setObjectName("channelPickerCb") - self._kind_cb.addItems(self._ALL_KINDS) - self._kind_cb.setCurrentText(self.dc.kind) - self._kind_cb.currentTextChanged.connect(self._on_kind_changed) - kind_row.addWidget(self._kind_cb) - kind_row.addStretch() - bl.addLayout(kind_row) - - self._help_lbl = QLabel(self._KIND_HELP.get(self.dc.kind, "")) - self._help_lbl.setObjectName("traceSource") - self._help_lbl.setWordWrap(True) - bl.addWidget(self._help_lbl) - - # Sources - src_grp = QGroupBox("Sources"); src_grp.setObjectName("cfgGlobalBar") - src_lay = QVBoxLayout(src_grp); src_lay.setContentsMargins(6,4,6,4); src_lay.setSpacing(4) - self._src_rows: List[QHBoxLayout] = [] - self._src_combos: List[QComboBox] = [] - self._src_container = QWidget() - self._src_lay = QVBoxLayout(self._src_container) - self._src_lay.setContentsMargins(0,0,0,0); self._src_lay.setSpacing(3) - for src in self.dc.sources: - self._add_source_row(src) - add_src_btn = QPushButton("+ Add Source") - add_src_btn.setObjectName("addTraceBtn") - add_src_btn.clicked.connect(self._add_source_row) - src_lay.addWidget(self._src_container) - src_lay.addWidget(add_src_btn) - bl.addWidget(src_grp) - - # Expression / Script editor (shown for script kinds only) - self._script_grp = QGroupBox("Code") - script_lay = QVBoxLayout(self._script_grp); script_lay.setContentsMargins(6,4,6,4) - self._code_edit = QTextEdit() - self._code_edit.setObjectName("codeEditor") - self._code_edit.setPlaceholderText("Enter expression or function body…") - self._code_edit.setMinimumHeight(80) - self._code_edit.setMaximumHeight(160) - if self.dc.kind == "expression": - self._code_edit.setPlainText(self.dc.expression) - else: - self._code_edit.setPlainText(self.dc.script) - script_lay.addWidget(self._code_edit) - bl.addWidget(self._script_grp) - - # Compile / Apply - apply_row = QHBoxLayout() - self._status_lbl = QLabel("") - self._status_lbl.setObjectName("traceSource") - apply_row.addWidget(self._status_lbl, 1) - apply_btn = QPushButton("✓ Compile & Apply") - apply_btn.setObjectName("applyButton") - apply_btn.clicked.connect(self._apply) - apply_row.addWidget(apply_btn) - bl.addLayout(apply_row) - - outer.addWidget(body) - self._update_script_visibility() - - def _add_source_row(self, src: Tuple[str, str] = None): - row = QHBoxLayout() - cb = _make_channel_combo(self.registry) - cb.setObjectName("channelPickerCb") - if src: - for i in range(cb.count()): - if cb.itemData(i) == src: - cb.setCurrentIndex(i); break - rm = QToolButton(); rm.setText("✕"); rm.setObjectName("traceRemoveBtn") - rm.setFixedSize(22, 22) - rm.clicked.connect(lambda: self._remove_source_row(row, cb)) - row.addWidget(cb, 1); row.addWidget(rm) - self._src_lay.addLayout(row) - self._src_combos.append(cb) - self._src_rows.append(row) - - def _remove_source_row(self, row, cb): - if cb in self._src_combos: - self._src_combos.remove(cb) - if row in self._src_rows: - self._src_rows.remove(row) - while row.count(): - item = row.takeAt(0) - if item.widget(): item.widget().deleteLater() - - def _on_kind_changed(self, kind: str): - self.dc.kind = kind - self._help_lbl.setText(self._KIND_HELP.get(kind, "")) - self._update_script_visibility() - - def _update_script_visibility(self): - show = self.dc.kind in self._SCRIPT_KINDS - self._script_grp.setVisible(show) - - def _on_enable(self, v: bool): - self.dc.enabled = v - self._apply() - - def _apply(self): - # Collect sources from combos - self.dc.sources = [cb.currentData() for cb in self._src_combos - if cb.currentData() is not None] - # Collect code - if self.dc.kind == "expression": - self.dc.expression = self._code_edit.toPlainText().strip() - elif self.dc.kind in ("function", "custom_script"): - self.dc.script = self._code_edit.toPlainText() - - err = self.processor.add_derived(self.dc) - if err: - self._status_lbl.setText(f"⚠ {err[:80]}") - self._status_lbl.setStyleSheet("color:#ef4444;") - else: - self._status_lbl.setText("✓ Applied") - self._status_lbl.setStyleSheet("color:#22c55e;") - self.changed.emit(self.dc) - - -# ══════════════════════════════════════════════════════════════════════════════ -# Channel visibility tab -# ══════════════════════════════════════════════════════════════════════════════ - -class ChannelVisibilityTab(QWidget): - visibility_changed = pyqtSignal() - - def __init__(self, registry: DeviceRegistry, processor: SignalProcessor): - super().__init__() - self.registry = registry - self.processor = processor - self._checks = {} # (dev_id, ch_id) -> QCheckBox - self._build() - - def _build(self): - scroll = QScrollArea(); scroll.setWidgetResizable(True) - scroll.setObjectName("deviceScroll") - container = QWidget() - lay = QVBoxLayout(container); lay.setContentsMargins(10,10,10,10); lay.setSpacing(6) - - for dev in self.registry.all_instances(): - grp = QGroupBox(f"{dev.info.icon} {dev.info.name} [{dev.info.device_id}]") - grp_lay = QVBoxLayout(grp); grp_lay.setSpacing(3) - for ch in dev.info.channels: - row = QHBoxLayout() - chk = QCheckBox(f"{ch.channel_id} — {ch.name}") - chk.setChecked(ch.enabled) - chk.setStyleSheet(f"color:{ch.color};") - unit_lbl = QLabel(ch.unit); unit_lbl.setObjectName("traceSource") - chk.toggled.connect(lambda v, c=ch: setattr(c, "enabled", v) or self.visibility_changed.emit()) - row.addWidget(chk, 1); row.addWidget(unit_lbl) - grp_lay.addLayout(row) - self._checks[(dev.info.device_id, ch.channel_id)] = chk - lay.addWidget(grp) - lay.addStretch() - scroll.setWidget(container) - root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0) - root.addWidget(scroll) - - -# ══════════════════════════════════════════════════════════════════════════════ -# SignalBuilderWindow — main floating window -# ══════════════════════════════════════════════════════════════════════════════ - -class SignalBuilderWindow(QWidget): - """ - Floating window with three tabs: - Filters | Derived Channels | Channel Visibility - """ - - pipeline_changed = pyqtSignal() - derived_changed = pyqtSignal() - visibility_changed = pyqtSignal() - closed = pyqtSignal() - - def __init__(self, registry: DeviceRegistry, - processor: SignalProcessor, parent=None): - super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.Tool) - self.registry = registry - self.processor = processor - self._pipeline_editors: dict = {} - self._derived_editors: List[DerivedEditor] = [] - self._color_idx = 0 - - self.setWindowTitle("Signal Builder") - self.setMinimumSize(640, 520) - self.resize(720, 620) - self._build() - - def _build(self): - root = QVBoxLayout(self); root.setContentsMargins(0,0,0,0); root.setSpacing(0) - - # Title bar - tb = QWidget(); tb.setObjectName("devWindowTitleBar"); tb.setFixedHeight(40) - tbl = QHBoxLayout(tb); tbl.setContentsMargins(12,0,12,0) - QLabel_t = QLabel("⚗ SIGNAL BUILDER"); QLabel_t.setObjectName("devWindowTitle") - tbl.addWidget(QLabel_t) - root.addWidget(tb) - - tabs = QTabWidget(); tabs.setObjectName("signalBuilderTabs") - root.addWidget(tabs, 1) - - # ── Tab 1: Filters ──────────────────────────────────────────── - tabs.addTab(self._build_filters_tab(), " Filters ") - - # ── Tab 2: Derived Channels ─────────────────────────────────── - tabs.addTab(self._build_derived_tab(), " Derived ") - - # ── Tab 3: Channel Visibility ───────────────────────────────── - vis_tab = ChannelVisibilityTab(self.registry, self.processor) - vis_tab.visibility_changed.connect(self.visibility_changed) - tabs.addTab(vis_tab, " Channels ") - - # ── Filters tab ─────────────────────────────────────────────────────── - - def _build_filters_tab(self): - w = QWidget() - lay = QVBoxLayout(w); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0) - - # Channel selector - top = QWidget(); top.setObjectName("cfgGlobalBar") - tl = QHBoxLayout(top); tl.setContentsMargins(10,6,10,6) - tl.addWidget(QLabel("Channel:")) - self._filter_ch_cb = _make_channel_combo(self.registry) - self._filter_ch_cb.setObjectName("channelPickerCb") - self._filter_ch_cb.currentIndexChanged.connect(self._on_filter_channel_changed) - tl.addWidget(self._filter_ch_cb, 1) - lay.addWidget(top) - - div = QFrame(); div.setFrameShape(QFrame.Shape.HLine); div.setObjectName("devWindowDivider") - lay.addWidget(div) - - # Pipeline editor area - scroll = QScrollArea(); scroll.setWidgetResizable(True); scroll.setObjectName("deviceScroll") - self._pipeline_container = QWidget() - self._pipeline_lay = QVBoxLayout(self._pipeline_container) - self._pipeline_lay.setContentsMargins(10,10,10,10); self._pipeline_lay.setSpacing(0) - self._pipeline_lay.addStretch() - scroll.setWidget(self._pipeline_container) - lay.addWidget(scroll, 1) - - # Trigger initial load - self._on_filter_channel_changed(0) - return w - - def _on_filter_channel_changed(self, _idx: int): - data = self._filter_ch_cb.currentData() - if not data: return - dev_id, ch_id = data - - # Clear old editor - while self._pipeline_lay.count() > 1: - item = self._pipeline_lay.takeAt(0) - if item.widget(): item.widget().deleteLater() - - # Get or create pipeline - pipeline = self.processor.get_pipeline(dev_id, ch_id) - if pipeline is None: - pipeline = ChannelPipeline(device_id=dev_id, channel_id=ch_id) - self.processor.set_pipeline(pipeline) - - editor = PipelineEditor(pipeline, self.processor) - editor.pipeline_changed.connect(self.pipeline_changed) - self._pipeline_lay.insertWidget(0, editor) - - # ── Derived tab ─────────────────────────────────────────────────────── - - def _build_derived_tab(self): - w = QWidget() - lay = QVBoxLayout(w); lay.setContentsMargins(0,0,0,0); lay.setSpacing(0) - - # Add derived button bar - top = QWidget(); top.setObjectName("cfgGlobalBar") - tl = QHBoxLayout(top); tl.setContentsMargins(10,6,10,6) - tl.addWidget(QLabel("New derived channel:")) - self._new_kind_cb = QComboBox(); self._new_kind_cb.setObjectName("channelPickerCb") - self._new_kind_cb.addItems(DerivedEditor._ALL_KINDS) - tl.addWidget(self._new_kind_cb) - add_btn = QPushButton("+ Create") - add_btn.setObjectName("addDeviceButton") - add_btn.clicked.connect(self._add_derived) - tl.addWidget(add_btn) - lay.addWidget(top) - - div = QFrame(); div.setFrameShape(QFrame.Shape.HLine); div.setObjectName("devWindowDivider") - lay.addWidget(div) - - # Derived editors scroll area - scroll = QScrollArea(); scroll.setWidgetResizable(True); scroll.setObjectName("deviceScroll") - self._derived_container = QWidget() - self._derived_lay = QVBoxLayout(self._derived_container) - self._derived_lay.setContentsMargins(10,10,10,10); self._derived_lay.setSpacing(10) - self._derived_lay.addStretch() - scroll.setWidget(self._derived_container) - lay.addWidget(scroll, 1) - - # Load existing derived channels - for dc in self.processor.get_derived(): - self._insert_derived_editor(dc) - - return w - - def _add_derived(self): - kind = self._new_kind_cb.currentText() - color = _DERIVED_COLORS[self._color_idx % len(_DERIVED_COLORS)] - self._color_idx += 1 - dc = DerivedChannel( - channel_id=f"derived_{len(self._derived_editors)}", - name=f"Derived {len(self._derived_editors) + 1}", - kind=kind, color=color, - ) - self._insert_derived_editor(dc) - - def _insert_derived_editor(self, dc: DerivedChannel): - editor = DerivedEditor(dc, self.registry, self.processor, self._color_idx) - editor.removed.connect(self._remove_derived_editor) - editor.changed.connect(lambda _: self.derived_changed.emit()) - self._derived_editors.append(editor) - self._derived_lay.insertWidget(self._derived_lay.count() - 1, editor) - - def _remove_derived_editor(self, editor: DerivedEditor): - self.processor.remove_derived(editor.dc.channel_id) - self._derived_editors.remove(editor) - self._derived_lay.removeWidget(editor); editor.deleteLater() - self.derived_changed.emit() - - def closeEvent(self, event: QCloseEvent): - self.closed.emit(); event.accept() -- cgit v1.2.3