summaryrefslogtreecommitdiff
path: root/ui/windows
diff options
context:
space:
mode:
Diffstat (limited to 'ui/windows')
-rw-r--r--ui/windows/config_window.py104
1 files changed, 78 insertions, 26 deletions
diff --git a/ui/windows/config_window.py b/ui/windows/config_window.py
index 7ec0355..9b864bf 100644
--- a/ui/windows/config_window.py
+++ b/ui/windows/config_window.py
@@ -8,12 +8,13 @@ Devices tab layout (2 columns):
Right — signal table (Device / Signal ID / Dir / On / Label / Unit / Min / Max)
Channels tab layout (2 columns):
- Left — Namespace browser (Signals / Channels / vars). Mostly a read-only
- reference, with one exception: each Signal row has a ⊕ button to
- create a passthrough Channel from it directly — the quick "make
- this Signal a Channel" path. See ui/namespace_panel.py — only
- this instance opts into that button (on_create_channel), not the
- read-only dockable Namespace panel on the main window.
+ Left — Namespace browser (Signals / Channels / vars, ui/namespace_panel.py
+ — plain read-only reference, reused as-is for the dockable
+ Namespace panel on the main window too) plus a "Create Channel"
+ button below it, local to this tab only: uses whichever rows are
+ selected (multi-select) as the new Channel's sources, or creates
+ an empty Channel if nothing's selected. See _on_create_channel_
+ clicked().
Right — derived / virtual channels — operations (filters/calibration/math/script)
are configured per Channel here, not on the Signals side.
"""
@@ -471,10 +472,20 @@ class ConfigWindow(QWidget):
ns_div = QFrame(); ns_div.setFrameShape(QFrame.Shape.HLine)
ns_div.setObjectName("devWindowDivider"); ns_l.addWidget(ns_div)
- self._namespace_panel = NamespacePanel(self.registry, self.processor,
- on_create_channel=self._on_create_channel)
+ self._namespace_panel = NamespacePanel(self.registry, self.processor)
self.processor.processed_data.connect(self._namespace_panel.on_processed_data)
ns_l.addWidget(self._namespace_panel, 1)
+
+ ns_create_bar = QWidget(); ns_create_bar.setObjectName("cfgGlobalBar")
+ nc_lay = QHBoxLayout(ns_create_bar); nc_lay.setContentsMargins(10, 7, 10, 7); nc_lay.setSpacing(6)
+ nc_lay.addStretch()
+ create_btn = QPushButton("+ Create Channel"); create_btn.setObjectName("addTraceBtn")
+ create_btn.setToolTip("Selected rows become the new Channel's sources — "
+ "none selected creates an empty Channel")
+ create_btn.clicked.connect(self._on_create_channel_clicked)
+ nc_lay.addWidget(create_btn)
+ ns_l.addWidget(ns_create_bar)
+
splitter.addWidget(ns_w)
# Right: derived channels only
@@ -489,29 +500,70 @@ class ConfigWindow(QWidget):
inner_splitters[0].widget(0).setVisible(False)
splitter.addWidget(self._pipeline_tab)
- # Channels side larger by default
- splitter.setStretchFactor(0, 1)
- splitter.setStretchFactor(1, 3)
+ # Namespace starts at its content-driven natural width (see
+ # NamespacePanel.natural_width() — a query only, not an enforced
+ # minimum: a hard setMinimumWidth() here used to fight QMainWindow's
+ # dock layout when this same NamespacePanel class is also docked
+ # stacked/tabbed with the Control Panel dock, ui/main_window.py).
+ # Stretch factor 0 means Channels absorbs window-resize growth;
+ # dragging the divider narrower than natural_width() just lets
+ # Source etc. scroll — the user's call.
+ splitter.setStretchFactor(0, 0)
+ splitter.setStretchFactor(1, 1)
+
+ def _init_ns_split():
+ ns_w0 = self._namespace_panel.natural_width()
+ total = splitter.width()
+ splitter.setSizes([ns_w0, max(1, total - ns_w0)])
+
+ QTimer.singleShot(0, _init_ns_split)
self.tabs.addTab(w, " Channels ")
- def _on_create_channel(self, dev_id: str, ch_id: str, ch_name: str, unit: str, color: str):
- """Create a passthrough Channel (kind="none") from this Signal,
- named after it — the "1 Signal -> 1 Channel" quick path, triggered
- from the ⊕ button on a Signal row in the Channels tab's Namespace
- panel (ui/namespace_panel.py). No-ops if a passthrough Channel for
- this exact Signal already exists."""
- existing = self.processor.get_derived()
- for dc in existing:
- if dc.kind == "none" and dc.sources == [(dev_id, ch_id)]:
- return
+ def _on_create_channel_clicked(self):
+ """Create Channel button below the Channels tab's Namespace panel —
+ local to this tab (see module docstring). Uses whatever Signal/
+ Channel rows are currently selected (ui/namespace_panel.py::
+ NamespacePanel.selected_references(), multi-select) as the new
+ Channel's sources; none selected creates an empty Channel."""
+ refs = self._namespace_panel.selected_references()
+ existing = self.processor.get_derived()
existing_names = {d.name for d in existing}
existing_ids = {d.channel_id for d in existing}
- name = _dedupe_name(ch_name, existing_names)
- channel_id = _slug_id(name, existing_ids)
- dc = Channel(channel_id=channel_id, name=name, kind="none",
- sources=[(dev_id, ch_id)], source_names=[ch_name],
- unit=unit, color=color or "#00d4ff")
+
+ if len(refs) == 1:
+ # Exactly one Signal/Channel selected — the "1 -> 1 passthrough"
+ # quick path. No-ops if one already exists for this exact source.
+ dev_id, ch_id, ch_name, unit, color = refs[0]
+ for dc in existing:
+ if dc.kind == "none" and dc.sources == [(dev_id, ch_id)]:
+ return
+ name = _dedupe_name(ch_name, existing_names)
+ channel_id = _slug_id(name, existing_ids)
+ dc = Channel(channel_id=channel_id, name=name, kind="none",
+ sources=[(dev_id, ch_id)], source_names=[ch_name],
+ unit=unit, color=color or "#00d4ff")
+ else:
+ # Zero selected -> empty Channel, same naming convention
+ # PipelineTab._add_virtual() uses for one. Two+ selected -> one
+ # Channel sourced from all of them. kind="none" here (not
+ # "expression") because it's the only kind that compiles
+ # trivially with no expression/script text set yet — Channel.
+ # compile() does `compile(f"__result__ = {self.expression}")`
+ # for "expression", which is a SyntaxError on the default blank
+ # string, so processor.add_derived() below would silently fail
+ # and never add the channel. _add_virtual() gets away with
+ # defaulting to "expression" because it never calls
+ # add_derived() itself — it hands the Channel to the editor and
+ # waits for the user's own Apply. This path calls add_derived()
+ # immediately, so it needs a kind that survives that. The user
+ # picks the real kind/expression afterward in the editor.
+ name = _dedupe_name(f"Channel {len(existing) + 1}", existing_names)
+ channel_id = _slug_id(name, existing_ids)
+ dc = Channel(channel_id=channel_id, name=name, kind="none",
+ sources=[(d, c) for d, c, *_ in refs],
+ source_names=[n for _, _, n, _, _ in refs])
+
err = self.processor.add_derived(dc)
if err is None:
self._pipeline_tab.refresh_derived()