From 0388bebbc806d23abab945d52a0f285f966b412a Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Thu, 4 Jun 2026 11:39:33 -0600 Subject: Fix layout canvas drag-drop: zone logic, pop-out bug, canvas height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix squeeze zone pop-out: _adjacent_tile used self._drag_tile (already None at drop time) to exclude the dragged tile from neighbor candidates. Pass dragged_idx explicitly via exclude_idx param instead. - Swap split/squeeze detection areas: outer 1/6 now triggers squeeze (insert-between), inner 1/6→1/3 band triggers split (bisect target). - Extend squeeze detection to gaps between tiles and canvas edges via a second pass in _zone_at that expands tile rects by _LC_GAP+2 and finds the closest outside edge. Canvas-edge squeeze falls through to bisect. - Dynamic canvas height: sync_panes sets height to max(160, n*(min_tile+gap)+2*gap) so 4+ panes never get crushed below minimum tile size. - Remove _tree_grid_size cap of 12: LCM was truncated prematurely, causing unequal rowspans in the strip chart for 5+ panes. Co-Authored-By: Claude Sonnet 4.6 --- ui/windows/plot_window.py | 49 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py index f929261..95b9fea 100644 --- a/ui/windows/plot_window.py +++ b/ui/windows/plot_window.py @@ -246,7 +246,7 @@ def tree_to_grid(node: dict, row: int, col: int, rowspan: int, colspan: int, def _tree_grid_size(node: dict) -> int: - """Minimal grid = LCM of all split denominators (leaf counts), capped at 12.""" + """Minimal grid = LCM of all split denominators (leaf counts).""" denoms: List[int] = [] _collect_denoms(node, denoms) if not denoms: @@ -254,8 +254,6 @@ def _tree_grid_size(node: dict) -> int: result = 1 for d in denoms: result = result * d // gcd(result, d) - if result >= 12: - return 12 return result @@ -536,6 +534,8 @@ class LayoutCanvas(QFrame): if tree is None or not _tree_valid(tree, n): tree = _tree_default(n) self._tree = tree + h = max(_LC_HEIGHT, n * (_LC_MIN_TILE + _LC_GAP) + 2 * _LC_GAP) + self.setFixedHeight(h) self._rebuild_tiles() def update_tile_title(self, pane_index: int, title: str) -> None: @@ -599,7 +599,7 @@ class LayoutCanvas(QFrame): def _zone_at(self, pos: QPoint) -> Optional[Tuple[int, str]]: """ Returns (pane_idx, zone) where zone is one of: - squeeze_left/right/top/bottom — outer 1/6, insert between tiles + squeeze_left/right/top/bottom — outer 1/6 of tile OR gap between tiles, insert between split_left/right/top/bottom — 1/6 to 1/3, bisect tile center — inner 1/3, swap/move """ @@ -643,19 +643,46 @@ class LayoutCanvas(QFrame): x_frac = min(rx, w - rx) / max(w, 1) y_frac = min(ry, h - ry) / max(h, 1) return (pane_idx, x_zone if x_frac <= y_frac else y_zone) + + # Second pass: mouse in the gap between tiles — squeeze only. + # Expand each tile rect by the gap width and find the closest edge. + gap = _LC_GAP + 2 + best_pane: Optional[int] = None + best_zone: Optional[str] = None + best_dist = float("inf") + for pane_idx, rect in self._tile_rects.items(): + if pane_idx == dragging_idx: + continue + if not rect.adjusted(-gap, -gap, gap, gap).contains(pos): + continue + dl = pos.x() - rect.left() + dr = rect.right() - pos.x() + dt = pos.y() - rect.top() + db = rect.bottom() - pos.y() + candidates = [] + if dl < 0: candidates.append((abs(dl), "squeeze_left")) + if dr < 0: candidates.append((abs(dr), "squeeze_right")) + if dt < 0: candidates.append((abs(dt), "squeeze_top")) + if db < 0: candidates.append((abs(db), "squeeze_bottom")) + if not candidates: + continue + dist, zone = min(candidates) + if dist < best_dist: + best_dist, best_pane, best_zone = dist, pane_idx, zone + if best_pane is not None: + return (best_pane, best_zone) return None - def _adjacent_tile(self, pane_idx: int, direction: str) -> Optional[int]: + def _adjacent_tile(self, pane_idx: int, direction: str, exclude_idx: int = -1) -> Optional[int]: """Find the tile immediately adjacent to pane_idx in the given direction.""" r = self._tile_rects.get(pane_idx) if r is None: return None - dragging_idx = self._drag_tile.pane_index if self._drag_tile else -1 tol = _LC_GAP + 3 best: Optional[int] = None best_dist = float("inf") for other_idx, other_r in self._tile_rects.items(): - if other_idx in (pane_idx, dragging_idx): + if other_idx in (pane_idx, exclude_idx): continue if direction == "left": dist = r.left() - other_r.right() @@ -728,7 +755,7 @@ class LayoutCanvas(QFrame): # Determine insert direction from zone name # split_* → bisect target tile - # squeeze_* → insert next to adjacent tile (fall back to split if no neighbor) + # squeeze_* → insert next to adjacent tile; bisect edge tile when at canvas boundary _OPPOSITE = {"left": "right", "right": "left", "top": "bottom", "bottom": "top"} _SPLIT_DIR = {"split_left": "left", "split_right": "right", "split_top": "top", "split_bottom": "bottom", @@ -738,7 +765,7 @@ class LayoutCanvas(QFrame): direction = _SPLIT_DIR.get(zone, "left") if zone.startswith("squeeze_"): - neighbor = self._adjacent_tile(target_idx, direction) + neighbor = self._adjacent_tile(target_idx, direction, exclude_idx=dragged_idx) if neighbor is not None: # Insert dragged tile to the opposite side of the neighbor # so it lands between neighbor and target @@ -754,9 +781,9 @@ class LayoutCanvas(QFrame): self._reposition_tiles() self.arrangement_changed.emit() return - # No neighbor — fall through to split behaviour + # No neighbor (canvas edge) — bisect the edge tile outward - # split (or squeeze with no neighbor) + # split, or squeeze at canvas edge with no neighbor new_tree = _tree_remove(self._tree, dragged_idx) if new_tree is None: self._reposition_tiles() -- cgit v1.2.3 From d2a555896e11a315084a00c34ef0ec77b25dcf70 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Thu, 4 Jun 2026 12:13:47 -0600 Subject: Fix split zone detection and canvas-edge squeeze behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split zone was unreachable: squeezed between outer squeeze (1/6) and center (1/3), leaving a 1/6-wide band impossible to target reliably. Users hitting the tile edge always triggered squeeze instead of split. - Remove squeeze from inside-tile detection entirely. Split now owns the outer 1/3 of each tile edge — large, easy to target. Center keeps the inner 1/3. Squeeze remains gap-detection only (second pass). - Fix canvas-edge squeeze (no adjacent neighbor): instead of bisecting the edge tile, wrap the entire remaining tree in a new directional split with the dragged tile placed at the outer position, then equalize — all tiles end up equal size. Co-Authored-By: Claude Sonnet 4.6 --- ui/windows/plot_window.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py index 95b9fea..bc8823f 100644 --- a/ui/windows/plot_window.py +++ b/ui/windows/plot_window.py @@ -599,8 +599,8 @@ class LayoutCanvas(QFrame): def _zone_at(self, pos: QPoint) -> Optional[Tuple[int, str]]: """ Returns (pane_idx, zone) where zone is one of: - squeeze_left/right/top/bottom — outer 1/6 of tile OR gap between tiles, insert between - split_left/right/top/bottom — 1/6 to 1/3, bisect tile + split_left/right/top/bottom — outer 1/3 of tile, bisect tile + squeeze_left/right/top/bottom — gap between tiles only (second pass) center — inner 1/3, swap/move """ dragging_idx = self._drag_tile.pane_index if self._drag_tile else -1 @@ -612,22 +612,16 @@ class LayoutCanvas(QFrame): rx = pos.x() - rect.x() ry = pos.y() - rect.y() w, h = rect.width(), rect.height() - sq_w = max(1, w // 6) sp_w = max(1, w // 3) - sq_h = max(1, h // 6) sp_h = max(1, h // 3) - # X-axis zone - if rx < sq_w: x_zone = "squeeze_left" - elif rx < sp_w: x_zone = "split_left" - elif rx > w - sq_w: x_zone = "squeeze_right" + # X-axis zone — outer 1/3 = split, inner 1/3 = center + if rx < sp_w: x_zone = "split_left" elif rx > w - sp_w: x_zone = "split_right" else: x_zone = "center" # Y-axis zone - if ry < sq_h: y_zone = "squeeze_top" - elif ry < sp_h: y_zone = "split_top" - elif ry > h - sq_h: y_zone = "squeeze_bottom" + if ry < sp_h: y_zone = "split_top" elif ry > h - sp_h: y_zone = "split_bottom" else: y_zone = "center" @@ -781,9 +775,28 @@ class LayoutCanvas(QFrame): self._reposition_tiles() self.arrangement_changed.emit() return - # No neighbor (canvas edge) — bisect the edge tile outward + # No neighbor — insert dragged at outer edge of full layout + new_tree = _tree_remove(self._tree, dragged_idx) + if new_tree is None: + self._reposition_tiles(); return + dragged_leaf = {"kind": "leaf", "pane": dragged_idx} + if direction == "left": + new_tree = {"kind": "hsplit", "ratio": 0.5, "first": dragged_leaf, "second": new_tree} + elif direction == "right": + new_tree = {"kind": "hsplit", "ratio": 0.5, "first": new_tree, "second": dragged_leaf} + elif direction == "top": + new_tree = {"kind": "vsplit", "ratio": 0.5, "first": dragged_leaf, "second": new_tree} + else: + new_tree = {"kind": "vsplit", "ratio": 0.5, "first": new_tree, "second": dragged_leaf} + new_tree = _tree_equalize_ratios(new_tree) + self._tree = new_tree + if self._cfg_ref is not None: + self._cfg_ref.tree = new_tree + self._reposition_tiles() + self.arrangement_changed.emit() + return - # split, or squeeze at canvas edge with no neighbor + # split — bisect target tile new_tree = _tree_remove(self._tree, dragged_idx) if new_tree is None: self._reposition_tiles() -- cgit v1.2.3