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/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/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/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 855ede76..0d13dd0f 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -325,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): 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/scene/scene.py b/opencodeblocks/graphics/scene/scene.py index 6708e1ea..fa99e84b 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: @@ -177,7 +179,34 @@ def serialize(self) -> OrderedDict: ('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): + """ 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"] = {} + 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() + 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 +214,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..aebd6ef0 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -3,11 +3,16 @@ """ Module for the OCB View """ +import json +import os +from typing import List, Tuple + 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 from opencodeblocks.graphics.socket import OCBSocket from opencodeblocks.graphics.edge import OCBEdge @@ -28,8 +33,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 @@ -56,10 +61,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) @@ -73,8 +80,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) @@ -84,8 +89,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) @@ -96,21 +99,13 @@ 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. 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() @@ -128,17 +123,43 @@ 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") 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) + 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 = [] + 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: + 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. """ if Qt.Modifier.CTRL == int(event.modifiers()): @@ -168,7 +189,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 @@ -177,16 +199,19 @@ 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. """ @@ -209,7 +234,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 @@ -219,7 +245,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: @@ -228,7 +254,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: 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