From 35a465f0885938259e0c8d7d929f276edf019dc5 Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 2 Jul 2026 23:26:52 +0200 Subject: [PATCH] feat(edit): clipboard copy/paste of nodes Add an internal clipboard on the controller with undoable macro paste, and Copy/Paste menu actions and shortcuts in the main window. Covered by a test. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/screenshot.png | Bin 0 -> 36959 bytes pipeline_editor/controller.py | 26 ++++++++++++++++++++++++++ pipeline_editor/main_window.py | 15 ++++++++++++++- tests/test_app.py | 12 ++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/screenshot.png diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..720a829da7b8cb2868ef51d6721128334e146edf GIT binary patch literal 36959 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 literal 0 HcmV?d00001 diff --git a/pipeline_editor/controller.py b/pipeline_editor/controller.py index c152e78..0a6a869 100644 --- a/pipeline_editor/controller.py +++ b/pipeline_editor/controller.py @@ -169,6 +169,7 @@ class EditorController: self.document = document self.library = library self.undo_stack = QUndoStack() + self._clipboard: list[dict] = [] def add_node(self, template_key: str, x: float, y: float) -> Optional[NodeModel]: tmpl = self.library.get(template_key) @@ -211,6 +212,31 @@ class EditorController: self.undo_stack.endMacro() return clones + def copy(self, node_ids) -> int: + """Snapshot the given nodes into the internal clipboard.""" + self._clipboard = [self.document.node(nid).to_dict() + for nid in node_ids if self.document.node(nid)] + return len(self._clipboard) + + def paste(self, offset=(20, 20)) -> list: + """Recreate clipboard nodes with fresh ids as one undoable step.""" + pasted = [] + if not self._clipboard: + return pasted + self.undo_stack.beginMacro("Paste") + for data in self._clipboard: + node = NodeModel.from_dict(data) + fresh = node.clone(self.document.next_id("n")) + fresh.x += offset[0] + fresh.y += offset[1] + self.undo_stack.push(AddNodeCommand(self.document, fresh)) + pasted.append(fresh) + self.undo_stack.endMacro() + return pasted + + def can_paste(self) -> bool: + return bool(self._clipboard) + def record_move(self, node_id: str, old, new) -> None: if old == new: return diff --git a/pipeline_editor/main_window.py b/pipeline_editor/main_window.py index 8244bfd..529ac0d 100644 --- a/pipeline_editor/main_window.py +++ b/pipeline_editor/main_window.py @@ -97,6 +97,10 @@ class MainWindow(QMainWindow): triggered=self.delete_selection) self.act_dup = QAction("Du&plicate", self, shortcut="Ctrl+D", triggered=self.duplicate_selection) + self.act_copy = QAction("&Copy", self, shortcut=QKeySequence.StandardKey.Copy, + triggered=self.copy_selection) + self.act_paste = QAction("&Paste", self, shortcut=QKeySequence.StandardKey.Paste, + triggered=self.paste_clipboard) self.act_zoom_in = QAction("Zoom In", self, shortcut=QKeySequence.StandardKey.ZoomIn, triggered=lambda: self.view.scale(1.2, 1.2)) @@ -128,7 +132,7 @@ class MainWindow(QMainWindow): em = m.addMenu("&Edit") em.addActions([self.act_undo, self.act_redo]) em.addSeparator() - em.addActions([self.act_delete, self.act_dup]) + em.addActions([self.act_copy, self.act_paste, self.act_dup, self.act_delete]) vm = m.addMenu("&View") vm.addActions([self.act_zoom_in, self.act_zoom_out, self.act_fit]) @@ -191,6 +195,15 @@ class MainWindow(QMainWindow): node_ids, _ = self._selected_ids() self.controller.duplicate(node_ids, offset=(self.config.grid_size, self.config.grid_size)) + def copy_selection(self) -> None: + node_ids, _ = self._selected_ids() + n = self.controller.copy(node_ids) + self.statusBar().showMessage(f"Copied {n} node(s)") + + def paste_clipboard(self) -> None: + g = self.config.grid_size + self.controller.paste(offset=(g, g)) + # -- view toggles ----------------------------------------------------- def _toggle_grid(self, checked) -> None: self.config.show_grid = checked diff --git a/tests/test_app.py b/tests/test_app.py index 9f46910..6adfead 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -59,6 +59,18 @@ def test_duplicate_selection(win): assert len(list(win.document.nodes())) == n_before + 1 +def test_copy_paste(win): + win.load_sample() + node = next(win.document.nodes()) + win.scene.node_items[node.node_id].setSelected(True) + n_before = len(list(win.document.nodes())) + win.copy_selection() + win.paste_clipboard() + assert len(list(win.document.nodes())) == n_before + 1 + win.controller.undo() # paste is one macro step + assert len(list(win.document.nodes())) == n_before + + def test_save_and_open_roundtrip(win, tmp_path): win.load_sample() path = tmp_path / "diagram.pipe"