diff --git a/pyflow/graphics/window.py b/pyflow/graphics/window.py index d10a7690..76a64eac 100644 --- a/pyflow/graphics/window.py +++ b/pyflow/graphics/window.py @@ -23,6 +23,7 @@ from pyflow.qss import loadStylesheets from pyflow.qss import __file__ as QSS_INIT_PATH +from pyflow.scene.clipboard import BlocksClipboard QSS_PATH = pathlib.Path(QSS_INIT_PATH).parent @@ -70,6 +71,9 @@ def __init__(self): self.readSettings() self.show() + # Block clipboard + self.clipboard = BlocksClipboard() + def createToolBars(self): """Does nothing, but is required by the QMainWindow interface.""" @@ -397,19 +401,19 @@ def onEditCut(self): """Cut the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): - current_window.scene.clipboard.cut() + self.clipboard.cut(current_window.scene) def onEditCopy(self): """Copy the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): - current_window.scene.clipboard.copy() + self.clipboard.copy(current_window.scene) def onEditPaste(self): """Paste the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): - current_window.scene.clipboard.paste() + self.clipboard.paste(current_window.scene) def onEditDelete(self): """Delete the selected items if not in edit mode.""" diff --git a/pyflow/scene/clipboard.py b/pyflow/scene/clipboard.py index d01e965c..c5694e0e 100644 --- a/pyflow/scene/clipboard.py +++ b/pyflow/scene/clipboard.py @@ -3,46 +3,40 @@ """ Module for the handling of scene clipboard operations. """ -from typing import TYPE_CHECKING, OrderedDict +from typing import TYPE_CHECKING, OrderedDict, Union from warnings import warn -import json -from PyQt5.QtWidgets import QApplication - from pyflow.core.edge import Edge if TYPE_CHECKING: from pyflow.scene import Scene - from pyflow.graphics.view import View - -class SceneClipboard: - """Helper object to handle clipboard operations on an Scene.""" +class BlocksClipboard: - def __init__(self, scene: "Scene"): - """Helper object to handle clipboard operations on an Scene. + """Helper object to handle clipboard operations on blocks.""" - Args: - scene: Scene reference. + def __init__(self): + """Helper object to handle clipboard operations on blocks.""" + self.blocks_data: Union[None, OrderedDict] = None - """ - self.scene = scene - - def cut(self): + def cut(self, scene: "Scene"): """Cut the selected items and put them into clipboard.""" - self._store(self._serializeSelected(delete=True)) + self._store(self._serializeSelected(scene, delete=True)) - def copy(self): + def copy(self, scene: "Scene"): """Copy the selected items into clipboard.""" - self._store(self._serializeSelected(delete=False)) + self._store(self._serializeSelected(scene, delete=False)) - def paste(self): + def paste(self, scene: "Scene"): """Paste the items in clipboard into the current scene.""" - self._deserializeData(self._gatherData()) + data = self._gatherData() + if data is not None: + self._deserializeData(data, scene) - def _serializeSelected(self, delete=False) -> OrderedDict: - selected_blocks, selected_edges = self.scene.sortedSelectedItems() + def _serializeSelected(self, scene: "Scene", delete=False) -> OrderedDict: + """Serialize the items in the scene""" + selected_blocks, selected_edges = scene.sortedSelectedItems() selected_sockets = {} # Gather selected sockets @@ -66,7 +60,7 @@ def _serializeSelected(self, delete=False) -> OrderedDict: ) if delete: # Remove selected items - self.scene.views()[0].deleteSelected() + scene.views()[0].deleteSelected() return data @@ -77,16 +71,18 @@ def _find_bbox_center(self, blocks_data): ymax = max(block["position"][1] + block["height"] for block in blocks_data) return (xmin + xmax) / 2, (ymin + ymax) / 2 - def _deserializeData(self, data: OrderedDict, set_selected=True): + def _deserializeData(self, data: OrderedDict, scene: "Scene", set_selected=True): + """Deserialize the items and put them in the scene""" + if data is None: return hashmap = {} - view = self.scene.views()[0] + view = scene.views()[0] mouse_pos = view.lastMousePos if set_selected: - self.scene.clearSelection() + scene.clearSelection() # Finding pasting bbox center bbox_center_x, bbox_center_y = self._find_bbox_center(data["blocks"]) @@ -97,7 +93,7 @@ def _deserializeData(self, data: OrderedDict, set_selected=True): # Create blocks for block_data in data["blocks"]: - block = self.scene.create_block(block_data, hashmap, restore_id=False) + block = scene.create_block(block_data, hashmap, restore_id=False) if set_selected: block.setSelected(True) block.setPos(block.x() + offset_x, block.y() + offset_y) @@ -109,21 +105,22 @@ def _deserializeData(self, data: OrderedDict, set_selected=True): if set_selected: edge.setSelected(True) - self.scene.addItem(edge) + scene.addItem(edge) hashmap.update({edge_data["id"]: edge}) - self.scene.history.checkpoint( - "Desiralized elements into scene", set_modified=True - ) + scene.history.checkpoint("Desiralized elements into scene", set_modified=True) def _store(self, data: OrderedDict): - str_data = json.dumps(data, indent=4) - QApplication.instance().clipboard().setText(str_data) - - def _gatherData(self) -> str: - str_data = QApplication.instance().clipboard().text() - try: - return json.loads(str_data) - except ValueError as valueerror: - warn(f"Clipboard text could not be loaded into json data: {valueerror}") + """Store the data in the clipboard if it is valid.""" + + if "blocks" not in data or not data["blocks"]: + self.blocks_data = None return + + self.blocks_data = data + + def _gatherData(self) -> Union[OrderedDict, None]: + """Return the data stored in the clipboard.""" + if self.blocks_data is None: + warn(f"No object is loaded") + return self.blocks_data diff --git a/pyflow/scene/scene.py b/pyflow/scene/scene.py index e7d08fc2..53502f4b 100644 --- a/pyflow/scene/scene.py +++ b/pyflow/scene/scene.py @@ -16,7 +16,6 @@ from pyflow.core.serializable import Serializable from pyflow.blocks.block import Block from pyflow.core.edge import Edge -from pyflow.scene.clipboard import SceneClipboard from pyflow.scene.history import SceneHistory from pyflow.core.kernel import Kernel from pyflow.scene.from_ipynb_conversion import ipynb_to_ipyg @@ -56,7 +55,6 @@ def __init__( self._has_been_modified_listeners = [] self.history = SceneHistory(self) - self.clipboard = SceneClipboard(self) self.kernel = Kernel() self.threadpool = QThreadPool() diff --git a/tests/unit/scene/test_clipboard.py b/tests/unit/scene/test_clipboard.py index 4321be3b..e9054dd9 100644 --- a/tests/unit/scene/test_clipboard.py +++ b/tests/unit/scene/test_clipboard.py @@ -7,12 +7,12 @@ from pytest_mock import MockerFixture import pytest_check as check -from pyflow.scene.clipboard import SceneClipboard +from pyflow.scene.clipboard import BlocksClipboard class TestSerializeSelected: - """SceneClipboard._serializeSelected""" + """BlocksClipboard._serializeSelected""" @pytest.fixture(autouse=True) def setup(self, mocker: MockerFixture): @@ -42,25 +42,25 @@ def setup(self, mocker: MockerFixture): edge.destination_socket.id = dummy_edges_links[i][1] self.scene.sortedSelectedItems.return_value = self.blocks, self.edges - self.clipboard = SceneClipboard(self.scene) + self.clipboard = BlocksClipboard() def test_serialize_selected_blocks(self, mocker: MockerFixture): """should allow for blocks serialization.""" - data = self.clipboard._serializeSelected() + data = self.clipboard._serializeSelected(self.scene) check.equal(data["blocks"], [block.serialize() for block in self.blocks]) def test_serialize_selected_edges(self, mocker: MockerFixture): """should allow for edges serialization.""" - data = self.clipboard._serializeSelected() + data = self.clipboard._serializeSelected(self.scene) check.equal(data["edges"], [edge.serialize() for edge in self.edges]) def test_serialize_partially_selected_edges(self, mocker: MockerFixture): """should not allow for partially selected edges serialization.""" self.scene.sortedSelectedItems.return_value = self.blocks[0], self.edges - data = self.clipboard._serializeSelected() + data = self.clipboard._serializeSelected(self.scene) check.equal(data["edges"], [self.edges[0].serialize()]) def test_serialize_delete(self, mocker: MockerFixture): """should allow for items deletion after serialization.""" - self.clipboard._serializeSelected(delete=True) + self.clipboard._serializeSelected(self.scene, delete=True) check.is_true(self.view.deleteSelected.called)