diff --git a/src/woodshop/gui/controller.py b/src/woodshop/gui/controller.py index 8e11999..37640fb 100644 --- a/src/woodshop/gui/controller.py +++ b/src/woodshop/gui/controller.py @@ -301,6 +301,23 @@ class Controller(QObject): self._do(lambda: self.scene.disconnect(part=part_id) if part_id else self.scene.disconnect()) + def feature_connection_ids(self, fid: str) -> list[str]: + return [c.id for c in self.scene.connections if fid in (c.anchor, c.moving)] + + def break_feature_connection(self, fid: str) -> None: + """Break the connection(s) that this specific feature is part of.""" + cids = self.feature_connection_ids(fid) + if not cids: + return + + def op(): + with self.scene.batch(): + for cid in cids: + self.scene.disconnect(cid=cid) + return f"Broke {len(cids)} connection(s) on {fid}." + + self._do(op) + def groups(self) -> list[list[str]]: return self.scene.groups() diff --git a/src/woodshop/gui/feature_panel.py b/src/woodshop/gui/feature_panel.py index d5b39ee..fe6e16a 100644 --- a/src/woodshop/gui/feature_panel.py +++ b/src/woodshop/gui/feature_panel.py @@ -6,8 +6,8 @@ from __future__ import annotations from PySide6.QtCore import Qt from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QGridLayout, QHBoxLayout, - QLabel, QListWidget, QListWidgetItem, QMessageBox, - QPushButton, QVBoxLayout, QWidget) + QLabel, QListWidget, QListWidgetItem, QMenu, + QMessageBox, QPushButton, QVBoxLayout, QWidget) from ..scene import FACES from .controller import Controller @@ -33,6 +33,8 @@ class FeaturePanel(QWidget): self.list = QListWidget() self.list.itemSelectionChanged.connect(self._on_row) + self.list.setContextMenuPolicy(Qt.CustomContextMenu) + self.list.customContextMenuRequested.connect(self._feat_menu) root.addWidget(self.list, 1) self.hint = QLabel("") @@ -136,6 +138,19 @@ class FeaturePanel(QWidget): if items: self.c.select_feature(items[0].data(Qt.UserRole)) + def _feat_menu(self, pos) -> None: + item = self.list.itemAt(pos) + if not item: + return + fid = item.data(Qt.UserRole) + self.c.select_feature(fid) + menu = QMenu(self) + if self.c.feature_connection_ids(fid): + menu.addAction("Break this connection", + lambda: self.c.break_feature_connection(fid)) + menu.addAction("Delete feature", self.c.delete_active_feature) + menu.exec(self.list.viewport().mapToGlobal(pos)) + def _preview(self) -> None: """Live: show a red ghost of the pending values (no commit until Apply).""" if self._loading or not self.c.active_feature: diff --git a/tests/test_gui_controller.py b/tests/test_gui_controller.py index 5e377a7..3252c42 100644 --- a/tests/test_gui_controller.py +++ b/tests/test_gui_controller.py @@ -124,6 +124,17 @@ def test_highlight_feature(tmp_path): assert c.preview is None +def test_break_feature_connection(tmp_path): + c = _controller(tmp_path) + c.place("2x4", 24); c.add_feature("mortise") # f1 on p1 + c.place("2x4", 12); c.add_feature("tenon") # f2 on p2 + c.scene.connect("f1", "f2") + assert c.feature_connection_ids("f1") == ["c1"] + c.break_feature_connection("f1") + assert c.scene.connections == [] + assert c.feature_connection_ids("f1") == [] + + def test_unknown_tool_is_safe(tmp_path): c = _controller(tmp_path) assert "unknown" in c.execute_call("wood-bogus", {}).lower()