Skip to content

Commit

Permalink
Merge pull request #229 from Bycelium/bugfix/paste-block
Browse files Browse the repository at this point in the history
Create a disinct clipboard for blocks
  • Loading branch information
FabienRoger authored Jan 22, 2022
2 parents e9ff438 + b45c1ce commit d422b47
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 53 deletions.
10 changes: 7 additions & 3 deletions pyflow/graphics/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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."""
Expand Down
79 changes: 38 additions & 41 deletions pyflow/scene/clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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"])
Expand All @@ -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)
Expand All @@ -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
2 changes: 0 additions & 2 deletions pyflow/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
14 changes: 7 additions & 7 deletions tests/unit/scene/test_clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

0 comments on commit d422b47

Please sign in to comment.