Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 Create a distinct clipboard for blocks #229

Merged
merged 3 commits into from
Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
MathisFederico marked this conversation as resolved.
Show resolved Hide resolved

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)