From a37c37eda7bec9ab30a01af3564e1f9f4692f834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 22 Nov 2021 23:19:01 +0100 Subject: [PATCH 1/8] :tada: Create new blocks on right click --- blocks/empty.ocbb | 18 +++++++++++++ opencodeblocks/graphics/scene/scene.py | 37 ++++++++++++++++++-------- opencodeblocks/graphics/view.py | 13 +++++++-- 3 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 blocks/empty.ocbb diff --git a/blocks/empty.ocbb b/blocks/empty.ocbb new file mode 100644 index 00000000..4ff5bdd3 --- /dev/null +++ b/blocks/empty.ocbb @@ -0,0 +1,18 @@ +{ + "title": "Empty", + "block_type": "code", + "source": "", + "stdout": "", + "image": "", + "splitter_pos": [88,41], + "width": 618, + "height": 184, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10, + "padding": 4.0 + } + } +} \ No newline at end of file diff --git a/opencodeblocks/graphics/scene/scene.py b/opencodeblocks/graphics/scene/scene.py index 6708e1ea..fa2ffde8 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/graphics/scene/scene.py @@ -176,8 +176,31 @@ def serialize(self) -> OrderedDict: ('blocks', [block.serialize() for block in blocks]), ('edges', [edge.serialize() for edge in edges]), ]) - - def deserialize(self, data: OrderedDict, hashmap:dict=None, restore_id=True): + + def create_block_from_file(self, filepath:str, x: float = 0, y: float = 0): + with open(filepath, 'r', encoding='utf-8') as file: + data = json.loads(file.read()) + data["position"] = [x,y] + data["sockets"] = {} + data["id"] = -1 + b = self.create_block(data,None,False) + + + def create_block(self, data: OrderedDict, hashmap: dict = None, restore_id:bool = True) -> OCBBlock: + block = None + if data['block_type'] == 'base': + block = OCBBlock() + elif data['block_type'] == 'code': + block = OCBCodeBlock() + else: + raise NotImplementedError() + block.deserialize(data, hashmap, restore_id) + self.addItem(block) + if hashmap is not None: + hashmap.update({data['id']: block}) + return block + + def deserialize(self, data: OrderedDict, hashmap: dict=None, restore_id:bool = True): self.clear() hashmap = hashmap if hashmap is not None else {} if restore_id: @@ -185,15 +208,7 @@ def deserialize(self, data: OrderedDict, hashmap:dict=None, restore_id=True): # Create blocks for block_data in data['blocks']: - if block_data['block_type'] == 'base': - block = OCBBlock() - elif block_data['block_type'] == 'code': - block = OCBCodeBlock() - else: - raise NotImplementedError() - block.deserialize(block_data, hashmap, restore_id) - self.addItem(block) - hashmap.update({block_data['id']: block}) + self.create_block(block_data, hashmap, restore_id) # Create edges for edge_data in data['edges']: diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index 6d399240..8b513d02 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -4,8 +4,8 @@ """ Module for the OCB View """ from PyQt5.QtCore import QEvent, QPointF, Qt -from PyQt5.QtGui import QMouseEvent, QPainter, QWheelEvent -from PyQt5.QtWidgets import QGraphicsView +from PyQt5.QtGui import QMouseEvent, QPainter, QWheelEvent, QContextMenuEvent +from PyQt5.QtWidgets import QGraphicsView, QMenu from PyQt5.sip import isdeleted from opencodeblocks.graphics.scene import OCBScene @@ -139,6 +139,15 @@ def rightMouseButtonRelease(self, event: QMouseEvent): super().mouseReleaseEvent(event) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) + + def contextMenuEvent(self, event: QContextMenuEvent): + menu = QMenu(self) + newAction = menu.addAction("New Empty Block") + action = menu.exec_(self.mapToGlobal(event.pos())) + if action == newAction: + p = self.mapToScene(event.pos()) + self.scene().create_block_from_file("blocks/empty.ocbb", p.x(), p.y()) + def wheelEvent(self, event: QWheelEvent): """ Handles zooming with mouse wheel events. """ if Qt.Modifier.CTRL == int(event.modifiers()): From 114d11699a62d817ebeb01ff6054f40f764f008f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 22 Nov 2021 23:21:56 +0100 Subject: [PATCH 2/8] :sparkles: autopep8 on scene.py for pylint --- opencodeblocks/graphics/scene/scene.py | 40 ++++++++++++++------------ opencodeblocks/graphics/view.py | 1 + 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/opencodeblocks/graphics/scene/scene.py b/opencodeblocks/graphics/scene/scene.py index fa2ffde8..d9678d6b 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/graphics/scene/scene.py @@ -25,10 +25,10 @@ class OCBScene(QGraphicsScene, Serializable): """ Scene for the OCB Window. """ def __init__(self, parent=None, - background_color:str="#393939", - grid_color:str="#292929", grid_light_color:str="#2f2f2f", - width:int=64000, height:int=64000, - grid_size:int=20, grid_squares:int=5): + background_color: str = "#393939", + grid_color: str = "#292929", grid_light_color: str = "#2f2f2f", + width: int = 64000, height: int = 64000, + grid_size: int = 20, grid_squares: int = 5): Serializable.__init__(self) QGraphicsScene.__init__(self, parent=parent) @@ -39,7 +39,8 @@ def __init__(self, parent=None, self.grid_squares = grid_squares self.width, self.height = width, height - self.setSceneRect(-self.width//2, -self.height//2, self.width, self.height) + self.setSceneRect(-self.width // 2, -self.height // + 2, self.width, self.height) self.setBackgroundBrush(self._background_color) self._has_been_modified = False @@ -52,13 +53,14 @@ def __init__(self, parent=None, def has_been_modified(self): """ True if the scene has been modified, False otherwise. """ return self._has_been_modified + @has_been_modified.setter - def has_been_modified(self, value:bool): + def has_been_modified(self, value: bool): self._has_been_modified = value for callback in self._has_been_modified_listeners: callback() - def addHasBeenModifiedListener(self, callback:FunctionType): + def addHasBeenModifiedListener(self, callback: FunctionType): """ Add a callback that will trigger when the scene has been modified. """ self._has_been_modified_listeners.append(callback) @@ -112,12 +114,12 @@ def drawGrid(self, painter: QPainter, rect: QRectF): painter.setPen(pen) painter.drawLines(*lines_light) - def save(self, filepath:str): + def save(self, filepath: str): """ Save the scene into filepath. """ self.save_to_ipyg(filepath) self.has_been_modified = False - def save_to_ipyg(self, filepath:str): + def save_to_ipyg(self, filepath: str): """ Save the scene into filepath as interactive python graph (.ipyg). """ if '.' not in filepath: filepath += '.ipyg' @@ -129,7 +131,7 @@ def save_to_ipyg(self, filepath:str): with open(filepath, 'w', encoding='utf-8') as file: file.write(json.dumps(self.serialize(), indent=4)) - def load(self, filepath:str): + def load(self, filepath: str): """ Load a saved scene. Args: @@ -145,7 +147,7 @@ def load(self, filepath:str): self.history.checkpoint("Loaded scene") self.has_been_modified = False - def load_from_ipyg(self, filepath:str): + def load_from_ipyg(self, filepath: str): """ Load an interactive python graph (.ipyg) into the scene. Args: @@ -176,17 +178,18 @@ def serialize(self) -> OrderedDict: ('blocks', [block.serialize() for block in blocks]), ('edges', [edge.serialize() for edge in edges]), ]) - - def create_block_from_file(self, filepath:str, x: float = 0, y: float = 0): + + def create_block_from_file( + self, filepath: str, x: float = 0, y: float = 0): with open(filepath, 'r', encoding='utf-8') as file: data = json.loads(file.read()) - data["position"] = [x,y] + data["position"] = [x, y] data["sockets"] = {} data["id"] = -1 - b = self.create_block(data,None,False) - + b = self.create_block(data, None, False) - def create_block(self, data: OrderedDict, hashmap: dict = None, restore_id:bool = True) -> OCBBlock: + def create_block(self, data: OrderedDict, hashmap: dict = None, + restore_id: bool = True) -> OCBBlock: block = None if data['block_type'] == 'base': block = OCBBlock() @@ -200,7 +203,8 @@ def create_block(self, data: OrderedDict, hashmap: dict = None, restore_id:bool hashmap.update({data['id']: block}) return block - def deserialize(self, data: OrderedDict, hashmap: dict=None, restore_id:bool = True): + def deserialize(self, data: OrderedDict, + hashmap: dict = None, restore_id: bool = True): self.clear() hashmap = hashmap if hashmap is not None else {} if restore_id: diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index 8b513d02..f7064814 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -141,6 +141,7 @@ def rightMouseButtonRelease(self, event: QMouseEvent): def contextMenuEvent(self, event: QContextMenuEvent): + """ Displays the context menu when inside a view """ menu = QMenu(self) newAction = menu.addAction("New Empty Block") action = menu.exec_(self.mapToGlobal(event.pos())) From aea5a2f7db1c1db5959557186a1da68996db874e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 22 Nov 2021 23:57:20 +0100 Subject: [PATCH 3/8] :tada: Support for custom blocks that can be created with right-click. A library with some default blocks is provided inside the `blocks` directory --- blocks/cnn_model.ocbb | 18 ++++++ blocks/import_ml.ocbb | 18 ++++++ opencodeblocks/graphics/scene/scene.py | 5 +- opencodeblocks/graphics/view.py | 77 ++++++++++++++++++-------- 4 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 blocks/cnn_model.ocbb create mode 100644 blocks/import_ml.ocbb diff --git a/blocks/cnn_model.ocbb b/blocks/cnn_model.ocbb new file mode 100644 index 00000000..56edefa6 --- /dev/null +++ b/blocks/cnn_model.ocbb @@ -0,0 +1,18 @@ +{ + "title": "CNN", + "block_type": "code", + "source": "input_size = 28\r\nclasses = 5\r\nmodel = Sequential()\r\nmodel.add(layers.Conv2D(input_size, kernel_size=(3,3), input_shape=(input_size,input_size,1)))\r\nmodel.add(layers.MaxPooling2D(pool_size=(2, 2)))\r\nmodel.add(layers.Flatten())\r\nmodel.add(layers.Dense(128, activation=tf.nn.relu))\r\nmodel.add(layers.Dropout(0.2))\r\nmodel.add(layers.Dense(classes,activation=tf.nn.softmax))\r\n\r\nmodel.compile(optimizer='adam', \r\n loss='sparse_categorical_crossentropy')", + "stdout": "", + "image": "", + "splitter_pos": [80,50], + "width": 600, + "height": 400, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10, + "padding": 4.0 + } + } +} \ No newline at end of file diff --git a/blocks/import_ml.ocbb b/blocks/import_ml.ocbb new file mode 100644 index 00000000..6b6440fa --- /dev/null +++ b/blocks/import_ml.ocbb @@ -0,0 +1,18 @@ +{ + "title": "Imports for ML", + "block_type": "code", + "source": "import tensorflow as tf\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.models import Sequential", + "stdout": "", + "image": "", + "splitter_pos": [80,50], + "width": 600, + "height": 400, + "metadata": { + "title_metadata": { + "color": "white", + "font": "Ubuntu", + "size": 10, + "padding": 4.0 + } + } +} \ No newline at end of file diff --git a/opencodeblocks/graphics/scene/scene.py b/opencodeblocks/graphics/scene/scene.py index d9678d6b..aeb27393 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/graphics/scene/scene.py @@ -181,15 +181,18 @@ def serialize(self) -> OrderedDict: def create_block_from_file( self, filepath: str, x: float = 0, y: float = 0): + """ Create a new block from a .ocbb file """ with open(filepath, 'r', encoding='utf-8') as file: data = json.loads(file.read()) data["position"] = [x, y] data["sockets"] = {} data["id"] = -1 - b = self.create_block(data, None, False) + self.create_block(data, None, False) def create_block(self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True) -> OCBBlock: + """ Create a new block from an OrderedDict """ + block = None if data['block_type'] == 'base': block = OCBBlock() diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index f7064814..a8b50392 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -3,6 +3,10 @@ """ Module for the OCB View """ +import json +import os +import time + from PyQt5.QtCore import QEvent, QPointF, Qt from PyQt5.QtGui import QMouseEvent, QPainter, QWheelEvent, QContextMenuEvent from PyQt5.QtWidgets import QGraphicsView, QMenu @@ -13,6 +17,8 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.graphics.blocks import OCBBlock +RIGHT_CLICK_SPEED = 0.5 # In seconds + MODE_NOOP = 0 MODE_EDGE_DRAG = 1 MODE_EDITING = 2 @@ -28,8 +34,8 @@ class OCBView(QGraphicsView): """ View for the OCB Window. """ - def __init__(self, scene:OCBScene, parent=None, - zoom_step:float=1.25, zoom_min:float=0.2, zoom_max:float=5): + def __init__(self, scene: OCBScene, parent=None, + zoom_step: float = 1.25, zoom_min: float = 0.2, zoom_max: float = 5): super().__init__(parent=parent) self.mode = MODE_NOOP self.zoom = 1 @@ -39,6 +45,8 @@ def __init__(self, scene:OCBScene, parent=None, self.lastMousePos = QPointF(0, 0) self.currentSelectedBlock = None + self.right_click_time = 0 + self.init_ui() self.setScene(scene) @@ -56,10 +64,12 @@ def init_ui(self): QGraphicsView.ViewportUpdateMode.FullViewportUpdate ) # Remove scroll bars - self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) # Zoom on cursor - self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) + self.setTransformationAnchor( + QGraphicsView.ViewportAnchor.AnchorUnderMouse) # Selection box self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) @@ -110,7 +120,7 @@ def leftMouseButtonPress(self, event: QMouseEvent): item_at_click = self.itemAt(event.pos()) if item_at_click is not None: while item_at_click.parentItem() is not None: - if isinstance(item_at_click,OCBBlock): + if isinstance(item_at_click, OCBBlock): break item_at_click = item_at_click.parentItem() @@ -131,6 +141,7 @@ def leftMouseButtonRelease(self, event: QMouseEvent): def rightMouseButtonPress(self, event: QMouseEvent): """ OCBView reaction to rightMouseButtonPress event. """ event = self.drag_scene(event, "press") + self.right_click_time = time.time() super().mousePressEvent(event) def rightMouseButtonRelease(self, event: QMouseEvent): @@ -138,16 +149,36 @@ def rightMouseButtonRelease(self, event: QMouseEvent): event = self.drag_scene(event, "release") super().mouseReleaseEvent(event) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) + if time.time() - self.right_click_time < RIGHT_CLICK_SPEED: + self.fastContextMenuEvent(event) + + def fastContextMenuEvent(self, event: QContextMenuEvent): + """ + Displays the context menu when inside a view + We don't use the default contextMenuEvent method to avoid + displaying a context menu when moving around the view. - def contextMenuEvent(self, event: QContextMenuEvent): - """ Displays the context menu when inside a view """ + For this method to be triggered, the click has to last less than RIGHT_CLICK_SPEED. + """ menu = QMenu(self) - newAction = menu.addAction("New Empty Block") - action = menu.exec_(self.mapToGlobal(event.pos())) - if action == newAction: - p = self.mapToScene(event.pos()) - self.scene().create_block_from_file("blocks/empty.ocbb", p.x(), p.y()) + actionPool = [] + blockTypes = os.listdir("blocks") + for b in blockTypes: + filepath = os.path.join("blocks", b) + with open(filepath, "r", encoding="utf-8") as file: + data = json.loads(file.read()) + if "title" not in data: + actionPool.append((filepath, menu.addAction(f"New Block"))) + else: + actionPool.append( + (filepath, menu.addAction(f"New {data['title']} Block"))) + + selectedAction = menu.exec_(self.mapToGlobal(event.pos())) + for filepath, action in actionPool: + if action == selectedAction: + p = self.mapToScene(event.pos()) + self.scene().create_block_from_file(filepath, p.x(), p.y()) def wheelEvent(self, event: QWheelEvent): """ Handles zooming with mouse wheel events. """ @@ -178,7 +209,8 @@ def bring_block_forward(self, block: OCBBlock): block: Block to bring forward. """ - if self.currentSelectedBlock is not None and not isdeleted(self.currentSelectedBlock): + if self.currentSelectedBlock is not None and not isdeleted( + self.currentSelectedBlock): self.currentSelectedBlock.setZValue(0) block.setZValue(1) self.currentSelectedBlock = block @@ -187,16 +219,16 @@ def drag_scene(self, event: QMouseEvent, action="press"): """ Drag the scene around. """ if action == "press": releaseEvent = QMouseEvent(QEvent.Type.MouseButtonRelease, - event.localPos(), event.screenPos(), - Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, event.modifiers()) + event.localPos(), event.screenPos(), + Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, event.modifiers()) super().mouseReleaseEvent(releaseEvent) self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) return QMouseEvent(event.type(), event.localPos(), event.screenPos(), - Qt.MouseButton.LeftButton, event.buttons() | Qt.MouseButton.LeftButton, - event.modifiers()) + Qt.MouseButton.LeftButton, event.buttons() | Qt.MouseButton.LeftButton, + event.modifiers()) return QMouseEvent(event.type(), event.localPos(), event.screenPos(), - Qt.MouseButton.LeftButton,event.buttons() & ~Qt.MouseButton.LeftButton, - event.modifiers()) + Qt.MouseButton.LeftButton, event.buttons() & ~Qt.MouseButton.LeftButton, + event.modifiers()) def drag_edge(self, event: QMouseEvent, action="press"): """ Create an edge by drag and drop. """ @@ -219,7 +251,8 @@ def drag_edge(self, event: QMouseEvent, action="press"): and item_at_click is not self.edge_drag.source_socket \ and item_at_click.socket_type != 'output': self.edge_drag.destination_socket = item_at_click - scene.history.checkpoint("Created edge by dragging", set_modified=True) + scene.history.checkpoint( + "Created edge by dragging", set_modified=True) else: self.edge_drag.remove() self.edge_drag = None @@ -229,7 +262,7 @@ def drag_edge(self, event: QMouseEvent, action="press"): self.edge_drag.destination = self.mapToScene(event.pos()) return event - def set_mode(self, mode:str): + def set_mode(self, mode: str): """ Change the view mode. Args: @@ -238,7 +271,7 @@ def set_mode(self, mode:str): """ self.mode = MODES[mode] - def is_mode(self, mode:str): + def is_mode(self, mode: str): """ Return True if the view is in the given mode. Args: From e42777e604a9681d5939b871f333eaeb320e0749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Tue, 23 Nov 2021 10:19:06 +0100 Subject: [PATCH 4/8] :tada: The scene is now dragged with the middle mouse button. Right click is reserved for context menu. --- opencodeblocks/graphics/blocks/block.py | 11 ++++---- opencodeblocks/graphics/scene/scene.py | 1 - opencodeblocks/graphics/view.py | 34 +++++-------------------- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 855ede76..2beecea7 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -321,11 +321,12 @@ def deserialize(self, data: dict, hashmap: dict = None, if 'splitter_pos' in data: self.splitter.setSizes(data['splitter_pos']) - for socket_data in data['sockets']: - socket = OCBSocket(block=self) - socket.deserialize(socket_data, hashmap, restore_id) - self.add_socket(socket) - hashmap.update({socket_data['id']: socket}) + if hashmap is not None: + for socket_data in data['sockets']: + socket = OCBSocket(block=self) + socket.deserialize(socket_data, hashmap, restore_id) + self.add_socket(socket) + hashmap.update({socket_data['id']: socket}) class OCBSplitterHandle(QSplitterHandle): diff --git a/opencodeblocks/graphics/scene/scene.py b/opencodeblocks/graphics/scene/scene.py index aeb27393..fa99e84b 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/graphics/scene/scene.py @@ -186,7 +186,6 @@ def create_block_from_file( data = json.loads(file.read()) data["position"] = [x, y] data["sockets"] = {} - data["id"] = -1 self.create_block(data, None, False) def create_block(self, data: OrderedDict, hashmap: dict = None, diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index a8b50392..b55afcf4 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -83,8 +83,6 @@ def mousePressEvent(self, event: QMouseEvent): self.middleMouseButtonPress(event) elif event.button() == Qt.MouseButton.LeftButton: self.leftMouseButtonPress(event) - elif event.button() == Qt.MouseButton.RightButton: - self.rightMouseButtonPress(event) else: super().mousePressEvent(event) @@ -94,8 +92,6 @@ def mouseReleaseEvent(self, event: QMouseEvent): self.middleMouseButtonRelease(event) elif event.button() == Qt.MouseButton.LeftButton: self.leftMouseButtonRelease(event) - elif event.button() == Qt.MouseButton.RightButton: - self.rightMouseButtonRelease(event) else: super().mouseReleaseEvent(event) @@ -106,14 +102,6 @@ def mouseMoveEvent(self, event: QMouseEvent) -> None: if event is not None: super().mouseMoveEvent(event) - def middleMouseButtonPress(self, event: QMouseEvent): - """ OCBView reaction to middleMouseButtonPress event. """ - super().mousePressEvent(event) - - def middleMouseButtonRelease(self, event: QMouseEvent): - """ OCBView reaction to middleMouseButtonRelease event. """ - super().mouseReleaseEvent(event) - def leftMouseButtonPress(self, event: QMouseEvent): """ OCBView reaction to leftMouseButtonPress event. """ # If clicked on a block, bring it forward. @@ -138,29 +126,19 @@ def leftMouseButtonRelease(self, event: QMouseEvent): if event is not None: super().mouseReleaseEvent(event) - def rightMouseButtonPress(self, event: QMouseEvent): - """ OCBView reaction to rightMouseButtonPress event. """ + def middleMouseButtonPress(self, event: QMouseEvent): + """ OCBView reaction to middleMouseButtonPress event. """ event = self.drag_scene(event, "press") - self.right_click_time = time.time() super().mousePressEvent(event) - def rightMouseButtonRelease(self, event: QMouseEvent): - """ OCBView reaction to rightMouseButtonRelease event. """ + def middleMouseButtonRelease(self, event: QMouseEvent): + """ OCBView reaction to middleMouseButtonRelease event. """ event = self.drag_scene(event, "release") super().mouseReleaseEvent(event) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) - if time.time() - self.right_click_time < RIGHT_CLICK_SPEED: - self.fastContextMenuEvent(event) - def fastContextMenuEvent(self, event: QContextMenuEvent): - """ - Displays the context menu when inside a view - - We don't use the default contextMenuEvent method to avoid - displaying a context menu when moving around the view. - - For this method to be triggered, the click has to last less than RIGHT_CLICK_SPEED. - """ + def contextMenuEvent(self, event: QContextMenuEvent): + """ Displays the context menu when inside a view """ menu = QMenu(self) actionPool = [] blockTypes = os.listdir("blocks") From 9defabb4944c28318eb8619122d674645fd41038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Tue, 23 Nov 2021 18:36:42 +0100 Subject: [PATCH 5/8] :sparkles: Refactor block list into it's own function --- opencodeblocks/graphics/view.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index b55afcf4..1f56eaf9 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -5,20 +5,19 @@ import json import os -import time +from typing import List, Tuple from PyQt5.QtCore import QEvent, QPointF, Qt from PyQt5.QtGui import QMouseEvent, QPainter, QWheelEvent, QContextMenuEvent from PyQt5.QtWidgets import QGraphicsView, QMenu from PyQt5.sip import isdeleted + from opencodeblocks.graphics.scene import OCBScene from opencodeblocks.graphics.socket import OCBSocket from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.graphics.blocks import OCBBlock -RIGHT_CLICK_SPEED = 0.5 # In seconds - MODE_NOOP = 0 MODE_EDGE_DRAG = 1 MODE_EDITING = 2 @@ -45,8 +44,6 @@ def __init__(self, scene: OCBScene, parent=None, self.lastMousePos = QPointF(0, 0) self.currentSelectedBlock = None - self.right_click_time = 0 - self.init_ui() self.setScene(scene) @@ -137,20 +134,25 @@ def middleMouseButtonRelease(self, event: QMouseEvent): super().mouseReleaseEvent(event) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) + def retreiveBlockTypes(self) -> List[Tuple[str]]: + block_type_files = os.listdir("blocks") + block_types = [] + for b in block_type_files: + filepath = os.path.join("blocks", b) + with open(filepath, "r", encoding="utf-8") as file: + data = json.loads(file.read()) + title = "New Block" + if "title" in data: + title = f"New {data['title']} Block" + block_types.append((filepath,title)) + return block_types + def contextMenuEvent(self, event: QContextMenuEvent): """ Displays the context menu when inside a view """ menu = QMenu(self) actionPool = [] - blockTypes = os.listdir("blocks") - for b in blockTypes: - filepath = os.path.join("blocks", b) - with open(filepath, "r", encoding="utf-8") as file: - data = json.loads(file.read()) - if "title" not in data: - actionPool.append((filepath, menu.addAction(f"New Block"))) - else: - actionPool.append( - (filepath, menu.addAction(f"New {data['title']} Block"))) + for filepath, block_name in self.retreiveBlockTypes(): + actionPool.append((filepath, menu.addAction(block_name))) selectedAction = menu.exec_(self.mapToGlobal(event.pos())) for filepath, action in actionPool: From 78d207df9fc56d0cb6e2a969cb2c03bd2d4ef01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 23 Nov 2021 19:36:57 +0100 Subject: [PATCH 6/8] :beetle: Fix deserialize when hashmap is None --- opencodeblocks/graphics/blocks/block.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 2beecea7..58f0af4e 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -308,6 +308,9 @@ def serialize(self) -> OrderedDict: def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None: + if hashmap is None: + hashmap = {} + if restore_id: self.id = data['id'] for dataname in ('title', 'block_type', 'source', 'stdout', @@ -321,12 +324,11 @@ def deserialize(self, data: dict, hashmap: dict = None, if 'splitter_pos' in data: self.splitter.setSizes(data['splitter_pos']) - if hashmap is not None: - for socket_data in data['sockets']: - socket = OCBSocket(block=self) - socket.deserialize(socket_data, hashmap, restore_id) - self.add_socket(socket) - hashmap.update({socket_data['id']: socket}) + for socket_data in data['sockets']: + socket = OCBSocket(block=self) + socket.deserialize(socket_data, hashmap, restore_id) + self.add_socket(socket) + hashmap.update({socket_data['id']: socket}) class OCBSplitterHandle(QSplitterHandle): From 9f9ead4f8ae3ba08cc8f1079b55bcf010befcb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 23 Nov 2021 19:40:55 +0100 Subject: [PATCH 7/8] :wrench: Refactor block deserialize --- opencodeblocks/graphics/blocks/block.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 58f0af4e..0d13dd0f 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -308,9 +308,6 @@ def serialize(self) -> OrderedDict: def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None: - if hashmap is None: - hashmap = {} - if restore_id: self.id = data['id'] for dataname in ('title', 'block_type', 'source', 'stdout', @@ -328,7 +325,8 @@ def deserialize(self, data: dict, hashmap: dict = None, socket = OCBSocket(block=self) socket.deserialize(socket_data, hashmap, restore_id) self.add_socket(socket) - hashmap.update({socket_data['id']: socket}) + if hashmap is not None: + hashmap.update({socket_data['id']: socket}) class OCBSplitterHandle(QSplitterHandle): From 482316b73209d6ea9cbc6a2697614d1b68c1cd98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 23 Nov 2021 19:50:19 +0100 Subject: [PATCH 8/8] :sparkles: Fix a few pylint issues --- opencodeblocks/graphics/blocks/blocksizegrip.py | 2 +- opencodeblocks/graphics/blocks/codeblock.py | 2 +- opencodeblocks/graphics/view.py | 11 +++++++---- opencodeblocks/graphics/window.py | 4 ++-- opencodeblocks/graphics/worker.py | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/opencodeblocks/graphics/blocks/blocksizegrip.py b/opencodeblocks/graphics/blocks/blocksizegrip.py index c86c5c32..2c8eb4ed 100644 --- a/opencodeblocks/graphics/blocks/blocksizegrip.py +++ b/opencodeblocks/graphics/blocks/blocksizegrip.py @@ -33,7 +33,7 @@ def mousePressEvent(self, mouseEvent: QMouseEvent): self.mouseY = mouseEvent.globalY() self.resizing = True - def mouseReleaseEvent(self, mouseEvent: QMouseEvent): + def mouseReleaseEvent(self, mouseEvent: QMouseEvent): # pylint:disable=unused-argument """ Stop the resizing """ self.resizing = False self.block.scene().history.checkpoint("Resized block", set_modified=True) diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/graphics/blocks/codeblock.py index 8db7f8dd..976ac50e 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/graphics/blocks/codeblock.py @@ -112,7 +112,7 @@ def image(self, value: str): ba = QByteArray.fromBase64(str.encode(self.image)) pixmap = QPixmap() pixmap.loadFromData(ba) - text = ''.format(self.image) + text = f'' self.output_panel.setText(text) @source.setter diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index 1f56eaf9..aebd6ef0 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -144,7 +144,7 @@ def retreiveBlockTypes(self) -> List[Tuple[str]]: title = "New Block" if "title" in data: title = f"New {data['title']} Block" - block_types.append((filepath,title)) + block_types.append((filepath, title)) return block_types def contextMenuEvent(self, event: QContextMenuEvent): @@ -200,14 +200,17 @@ def drag_scene(self, event: QMouseEvent, action="press"): if action == "press": releaseEvent = QMouseEvent(QEvent.Type.MouseButtonRelease, event.localPos(), event.screenPos(), - Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, event.modifiers()) + Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, + event.modifiers()) super().mouseReleaseEvent(releaseEvent) self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) return QMouseEvent(event.type(), event.localPos(), event.screenPos(), - Qt.MouseButton.LeftButton, event.buttons() | Qt.MouseButton.LeftButton, + Qt.MouseButton.LeftButton, + event.buttons() | Qt.MouseButton.LeftButton, event.modifiers()) return QMouseEvent(event.type(), event.localPos(), event.screenPos(), - Qt.MouseButton.LeftButton, event.buttons() & ~Qt.MouseButton.LeftButton, + Qt.MouseButton.LeftButton, + event.buttons() & ~Qt.MouseButton.LeftButton, event.modifiers()) def drag_edge(self, event: QMouseEvent, action="press"): diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index b355956a..7e0b134e 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -176,8 +176,8 @@ def createMenus(self): def updateThemeMenu(self): self.thememenu.clear() theme_names = theme_manager().list_themes() - for i in range(len(theme_names)): - action = self.thememenu.addAction(theme_names[i]) + for i, theme in enumerate(theme_names): + action = self.thememenu.addAction(theme) action.setCheckable(True) action.setChecked(i == theme_manager().selected_theme_index) action.triggered.connect(self.themeMapper.map) diff --git a/opencodeblocks/graphics/worker.py b/opencodeblocks/graphics/worker.py index 331d6b15..333f451e 100644 --- a/opencodeblocks/graphics/worker.py +++ b/opencodeblocks/graphics/worker.py @@ -18,7 +18,7 @@ class Worker(QRunnable): def __init__(self, kernel, code): """ Initialize the worker object. """ - super(Worker, self).__init__() + super().__init__() self.kernel = kernel self.code = code