summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-03 15:05:36 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-03 15:05:36 -0600
commit7b926faab3dcd52e45fc1b4b24f955a4dcf66959 (patch)
treeede3018b2557d9d06d50f8439f91767c8795776f /ui
parent93e7d0ecdc090fd4f0231b13933fc0fa095a1f22 (diff)
Add drag-and-snap tiling layout canvas to Plot Builder
Replace layout radio buttons (Stacked/Side-by-Side/Grid) with a visual LayoutCanvas widget. Pane tiles snap to grid cells on drag-drop; dropping on an occupied cell swaps positions. Ghost highlight shows snap target during drag. - PaneSpec gains row/col fields (None = unassigned; backward compat preserved) - LayoutConfig gains "free" mode; strip_chart reads explicit positions - PaneBlock loses ▲▼ move buttons; reordering is canvas-only - QSS entries added for layoutTile, layoutTileDragging, layoutCanvasBar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'ui')
-rw-r--r--ui/strip_chart.py13
-rw-r--r--ui/style_dark.qss40
-rw-r--r--ui/style_light.qss8
-rw-r--r--ui/windows/plot_window.py317
4 files changed, 304 insertions, 74 deletions
diff --git a/ui/strip_chart.py b/ui/strip_chart.py
index 523102d..d855038 100644
--- a/ui/strip_chart.py
+++ b/ui/strip_chart.py
@@ -95,12 +95,14 @@ class StripChartWidget(QWidget):
n = len(cfg.panes)
# Determine row/col for each pane
- if cfg.layout_mode == "sidebyside":
+ if cfg.layout_mode == "free":
+ positions = [(spec.row or 0, spec.col or 0) for spec in cfg.panes]
+ elif cfg.layout_mode == "sidebyside":
positions = [(0, c) for c in range(n)]
elif cfg.layout_mode == "grid":
cols = max(1, cfg.grid_cols)
positions = [(i // cols, i % cols) for i in range(n)]
- else: # stacked
+ else: # stacked (legacy default)
positions = [(r, 0) for r in range(n)]
ref_plot = None
@@ -112,7 +114,7 @@ class StripChartWidget(QWidget):
plot.getAxis("left").setStyle(tickFont=mf)
plot.getAxis("bottom").setStyle(tickFont=mf)
- # Hide bottom axis labels for all but bottom row in stacked
+ # Hide bottom axis labels for non-bottom panes in stacked legacy mode
if cfg.layout_mode == "stacked" and idx < n - 1:
plot.getAxis("bottom").setStyle(showValues=False)
plot.getAxis("bottom").setHeight(0)
@@ -133,10 +135,9 @@ class StripChartWidget(QWidget):
else:
plot.enableAutoRange(axis="y")
- # Set column stretch (relative weight)
+ # Apply weight to row and column stretch
self._gw.ci.layout.setColumnStretchFactor(col, spec.weight)
- if cfg.layout_mode == "stacked":
- self._gw.ci.layout.setRowStretchFactor(row, spec.weight)
+ self._gw.ci.layout.setRowStretchFactor(row, spec.weight)
for tr in spec.traces:
if not tr.visible: continue
diff --git a/ui/style_dark.qss b/ui/style_dark.qss
index 2c24d1c..e562365 100644
--- a/ui/style_dark.qss
+++ b/ui/style_dark.qss
@@ -563,3 +563,43 @@ QFrame#controlWidgetWrapper {
background: transparent;
border: none;
}
+
+/* ── Layout canvas ───────────────────────────────────────────────── */
+QWidget#layoutCanvasBar {
+ background-color: #0f1521;
+ border-bottom: 1px solid #1c2540;
+}
+QLabel#layoutCanvasLabel {
+ font-family: "IBM Plex Mono", monospace;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 2px;
+ color: #4338ca;
+ padding: 0 2px;
+}
+QFrame#layoutCanvasWidget {
+ background-color: #0b0e13;
+ border: 1px solid #2a3558;
+ border-radius: 4px;
+}
+QFrame#layoutTile {
+ background-color: #1c2540;
+ border: 1px solid #2a3558;
+ border-radius: 5px;
+}
+QFrame#layoutTile:hover {
+ border-color: #4338ca;
+ background-color: #212c52;
+}
+QFrame#layoutTileDragging {
+ background-color: #1e3a5f;
+ border: 2px solid #3b82f6;
+ border-radius: 5px;
+}
+QLabel#layoutTileLabel {
+ font-family: "IBM Plex Mono", monospace;
+ font-size: 11px;
+ font-weight: 700;
+ color: #8b9dc3;
+ background: transparent;
+}
diff --git a/ui/style_light.qss b/ui/style_light.qss
index 38b91f8..e0a1987 100644
--- a/ui/style_light.qss
+++ b/ui/style_light.qss
@@ -124,3 +124,11 @@ QMenu#profileMenu { background:#ffffff; border:1px solid #e2e8f0; border-radius:
QMenu#profileMenu::item { padding:7px 20px; color:#334155; }
QMenu#profileMenu::item:selected { background:#dbeafe; color:#1d4ed8; }
QMenu#profileMenu::separator { height:1px; background:#e2e8f0; margin:3px 8px; }
+/* Layout canvas */
+QWidget#layoutCanvasBar { background-color:#f8fafc; border-bottom:1px solid #e2e8f0; }
+QLabel#layoutCanvasLabel { font-family:"IBM Plex Mono",monospace; font-size:10px; font-weight:700; letter-spacing:2px; color:#4338ca; }
+QFrame#layoutCanvasWidget { background-color:#f1f5f9; border:1px solid #cbd5e1; border-radius:4px; }
+QFrame#layoutTile { background-color:#e2e8f0; border:1px solid #94a3b8; border-radius:5px; }
+QFrame#layoutTile:hover { border-color:#4338ca; background-color:#c7d2fe; }
+QFrame#layoutTileDragging { background-color:#bfdbfe; border:2px solid #3b82f6; border-radius:5px; }
+QLabel#layoutTileLabel { font-family:"IBM Plex Mono",monospace; font-size:11px; font-weight:700; color:#334155; background:transparent; }
diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py
index 0207af1..91799f9 100644
--- a/ui/windows/plot_window.py
+++ b/ui/windows/plot_window.py
@@ -4,8 +4,8 @@ ui/windows/plot_window.py
PLOT window — toolbar section 4.
Lets operators:
- • Choose layout: Stacked (rows) or Side-by-Side (columns) or Grid (N×M)
- • Add/remove/reorder subplot panes
+ • Arrange subplot panes by dragging tiles on the layout canvas (tiling WM style)
+ • Add/remove subplot panes
• Assign any channel (physical or derived) to any pane
• Set X-axis: Time (elapsed s) OR any channel (e.g. strain → stress vs strain)
• Name axes, set Y auto/fixed range, grid, relative size weight
@@ -19,16 +19,16 @@ from __future__ import annotations
import json
from copy import deepcopy
from dataclasses import dataclass, field, asdict
-from typing import List, Optional, Tuple
+from typing import Dict, List, Optional, Tuple
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QScrollArea, QFrame, QLineEdit, QDoubleSpinBox, QSpinBox,
- QCheckBox, QComboBox, QColorDialog, QToolButton, QButtonGroup,
- QFileDialog, QMessageBox, QRadioButton, QGroupBox, QSizePolicy,
+ QCheckBox, QComboBox, QColorDialog, QToolButton,
+ QFileDialog, QMessageBox,
)
-from PyQt6.QtCore import Qt, pyqtSignal
-from PyQt6.QtGui import QColor, QCloseEvent
+from PyQt6.QtCore import Qt, pyqtSignal, QPoint, QRect
+from PyQt6.QtGui import QColor, QCloseEvent, QPainter, QPen
from devices.device_registry import DeviceRegistry
from core.signal_processor import SignalProcessor
@@ -60,11 +60,13 @@ class PaneSpec:
grid: bool = True
weight: int = 1 # relative size
traces: List[TraceSpec] = field(default_factory=list)
+ row: Optional[int] = None # canvas grid position; None = unassigned
+ col: Optional[int] = None
@dataclass
class LayoutConfig:
panes: List[PaneSpec] = field(default_factory=list)
- layout_mode: str = "stacked" # stacked | sidebyside | grid
+ layout_mode: str = "stacked" # stacked | sidebyside | grid | free
grid_cols: int = 2
time_window_s: float = 30.0
link_x: bool = True
@@ -169,10 +171,9 @@ class TraceRow(QFrame):
# ══════════════════════════════════════════════════════════════════════════════
class PaneBlock(QFrame):
- removed = pyqtSignal(object)
- changed = pyqtSignal()
- move_up = pyqtSignal(object)
- move_dn = pyqtSignal(object)
+ removed = pyqtSignal(object)
+ changed = pyqtSignal()
+ title_changed = pyqtSignal(object, str) # (self, new_title)
def __init__(self, spec: PaneSpec, registry: DeviceRegistry,
processor: SignalProcessor):
@@ -188,11 +189,11 @@ class PaneBlock(QFrame):
hdr = QWidget(); hdr.setObjectName("plotBlockHeader"); hdr.setFixedHeight(32)
hl = QHBoxLayout(hdr); hl.setContentsMargins(8,0,6,0); hl.setSpacing(4)
self._title = QLineEdit(self.spec.title); self._title.setObjectName("plotBlockTitle")
- self._title.textChanged.connect(lambda t: setattr(self.spec,"title",t))
- hl.addWidget(self._title,1)
- for txt, sig in [("▲",self.move_up),("▼",self.move_dn)]:
- b = QToolButton(); b.setText(txt); b.setObjectName("plotMoveBtn")
- b.setFixedSize(22,22); b.clicked.connect(lambda _,s=sig: s.emit(self)); hl.addWidget(b)
+ self._title.textChanged.connect(lambda t: (
+ setattr(self.spec, "title", t),
+ self.title_changed.emit(self, t),
+ ))
+ hl.addWidget(self._title, 1)
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)
@@ -214,7 +215,6 @@ class PaneBlock(QFrame):
for dc in self.processor.get_derived():
self._x_cb.addItem(f"[virtual] {dc.channel_id}",
userData=f"derived/{dc.channel_id}")
- # Select current
for i in range(self._x_cb.count()):
if self._x_cb.itemData(i) == self.spec.x_source:
self._x_cb.setCurrentIndex(i); break
@@ -305,6 +305,200 @@ class PaneBlock(QFrame):
# ══════════════════════════════════════════════════════════════════════════════
+# Layout canvas — drag-and-snap tiling pane arranger
+# ══════════════════════════════════════════════════════════════════════════════
+
+_CANVAS_CELL_W = 120
+_CANVAS_CELL_H = 76
+_CANVAS_GAP = 6
+_CANVAS_COLS = 4
+
+
+class PaneTile(QFrame):
+ def __init__(self, pane_index: int, title: str, parent: QWidget) -> None:
+ super().__init__(parent)
+ self.pane_index = pane_index
+ self.setObjectName("layoutTile")
+ self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
+ lay = QVBoxLayout(self)
+ lay.setContentsMargins(4, 4, 4, 4)
+ self._lbl = QLabel(title)
+ self._lbl.setObjectName("layoutTileLabel")
+ self._lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self._lbl.setWordWrap(True)
+ lay.addWidget(self._lbl)
+
+ def set_title(self, title: str) -> None:
+ self._lbl.setText(title)
+
+ def set_dragging(self, v: bool) -> None:
+ self.setObjectName("layoutTileDragging" if v else "layoutTile")
+ self.style().unpolish(self)
+ self.style().polish(self)
+ self._lbl.setObjectName("layoutTileLabel")
+ self._lbl.style().unpolish(self._lbl)
+ self._lbl.style().polish(self._lbl)
+
+
+class LayoutCanvas(QFrame):
+ arrangement_changed = pyqtSignal()
+
+ def __init__(self, panes: List[PaneSpec], parent: Optional[QWidget] = None) -> None:
+ super().__init__(parent)
+ self.setObjectName("layoutCanvasWidget")
+ self.setMouseTracking(True)
+ self._panes: List[PaneSpec] = panes
+ self._tiles: List[PaneTile] = []
+ self._grid: Dict[Tuple[int, int], int] = {}
+ self._drag_tile: Optional[PaneTile] = None
+ self._drag_offset: QPoint = QPoint()
+ self._ghost_cell: Optional[Tuple[int,int]] = None
+ self._assign_legacy_positions()
+ self._rebuild_tiles()
+
+ # ── Public ────────────────────────────────────────────────────────────────
+
+ def sync_panes(self, panes: List[PaneSpec]) -> None:
+ self._panes = panes
+ self._drag_tile = None
+ self._ghost_cell = None
+ self._assign_legacy_positions()
+ self._rebuild_tiles()
+
+ def update_tile_title(self, pane_index: int, title: str) -> None:
+ if 0 <= pane_index < len(self._tiles):
+ self._tiles[pane_index].set_title(title)
+
+ # ── Position assignment ───────────────────────────────────────────────────
+
+ def _assign_legacy_positions(self) -> None:
+ occupied = {(p.row, p.col) for p in self._panes
+ if p.row is not None and p.col is not None}
+ for pane in self._panes:
+ if pane.row is None or pane.col is None:
+ row = 0
+ while (row, 0) in occupied:
+ row += 1
+ pane.row, pane.col = row, 0
+ occupied.add((row, 0))
+
+ # ── Rebuild ───────────────────────────────────────────────────────────────
+
+ def _rebuild_tiles(self) -> None:
+ for t in self._tiles:
+ t.deleteLater()
+ self._tiles.clear()
+ self._grid.clear()
+ for i, pane in enumerate(self._panes):
+ tile = PaneTile(i, pane.title, self)
+ tile.setGeometry(self._cell_rect(pane.row, pane.col))
+ tile.show()
+ self._tiles.append(tile)
+ self._grid[(pane.row, pane.col)] = i
+ self._resize_canvas()
+
+ def _resize_canvas(self) -> None:
+ if not self._panes:
+ self.setFixedSize(
+ _CANVAS_COLS * (_CANVAS_CELL_W + _CANVAS_GAP) + _CANVAS_GAP,
+ _CANVAS_CELL_H + 2 * _CANVAS_GAP,
+ )
+ return
+ max_row = max((p.row for p in self._panes if p.row is not None), default=0)
+ h = (max_row + 1) * (_CANVAS_CELL_H + _CANVAS_GAP) + _CANVAS_GAP
+ w = _CANVAS_COLS * (_CANVAS_CELL_W + _CANVAS_GAP) + _CANVAS_GAP
+ self.setFixedSize(w, h)
+
+ def _cell_rect(self, row: int, col: int) -> QRect:
+ x = _CANVAS_GAP + col * (_CANVAS_CELL_W + _CANVAS_GAP)
+ y = _CANVAS_GAP + row * (_CANVAS_CELL_H + _CANVAS_GAP)
+ return QRect(x, y, _CANVAS_CELL_W, _CANVAS_CELL_H)
+
+ def _tile_at(self, pos: QPoint) -> Optional[PaneTile]:
+ for tile in reversed(self._tiles):
+ if tile.geometry().contains(pos):
+ return tile
+ return None
+
+ def _nearest_cell(self, pos: QPoint) -> Tuple[int, int]:
+ max_row = max((p.row for p in self._panes if p.row is not None), default=0)
+ max_rows = max(max_row + 2, len(self._panes))
+ col = max(0, min(_CANVAS_COLS - 1, pos.x() // (_CANVAS_CELL_W + _CANVAS_GAP)))
+ row = max(0, min(max_rows - 1, pos.y() // (_CANVAS_CELL_H + _CANVAS_GAP)))
+ return (int(row), int(col))
+
+ # ── Drag ─────────────────────────────────────────────────────────────────
+
+ def mousePressEvent(self, event) -> None:
+ if event.button() != Qt.MouseButton.LeftButton:
+ return
+ tile = self._tile_at(event.pos())
+ if tile is None:
+ return
+ self._drag_tile = tile
+ self._drag_offset = event.pos() - tile.pos()
+ tile.set_dragging(True)
+ tile.raise_()
+
+ def mouseMoveEvent(self, event) -> None:
+ if self._drag_tile is None:
+ return
+ self._drag_tile.move(event.pos() - self._drag_offset)
+ snap = self._nearest_cell(event.pos())
+ if snap != self._ghost_cell:
+ self._ghost_cell = snap
+ self.update()
+
+ def mouseReleaseEvent(self, event) -> None:
+ if self._drag_tile is None:
+ return
+ snap = self._nearest_cell(event.pos())
+ self._do_drop(self._drag_tile.pane_index, snap)
+ self._drag_tile.set_dragging(False)
+ self._drag_tile = None
+ self._ghost_cell = None
+ self.update()
+
+ def _do_drop(self, pane_index: int, target: Tuple[int, int]) -> None:
+ dragged = self._panes[pane_index]
+ old = (dragged.row, dragged.col)
+
+ if old == target:
+ self._tiles[pane_index].setGeometry(self._cell_rect(*old))
+ return
+
+ occupant_idx = self._grid.get(target)
+
+ if occupant_idx is not None:
+ occupant = self._panes[occupant_idx]
+ occupant.row, occupant.col = old
+ self._tiles[occupant_idx].setGeometry(self._cell_rect(*old))
+ self._grid[old] = occupant_idx
+ else:
+ self._grid.pop(old, None)
+
+ dragged.row, dragged.col = target
+ self._tiles[pane_index].setGeometry(self._cell_rect(*target))
+ self._grid[target] = pane_index
+
+ self._resize_canvas()
+ self.arrangement_changed.emit()
+
+ # ── Ghost highlight ───────────────────────────────────────────────────────
+
+ def paintEvent(self, event) -> None:
+ super().paintEvent(event)
+ if self._ghost_cell is None:
+ return
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+ r = self._cell_rect(*self._ghost_cell)
+ painter.setPen(QPen(QColor("#3b82f6"), 2))
+ painter.setBrush(QColor(59, 130, 246, 40))
+ painter.drawRoundedRect(r, 4, 4)
+
+
+# ══════════════════════════════════════════════════════════════════════════════
# PlotWindow
# ══════════════════════════════════════════════════════════════════════════════
@@ -321,7 +515,7 @@ class PlotWindow(QWidget):
else build_default_layout(registry, processor)
self.setWindowTitle("Plot Builder")
self.setMinimumSize(720, 560)
- self.resize(820, 700)
+ self.resize(820, 760)
self._blocks: List[PaneBlock] = []
self._build()
self._populate()
@@ -337,31 +531,10 @@ class PlotWindow(QWidget):
div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
div.setObjectName("devWindowDivider"); root.addWidget(div)
- # Global settings
+ # Global settings bar
glob = QWidget(); glob.setObjectName("cfgGlobalBar")
gl = QHBoxLayout(glob); gl.setContentsMargins(12,7,12,7); gl.setSpacing(12)
- # Layout mode
- gl.addWidget(QLabel("Layout:"))
- self._stacked_rb = QRadioButton("Stacked")
- self._side_rb = QRadioButton("Side-by-Side")
- self._grid_rb = QRadioButton("Grid")
- mode_grp = QButtonGroup(self)
- for rb in (self._stacked_rb, self._side_rb, self._grid_rb):
- rb.setObjectName("cfgLiveChk"); mode_grp.addButton(rb)
- mode_map = {"stacked": self._stacked_rb,
- "sidebyside": self._side_rb, "grid": self._grid_rb}
- mode_map.get(self.cfg.layout_mode, self._stacked_rb).setChecked(True)
- for rb, key in [(self._stacked_rb,"stacked"),(self._side_rb,"sidebyside"),(self._grid_rb,"grid")]:
- rb.toggled.connect(lambda v, k=key: self._set_mode(k) if v else None)
- gl.addWidget(rb)
-
- gl.addWidget(QLabel("Cols:"))
- self._cols = QSpinBox(); self._cols.setRange(1,6); self._cols.setValue(self.cfg.grid_cols)
- self._cols.setFixedWidth(44); self._cols.setObjectName("traceWidthSpin")
- self._cols.valueChanged.connect(lambda v: setattr(self.cfg,"grid_cols",v))
- gl.addWidget(self._cols)
-
gl.addWidget(QLabel("Window:"))
self._win = QDoubleSpinBox(); self._win.setRange(1,3600)
self._win.setSuffix(" s"); self._win.setValue(self.cfg.time_window_s)
@@ -387,6 +560,18 @@ class PlotWindow(QWidget):
add_btn.clicked.connect(self._add_pane); gl.addWidget(add_btn)
root.addWidget(glob)
+ # Layout canvas section
+ canvas_bar = QWidget(); canvas_bar.setObjectName("layoutCanvasBar")
+ cb_lay = QVBoxLayout(canvas_bar)
+ cb_lay.setContentsMargins(10, 6, 10, 8); cb_lay.setSpacing(4)
+ clbl = QLabel("LAYOUT — drag tiles to arrange")
+ clbl.setObjectName("layoutCanvasLabel")
+ cb_lay.addWidget(clbl)
+ self._canvas = LayoutCanvas(self.cfg.panes, self)
+ self._canvas.arrangement_changed.connect(self._maybe_live)
+ cb_lay.addWidget(self._canvas)
+ root.addWidget(canvas_bar)
+
div2 = QFrame(); div2.setFrameShape(QFrame.Shape.HLine)
div2.setObjectName("devWindowDivider"); root.addWidget(div2)
@@ -412,45 +597,39 @@ class PlotWindow(QWidget):
ap.clicked.connect(self._apply); bl.addWidget(ap)
root.addWidget(btm)
- def _set_mode(self, mode: str):
- self.cfg.layout_mode = mode
- self._maybe_live()
-
def _populate(self):
- for b in self._blocks: self._blay.removeWidget(b); b.deleteLater()
+ for b in self._blocks:
+ self._blay.removeWidget(b); b.deleteLater()
self._blocks.clear()
- for spec in self.cfg.panes: self._insert(spec)
+ for spec in self.cfg.panes:
+ self._insert(spec)
+ self._canvas.sync_panes(self.cfg.panes)
+ self.cfg.layout_mode = "free"
def _insert(self, spec: PaneSpec):
blk = PaneBlock(spec, self.registry, self.processor)
- blk.removed.connect(self._rm); blk.changed.connect(self._maybe_live)
- blk.move_up.connect(self._mv_up); blk.move_dn.connect(self._mv_dn)
+ blk.removed.connect(self._rm)
+ blk.changed.connect(self._maybe_live)
+ blk.title_changed.connect(self._on_tile_title)
self._blocks.append(blk)
self._blay.insertWidget(self._blay.count()-1, blk)
+ def _on_tile_title(self, blk: PaneBlock, title: str) -> None:
+ if blk in self._blocks:
+ self._canvas.update_tile_title(self._blocks.index(blk), title)
+
def _add_pane(self):
spec = PaneSpec(title=f"Plot {len(self.cfg.panes)+1}")
- self.cfg.panes.append(spec); self._insert(spec); self._maybe_live()
+ self.cfg.panes.append(spec)
+ self._insert(spec)
+ self._canvas.sync_panes(self.cfg.panes)
+ self.cfg.layout_mode = "free"
+ self._maybe_live()
def _rm(self, blk):
if blk.spec in self.cfg.panes: self.cfg.panes.remove(blk.spec)
self._blocks.remove(blk); self._blay.removeWidget(blk); blk.deleteLater()
- self._maybe_live()
-
- def _mv_up(self, blk):
- i = self._blocks.index(blk)
- if i==0: return
- self.cfg.panes.insert(i-1,self.cfg.panes.pop(i))
- self._blocks.insert(i-1,self._blocks.pop(i))
- self._blay.removeWidget(blk); self._blay.insertWidget(i-1,blk)
- self._maybe_live()
-
- def _mv_dn(self, blk):
- i = self._blocks.index(blk)
- if i>=len(self._blocks)-1: return
- self.cfg.panes.insert(i+1,self.cfg.panes.pop(i))
- self._blocks.insert(i+1,self._blocks.pop(i))
- self._blay.removeWidget(blk); self._blay.insertWidget(i+1,blk)
+ self._canvas.sync_panes(self.cfg.panes)
self._maybe_live()
def _maybe_live(self):
@@ -461,7 +640,8 @@ class PlotWindow(QWidget):
def _reset(self):
self.cfg = build_default_layout(self.registry, self.processor)
self._win.setValue(self.cfg.time_window_s)
- self._populate(); self._maybe_live()
+ self._populate()
+ self._maybe_live()
def _export(self):
path, _ = QFileDialog.getSaveFileName(self,"Export","layout.json","JSON (*.json)")
@@ -473,7 +653,8 @@ class PlotWindow(QWidget):
try:
self.cfg = LayoutConfig.from_json(open(path).read())
self._win.setValue(self.cfg.time_window_s)
- self._populate(); self._maybe_live()
+ self._populate()
+ self._maybe_live()
except Exception as e:
QMessageBox.critical(self,"Import failed",str(e))