From e92256d4c45c2c9b771460fe49e6e85ff543cdb7 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 21 Jan 2022 11:34:53 +0100 Subject: [PATCH 1/3] :beetle: Fix clipboard bug by avoiding using the application clipboard --- pyflow/scene/clipboard.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/pyflow/scene/clipboard.py b/pyflow/scene/clipboard.py index d01e965c..367429a4 100644 --- a/pyflow/scene/clipboard.py +++ b/pyflow/scene/clipboard.py @@ -3,7 +3,7 @@ """ 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 @@ -28,6 +28,7 @@ def __init__(self, scene: "Scene"): """ self.scene = scene + self.objects: Union[None, OrderedDict] = None def cut(self): """Cut the selected items and put them into clipboard.""" @@ -39,9 +40,12 @@ def copy(self): def paste(self): """Paste the items in clipboard into the current scene.""" - self._deserializeData(self._gatherData()) + data = self._gatherData() + if data is not None: + self._deserializeData(data) def _serializeSelected(self, delete=False) -> OrderedDict: + """Serialize the items in the scene""" selected_blocks, selected_edges = self.scene.sortedSelectedItems() selected_sockets = {} @@ -78,6 +82,8 @@ def _find_bbox_center(self, blocks_data): return (xmin + xmax) / 2, (ymin + ymax) / 2 def _deserializeData(self, data: OrderedDict, set_selected=True): + """Deserialize the items and put them in the scene""" + if data is None: return @@ -117,13 +123,16 @@ def _deserializeData(self, data: OrderedDict, set_selected=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.objects = None return + + self.objects = data + + def _gatherData(self) -> Union[OrderedDict, None]: + """Return the data stored in the clipboard.""" + if self.objects is None: + warn(f"No object is loaded") + return self.objects From ec37a14dc27d67585ae85dc70a3faadb20ddc157 Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 21 Jan 2022 11:48:32 +0100 Subject: [PATCH 2/3] :wrench: Make the blocks clipboard global --- pyflow/graphics/window.py | 10 +++++--- pyflow/scene/clipboard.py | 37 ++++++++++++------------------ pyflow/scene/scene.py | 2 -- tests/unit/scene/test_clipboard.py | 9 +++++--- 4 files changed, 28 insertions(+), 30 deletions(-) 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 367429a4..716ead51 100644 --- a/pyflow/scene/clipboard.py +++ b/pyflow/scene/clipboard.py @@ -6,40 +6,33 @@ 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. - - """ - self.scene = scene - self.objects: Union[None, OrderedDict] = None + def __init__(self): + """Helper object to handle clipboard operations on blocks.""" + self.blocks_data: Union[None, OrderedDict] = None - def cut(self): + def cut(self, scene: "Scene"): """Cut the selected items and put them into clipboard.""" + self.scene = scene self._store(self._serializeSelected(delete=True)) - def copy(self): + def copy(self, scene: "Scene"): """Copy the selected items into clipboard.""" + self.scene = scene self._store(self._serializeSelected(delete=False)) - def paste(self): + def paste(self, scene: "Scene"): """Paste the items in clipboard into the current scene.""" + self.scene = scene data = self._gatherData() if data is not None: self._deserializeData(data) @@ -126,13 +119,13 @@ def _store(self, data: OrderedDict): """Store the data in the clipboard if it is valid.""" if "blocks" not in data or not data["blocks"]: - self.objects = None + self.blocks_data = None return - self.objects = data + self.blocks_data = data def _gatherData(self) -> Union[OrderedDict, None]: """Return the data stored in the clipboard.""" - if self.objects is None: + if self.blocks_data is None: warn(f"No object is loaded") - return self.objects + 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..07f72ee1 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,7 +42,10 @@ 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() + # Manually set the clipboard current scene + # Because private functions of the clipboard are used + self.clipboard.scene = self.scene def test_serialize_selected_blocks(self, mocker: MockerFixture): """should allow for blocks serialization.""" From b45c1cef286a98e53a83d9e2b6b13e87057d126c Mon Sep 17 00:00:00 2001 From: Fabien Roger Date: Fri, 21 Jan 2022 18:35:28 +0100 Subject: [PATCH 3/3] :wrench: Make explicit that scene is not an attribute of the clipboard --- pyflow/scene/clipboard.py | 29 ++++++++++++----------------- tests/unit/scene/test_clipboard.py | 11 ++++------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/pyflow/scene/clipboard.py b/pyflow/scene/clipboard.py index 716ead51..c5694e0e 100644 --- a/pyflow/scene/clipboard.py +++ b/pyflow/scene/clipboard.py @@ -22,24 +22,21 @@ def __init__(self): def cut(self, scene: "Scene"): """Cut the selected items and put them into clipboard.""" - self.scene = scene - self._store(self._serializeSelected(delete=True)) + self._store(self._serializeSelected(scene, delete=True)) def copy(self, scene: "Scene"): """Copy the selected items into clipboard.""" - self.scene = scene - self._store(self._serializeSelected(delete=False)) + self._store(self._serializeSelected(scene, delete=False)) def paste(self, scene: "Scene"): """Paste the items in clipboard into the current scene.""" - self.scene = scene data = self._gatherData() if data is not None: - self._deserializeData(data) + self._deserializeData(data, scene) - def _serializeSelected(self, delete=False) -> OrderedDict: + def _serializeSelected(self, scene: "Scene", delete=False) -> OrderedDict: """Serialize the items in the scene""" - selected_blocks, selected_edges = self.scene.sortedSelectedItems() + selected_blocks, selected_edges = scene.sortedSelectedItems() selected_sockets = {} # Gather selected sockets @@ -63,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 @@ -74,7 +71,7 @@ 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: @@ -82,10 +79,10 @@ def _deserializeData(self, data: OrderedDict, set_selected=True): 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"]) @@ -96,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) @@ -108,12 +105,10 @@ 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): """Store the data in the clipboard if it is valid.""" diff --git a/tests/unit/scene/test_clipboard.py b/tests/unit/scene/test_clipboard.py index 07f72ee1..e9054dd9 100644 --- a/tests/unit/scene/test_clipboard.py +++ b/tests/unit/scene/test_clipboard.py @@ -43,27 +43,24 @@ def setup(self, mocker: MockerFixture): self.scene.sortedSelectedItems.return_value = self.blocks, self.edges self.clipboard = BlocksClipboard() - # Manually set the clipboard current scene - # Because private functions of the clipboard are used - self.clipboard.scene = self.scene 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)