diff --git a/README.md b/README.md index 9164c457..e29c549d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # OpenCodeBlocks +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Pytest badge](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/ddd03302fd7c4849b452959753bc0939)](https://www.codacy.com/gh/MathisFederico/OpenCodeBlocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/OpenCodeBlocks&utm_campaign=Badge_Grade) -[![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml) [![Total coverage Codacy Badge](https://app.codacy.com/project/badge/Coverage/ddd03302fd7c4849b452959753bc0939)](https://www.codacy.com/gh/MathisFederico/OpenCodeBlocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/OpenCodeBlocks&utm_campaign=Badge_Coverage) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml) [![Unit coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_unit_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) [![Integration coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_integration_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) [![Licence - GPLv3](https://img.shields.io/github/license/MathisFederico/Crafting?style=plastic)](https://www.gnu.org/licenses/) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) + OpenCodeBlocks is an open-source tool for modular visual programing in python. diff --git a/examples/mnist.ipyg b/examples/mnist.ipyg index 35c2b1f9..c7393866 100644 --- a/examples/mnist.ipyg +++ b/examples/mnist.ipyg @@ -64,14 +64,14 @@ "stdout": "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\n", "splitter_pos": [ 0, - 278 + 286 ], "position": [ 2330.066406249998, -595.3554687499998 ], - "width": 301, - "height": 333, + "width": 317, + "height": 341, "metadata": { "title_metadata": { "color": "white", @@ -145,15 +145,15 @@ "source": "from tensorflow.keras.datasets import mnist\r\n(x_train, y_train), (x_test, y_test) = mnist.load_data()\r\n", "stdout": "", "splitter_pos": [ - 86, + 148, 0 ], "position": [ -877.3242187500001, -354.52734375000006 ], - "width": 850, - "height": 141, + "width": 785, + "height": 203, "metadata": { "title_metadata": { "color": "white", @@ -167,7 +167,7 @@ "id": 2443478910728, "type": "output", "position": [ - 850.0, + 785.0, 42.0 ], "metadata": { @@ -186,15 +186,15 @@ "source": "x_train = x_train.astype('float32') / 255.0\r\nx_test = x_test.astype('float32') / 255.0\r\n\r\nx_train = x_train.reshape(x_train.shape[0], 28, 28, 1)\r\nx_test = x_test.reshape(x_test.shape[0], 28, 28, 1)\r\n\r\nprint('train:', x_train.shape, '|test:', x_test.shape)\r\n", "stdout": "train: (60000, 28, 28, 1) |test: (10000, 28, 28, 1)\n", "splitter_pos": [ - 206, - 85 + 85, + 174 ], "position": [ 44.48828125000068, -370.21484374999983 ], - "width": 855, - "height": 346, + "width": 803, + "height": 314, "metadata": { "title_metadata": { "color": "white", @@ -222,7 +222,7 @@ "id": 2443478983880, "type": "output", "position": [ - 855.0, + 803.0, 42.0 ], "metadata": { @@ -283,14 +283,14 @@ "stdout": "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\n", "splitter_pos": [ 0, - 274 + 278 ], "position": [ - 103.60937500000011, - -734.9375 + 104.60937500000011, + -717.9375 ], - "width": 300, - "height": 329, + "width": 321, + "height": 333, "metadata": { "title_metadata": { "color": "white", diff --git a/opencodeblocks/__init__.py b/opencodeblocks/__init__.py index 1de73c48..8b299e47 100644 --- a/opencodeblocks/__init__.py +++ b/opencodeblocks/__init__.py @@ -4,6 +4,6 @@ """ OpenCodeBlocks: An open-source tool for modular visual programing in python """ -__appname__ = 'OpenCodeBlocks' -__author__ = 'Mathïs Fédérico' -__version__ = '0.0.1' +__appname__ = "OpenCodeBlocks" +__author__ = "Mathïs Fédérico" +__version__ = "0.0.1" diff --git a/opencodeblocks/__main__.py b/opencodeblocks/__main__.py index 3845a866..b3141db5 100644 --- a/opencodeblocks/__main__.py +++ b/opencodeblocks/__main__.py @@ -9,13 +9,13 @@ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from qtpy.QtWidgets import QApplication -from opencodeblocks.graphics.window import OCBWindow +from opencodeblocks.window import OCBWindow sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) - app.setStyle('Fusion') + app.setStyle("Fusion") wnd = OCBWindow() if len(sys.argv) > 1: wnd.createNewMdiChild(sys.argv[1]) diff --git a/opencodeblocks/graphics/blocks/__init__.py b/opencodeblocks/blocks/__init__.py similarity index 52% rename from opencodeblocks/graphics/blocks/__init__.py rename to opencodeblocks/blocks/__init__.py index 893604f8..41b23a2b 100644 --- a/opencodeblocks/graphics/blocks/__init__.py +++ b/opencodeblocks/blocks/__init__.py @@ -3,10 +3,7 @@ """ Module for the OCB Blocks of different types. """ -from opencodeblocks.graphics.blocks.block import OCBBlock -from opencodeblocks.graphics.blocks.codeblock import OCBCodeBlock +from opencodeblocks.blocks.block import OCBBlock +from opencodeblocks.blocks.codeblock import OCBCodeBlock -BLOCKS = { - 'base': OCBBlock, - 'code': OCBCodeBlock -} +BLOCKS = {"base": OCBBlock, "code": OCBCodeBlock} diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py new file mode 100644 index 00000000..fc7d2896 --- /dev/null +++ b/opencodeblocks/blocks/block.py @@ -0,0 +1,319 @@ +# OpenCodeBlock an open-source tool for modular visual programing in python +# Copyright (C) 2021 Mathïs FEDERICO + +""" Module for the base OCB Block. """ + +from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple, Union + +from PyQt5.QtCore import QPointF, QRectF, Qt +from PyQt5.QtGui import QBrush, QPen, QColor, QPainter, QPainterPath +from PyQt5.QtWidgets import ( + QGraphicsItem, + QGraphicsProxyWidget, + QGraphicsSceneMouseEvent, + QStyleOptionGraphicsItem, + QWidget, +) + +from opencodeblocks.serializable import Serializable +from opencodeblocks.socket import OCBSocket +from opencodeblocks.blocks.widgets import OCBTitle, OCBSplitter, BlockSizeGrip + +if TYPE_CHECKING: + from opencodeblocks.scene.scene import OCBScene + + +class OCBBlock(QGraphicsItem, Serializable): + + """Base class for blocks in OpenCodeBlocks.""" + + def __init__( + self, + block_type: str = "base", + source: str = "", + position: tuple = (0, 0), + width: int = 300, + height: int = 200, + edge_size: float = 10.0, + title: Union[OCBTitle, str] = "New block", + parent: Optional["QGraphicsItem"] = None, + ): + """Base class for blocks in OpenCodeBlocks. + + Args: + block_type: Block type. + source: Block source text. + position: Block position in the scene. + width: Block width. + height: Block height. + edge_size: Block edges size. + title: Block Title. + parent: Parent of the block. + + """ + QGraphicsItem.__init__(self, parent=parent) + Serializable.__init__(self) + + self.block_type = block_type + self.source = source + self.stdout = "" + self.setPos(QPointF(*position)) + self.sockets_in = [] + self.sockets_out = [] + + self._pen_outline = QPen(QColor("#7F000000")) + self._pen_outline_selected = QPen(QColor("#FFFFA637")) + self._brush_background = QBrush(QColor("#E3212121")) + + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) + + self.setAcceptHoverEvents(True) + + self.holder = QGraphicsProxyWidget(self) + self.root = QWidget() + self.root.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self.root.setGeometry(0, 0, int(width), int(height)) + + if isinstance(title, OCBTitle): + self.title_widget = title + else: + self.title_widget = OCBTitle(title) + self.title_widget.setParent(self.root) + self.title_widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + self.splitter = OCBSplitter(self, Qt.Orientation.Vertical, self.root) + self.size_grip = BlockSizeGrip(self) + + # DO NOT TRUST codacy !!! type(self) should be used, not isinstance. + if type(self) == OCBBlock: + # This has to be called at the end of the constructor of + # every class inheriting this. + self.holder.setWidget(self.root) + + self.edge_size = edge_size + self.min_width = 300 + self.min_height = 100 + self.width = width + self.height = height + + self.moved = False + self.metadata = { + "title_metadata": self.title_widget.serialize(), + } + + def scene(self) -> "OCBScene": + """Get the current OCBScene containing the block.""" + return super().scene() + + def boundingRect(self) -> QRectF: + """Get the the block bounding box.""" + return QRectF(0, 0, self.width, self.height).normalized() + + def paint( + self, + painter: QPainter, + option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument + widget: Optional[QWidget] = None, + ): # pylint:disable=unused-argument + """Paint the block.""" + + # content + path_content = QPainterPath() + path_content.setFillRule(Qt.FillRule.WindingFill) + path_content.addRoundedRect( + 0, 0, self.width, self.height, self.edge_size, self.edge_size + ) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(self._brush_background) + painter.drawPath(path_content.simplified()) + + # outline + path_outline = QPainterPath() + path_outline.addRoundedRect( + 0, 0, self.width, self.height, self.edge_size, self.edge_size + ) + painter.setPen( + self._pen_outline_selected if self.isSelected() else self._pen_outline + ) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawPath(path_outline.simplified()) + + def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]: + """Get a socket position to place them on the block sides.""" + if socket.socket_type == "input": + x = 0 + sockets = self.sockets_in + else: + x = self.width + sockets = self.sockets_out + + y_offset = self.title_widget.height() + 2 * socket.radius + if len(sockets) < 2: + y = y_offset + else: + side_lenght = self.height - y_offset - 2 * socket.radius - self.edge_size + y = y_offset + side_lenght * sockets.index(socket) / (len(sockets) - 1) + return x, y + + def update_sockets(self): + """Update the sockets positions.""" + for socket in self.sockets_in + self.sockets_out: + socket.setPos(*self.get_socket_pos(socket)) + + def add_socket(self, socket: OCBSocket): + """Add a socket to the block.""" + if socket.socket_type == "input": + self.sockets_in.append(socket) + else: + self.sockets_out.append(socket) + self.update_sockets() + + def remove_socket(self, socket: OCBSocket): + """Remove a socket from the block.""" + if socket.socket_type == "input": + self.sockets_in.remove(socket) + else: + self.sockets_out.remove(socket) + socket.remove() + self.update_sockets() + + def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): + """OCBBlock reaction to a mouseReleaseEvent.""" + if self.moved: + self.moved = False + self.scene().history.checkpoint("Moved block", set_modified=True) + super().mouseReleaseEvent(event) + + def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): + """OCBBlock reaction to a mouseMoveEvent.""" + super().mouseMoveEvent(event) + self.moved = True + + def remove(self): + """Remove the block from the scene containing it.""" + scene = self.scene() + for socket in self.sockets_in + self.sockets_out: + self.remove_socket(socket) + if scene is not None: + scene.removeItem(self) + + def update_title(self): + """Update the block title position.""" + self.title_widget.setGeometry( + int(self.edge_size + self.title_widget.left_offset), + int(self.edge_size / 2), + int(self.width - self.edge_size * 3), + int(self.title_widget.height()), + ) + + def update_grip(self): + """Update the block title position.""" + self.size_grip.setGeometry( + int(self.width - self.edge_size * 2), + int(self.height - self.edge_size * 2), + int(self.edge_size * 1.7), + int(self.edge_size * 1.7), + ) + + def update_splitter(self): + """Update the block splitter.""" + # We make the resizing of splitter only affect + # the last element of the split view + sizes = self.splitter.sizes() + old_height = self.splitter.height() + self.splitter.setGeometry( + int(self.edge_size), + int(self.edge_size + self.title_widget.height()), + int(self.width - self.edge_size * 2), + int(self.height - self.edge_size * 2 - self.title_widget.height()), + ) + if len(sizes) > 1: + height_delta = self.splitter.height() - old_height + sizes[-1] += height_delta + self.splitter.setSizes(sizes) + + def update_all(self): + """Update everything.""" + self.update_sockets() + self.update_splitter() + self.update_grip() + self.update_title() + + @property + def title(self): + """Block title.""" + return self.title_widget.text() + + @title.setter + def title(self, value: str): + if hasattr(self, "title_widget"): + self.title_widget.setText(value) + + @property + def width(self): + """Block width.""" + return self.root.width() + + @width.setter + def width(self, value: float): + self.root.setGeometry(0, 0, int(value), self.root.height()) + + @property + def height(self): + """Block height.""" + return self.root.height() + + @height.setter + def height(self, value: float): + self.root.setGeometry(0, 0, self.root.width(), int(value)) + + def serialize(self) -> OrderedDict: + metadata = OrderedDict(sorted(self.metadata.items())) + return OrderedDict( + [ + ("id", self.id), + ("title", self.title), + ("block_type", self.block_type), + ("source", self.source), + ("stdout", self.stdout), + ("splitter_pos", self.splitter.sizes()), + ("position", [self.pos().x(), self.pos().y()]), + ("width", self.width), + ("height", self.height), + ("metadata", metadata), + ( + "sockets", + [ + socket.serialize() + for socket in self.sockets_in + self.sockets_out + ], + ), + ] + ) + + def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None: + if restore_id: + self.id = data["id"] + for dataname in ("title", "block_type", "source", "stdout", "width", "height"): + setattr(self, dataname, data[dataname]) + + self.setPos(QPointF(*data["position"])) + self.metadata = dict(data["metadata"]) + + self.title_widget = OCBTitle(data["title"]) + self.title_widget.deserialize( + self.metadata["title_metadata"], hashmap, restore_id + ) + + 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) + if hashmap is not None: + hashmap.update({socket_data["id"]: socket}) + + self.update_all() diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py similarity index 70% rename from opencodeblocks/graphics/blocks/codeblock.py rename to opencodeblocks/blocks/codeblock.py index aba18bb7..0a3142bb 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -3,15 +3,14 @@ """ Module for the base OCB Code Block. """ -from PyQt5.QtCore import QByteArray -from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QPushButton, QTextEdit from ansi2html import Ansi2HTMLConverter -from opencodeblocks.graphics.blocks.block import OCBBlock -from opencodeblocks.graphics.pyeditor import PythonEditor -from opencodeblocks.graphics.worker import Worker +from PyQt5.QtWidgets import QPushButton, QTextEdit + +from opencodeblocks.blocks.block import OCBBlock +from opencodeblocks.pyeditor import PythonEditor +from opencodeblocks.worker import Worker conv = Ansi2HTMLConverter() @@ -32,7 +31,7 @@ def __init__(self, **kwargs): self.source_editor = PythonEditor(self) - super().__init__(block_type='code', **kwargs) + super().__init__(block_type="code", **kwargs) self.output_panel_height = self.height / 3 self._min_output_panel_height = 20 @@ -55,18 +54,18 @@ def __init__(self, **kwargs): self.update_all() # Set the geometry of display and source_editor def init_output_panel(self): - """ Initialize the output display widget: QLabel """ + """Initialize the output display widget: QLabel""" output_panel = QTextEdit() output_panel.setReadOnly(True) output_panel.setFont(self.source_editor.font()) return output_panel def init_run_button(self): - """ Initialize the run button """ + """Initialize the run button""" run_button = QPushButton(">", self.root) - run_button.setMinimumWidth(int(self.edge_size)) + run_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) + run_button.move(self.edge_size, self.edge_size // 2) run_button.clicked.connect(self.run_code) - run_button.raise_() return run_button def run_code(self): @@ -81,26 +80,38 @@ def run_code(self): worker.signals.image.connect(self.handle_image) self.source_editor.threadpool.start(worker) - def update_all(self): - """ Update the code block parts. """ - super().update_all() - if hasattr(self, 'run_button'): - self.run_button.setGeometry( - int(self.edge_size), - int(self.edge_size / 2), - int(2.5 * self.edge_size), - int(2.5 * self.edge_size) - ) - + def update_title(self): + """Update the block title position.""" + button_pos = self.run_button.mapToParent(self.run_button.pos()).x() + offset = button_pos + self.run_button.width() + # self.title_widget.setMaximumWidth(self.width) + self.title_widget.move(offset, int(self.edge_size / 4)) + self.title_widget.setFixedWidth(self.width - 2 * offset) + # self.title_widget.setGeometry( + # int(offset), + # int(self.edge_size / 4), + # int(self.width), + # int(self.title_widget.height()), + # ) + # print(self.width, self.title_widget.width()) + # print(self.title_widget.getTextMargins()) + + def update_output_panel(self): + """Update the output panel""" # Close output panel if no output if self.stdout == "": self.previous_splitter_size = self.splitter.sizes() self.output_closed = True self.splitter.setSizes([1, 0]) + def update_all(self): + """Update the code block parts.""" + super().update_all() + self.update_output_panel() + @property def source(self) -> str: - """ Source code. """ + """Source code.""" return self.source_editor.text() @source.setter @@ -114,8 +125,8 @@ def stdout(self) -> str: @stdout.setter def stdout(self, value: str): self._stdout = value - if hasattr(self, 'output_panel'): - if value.startswith(''): + if hasattr(self, "output_panel"): + if value.startswith(""): display_text = self.b64_to_html(value[5:]) else: display_text = self.str_to_html(value) @@ -138,18 +149,19 @@ def str_to_html(text: str): # Convert ANSI escape codes to HTML text = conv.convert(text) # Replace background color - text = text.replace('background-color: #000000', - 'background-color: transparent') + text = text.replace( + "background-color: #000000", "background-color: transparent" + ) return text def handle_stdout(self, value: str): - """ Handle the stdout signal """ + """Handle the stdout signal""" # If there is a new line # Save every line but the last one - if value.find('\n') != -1: - lines = value.split('\n') - self._cached_stdout += '\n'.join(lines[:-1]) + '\n' + if value.find("\n") != -1: + lines = value.split("\n") + self._cached_stdout += "\n".join(lines[:-1]) + "\n" value = lines[-1] # Update the last line only @@ -160,5 +172,5 @@ def b64_to_html(image: str): return f'' def handle_image(self, image: str): - """ Handle the image signal """ - self.stdout = '' + image + """Handle the image signal""" + self.stdout = "" + image diff --git a/opencodeblocks/blocks/widgets/__init__.py b/opencodeblocks/blocks/widgets/__init__.py new file mode 100644 index 00000000..fa91155c --- /dev/null +++ b/opencodeblocks/blocks/widgets/__init__.py @@ -0,0 +1,8 @@ +# OpenCodeBlock an open-source tool for modular visual programing in python +# Copyright (C) 2021 Mathïs FEDERICO + +""" Module for the OCB Blocks Widgets. """ + +from opencodeblocks.blocks.widgets.title import OCBTitle +from opencodeblocks.blocks.widgets.splitter import OCBSplitter +from opencodeblocks.blocks.widgets.blocksizegrip import BlockSizeGrip diff --git a/opencodeblocks/graphics/blocks/blocksizegrip.py b/opencodeblocks/blocks/widgets/blocksizegrip.py similarity index 66% rename from opencodeblocks/graphics/blocks/blocksizegrip.py rename to opencodeblocks/blocks/widgets/blocksizegrip.py index 2c8eb4ed..bb85a4fe 100644 --- a/opencodeblocks/graphics/blocks/blocksizegrip.py +++ b/opencodeblocks/blocks/widgets/blocksizegrip.py @@ -1,4 +1,3 @@ - """ Implements the SizeGrip Widget for the Blocks. @@ -6,45 +5,52 @@ resize a block. """ +from typing import TYPE_CHECKING + from PyQt5.QtCore import QPoint -from PyQt5.QtWidgets import QGraphicsItem, QSizeGrip, QWidget +from PyQt5.QtWidgets import QSizeGrip from PyQt5.QtGui import QMouseEvent +if TYPE_CHECKING: + from opencodeblocks.blocks.block import OCBBlock + class BlockSizeGrip(QSizeGrip): - """ A grip to resize a block """ + """A grip to resize a block""" - def __init__(self, block: QGraphicsItem, parent: QWidget = None): - """ - Constructor for BlockSizeGrip + def __init__(self, block: "OCBBlock"): + """Constructor for BlockSizeGrip + + Args: + block: OCBBlock holding the QSizeGrip. - block is the QGraphicsItem holding the QSizeGrip. - It's usually an OCBBlock """ - super().__init__(parent) + super().__init__(block.root) self.mouseX = 0 self.mouseY = 0 self.block = block self.resizing = False def mousePressEvent(self, mouseEvent: QMouseEvent): - """ Start the resizing """ + """Start the resizing""" self.mouseX = mouseEvent.globalX() self.mouseY = mouseEvent.globalY() self.resizing = True - def mouseReleaseEvent(self, mouseEvent: QMouseEvent): # pylint:disable=unused-argument - """ Stop the resizing """ + 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) @property def _zoom(self) -> float: - """ Returns how much the scene is """ + """Returns how much the scene is""" return self.block.scene().views()[0].zoom def mouseMoveEvent(self, mouseEvent: QMouseEvent): - """ Performs resizing of the root widget """ + """Performs resizing of the root widget""" transformed_pt1 = self.block.mapFromScene(QPoint(0, 0)) transformed_pt2 = self.block.mapFromScene(QPoint(1, 1)) @@ -58,16 +64,11 @@ def mouseMoveEvent(self, mouseEvent: QMouseEvent): # relative to the grip, so if the grip moves, the deltaX and deltaY changes. # This creates a shaking effect when resizing. We use global to not # have this effect. - new_width = max( - self.block.width + int(delta_x), - self.block.min_width - ) - new_height = max( - self.block.height + int(delta_y), - self.block.min_height - ) + new_width = max(self.block.width + int(delta_x), self.block.min_width) + new_height = max(self.block.height + int(delta_y), self.block.min_height) self.parent().setGeometry(0, 0, new_width, new_height) + self.block.update_all() self.mouseX = mouseEvent.globalX() diff --git a/opencodeblocks/blocks/widgets/splitter.py b/opencodeblocks/blocks/widgets/splitter.py new file mode 100644 index 00000000..b8a213ba --- /dev/null +++ b/opencodeblocks/blocks/widgets/splitter.py @@ -0,0 +1,36 @@ +# OpenCodeBlock an open-source tool for modular visual programing in python +# Copyright (C) 2021 Mathïs FEDERICO + +""" Module for the OCBSplitter Widget. """ + +from typing import TYPE_CHECKING + +from PyQt5.QtWidgets import QSplitter, QSplitterHandle, QWidget +from PyQt5.QtGui import QMouseEvent + +if TYPE_CHECKING: + from opencodeblocks.blocks import OCBBlock + + +class OCBSplitterHandle(QSplitterHandle): + """A handle for splitters with undoable events""" + + def mouseReleaseEvent(self, event: "QMouseEvent"): + """When releasing the handle, save the state to history""" + scene = self.parent().block.scene() + if scene is not None: + scene.history.checkpoint("Resize block", set_modified=True) + return super().mouseReleaseEvent(event) + + +class OCBSplitter(QSplitter): + """A spliter with undoable events""" + + def __init__(self, block: "OCBBlock", orientation: int, parent: QWidget): + """Create a new OCBSplitter""" + super().__init__(orientation, parent) + self.block = block + + def createHandle(self): + """Return the middle handle of the splitter""" + return OCBSplitterHandle(self.orientation(), self) diff --git a/opencodeblocks/blocks/widgets/title.py b/opencodeblocks/blocks/widgets/title.py new file mode 100644 index 00000000..a0541906 --- /dev/null +++ b/opencodeblocks/blocks/widgets/title.py @@ -0,0 +1,119 @@ +# OpenCodeBlock an open-source tool for modular visual programing in python +# Copyright (C) 2021 Mathïs FEDERICO + +""" Module for the OCBTitle Widget. """ + +import time +from typing import OrderedDict +from PyQt5.QtCore import Qt + +from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent +from PyQt5.QtWidgets import QLineEdit + +from opencodeblocks.serializable import Serializable + + +class OCBTitle(QLineEdit, Serializable): + """The title of an OCBBlock. Needs to be double clicked to interact""" + + def __init__( + self, + text: str, + color: str = "white", + font: str = "Ubuntu", + size: int = 12, + ): + """Create a new title for an OCBBlock + + Args: + text: Block title. + color: Color of the block title. + font: Font of the block title. + size: Size of the block title. + + """ + Serializable.__init__(self) + QLineEdit.__init__(self, text, None) + self.init_ui(color, font, size) + self.clickTime = None + self.setReadOnly(True) + + def init_ui( + self, + color: str = "white", + font: str = "Ubuntu", + size: int = 12, + ): + """Apply title parameters + + Args: + color: Color of the title. + font: Font of the title. + size: Size of the title. + + """ + self.setFixedHeight(int(3 * size)) + self.setFont(QFont(font, size)) + self.color = color + self.setStyleSheet( + f""" + QLineEdit {{ + color : {self.color}; + background-color: #FF0000; + border:none; + }}""" + ) + + def mousePressEvent(self, event: QMouseEvent): + """ + Detect double clicks and single clicks are react accordingly by + dispatching the event to the parent or the current widget + """ + if self.clickTime is None or ( + self.isReadOnly() and time.time() - self.clickTime > 0.3 + ): + self.parent().mousePressEvent(event) + elif self.isReadOnly(): + self.mouseDoubleClickEvent(event) + super().mousePressEvent(event) + else: + super().mousePressEvent(event) + self.clickTime = time.time() + + def focusOutEvent(self, event: QFocusEvent): + """The title is read-only when focused is lost""" + self.setReadOnly(True) + self.deselect() + + def mouseDoubleClickEvent(self, event: QMouseEvent): + """Toggle readonly mode when double clicking""" + self.setReadOnly(not self.isReadOnly()) + if not self.isReadOnly(): + self.setFocus(Qt.FocusReason.MouseFocusReason) + + def serialize(self) -> OrderedDict: + """Serialize the object as an ordered dictionary.""" + return OrderedDict( + [ + ("id", self.id), + ("color", self.color), + ("font", self.font().family()), + ("size", self.font().pointSize()), + ] + ) + + def deserialize( + self, data: OrderedDict, hashmap: dict = None, restore_id=True + ) -> None: + """Deserialize the object from an ordered dictionary. + + Args: + data: Dictionnary containing data do deserialize from. + hashmap: Dictionnary mapping a hash code into knowed objects. + restore_id: If True, the id will be restored using the given data. + If False, a new id will be generated. + + """ + if restore_id: + self.id = data.get("id", id(self)) + self.init_ui(data["color"], data["font"], data["size"]) diff --git a/opencodeblocks/core/__init__.py b/opencodeblocks/core/__init__.py deleted file mode 100644 index 58401fca..00000000 --- a/opencodeblocks/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# OpenCodeBlock an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/edge.py similarity index 58% rename from opencodeblocks/graphics/edge.py rename to opencodeblocks/edge.py index e4b33722..a4f270cc 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/edge.py @@ -3,28 +3,32 @@ """ Module for the OCB Edge. """ -from __future__ import annotations - from typing import Optional, OrderedDict from PyQt5.QtCore import QPointF, Qt from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen from PyQt5.QtWidgets import QGraphicsPathItem, QStyleOptionGraphicsItem, QWidget -from opencodeblocks.core.serializable import Serializable -from opencodeblocks.graphics.socket import OCBSocket +from opencodeblocks.serializable import Serializable +from opencodeblocks.socket import OCBSocket class OCBEdge(QGraphicsPathItem, Serializable): - """ Base class for directed edges in OpenCodeBlocks. """ - - def __init__(self, edge_width: float = 4.0, path_type='bezier', - edge_color="#001000", edge_selected_color="#00ff00", - source: QPointF = QPointF(0, 0), destination: QPointF = QPointF(0, 0), - source_socket: OCBSocket = None, destination_socket: OCBSocket = None - ): - """ Base class for edges in OpenCodeBlocks. + """Base class for directed edges in OpenCodeBlocks.""" + + def __init__( + self, + edge_width: float = 4.0, + path_type="bezier", + edge_color="#001000", + edge_selected_color="#00ff00", + source: QPointF = QPointF(0, 0), + destination: QPointF = QPointF(0, 0), + source_socket: OCBSocket = None, + destination_socket: OCBSocket = None, + ): + """Base class for edges in OpenCodeBlocks. Args: edge_width: Width of the edge. @@ -62,35 +66,38 @@ def __init__(self, edge_width: float = 4.0, path_type='bezier', self._destination = destination self.update_path() - def remove_from_socket(self, socket_type='source'): - """ Remove the edge from the sockets it is snaped to on the given socket_type. + def remove_from_socket(self, socket_type="source"): + """Remove the edge from the sockets it is snaped to on the given socket_type. Args: socket_type: One of ('source', 'destination'). """ - socket_name = f'{socket_type}_socket' + socket_name = f"{socket_type}_socket" socket = getattr(self, socket_name, OCBSocket) if socket is not None: socket.remove_edge(self) setattr(self, socket_name, None) def remove_from_sockets(self): - """ Remove the edge from all sockets it is snaped to. """ - self.remove_from_socket('source') - self.remove_from_socket('destination') + """Remove the edge from all sockets it is snaped to.""" + self.remove_from_socket("source") + self.remove_from_socket("destination") def remove(self): - """ Remove the edge from the scene in which it is drawn. """ + """Remove the edge from the scene in which it is drawn.""" scene = self.scene() if scene is not None: self.remove_from_sockets() scene.removeItem(self) - def paint(self, painter: QPainter, - option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument - widget: Optional[QWidget] = None): # pylint:disable=unused-argument - """ Paint the edge. """ + def paint( + self, + painter: QPainter, + option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument + widget: Optional[QWidget] = None, + ): # pylint:disable=unused-argument + """Paint the edge.""" self.update_path() pen = self._pen_dragging if self.destination_socket is None else self._pen painter.setPen(self._pen_selected if self.isSelected() else pen) @@ -98,22 +105,22 @@ def paint(self, painter: QPainter, painter.drawPath(self.path()) def update_path(self): - """ Update the edge path depending on the path_type. """ + """Update the edge path depending on the path_type.""" path = QPainterPath(self.source) - if self.path_type == 'direct': + if self.path_type == "direct": path.lineTo(self.destination) - elif self.path_type == 'bezier': + elif self.path_type == "bezier": sx, sy = self.source.x(), self.source.y() dx, dy = self.destination.x(), self.destination.y() mid_dist = (dx - sx) / 2 path.cubicTo(sx + mid_dist, sy, dx - mid_dist, dy, dx, dy) else: - raise NotImplementedError(f'Unknowed path type: {self.path_type}') + raise NotImplementedError(f"Unknowed path type: {self.path_type}") self.setPath(path) @property def source(self) -> QPointF: - """ Source point of the directed edge. """ + """Source point of the directed edge.""" if self.source_socket is not None: return self.source_socket.scenePos() return self._source @@ -128,7 +135,7 @@ def source(self, value: QPointF): @property def source_socket(self) -> OCBSocket: - """ Source socket of the directed edge. """ + """Source socket of the directed edge.""" return self._source_socket @source_socket.setter @@ -140,7 +147,7 @@ def source_socket(self, value: OCBSocket): @property def destination(self) -> QPointF: - """ Destination point of the directed edge. """ + """Destination point of the directed edge.""" if self.destination_socket is not None: return self.destination_socket.scenePos() return self._destination @@ -155,7 +162,7 @@ def destination(self, value: QPointF): @property def destination_socket(self) -> OCBSocket: - """ Destination socket of the directed edge. """ + """Destination socket of the directed edge.""" return self._destination_socket @destination_socket.setter @@ -166,32 +173,58 @@ def destination_socket(self, value: OCBSocket): self.destination = value.scenePos() def serialize(self) -> OrderedDict: - return OrderedDict([ - ('id', self.id), - ('path_type', self.path_type), - ('source', OrderedDict([ - ('block', - self.source_socket.block.id if self.source_socket else None), - ('socket', - self.source_socket.id if self.source_socket else None) - ])), - ('destination', OrderedDict([ - ('block', - self.destination_socket.block.id if self.destination_socket else None), - ('socket', - self.destination_socket.id if self.destination_socket else None) - ])) - ]) + return OrderedDict( + [ + ("id", self.id), + ("path_type", self.path_type), + ( + "source", + OrderedDict( + [ + ( + "block", + self.source_socket.block.id + if self.source_socket + else None, + ), + ( + "socket", + self.source_socket.id if self.source_socket else None, + ), + ] + ), + ), + ( + "destination", + OrderedDict( + [ + ( + "block", + self.destination_socket.block.id + if self.destination_socket + else None, + ), + ( + "socket", + self.destination_socket.id + if self.destination_socket + else None, + ), + ] + ), + ), + ] + ) def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): if restore_id: - self.id = data['id'] - self.path_type = data['path_type'] + self.id = data["id"] + self.path_type = data["path_type"] try: - self.source_socket = hashmap[data['source']['socket']] + self.source_socket = hashmap[data["source"]["socket"]] self.source_socket.add_edge(self, is_destination=False) - self.destination_socket = hashmap[data['destination']['socket']] + self.destination_socket = hashmap[data["destination"]["socket"]] self.destination_socket.add_edge(self, is_destination=True) self.update_path() except KeyError: diff --git a/opencodeblocks/graphics/function_parsing.py b/opencodeblocks/function_parsing.py similarity index 94% rename from opencodeblocks/graphics/function_parsing.py rename to opencodeblocks/function_parsing.py index 7c18b9ac..d200ca04 100644 --- a/opencodeblocks/graphics/function_parsing.py +++ b/opencodeblocks/function_parsing.py @@ -1,8 +1,7 @@ - """ Module for code parsing and code execution """ from typing import List, Tuple -from opencodeblocks.graphics.kernel import Kernel +from opencodeblocks.kernel import Kernel kernel = Kernel() @@ -119,11 +118,11 @@ def execute_function(code: str, *args, **kwargs) -> str: """ function_name = get_function_name(code) - execution_code = f'{function_name}(' + execution_code = f"{function_name}(" for arg in args: - execution_code += f'{arg},' + execution_code += f"{arg}," for name, value in kwargs.items(): - execution_code += f'{name}={value},' + execution_code += f"{name}={value}," run_cell(code) - return run_cell(execution_code + ')') + return run_cell(execution_code + ")") diff --git a/opencodeblocks/graphics/__init__.py b/opencodeblocks/graphics/__init__.py deleted file mode 100644 index 58401fca..00000000 --- a/opencodeblocks/graphics/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# OpenCodeBlock an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py deleted file mode 100644 index 5dd9852a..00000000 --- a/opencodeblocks/graphics/blocks/block.py +++ /dev/null @@ -1,352 +0,0 @@ -# OpenCodeBlock an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO - -""" Module for the base OCB Block. """ - -from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple - -from PyQt5.QtCore import QPointF, QRectF, Qt -from PyQt5.QtGui import QBrush, QMouseEvent, QPen, QColor, QFont, QPainter, QPainterPath -from PyQt5.QtWidgets import QGraphicsItem, QGraphicsProxyWidget, \ - QGraphicsSceneMouseEvent, QLabel, QSplitter, QSplitterHandle, \ - QStyleOptionGraphicsItem, QWidget - -from opencodeblocks.core.serializable import Serializable -from opencodeblocks.graphics.socket import OCBSocket -from opencodeblocks.graphics.blocks.blocksizegrip import BlockSizeGrip - -if TYPE_CHECKING: - from opencodeblocks.graphics.scene.scene import OCBScene - - -class OCBBlock(QGraphicsItem, Serializable): - - """ Base class for blocks in OpenCodeBlocks. """ - - def __init__(self, block_type: str = 'base', source: str = '', position: tuple = (0, 0), - width: int = 300, height: int = 200, edge_size: float = 10.0, - title: str = 'New block', title_color: str = 'white', title_font: str = "Ubuntu", - title_size: int = 10, title_padding=4.0, parent: Optional['QGraphicsItem'] = None): - """ Base class for blocks in OpenCodeBlocks. - - Args: - block_type: Block type. - source: Block source text. - position: Block position in the scene. - width: Block width. - height: Block height. - edge_size: Block edges size. - title: Block title. - title_color: Color of the block title. - title_font: Font of the block title. - title_size: Size of the block title. - title_padding: Padding of the block title. - parent: Parent of the block. - - """ - QGraphicsItem.__init__(self, parent=parent) - Serializable.__init__(self) - - self.block_type = block_type - self.source = source - self.stdout = "" - self.setPos(QPointF(*position)) - self.sockets_in = [] - self.sockets_out = [] - - self.title_height = 3 * title_size - self.title = title - self.title_left_offset = 0 - - self._pen_outline = QPen(QColor("#7F000000")) - self._pen_outline_selected = QPen(QColor("#FFFFA637")) - - self._brush_title = QBrush(QColor("#FF313131")) - self._brush_background = QBrush(QColor("#E3212121")) - - self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) - self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) - - self.setAcceptHoverEvents(True) - - self.holder = QGraphicsProxyWidget(self) - self.root = QWidget() - self.root.setAttribute(Qt.WA_TranslucentBackground) - self.root.setGeometry( - 0, 0, - int(width), - int(height) - ) - - self.title_widget = QLabel(self.title, self.root) - self.title_widget.setAttribute(Qt.WA_TransparentForMouseEvents) - self.title_widget.setAttribute(Qt.WA_TranslucentBackground) - self.setTitleGraphics( - title_color, - title_font, - title_size, - title_padding - ) - - self.splitter = OCBSplitter(self, Qt.Vertical, self.root) - - self.size_grip = BlockSizeGrip(self, self.root) - - if type(self) == OCBBlock: # DO NOT TRUST codacy !!! isinstance != type - # This has to be called at the end of the constructor of - # every class inheriting this. - self.holder.setWidget(self.root) - - self.edge_size = edge_size - self.min_width = 300 - self.min_height = 100 - self.width = width - self.height = height - - self.moved = False - self.metadata = { - 'title_metadata': { - 'color': title_color, - 'font': title_font, - 'size': title_size, - 'padding': title_padding, - }, - } - - def scene(self) -> 'OCBScene': - """ Get the current OCBScene containing the block. """ - return super().scene() - - def boundingRect(self) -> QRectF: - """ Get the the block bounding box. """ - return QRectF(0, 0, self.width, self.height).normalized() - - def setTitleGraphics(self, color: str, font: str, - size: int, padding: float): - """ Set the title graphics. - - Args: - color: title color. - font: title font. - size: title size. - padding: title padding. - - """ - self.title_widget.setMargin(int(padding)) - self.title_widget.setStyleSheet(f"QLabel {{ color : {color} }}") - self.title_widget.setFont(QFont(font, size)) - - def paint(self, painter: QPainter, - option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument - widget: Optional[QWidget] = None): # pylint:disable=unused-argument - """ Paint the block. """ - - # content - path_content = QPainterPath() - path_content.setFillRule(Qt.FillRule.WindingFill) - path_content.addRoundedRect(0, 0, self.width, self.height, - self.edge_size, self.edge_size) - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(self._brush_background) - painter.drawPath(path_content.simplified()) - - # outline - path_outline = QPainterPath() - path_outline.addRoundedRect(0, 0, self.width, self.height, - self.edge_size, self.edge_size) - painter.setPen( - self._pen_outline_selected if self.isSelected() else self._pen_outline) - painter.setBrush(Qt.BrushStyle.NoBrush) - painter.drawPath(path_outline.simplified()) - - def _is_in_resize_area(self, pos: QPointF): - """ Return True if the given position is in the block resize_area. """ - return self.width - self.edge_size < pos.x() \ - and self.height - self.edge_size < pos.y() - - def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]: - """ Get a socket position to place them on the block sides. """ - if socket.socket_type == 'input': - x = 0 - sockets = self.sockets_in - else: - x = self.width - sockets = self.sockets_out - - y_offset = self.title_height + 2 * socket.radius - if len(sockets) < 2: - y = y_offset - else: - side_lenght = self.height - y_offset - 2 * socket.radius - self.edge_size - y = y_offset + side_lenght * \ - sockets.index(socket) / (len(sockets) - 1) - return x, y - - def update_sockets(self): - """ Update the sockets positions. """ - for socket in self.sockets_in + self.sockets_out: - socket.setPos(*self.get_socket_pos(socket)) - - def add_socket(self, socket: OCBSocket): - """ Add a socket to the block. """ - if socket.socket_type == 'input': - self.sockets_in.append(socket) - else: - self.sockets_out.append(socket) - self.update_sockets() - - def remove_socket(self, socket: OCBSocket): - """ Remove a socket from the block. """ - if socket.socket_type == 'input': - self.sockets_in.remove(socket) - else: - self.sockets_out.remove(socket) - socket.remove() - self.update_sockets() - - def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): - """ OCBBlock reaction to a mouseReleaseEvent. """ - if self.moved: - self.moved = False - self.scene().history.checkpoint("Moved block", set_modified=True) - super().mouseReleaseEvent(event) - - def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): - """ OCBBlock reaction to a mouseMoveEvent. """ - super().mouseMoveEvent(event) - self.moved = True - - def remove(self): - """ Remove the block from the scene containing it. """ - scene = self.scene() - for socket in self.sockets_in + self.sockets_out: - self.remove_socket(socket) - if scene is not None: - scene.removeItem(self) - - def update_all(self): - """ Update sockets and title. """ - self.update_sockets() - if hasattr(self, 'title_widget'): - # We make the resizing of splitter only affect - # the last element of the split view - sizes = self.splitter.sizes() - old_height = self.splitter.height() - self.splitter.setGeometry( - int(self.edge_size), - int(self.edge_size + self.title_height), - int(self.width - self.edge_size * 2), - int(self.height - self.edge_size * 2 - self.title_height) - ) - if len(sizes) > 1: - height_delta = self.splitter.height() - old_height - sizes[-1] += height_delta - self.splitter.setSizes(sizes) - - self.title_widget.setGeometry( - int(self.edge_size + self.title_left_offset), - int(self.edge_size / 2), - int(self.width - 2 * self.edge_size), - int(self.title_height) - ) - self.size_grip.setGeometry( - int(self.width - self.edge_size * 2), - int(self.height - self.edge_size * 2), - int(self.edge_size * 1.7), - int(self.edge_size * 1.7) - ) - - @property - def title(self): - """ Block title. """ - return self._title - - @title.setter - def title(self, value: str): - self._title = value - if hasattr(self, 'title_widget'): - self.title_widget.setText(self._title) - - @property - def width(self): - """ Block width. """ - return self.root.width() - - @width.setter - def width(self, value: float): - self.root.setGeometry(0, 0, int(value), self.root.height()) - self.update_all() - - @property - def height(self): - """ Block height. """ - return self.root.height() - - @height.setter - def height(self, value: float): - self.root.setGeometry(0, 0, self.root.width(), int(value)) - self.update_all() - - def serialize(self) -> OrderedDict: - metadata = OrderedDict(sorted(self.metadata.items())) - return OrderedDict([ - ('id', self.id), - ('title', self.title), - ('block_type', self.block_type), - ('source', self.source), - ('stdout', self.stdout), - ('splitter_pos', self.splitter.sizes()), - ('position', [self.pos().x(), self.pos().y()]), - ('width', self.width), - ('height', self.height), - ('metadata', metadata), - ('sockets', [socket.serialize() - for socket in self.sockets_in + self.sockets_out]), - ]) - - def deserialize(self, data: dict, hashmap: dict = None, - restore_id=True) -> None: - if restore_id: - self.id = data['id'] - for dataname in ('title', 'block_type', 'source', 'stdout', 'width', 'height'): - setattr(self, dataname, data[dataname]) - - self.setPos(QPointF(*data['position'])) - self.metadata = dict(data['metadata']) - self.setTitleGraphics(**self.metadata['title_metadata']) - - 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) - if hashmap is not None: - hashmap.update({socket_data['id']: socket}) - - self.update_all() - - -class OCBSplitterHandle(QSplitterHandle): - """ A handle for splitters with undoable events """ - - def mouseReleaseEvent(self, evt: QMouseEvent): - """ When releasing the handle, save the state to history """ - scene = self.parent().block.scene() - if scene is not None: - scene.history.checkpoint("Resize block", set_modified=True) - return super().mouseReleaseEvent(evt) - - -class OCBSplitter(QSplitter): - """ A spliter with undoable events """ - - def __init__(self, block: OCBBlock, orientation: int, parent: QWidget): - """ Create a new OCBSplitter """ - super().__init__(orientation, parent) - self.block = block - - def createHandle(self): - """ Return the middle handle of the splitter """ - return OCBSplitterHandle(self.orientation(), self) diff --git a/opencodeblocks/graphics/kernel.py b/opencodeblocks/kernel.py similarity index 77% rename from opencodeblocks/graphics/kernel.py rename to opencodeblocks/kernel.py index bdf64cd7..dc14d372 100644 --- a/opencodeblocks/graphics/kernel.py +++ b/opencodeblocks/kernel.py @@ -1,4 +1,3 @@ - """ Module to create and manage ipython kernels """ import queue @@ -6,7 +5,7 @@ from jupyter_client.manager import start_new_kernel -class Kernel(): +class Kernel: """jupyter_client kernel used to execute code and return output""" @@ -24,27 +23,27 @@ def message_to_output(self, message: dict) -> Tuple[str, str]: single output found in the message in that order of priority: image > text data > text print > error > nothing """ - message_type = 'None' - if 'data' in message: - if 'image/png' in message['data']: - message_type = 'image' + message_type = "None" + if "data" in message: + if "image/png" in message["data"]: + message_type = "image" # output an image (from plt.plot or plt.imshow) - out = message['data']['image/png'] + out = message["data"]["image/png"] else: - message_type = 'text' + message_type = "text" # output data as str (for example if code="a=10\na") - out = message['data']['text/plain'] - elif 'name' in message and message['name'] == "stdout": - message_type = 'text' + out = message["data"]["text/plain"] + elif "name" in message and message["name"] == "stdout": + message_type = "text" # output a print (print("Hello World")) - out = message['text'] - elif 'traceback' in message: - message_type = 'text' + out = message["text"] + elif "traceback" in message: + message_type = "text" # output an error - out = '\n'.join(message['traceback']) + out = "\n".join(message["traceback"]) else: - message_type = 'text' - out = '' + message_type = "text" + out = "" return out, message_type def execute(self, code: str) -> str: @@ -80,8 +79,8 @@ def get_message(self) -> Tuple[str, bool]: """ done = False try: - message = self.client.get_iopub_msg(timeout=1000)['content'] - if 'execution_state' in message and message['execution_state'] == 'idle': + message = self.client.get_iopub_msg(timeout=1000)["content"] + if "execution_state" in message and message["execution_state"] == "idle": done = True except queue.Empty: message = None diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/pyeditor.py similarity index 79% rename from opencodeblocks/graphics/pyeditor.py rename to opencodeblocks/pyeditor.py index 0ab39a94..47af41e2 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/pyeditor.py @@ -4,27 +4,35 @@ """ Module for OCB in block python editor. """ from typing import TYPE_CHECKING, List + from PyQt5.QtCore import QThreadPool, Qt -from PyQt5.QtGui import QFocusEvent, QFont, QFontMetrics, QColor, QMouseEvent, QWheelEvent +from PyQt5.QtGui import ( + QFocusEvent, + QFont, + QFontMetrics, + QColor, + QMouseEvent, + QWheelEvent, +) from PyQt5.Qsci import QsciScintilla, QsciLexerPython -from opencodeblocks.graphics.theme_manager import theme_manager -from opencodeblocks.graphics.blocks.block import OCBBlock -from opencodeblocks.graphics.kernel import Kernel +from opencodeblocks.theme_manager import theme_manager +from opencodeblocks.blocks.block import OCBBlock +from opencodeblocks.kernel import Kernel kernel = Kernel() threadpool = QThreadPool() if TYPE_CHECKING: - from opencodeblocks.graphics.view import OCBView + from opencodeblocks.view import OCBView class PythonEditor(QsciScintilla): - """ In-block python editor for OpenCodeBlocks. """ + """In-block python editor for OpenCodeBlocks.""" def __init__(self, block: OCBBlock): - """ In-block python editor for OpenCodeBlocks. + """In-block python editor for OpenCodeBlocks. Args: block: Block in which to add the python editor widget. @@ -64,7 +72,7 @@ def __init__(self, block: OCBBlock): self.setWindowFlags(Qt.WindowType.FramelessWindowHint) def update_theme(self): - """ Change the font and colors of the editor to match the current theme """ + """Change the font and colors of the editor to match the current theme""" font = QFont() font.setFamily(theme_manager().recommended_font_family) font.setFixedPitch(True) @@ -86,19 +94,19 @@ def update_theme(self): lexer.setFont(font) self.setLexer(lexer) - def views(self) -> List['OCBView']: - """ Get the views in which the python_editor is present. """ + def views(self) -> List["OCBView"]: + """Get the views in which the python_editor is present.""" return self.block.scene().views() def wheelEvent(self, event: QWheelEvent) -> None: - """ How PythonEditor handles wheel events """ + """How PythonEditor handles wheel events""" if self.mode == "EDITING" and event.angleDelta().x() == 0: event.accept() return super().wheelEvent(event) @property def mode(self) -> int: - """ PythonEditor current mode """ + """PythonEditor current mode""" return self._mode @mode.setter @@ -108,12 +116,12 @@ def mode(self, value: str): view.set_mode(value) def mousePressEvent(self, event: QMouseEvent) -> None: - """ PythonEditor reaction to PyQt mousePressEvent events. """ + """PythonEditor reaction to PyQt mousePressEvent events.""" if event.buttons() & Qt.MouseButton.LeftButton: self.mode = "EDITING" return super().mousePressEvent(event) def focusOutEvent(self, event: QFocusEvent): - """ PythonEditor reaction to PyQt focusOut events. """ + """PythonEditor reaction to PyQt focusOut events.""" self.mode = "NOOP" return super().focusOutEvent(event) diff --git a/opencodeblocks/graphics/qss/__init__.py b/opencodeblocks/qss/__init__.py similarity index 80% rename from opencodeblocks/graphics/qss/__init__.py rename to opencodeblocks/qss/__init__.py index a44a510c..208e3fc4 100644 --- a/opencodeblocks/graphics/qss/__init__.py +++ b/opencodeblocks/qss/__init__.py @@ -8,14 +8,14 @@ from PyQt5.QtCore import QFile from PyQt5.QtWidgets import QApplication -from opencodeblocks.graphics.qss import dark_resources +from opencodeblocks.qss import dark_resources def loadStylesheets(filenames: List[str]): - styles = '' + styles = "" for filename in filenames: file = QFile(filename) file.open(QFile.ReadOnly | QFile.Text) stylesheet = file.readAll() - styles += "\n" + str(stylesheet, encoding='utf-8') + styles += "\n" + str(stylesheet, encoding="utf-8") QApplication.instance().setStyleSheet(styles) diff --git a/opencodeblocks/graphics/qss/dark_resources.py b/opencodeblocks/qss/dark_resources.py similarity index 100% rename from opencodeblocks/graphics/qss/dark_resources.py rename to opencodeblocks/qss/dark_resources.py diff --git a/opencodeblocks/graphics/qss/ocb.qss b/opencodeblocks/qss/ocb.qss similarity index 100% rename from opencodeblocks/graphics/qss/ocb.qss rename to opencodeblocks/qss/ocb.qss diff --git a/opencodeblocks/graphics/qss/ocb_dark.qss b/opencodeblocks/qss/ocb_dark.qss similarity index 100% rename from opencodeblocks/graphics/qss/ocb_dark.qss rename to opencodeblocks/qss/ocb_dark.qss diff --git a/opencodeblocks/graphics/scene/__init__.py b/opencodeblocks/scene/__init__.py similarity index 78% rename from opencodeblocks/graphics/scene/__init__.py rename to opencodeblocks/scene/__init__.py index efb3c80c..5da0fc6b 100644 --- a/opencodeblocks/graphics/scene/__init__.py +++ b/opencodeblocks/scene/__init__.py @@ -3,4 +3,4 @@ """ Module for the OCBScene creation and manipulations. """ -from opencodeblocks.graphics.scene.scene import OCBScene +from opencodeblocks.scene.scene import OCBScene diff --git a/opencodeblocks/graphics/scene/clipboard.py b/opencodeblocks/scene/clipboard.py similarity index 58% rename from opencodeblocks/graphics/scene/clipboard.py rename to opencodeblocks/scene/clipboard.py index c8e02fd6..1d381b12 100644 --- a/opencodeblocks/graphics/scene/clipboard.py +++ b/opencodeblocks/scene/clipboard.py @@ -9,20 +9,20 @@ import json from PyQt5.QtWidgets import QApplication -from opencodeblocks.graphics.blocks import OCBBlock, OCBCodeBlock -from opencodeblocks.graphics.edge import OCBEdge +from opencodeblocks.blocks import OCBBlock, OCBCodeBlock +from opencodeblocks.edge import OCBEdge if TYPE_CHECKING: - from opencodeblocks.graphics.scene import OCBScene - from opencodeblocks.graphics.view import OCBView + from opencodeblocks.scene import OCBScene + from opencodeblocks.view import OCBView -class SceneClipboard(): +class SceneClipboard: - """ Helper object to handle clipboard operations on an OCBScene. """ + """Helper object to handle clipboard operations on an OCBScene.""" - def __init__(self, scene:'OCBScene'): - """ Helper object to handle clipboard operations on an OCBScene. + def __init__(self, scene: "OCBScene"): + """Helper object to handle clipboard operations on an OCBScene. Args: scene: Scene reference. @@ -31,36 +31,44 @@ def __init__(self, scene:'OCBScene'): self.scene = scene def cut(self): - """ Cut the selected items and put them into clipboard. """ + """Cut the selected items and put them into clipboard.""" self._store(self._serializeSelected(delete=True)) def copy(self): - """ Copy the selected items into clipboard. """ + """Copy the selected items into clipboard.""" self._store(self._serializeSelected(delete=False)) def paste(self): - """ Paste the items in clipboard into the current scene. """ + """Paste the items in clipboard into the current scene.""" self._deserializeData(self._gatherData()) def _serializeSelected(self, delete=False) -> OrderedDict: selected_blocks, selected_edges = self.scene.sortedSelectedItems() selected_sockets = {} - for block in selected_blocks: # Gather selected sockets + for block in selected_blocks: # Gather selected sockets for socket in block.sockets_in + block.sockets_out: selected_sockets[socket.id] = socket - for edge in selected_edges: # Filter edges that are not fully connected to selected sockets - if edge.source_socket.id not in selected_sockets or \ - edge.destination_socket.id not in selected_sockets: + for ( + edge + ) in ( + selected_edges + ): # Filter edges that are not fully connected to selected sockets + if ( + edge.source_socket.id not in selected_sockets + or edge.destination_socket.id not in selected_sockets + ): selected_edges.remove(edge) - data = OrderedDict([ - ('blocks', [block.serialize() for block in selected_blocks]), - ('edges', [edge.serialize() for edge in selected_edges]) - ]) + data = OrderedDict( + [ + ("blocks", [block.serialize() for block in selected_blocks]), + ("edges", [edge.serialize() for edge in selected_edges]), + ] + ) - if delete: # Remove selected items + if delete: # Remove selected items self.scene.views()[0].deleteSelected() return data @@ -68,7 +76,7 @@ def _serializeSelected(self, delete=False) -> OrderedDict: def _find_bbox_center(self, blocks_data): xmin, xmax, ymin, ymax = 0, 0, 0, 0 for block_data in blocks_data: - x, y = block_data['position'] + x, y = block_data["position"] if x < xmin: xmin = x if x > xmax: @@ -79,7 +87,7 @@ def _find_bbox_center(self, blocks_data): ymax = y return (xmin + xmax) / 2, (ymin + ymax) / 2 - def _deserializeData(self, data:OrderedDict, set_selected=True): + def _deserializeData(self, data: OrderedDict, set_selected=True): hashmap = {} view = self.scene.views()[0] @@ -88,18 +96,21 @@ def _deserializeData(self, data:OrderedDict, set_selected=True): self.scene.clearSelection() # Finding pasting bbox center - bbox_center_x, bbox_center_y = self._find_bbox_center(data['blocks']) - offset_x, offset_y = mouse_pos.x() - bbox_center_x, mouse_pos.y() - bbox_center_y + bbox_center_x, bbox_center_y = self._find_bbox_center(data["blocks"]) + offset_x, offset_y = ( + mouse_pos.x() - bbox_center_x, + mouse_pos.y() - bbox_center_y, + ) # Create blocks - for block_data in data['blocks']: - block_type = block_data['block_type'] - if block_type == 'base': + for block_data in data["blocks"]: + block_type = block_data["block_type"] + if block_type == "base": block = OCBBlock() - elif block_type == 'code': + elif block_type == "code": block = OCBCodeBlock() else: - raise NotImplementedError(f'Unsupported block type: {block_type}') + raise NotImplementedError(f"Unsupported block type: {block_type}") block.deserialize(block_data, hashmap, restore_id=False) block_pos = block.pos() @@ -111,19 +122,20 @@ def _deserializeData(self, data:OrderedDict, set_selected=True): hashmap.update({block.id: block}) # Create edges - for edge_data in data['edges']: + for edge_data in data["edges"]: edge = OCBEdge() edge.deserialize(edge_data, hashmap, restore_id=False) if set_selected: edge.setSelected(True) self.scene.addItem(edge) - hashmap.update({edge_data['id']: edge}) + hashmap.update({edge_data["id"]: edge}) - self.scene.history.checkpoint('Desiralized elements into scene', set_modified=True) + self.scene.history.checkpoint( + "Desiralized elements into scene", set_modified=True + ) - - def _store(self, data:OrderedDict): + def _store(self, data: OrderedDict): str_data = json.dumps(data, indent=4) QApplication.instance().clipboard().setText(str_data) diff --git a/opencodeblocks/graphics/scene/history.py b/opencodeblocks/scene/history.py similarity index 63% rename from opencodeblocks/graphics/scene/history.py rename to opencodeblocks/scene/history.py index a6baabe0..945079fe 100644 --- a/opencodeblocks/graphics/scene/history.py +++ b/opencodeblocks/scene/history.py @@ -6,11 +6,11 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from opencodeblocks.graphics.scene import OCBScene + from opencodeblocks.scene import OCBScene -class SceneHistory(): - """ Helper object to handle undo/redo operations on an OCBScene. +class SceneHistory: + """Helper object to handle undo/redo operations on an OCBScene. Args: scene: Scene reference. @@ -18,42 +18,39 @@ class SceneHistory(): """ - def __init__(self, scene:'OCBScene', max_stack:int = 50): + def __init__(self, scene: "OCBScene", max_stack: int = 50): self.scene = scene self.history_stack = [] self.current = -1 self.max_stack = max_stack def undo(self): - """ Undo the last action by moving the current stamp backward and restoring. """ + """Undo the last action by moving the current stamp backward and restoring.""" if len(self.history_stack) > 0 and self.current > 0: self.current -= 1 self.restore() def redo(self): - """ Redo the last undone action by moving the current stamp forward and restoring. """ + """Redo the last undone action by moving the current stamp forward and restoring.""" if len(self.history_stack) > 0 and self.current + 1 < len(self.history_stack): self.current += 1 self.restore() - def checkpoint(self, description:str, set_modified=True): - """ Store a snapshot of the scene in the history stack. + def checkpoint(self, description: str, set_modified=True): + """Store a snapshot of the scene in the history stack. Args: description: Description given to this checkpoint. set_modified: Whether the scene should be considered modified. """ - history_stamp = { - 'description': description, - 'snapshot': self.scene.serialize() - } + history_stamp = {"description": description, "snapshot": self.scene.serialize()} self.store(history_stamp) if set_modified: self.scene.has_been_modified = True - def store(self, data:Any): - """ Store new data in the history stack, updating current checkpoint. + def store(self, data: Any): + """Store new data in the history stack, updating current checkpoint. Remove data that would be forward in the history stack. Args: @@ -61,18 +58,18 @@ def store(self, data:Any): """ if self.current + 1 < len(self.history_stack): - self.history_stack = self.history_stack[0:self.current+1] + self.history_stack = self.history_stack[0 : self.current + 1] self.history_stack.append(data) if len(self.history_stack) > self.max_stack: self.history_stack.pop(0) - self.current = min(self.current + 1, len(self.history_stack)-1) + self.current = min(self.current + 1, len(self.history_stack) - 1) def restore(self): - """ Restore the scene using the snapshot pointed by current in the history stack. """ + """Restore the scene using the snapshot pointed by current in the history stack.""" if len(self.history_stack) >= 0 and self.current >= 0: stamp = self.history_stack[self.current] - snapshot = stamp['snapshot'] + snapshot = stamp["snapshot"] self.scene.deserialize(snapshot) diff --git a/opencodeblocks/graphics/scene/scene.py b/opencodeblocks/scene/scene.py similarity index 64% rename from opencodeblocks/graphics/scene/scene.py rename to opencodeblocks/scene/scene.py index fa99e84b..0a8003a9 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -6,29 +6,38 @@ import math import json from types import FunctionType -from typing import List, OrderedDict, Union +from typing import TYPE_CHECKING, List, OrderedDict, Union from PyQt5.QtCore import QLine, QRectF from PyQt5.QtGui import QColor, QPainter, QPen from PyQt5.QtWidgets import QGraphicsScene -from opencodeblocks.core.serializable import Serializable -from opencodeblocks.graphics.blocks.block import OCBBlock -from opencodeblocks.graphics.blocks.codeblock import OCBCodeBlock -from opencodeblocks.graphics.edge import OCBEdge -from opencodeblocks.graphics.scene.clipboard import SceneClipboard -from opencodeblocks.graphics.scene.history import SceneHistory +from opencodeblocks.serializable import Serializable +from opencodeblocks.blocks.block import OCBBlock +from opencodeblocks.blocks.codeblock import OCBCodeBlock +from opencodeblocks.edge import OCBEdge +from opencodeblocks.scene.clipboard import SceneClipboard +from opencodeblocks.scene.history import SceneHistory +if TYPE_CHECKING: + from opencodeblocks.view import OCBView -class OCBScene(QGraphicsScene, Serializable): - """ Scene for the OCB Window. """ +class OCBScene(QGraphicsScene, Serializable): - 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): + """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, + ): Serializable.__init__(self) QGraphicsScene.__init__(self, parent=parent) @@ -39,8 +48,7 @@ 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 @@ -51,7 +59,7 @@ def __init__(self, parent=None, @property def has_been_modified(self): - """ True if the scene has been modified, False otherwise. """ + """True if the scene has been modified, False otherwise.""" return self._has_been_modified @has_been_modified.setter @@ -61,11 +69,11 @@ def has_been_modified(self, value: bool): callback() def addHasBeenModifiedListener(self, callback: FunctionType): - """ Add a callback that will trigger when the scene has been modified. """ + """Add a callback that will trigger when the scene has been modified.""" self._has_been_modified_listeners.append(callback) def sortedSelectedItems(self) -> List[Union[OCBBlock, OCBEdge]]: - """ Returns the selected blocks and selected edges in two separate lists. """ + """Returns the selected blocks and selected edges in two separate lists.""" selected_blocks, selected_edges = [], [] for item in self.selectedItems(): if isinstance(item, OCBBlock): @@ -75,12 +83,12 @@ def sortedSelectedItems(self) -> List[Union[OCBBlock, OCBEdge]]: return selected_blocks, selected_edges def drawBackground(self, painter: QPainter, rect: QRectF): - """ Draw the Scene background """ + """Draw the Scene background""" super().drawBackground(painter, rect) self.drawGrid(painter, rect) def drawGrid(self, painter: QPainter, rect: QRectF): - """ Draw the background grid """ + """Draw the background grid""" left = int(math.floor(rect.left())) top = int(math.floor(rect.top())) right = int(math.ceil(rect.right())) @@ -115,51 +123,51 @@ def drawGrid(self, painter: QPainter, rect: QRectF): painter.drawLines(*lines_light) def save(self, filepath: str): - """ Save the scene into filepath. """ + """Save the scene into filepath.""" self.save_to_ipyg(filepath) self.has_been_modified = False def save_to_ipyg(self, filepath: str): - """ Save the scene into filepath as interactive python graph (.ipyg). """ - if '.' not in filepath: - filepath += '.ipyg' + """Save the scene into filepath as interactive python graph (.ipyg).""" + if "." not in filepath: + filepath += ".ipyg" - extention_format = filepath.split('.')[-1] - if extention_format != 'ipyg': + extention_format = filepath.split(".")[-1] + if extention_format != "ipyg": raise NotImplementedError(f"Unsupported format {extention_format}") - with open(filepath, 'w', encoding='utf-8') as file: + with open(filepath, "w", encoding="utf-8") as file: file.write(json.dumps(self.serialize(), indent=4)) def load(self, filepath: str): - """ Load a saved scene. + """Load a saved scene. Args: filepath: Path to the file to load. """ - if filepath.endswith('.ipyg'): + if filepath.endswith(".ipyg"): data = self.load_from_ipyg(filepath) else: - extention_format = filepath.split('.')[-1] + extention_format = filepath.split(".")[-1] raise NotImplementedError(f"Unsupported format {extention_format}") self.deserialize(data) self.history.checkpoint("Loaded scene") self.has_been_modified = False def load_from_ipyg(self, filepath: str): - """ Load an interactive python graph (.ipyg) into the scene. + """Load an interactive python graph (.ipyg) into the scene. Args: filepath: Path to the .ipyg file to load. """ - with open(filepath, 'r', encoding='utf-8') as file: + with open(filepath, "r", encoding="utf-8") as file: data = json.loads(file.read()) return data def clear(self): - """ Clear the scene from all items. """ + """Clear the scene from all items.""" self.has_been_modified = False return super().clear() @@ -173,52 +181,58 @@ def serialize(self) -> OrderedDict: edges.append(item) blocks.sort(key=lambda x: x.id) edges.sort(key=lambda x: x.id) - return OrderedDict([ - ('id', self.id), - ('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): - """ Create a new block from a .ocbb file """ - with open(filepath, 'r', encoding='utf-8') as file: + return OrderedDict( + [ + ("id", self.id), + ("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): + """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 """ + def views(self) -> List["OCBView"]: + return super().views() + + 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': + if data["block_type"] == "base": block = OCBBlock() - elif data['block_type'] == 'code': + 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}) + 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: - self.id = data['id'] + self.id = data["id"] # Create blocks - for block_data in data['blocks']: + for block_data in data["blocks"]: self.create_block(block_data, hashmap, restore_id) # Create edges - for edge_data in data['edges']: + for edge_data in data["edges"]: edge = OCBEdge() edge.deserialize(edge_data, hashmap, restore_id) self.addItem(edge) - hashmap.update({edge_data['id']: edge}) + hashmap.update({edge_data["id"]: edge}) diff --git a/opencodeblocks/core/serializable.py b/opencodeblocks/serializable.py similarity index 69% rename from opencodeblocks/core/serializable.py rename to opencodeblocks/serializable.py index 52c8675f..f997d270 100644 --- a/opencodeblocks/core/serializable.py +++ b/opencodeblocks/serializable.py @@ -6,19 +6,21 @@ from typing import OrderedDict -class Serializable(): +class Serializable: - """ Serializable base for serializable objects. """ + """Serializable base for serializable objects.""" def __init__(self): self.id = id(self) def serialize(self) -> OrderedDict: - """ Serialize the object as an ordered dictionary. """ + """Serialize the object as an ordered dictionary.""" raise NotImplementedError() - def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True) -> None: - """ Deserialize the object from an ordered dictionary. + def deserialize( + self, data: OrderedDict, hashmap: dict = None, restore_id=True + ) -> None: + """Deserialize the object from an ordered dictionary. Args: data: Dictionnary containing data do deserialize from. diff --git a/opencodeblocks/graphics/socket.py b/opencodeblocks/socket.py similarity index 50% rename from opencodeblocks/graphics/socket.py rename to opencodeblocks/socket.py index 484ad501..614f7a7a 100644 --- a/opencodeblocks/graphics/socket.py +++ b/opencodeblocks/socket.py @@ -11,22 +11,28 @@ from PyQt5.QtGui import QBrush, QColor, QPainter, QPen, QPolygon from PyQt5.QtWidgets import QGraphicsItem, QStyleOptionGraphicsItem, QWidget -from opencodeblocks.core.serializable import Serializable +from opencodeblocks.serializable import Serializable if TYPE_CHECKING: - from opencodeblocks.graphics.edge import OCBEdge - from opencodeblocks.graphics.blocks.block import OCBBlock + from opencodeblocks.edge import OCBEdge + from opencodeblocks.blocks import OCBBlock class OCBSocket(QGraphicsItem, Serializable): - """ Base class for sockets in OpenCodeBlocks. """ + """Base class for sockets in OpenCodeBlocks.""" - def __init__(self, block: 'OCBBlock', - socket_type: str = 'undefined', flow_type: str = 'exe', - radius: float = 6.0, color: str = '#FF55FFF0', - linewidth: float = 1.0, linecolor: str = '#FF000000'): - """ Base class for sockets in OpenCodeBlocks. + def __init__( + self, + block: "OCBBlock", + socket_type: str = "undefined", + flow_type: str = "exe", + radius: float = 6.0, + color: str = "#FF55FFF0", + linewidth: float = 1.0, + linecolor: str = "#FF000000", + ): + """Base class for sockets in OpenCodeBlocks. Args: block: Block containing the socket. @@ -42,7 +48,7 @@ def __init__(self, block: 'OCBBlock', self.block = block QGraphicsItem.__init__(self, parent=self.block) - self.edges: List['OCBEdge'] = [] + self.edges: List["OCBEdge"] = [] self.socket_type = socket_type self.flow_type = flow_type @@ -52,30 +58,31 @@ def __init__(self, block: 'OCBBlock', self._brush = QBrush(QColor(color)) self.metadata = { - 'radius': radius, - 'color': color, - 'linewidth': linewidth, - 'linecolor': linecolor, + "radius": radius, + "color": color, + "linewidth": linewidth, + "linecolor": linecolor, } - def add_edge(self, edge: 'OCBEdge', is_destination: bool): - """ Add a given edge to the socket edges. """ + def add_edge(self, edge: "OCBEdge", is_destination: bool): + """Add a given edge to the socket edges.""" if not self._allow_multiple_edges: for prev_edge in self.edges: prev_edge.remove() - if self.flow_type == 'exe': - if ((is_destination and self.socket_type != 'input') or - (not is_destination and self.socket_type != 'output')): + if self.flow_type == "exe": + if (is_destination and self.socket_type != "input") or ( + not is_destination and self.socket_type != "output" + ): edge.remove() return self.edges.append(edge) - def remove_edge(self, edge: 'OCBEdge'): - """ Remove a given edge from the socket edges. """ + def remove_edge(self, edge: "OCBEdge"): + """Remove a given edge from the socket edges.""" self.edges.remove(edge) def remove(self): - """ Remove the socket and all its edges from the scene it is in. """ + """Remove the socket and all its edges from the scene it is in.""" for edge in self.edges: edge.remove() scene = self.scene() @@ -84,48 +91,53 @@ def remove(self): @property def _allow_multiple_edges(self): - if self.flow_type == 'exe': + if self.flow_type == "exe": return True raise NotImplementedError - def paint(self, painter: QPainter, - option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument - widget: Optional[QWidget] = None): # pylint:disable=unused-argument - """ Paint the socket. """ + def paint( + self, + painter: QPainter, + option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument + widget: Optional[QWidget] = None, + ): # pylint:disable=unused-argument + """Paint the socket.""" painter.setBrush(self._brush) painter.setPen(self._pen) r = self.radius - if self.flow_type == 'exe': - angles = [0, 2*math.pi/3, -2*math.pi/3] + if self.flow_type == "exe": + angles = [0, 2 * math.pi / 3, -2 * math.pi / 3] right_triangle_points = [ - QPoint(int(r*math.cos(angle)), int(r*math.sin(angle))) + QPoint(int(r * math.cos(angle)), int(r * math.sin(angle))) for angle in angles ] painter.drawPolygon(QPolygon(right_triangle_points)) else: - painter.drawEllipse(int(-r), int(-r), int(2*r), int(2*r)) + painter.drawEllipse(int(-r), int(-r), int(2 * r), int(2 * r)) def boundingRect(self) -> QRectF: - """ Get the socket bounding box. """ + """Get the socket bounding box.""" r = self.radius - return QRectF(-r, -r, 2*r, 2*r) + return QRectF(-r, -r, 2 * r, 2 * r) def serialize(self) -> OrderedDict: metadata = OrderedDict(sorted(self.metadata.items())) - return OrderedDict([ - ('id', self.id), - ('type', self.socket_type), - ('position', [self.pos().x(), self.pos().y()]), - ('metadata', metadata) - ]) + return OrderedDict( + [ + ("id", self.id), + ("type", self.socket_type), + ("position", [self.pos().x(), self.pos().y()]), + ("metadata", metadata), + ] + ) def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): if restore_id: - self.id = data['id'] - self.socket_type = data['type'] - self.setPos(QPointF(*data['position'])) - - self.metadata = dict(data['metadata']) - self._pen.setColor(QColor(self.metadata['linecolor'])) - self._pen.setWidth(int(self.metadata['linewidth'])) - self._brush.setColor(QColor(self.metadata['color'])) + self.id = data["id"] + self.socket_type = data["type"] + self.setPos(QPointF(*data["position"])) + + self.metadata = dict(data["metadata"]) + self._pen.setColor(QColor(self.metadata["linecolor"])) + self._pen.setWidth(int(self.metadata["linewidth"])) + self._brush.setColor(QColor(self.metadata["color"])) diff --git a/opencodeblocks/graphics/theme.py b/opencodeblocks/theme.py similarity index 83% rename from opencodeblocks/graphics/theme.py rename to opencodeblocks/theme.py index 57171f3f..30182858 100644 --- a/opencodeblocks/graphics/theme.py +++ b/opencodeblocks/theme.py @@ -9,7 +9,7 @@ class Theme: - """ Class holding the details of a specific theme""" + """Class holding the details of a specific theme""" def __init__(self, name: str, json_str: str = "{}"): """ @@ -23,7 +23,7 @@ def __init__(self, name: str, json_str: str = "{}"): "keyword_color": "#569CD6", "classname_color": "#4EC9B0", "literal_color": "#7FB347", - "operator_color": "#D8D8D8" + "operator_color": "#D8D8D8", } for (property_name, property_value) in known_properties.items(): if property_name in json_obj: @@ -33,7 +33,7 @@ def __init__(self, name: str, json_str: str = "{}"): self.name = name def apply_to_lexer(self, lexer: QsciLexerPython): - """ Make the given lexer follow the theme """ + """Make the given lexer follow the theme""" lexer.setDefaultPaper(QColor("#1E1E1E")) lexer.setDefaultColor(QColor("#D4D4D4")) @@ -51,16 +51,10 @@ def apply_to_lexer(self, lexer: QsciLexerPython): for string_type in string_types: lexer.setColor(QColor(self.string_color), string_type) - lexer.setColor( - QColor( - self.function_color), - QsciLexerPython.FunctionMethodName) + lexer.setColor(QColor(self.function_color), QsciLexerPython.FunctionMethodName) lexer.setColor(QColor(self.keyword_color), QsciLexerPython.Keyword) lexer.setColor(QColor(self.classname_color), QsciLexerPython.ClassName) lexer.setColor(QColor(self.literal_color), QsciLexerPython.Number) lexer.setColor(QColor(self.operator_color), QsciLexerPython.Operator) - lexer.setColor( - QColor( - self.comment_color), - QsciLexerPython.CommentBlock) + lexer.setColor(QColor(self.comment_color), QsciLexerPython.CommentBlock) lexer.setColor(QColor(self.comment_color), QsciLexerPython.Comment) diff --git a/opencodeblocks/graphics/theme_manager.py b/opencodeblocks/theme_manager.py similarity index 81% rename from opencodeblocks/graphics/theme_manager.py rename to opencodeblocks/theme_manager.py index 9c088271..f36d83d5 100644 --- a/opencodeblocks/graphics/theme_manager.py +++ b/opencodeblocks/theme_manager.py @@ -11,16 +11,16 @@ from PyQt5.QtGui import QFontDatabase from PyQt5.QtCore import pyqtSignal, QObject -from opencodeblocks.graphics.theme import Theme +from opencodeblocks.theme import Theme class ThemeManager(QObject): - """ Class loading theme files and providing the options set in those files """ + """Class loading theme files and providing the options set in those files""" themeChanged = pyqtSignal() def __init__(self, parent=None): - """ Load the default themes and the fonts available to construct the ThemeManager """ + """Load the default themes and the fonts available to construct the ThemeManager""" super().__init__(parent) self._preferred_fonts = ["Inconsolata", "Roboto Mono", "Courier"] self.recommended_font_family = "Monospace" @@ -39,7 +39,7 @@ def __init__(self, parent=None): full_path = os.path.join(theme_path, p) if os.path.isfile(full_path) and full_path.endswith(".theme"): name = os.path.splitext(os.path.basename(p))[0] - with open(full_path, 'r', encoding="utf-8") as f: + with open(full_path, "r", encoding="utf-8") as f: theme = Theme(name, f.read()) self._themes.append(theme) @@ -53,11 +53,11 @@ def selected_theme_index(self, value: int): self.themeChanged.emit() def list_themes(self) -> List[str]: - """ List the themes """ + """List the themes""" return [theme.name for theme in self._themes] def current_theme(self) -> Theme: - """ Return the current theme """ + """Return the current theme""" return self._themes[self.selected_theme_index] @@ -65,7 +65,7 @@ def current_theme(self) -> Theme: def theme_manager(): - """ Retreive the theme manager of the application """ + """Retreive the theme manager of the application""" global theme_handle if theme_handle is None: theme_handle = ThemeManager() diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/view.py similarity index 68% rename from opencodeblocks/graphics/view.py rename to opencodeblocks/view.py index c76ffd91..7a5e624e 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/view.py @@ -13,28 +13,34 @@ 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 +from opencodeblocks.scene import OCBScene +from opencodeblocks.socket import OCBSocket +from opencodeblocks.edge import OCBEdge +from opencodeblocks.blocks import OCBBlock class OCBView(QGraphicsView): - """ View for the OCB Window. """ + """View for the OCB Window.""" MODE_NOOP = 0 MODE_EDGE_DRAG = 1 MODE_EDITING = 2 MODES = { - 'NOOP': MODE_NOOP, - 'EDGE_DRAG': MODE_EDGE_DRAG, - 'EDITING': MODE_EDITING, + "NOOP": MODE_NOOP, + "EDGE_DRAG": MODE_EDGE_DRAG, + "EDITING": MODE_EDITING, } - 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 = self.MODE_NOOP self.zoom = 1 @@ -48,34 +54,30 @@ def __init__(self, scene: OCBScene, parent=None, self.setScene(scene) def init_ui(self): - """ Initialize the custom OCB View UI. """ + """Initialize the custom OCB View UI.""" # Antialiasing self.setRenderHints( - QPainter.RenderHint.Antialiasing | - QPainter.RenderHint.HighQualityAntialiasing | - QPainter.RenderHint.TextAntialiasing | - QPainter.RenderHint.SmoothPixmapTransform + QPainter.RenderHint.Antialiasing + | QPainter.RenderHint.HighQualityAntialiasing + | QPainter.RenderHint.TextAntialiasing + | QPainter.RenderHint.SmoothPixmapTransform ) # Better Update - self.setViewportUpdateMode( - QGraphicsView.ViewportUpdateMode.FullViewportUpdate - ) + self.setViewportUpdateMode(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) def scene(self) -> OCBScene: - """ Get current OCBScene. """ + """Get current OCBScene.""" return super().scene() def mousePressEvent(self, event: QMouseEvent): - """ Dispatch Qt's mousePress events to corresponding functions below. """ + """Dispatch Qt's mousePress events to corresponding functions below.""" if event.button() == Qt.MouseButton.MiddleButton: self.middleMouseButtonPress(event) elif event.button() == Qt.MouseButton.LeftButton: @@ -93,14 +95,14 @@ def mouseReleaseEvent(self, event: QMouseEvent): super().mouseReleaseEvent(event) def mouseMoveEvent(self, event: QMouseEvent) -> None: - """ OCBView reaction to mouseMoveEvent. """ + """OCBView reaction to mouseMoveEvent.""" self.lastMousePos = self.mapToScene(event.pos()) - self.drag_edge(event, 'move') + self.drag_edge(event, "move") if event is not None: super().mouseMoveEvent(event) def leftMouseButtonPress(self, event: QMouseEvent): - """ OCBView reaction to leftMouseButtonPress event. """ + """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: @@ -113,48 +115,48 @@ def leftMouseButtonPress(self, event: QMouseEvent): self.bring_block_forward(item_at_click) # If clicked on a socket, start dragging an edge. - event = self.drag_edge(event, 'press') + event = self.drag_edge(event, "press") if event is not None: super().mousePressEvent(event) def leftMouseButtonRelease(self, event: QMouseEvent): - """ OCBView reaction to leftMouseButtonRelease event. """ - event = self.drag_edge(event, 'release') + """OCBView reaction to leftMouseButtonRelease event.""" + event = self.drag_edge(event, "release") if event is not None: super().mouseReleaseEvent(event) def middleMouseButtonPress(self, event: QMouseEvent): - """ OCBView reaction to middleMouseButtonPress event. """ + """OCBView reaction to middleMouseButtonPress event.""" if self.itemAt(event.pos()) is None: event = self.drag_scene(event, "press") super().mousePressEvent(event) def middleMouseButtonRelease(self, event: QMouseEvent): - """ OCBView reaction to middleMouseButtonRelease event. """ + """OCBView reaction to middleMouseButtonRelease event.""" event = self.drag_scene(event, "release") super().mouseReleaseEvent(event) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) def centerView(self, x: float, y: float): - """ Move the view so that the position (x,y) is centered. """ + """Move the view so that the position (x,y) is centered.""" hsb = self.horizontalScrollBar() vsb = self.verticalScrollBar() hsb.setValue(x * self.zoom - self.width() / 2) vsb.setValue(y * self.zoom - self.height() / 2) def getDistanceToCenter(self, x: float, y: float) -> Tuple[float]: - """ Return the vector from the (x,y) position given to the center of the view """ + """Return the vector from the (x,y) position given to the center of the view""" ypos = self.verticalScrollBar().value() xpos = self.horizontalScrollBar().value() return ( xpos - x * self.zoom + self.width() / 2, - ypos - y * self.zoom + self.height() / 2 + ypos - y * self.zoom + self.height() / 2, ) def moveViewOnArrow(self, event: QKeyEvent) -> bool: """ - OCBView reaction to an arrow key being pressed. - Returns True if the event was handled. + OCBView reaction to an arrow key being pressed. + Returns True if the event was handled. """ # The focusItem has priority for this event if self.scene().focusItem() is not None: @@ -175,16 +177,16 @@ def moveViewOnArrow(self, event: QKeyEvent) -> bool: for block in code_blocks: block_center_x = block.x() + block.width / 2 block_center_y = block.y() + block.height / 2 - xdist, ydist = self.getDistanceToCenter( - block_center_x, block_center_y) - dist_array.append(( - block_center_x, - block_center_y, - xdist, - ydist, - max(abs(xdist), abs(ydist)) - - )) + xdist, ydist = self.getDistanceToCenter(block_center_x, block_center_y) + dist_array.append( + ( + block_center_x, + block_center_y, + xdist, + ydist, + max(abs(xdist), abs(ydist)), + ) + ) if key_id == Qt.Key.Key_Up: dist_array = filter(lambda pos: pos[3] > 1, dist_array) @@ -206,16 +208,20 @@ def moveViewOnArrow(self, event: QKeyEvent) -> bool: return True def keyPressEvent(self, event: QKeyEvent): - """ OCBView reaction to a key being pressed """ + """OCBView reaction to a key being pressed""" key_id = event.key() - if key_id in [Qt.Key.Key_Up, Qt.Key.Key_Down, - Qt.Key.Key_Left, Qt.Key.Key_Right]: + if key_id in [ + Qt.Key.Key_Up, + Qt.Key.Key_Down, + Qt.Key.Key_Left, + Qt.Key.Key_Right, + ]: if self.moveViewOnArrow(event): return super().keyPressEvent(event) def retreiveBlockTypes(self) -> List[Tuple[str]]: - """ Retreive the list of stored blocks. """ + """Retreive the list of stored blocks.""" block_type_files = os.listdir("blocks") block_types = [] for b in block_type_files: @@ -225,14 +231,14 @@ def retreiveBlockTypes(self) -> List[Tuple[str]]: title = "New Block" if "title" in data: title = f"New {data['title']} Block" - if data['title'] == "Empty": + if data["title"] == "Empty": block_types[:0] = [(filepath, title)] else: block_types.append((filepath, title)) return block_types def contextMenuEvent(self, event: QContextMenuEvent): - """ Displays the context menu when inside a view """ + """Displays the context menu when inside a view""" menu = QMenu(self) actionPool = [] for filepath, block_name in self.retreiveBlockTypes(): @@ -245,7 +251,7 @@ def contextMenuEvent(self, event: QContextMenuEvent): self.scene().create_block_from_file(filepath, p.x(), p.y()) def wheelEvent(self, event: QWheelEvent): - """ Handles zooming with mouse wheel events. """ + """Handles zooming with mouse wheel events.""" if Qt.Modifier.CTRL == int(event.modifiers()): # calculate zoom if event.angleDelta().y() > 0: @@ -260,66 +266,84 @@ def wheelEvent(self, event: QWheelEvent): super().wheelEvent(event) def deleteSelected(self): - """ Delete selected items from the current scene. """ + """Delete selected items from the current scene.""" scene = self.scene() for selected_item in scene.selectedItems(): selected_item.remove() scene.history.checkpoint("Delete selected elements", set_modified=True) def bring_block_forward(self, block: OCBBlock): - """ Move the selected block in front of other blocks. + """Move the selected block in front of other blocks. Args: block: Block to bring forward. """ if self.currentSelectedBlock is not None and not isdeleted( - self.currentSelectedBlock): + self.currentSelectedBlock + ): self.currentSelectedBlock.setZValue(0) block.setZValue(1) self.currentSelectedBlock = block def drag_scene(self, event: QMouseEvent, action="press"): - """ Drag the scene around. """ + """Drag the scene around.""" if action == "press": - releaseEvent = QMouseEvent(QEvent.Type.MouseButtonRelease, - event.localPos(), event.screenPos(), - Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, - event.modifiers()) + releaseEvent = QMouseEvent( + QEvent.Type.MouseButtonRelease, + 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()) - return QMouseEvent(event.type(), event.localPos(), event.screenPos(), - 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(), + ) + return QMouseEvent( + event.type(), + event.localPos(), + event.screenPos(), + 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. """ + """Create an edge by drag and drop.""" item_at_click = self.itemAt(event.pos()) scene = self.scene() if action == "press": - if isinstance(item_at_click, OCBSocket) \ - and self.mode != self.MODE_EDGE_DRAG\ - and item_at_click.socket_type != 'input': + if ( + isinstance(item_at_click, OCBSocket) + and self.mode != self.MODE_EDGE_DRAG + and item_at_click.socket_type != "input" + ): self.mode = self.MODE_EDGE_DRAG self.edge_drag = OCBEdge( source_socket=item_at_click, - destination=self.mapToScene(event.pos()) + destination=self.mapToScene(event.pos()), ) scene.addItem(self.edge_drag) return elif action == "release": if self.mode == self.MODE_EDGE_DRAG: - if isinstance(item_at_click, OCBSocket) \ - and item_at_click is not self.edge_drag.source_socket \ - and item_at_click.socket_type != 'output': + if ( + isinstance(item_at_click, OCBSocket) + 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) + "Created edge by dragging", set_modified=True + ) else: self.edge_drag.remove() self.edge_drag = None @@ -330,7 +354,7 @@ def drag_edge(self, event: QMouseEvent, action="press"): return event def set_mode(self, mode: str): - """ Change the view mode. + """Change the view mode. Args: mode: Mode key to change to, must in present in MODES. @@ -339,7 +363,7 @@ def set_mode(self, mode: str): self.mode = self.MODES[mode] def is_mode(self, mode: str): - """ Return True if the view is in the given mode. + """Return True if the view is in the given mode. Args: mode: Mode key to compare to, must in present in MODES. diff --git a/opencodeblocks/graphics/widget.py b/opencodeblocks/widget.py similarity index 79% rename from opencodeblocks/graphics/widget.py rename to opencodeblocks/widget.py index 761b5ebf..5bd2d097 100644 --- a/opencodeblocks/graphics/widget.py +++ b/opencodeblocks/widget.py @@ -8,13 +8,13 @@ from PyQt5.QtWidgets import QVBoxLayout, QWidget from PyQt5.QtCore import Qt -from opencodeblocks.graphics.scene import OCBScene -from opencodeblocks.graphics.view import OCBView +from opencodeblocks.scene import OCBScene +from opencodeblocks.view import OCBView class OCBWidget(QWidget): - """ Window for the OCB application. """ + """Window for the OCB application.""" def __init__(self, parent=None): super().__init__(parent) @@ -35,9 +35,9 @@ def __init__(self, parent=None): self.savepath = None def updateTitle(self): - """ Update the window title. """ + """Update the window title.""" if self.savepath is None: - title = 'New Graph' + title = "New Graph" else: title = os.path.basename(self.savepath) if self.isModified(): @@ -45,12 +45,12 @@ def updateTitle(self): self.setWindowTitle(title) def isModified(self) -> bool: - """ Return True if the scene has been modified, False otherwise. """ + """Return True if the scene has been modified, False otherwise.""" return self.scene.has_been_modified @property def savepath(self): - """ Current cached file save path. Update window title when set.""" + """Current cached file save path. Update window title when set.""" return self._savepath @savepath.setter diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/window.py similarity index 58% rename from opencodeblocks/graphics/window.py rename to opencodeblocks/window.py index d50f1b7e..1a87191f 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/window.py @@ -4,37 +4,45 @@ """ Module for the OCB Window """ import os + from PyQt5.QtCore import QPoint, QSettings, QSize, Qt, QSignalMapper from PyQt5.QtGui import QCloseEvent, QKeySequence - -from PyQt5.QtWidgets import QDockWidget, QListWidget, QWidget, QAction, QFileDialog, QMainWindow,\ - QMessageBox, QMdiArea - -from opencodeblocks.graphics.widget import OCBWidget -from opencodeblocks.graphics.theme_manager import theme_manager - -from opencodeblocks.graphics.qss import loadStylesheets +from PyQt5.QtWidgets import ( + QDockWidget, + QListWidget, + QWidget, + QAction, + QFileDialog, + QMainWindow, + QMessageBox, + QMdiArea, +) + +from opencodeblocks.widget import OCBWidget +from opencodeblocks.theme_manager import theme_manager +from opencodeblocks.qss import loadStylesheets class OCBWindow(QMainWindow): - """ Main window of the OpenCodeBlocks Qt-based application. """ + """Main window of the OpenCodeBlocks Qt-based application.""" def __init__(self): super().__init__() self.stylesheet_filename = os.path.join( - os.path.dirname(__file__), 'qss', 'ocb.qss') - loadStylesheets(( - os.path.join(os.path.dirname(__file__), 'qss', 'ocb_dark.qss'), - self.stylesheet_filename - )) + os.path.dirname(__file__), "qss", "ocb.qss" + ) + loadStylesheets( + ( + os.path.join(os.path.dirname(__file__), "qss", "ocb_dark.qss"), + self.stylesheet_filename, + ) + ) self.mdiArea = QMdiArea() - self.mdiArea.setHorizontalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAsNeeded) - self.mdiArea.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.mdiArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.mdiArea.setViewMode(QMdiArea.ViewMode.TabbedView) self.mdiArea.setDocumentMode(True) self.mdiArea.setTabsMovable(True) @@ -90,59 +98,122 @@ def updateMenus(self): pass def createActions(self): - """ Create all menu actions. """ + """Create all menu actions.""" # File - self.actNew = QAction('&New', statusTip='Create new ipygraph', - shortcut='Ctrl+N', triggered=self.onFileNew) - self.actOpen = QAction('&Open', statusTip='Open an ipygraph', - shortcut='Ctrl+O', triggered=self.onFileOpen) - self.actSave = QAction('&Save', statusTip='Save the ipygraph', - shortcut='Ctrl+S', triggered=self.onFileSave) - self.actSaveAs = QAction('Save &As...', statusTip='Save the ipygraph as...', - shortcut='Ctrl+Shift+S', triggered=self.onFileSaveAs) - self.actQuit = QAction('&Quit', statusTip='Save and Quit the application', - shortcut='Ctrl+Q', triggered=self.close) + self.actNew = QAction( + "&New", + statusTip="Create new ipygraph", + shortcut="Ctrl+N", + triggered=self.onFileNew, + ) + self.actOpen = QAction( + "&Open", + statusTip="Open an ipygraph", + shortcut="Ctrl+O", + triggered=self.onFileOpen, + ) + self.actSave = QAction( + "&Save", + statusTip="Save the ipygraph", + shortcut="Ctrl+S", + triggered=self.onFileSave, + ) + self.actSaveAs = QAction( + "Save &As...", + statusTip="Save the ipygraph as...", + shortcut="Ctrl+Shift+S", + triggered=self.onFileSaveAs, + ) + self.actQuit = QAction( + "&Quit", + statusTip="Save and Quit the application", + shortcut="Ctrl+Q", + triggered=self.close, + ) # Edit - self.actUndo = QAction('&Undo', statusTip='Undo last operation', - shortcut='Ctrl+Z', triggered=self.onEditUndo) - self.actRedo = QAction('&Redo', statusTip='Redo last operation', - shortcut='Ctrl+Y', triggered=self.onEditRedo) - self.actCut = QAction('Cu&t', statusTip='Cut to clipboard', - shortcut='Ctrl+X', triggered=self.onEditCut) - self.actCopy = QAction('&Copy', statusTip='Copy to clipboard', - shortcut='Ctrl+C', triggered=self.onEditCopy) - self.actPaste = QAction('&Paste', statusTip='Paste from clipboard', - shortcut='Ctrl+V', triggered=self.onEditPaste) - self.actDel = QAction('&Del', statusTip='Delete selected items', - shortcut='Del', triggered=self.onEditDelete) + self.actUndo = QAction( + "&Undo", + statusTip="Undo last operation", + shortcut="Ctrl+Z", + triggered=self.onEditUndo, + ) + self.actRedo = QAction( + "&Redo", + statusTip="Redo last operation", + shortcut="Ctrl+Y", + triggered=self.onEditRedo, + ) + self.actCut = QAction( + "Cu&t", + statusTip="Cut to clipboard", + shortcut="Ctrl+X", + triggered=self.onEditCut, + ) + self.actCopy = QAction( + "&Copy", + statusTip="Copy to clipboard", + shortcut="Ctrl+C", + triggered=self.onEditCopy, + ) + self.actPaste = QAction( + "&Paste", + statusTip="Paste from clipboard", + shortcut="Ctrl+V", + triggered=self.onEditPaste, + ) + self.actDel = QAction( + "&Del", + statusTip="Delete selected items", + shortcut="Del", + triggered=self.onEditDelete, + ) # Window - self.actClose = QAction("Cl&ose", self, - statusTip="Close the active window", - triggered=self.mdiArea.closeActiveSubWindow) - self.actCloseAll = QAction("Close &All", self, - statusTip="Close all the windows", - triggered=self.mdiArea.closeAllSubWindows) - self.actTile = QAction("&Tile", self, statusTip="Tile the windows", - triggered=self.mdiArea.tileSubWindows) - self.actCascade = QAction("&Cascade", self, - statusTip="Cascade the windows", - triggered=self.mdiArea.cascadeSubWindows) - self.actNext = QAction("Ne&xt", self, - shortcut=QKeySequence.StandardKey.NextChild, - statusTip="Move the focus to the next window", - triggered=self.mdiArea.activateNextSubWindow) - self.actPrevious = QAction("Pre&vious", self, - shortcut=QKeySequence.StandardKey.PreviousChild, - statusTip="Move the focus to the previous window", - triggered=self.mdiArea.activatePreviousSubWindow) + self.actClose = QAction( + "Cl&ose", + self, + statusTip="Close the active window", + triggered=self.mdiArea.closeActiveSubWindow, + ) + self.actCloseAll = QAction( + "Close &All", + self, + statusTip="Close all the windows", + triggered=self.mdiArea.closeAllSubWindows, + ) + self.actTile = QAction( + "&Tile", + self, + statusTip="Tile the windows", + triggered=self.mdiArea.tileSubWindows, + ) + self.actCascade = QAction( + "&Cascade", + self, + statusTip="Cascade the windows", + triggered=self.mdiArea.cascadeSubWindows, + ) + self.actNext = QAction( + "Ne&xt", + self, + shortcut=QKeySequence.StandardKey.NextChild, + statusTip="Move the focus to the next window", + triggered=self.mdiArea.activateNextSubWindow, + ) + self.actPrevious = QAction( + "Pre&vious", + self, + shortcut=QKeySequence.StandardKey.PreviousChild, + statusTip="Move the focus to the previous window", + triggered=self.mdiArea.activatePreviousSubWindow, + ) self.actSeparator = QAction(self) self.actSeparator.setSeparator(True) def createMenus(self): - """ Create the File menu with linked shortcuts. """ - self.filemenu = self.menuBar().addMenu('&File') + """Create the File menu with linked shortcuts.""" + self.filemenu = self.menuBar().addMenu("&File") self.filemenu.addAction(self.actNew) self.filemenu.addAction(self.actOpen) self.filemenu.addSeparator() @@ -151,7 +222,7 @@ def createMenus(self): self.filemenu.addSeparator() self.filemenu.addAction(self.actQuit) - self.editmenu = self.menuBar().addMenu('&Edit') + self.editmenu = self.menuBar().addMenu("&Edit") self.editmenu.addAction(self.actUndo) self.editmenu.addAction(self.actRedo) self.editmenu.addSeparator() @@ -161,8 +232,8 @@ def createMenus(self): self.editmenu.addSeparator() self.editmenu.addAction(self.actDel) - self.viewmenu = self.menuBar().addMenu('&View') - self.thememenu = self.viewmenu.addMenu('Theme') + self.viewmenu = self.menuBar().addMenu("&View") + self.thememenu = self.viewmenu.addMenu("Theme") self.thememenu.aboutToShow.connect(self.updateThemeMenu) self.windowMenu = self.menuBar().addMenu("&Window") @@ -201,7 +272,7 @@ def updateWindowMenu(self): text = f"{i + 1} {child.windowTitle()}" if i < 9: - text = '&' + text + text = "&" + text action = self.windowMenu.addAction(text) action.setCheckable(True) @@ -210,7 +281,7 @@ def updateWindowMenu(self): self.windowMapper.setMapping(action, window) def createNewMdiChild(self, filename: str = None): - """ Create a new graph subwindow loading a file if a path is given. """ + """Create a new graph subwindow loading a file if a path is given.""" ocb_widget = OCBWidget() if filename is not None: ocb_widget.scene.load(filename) @@ -218,15 +289,14 @@ def createNewMdiChild(self, filename: str = None): return self.mdiArea.addSubWindow(ocb_widget) def onFileNew(self): - """ Create a new file. """ + """Create a new file.""" subwnd = self.createNewMdiChild() subwnd.show() def onFileOpen(self): - """ Open a file. """ - filename, _ = QFileDialog.getOpenFileName( - self, 'Open ipygraph from file') - if filename == '': + """Open a file.""" + filename, _ = QFileDialog.getOpenFileName(self, "Open ipygraph from file") + if filename == "": return if os.path.isfile(filename): subwnd = self.createNewMdiChild(filename) @@ -234,7 +304,7 @@ def onFileOpen(self): self.statusbar.showMessage(f"Successfully loaded {filename}", 2000) def onFileSave(self) -> bool: - """ Save file. + """Save file. Returns: True if the file was successfully saved, False otherwise. @@ -246,11 +316,12 @@ def onFileSave(self) -> bool: return self.onFileSaveAs() current_window.save() self.statusbar.showMessage( - f"Successfully saved ipygraph at {current_window.savepath}", 2000) + f"Successfully saved ipygraph at {current_window.savepath}", 2000 + ) return True def onFileSaveAs(self) -> bool: - """ Save file in a given directory, caching savepath for quick save. + """Save file in a given directory, caching savepath for quick save. Returns: True if the file was successfully saved, False otherwise. @@ -258,9 +329,8 @@ def onFileSaveAs(self) -> bool: """ current_window = self.activeMdiChild() if current_window is not None: - filename, _ = QFileDialog.getSaveFileName( - self, 'Save ipygraph to file') - if filename == '': + filename, _ = QFileDialog.getSaveFileName(self, "Save ipygraph to file") + if filename == "": return False current_window.savepath = filename self.onFileSave() @@ -269,41 +339,41 @@ def onFileSaveAs(self) -> bool: @staticmethod def is_not_editing(current_window: OCBWidget): - """ True if current_window exists and is not in editing mode. """ - return current_window is not None and not current_window.view.is_mode('EDITING') + """True if current_window exists and is not in editing mode.""" + return current_window is not None and not current_window.view.is_mode("EDITING") def onEditUndo(self): - """ Undo last operation if not in edit mode. """ + """Undo last operation if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.scene.history.undo() def onEditRedo(self): - """ Redo last operation if not in edit mode. """ + """Redo last operation if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.scene.history.redo() def onEditCut(self): - """ Cut the selected items if not in edit mode. """ + """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() def onEditCopy(self): - """ Copy the selected items if not in edit mode. """ + """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() def onEditPaste(self): - """ Paste the selected items if not in edit mode. """ + """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() def onEditDelete(self): - """ Delete the selected items if not in edit mode. """ + """Delete the selected items if not in edit mode.""" current_window = self.activeMdiChild() if self.is_not_editing(current_window): current_window.view.deleteSelected() @@ -316,7 +386,7 @@ def onEditDelete(self): # event.ignore() def closeEvent(self, event: QCloseEvent): - """ Save and quit the application. """ + """Save and quit the application.""" self.mdiArea.closeAllSubWindows() if self.mdiArea.currentSubWindow(): event.ignore() @@ -325,7 +395,7 @@ def closeEvent(self, event: QCloseEvent): event.accept() def maybeSave(self) -> bool: - """ Ask for save and returns if the file should be closed. + """Ask for save and returns if the file should be closed. Returns: True if the file should be closed, False otherwise. @@ -334,13 +404,14 @@ def maybeSave(self) -> bool: if not self.isModified(): return True - answer = QMessageBox.warning(self, "About to loose you work?", - "The file has been modified.\n" - "Do you want to save your changes?", - QMessageBox.StandardButton.Save | - QMessageBox.StandardButton.Discard | - QMessageBox.StandardButton.Cancel - ) + answer = QMessageBox.warning( + self, + "About to loose you work?", + "The file has been modified.\n" "Do you want to save your changes?", + QMessageBox.StandardButton.Save + | QMessageBox.StandardButton.Discard + | QMessageBox.StandardButton.Cancel, + ) if answer == QMessageBox.StandardButton.Save: return self.onFileSave() @@ -349,26 +420,26 @@ def maybeSave(self) -> bool: return False def activeMdiChild(self) -> OCBWidget: - """ Get the active OCBWidget if existing. """ + """Get the active OCBWidget if existing.""" activeSubWindow = self.mdiArea.activeSubWindow() if activeSubWindow is not None: return activeSubWindow.widget() return None def readSettings(self): - settings = QSettings('AutopIA', 'OpenCodeBlocks') - pos = settings.value('pos', QPoint(200, 200)) - size = settings.value('size', QSize(400, 400)) + settings = QSettings("AutopIA", "OpenCodeBlocks") + pos = settings.value("pos", QPoint(200, 200)) + size = settings.value("size", QSize(400, 400)) self.move(pos) self.resize(size) - if settings.value('isMaximized', False) == 'true': + if settings.value("isMaximized", False) == "true": self.showMaximized() def writeSettings(self): - settings = QSettings('AutopIA', 'OpenCodeBlocks') - settings.setValue('pos', self.pos()) - settings.setValue('size', self.size()) - settings.setValue('isMaximized', self.isMaximized()) + settings = QSettings("AutopIA", "OpenCodeBlocks") + settings.setValue("pos", self.pos()) + settings.setValue("size", self.size()) + settings.setValue("isMaximized", self.isMaximized()) def setActiveSubWindow(self, window): if window: diff --git a/opencodeblocks/graphics/worker.py b/opencodeblocks/worker.py similarity index 77% rename from opencodeblocks/graphics/worker.py rename to opencodeblocks/worker.py index 333f451e..5f136d51 100644 --- a/opencodeblocks/graphics/worker.py +++ b/opencodeblocks/worker.py @@ -8,16 +8,17 @@ class WorkerSignals(QObject): - """ Defines the signals available from a running worker thread. """ + """Defines the signals available from a running worker thread.""" + stdout = pyqtSignal(str) image = pyqtSignal(str) class Worker(QRunnable): - """ Worker thread """ + """Worker thread""" def __init__(self, kernel, code): - """ Initialize the worker object. """ + """Initialize the worker object.""" super().__init__() self.kernel = kernel @@ -25,7 +26,7 @@ def __init__(self, kernel, code): self.signals = WorkerSignals() async def run_code(self): - """ Run the code in the block """ + """Run the code in the block""" # Execute the code self.kernel.client.execute(self.code) done = False @@ -34,13 +35,13 @@ async def run_code(self): # Save kernel message and send it to the GUI output, output_type, done = self.kernel.update_output() if done is False: - if output_type == 'text': + if output_type == "text": self.signals.stdout.emit(output) - elif output_type == 'image': + elif output_type == "image": self.signals.image.emit(output) def run(self): - """ Execute the run_code method asynchronously. """ + """Execute the run_code method asynchronously.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(self.run_code()) diff --git a/requirements-dev.txt b/requirements-dev.txt index c49f27fb..889af2ec 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,4 +7,4 @@ pytest-qt pyautogui pylint>=2.12 pylint-pytest -autopep8 \ No newline at end of file +black \ No newline at end of file diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index 3c741b46..36d67986 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -11,18 +11,17 @@ from PyQt5.QtCore import QPointF -from opencodeblocks.graphics.blocks.codeblock import OCBBlock -from opencodeblocks.graphics.window import OCBWindow -from opencodeblocks.graphics.widget import OCBWidget +from opencodeblocks.blocks.codeblock import OCBBlock +from opencodeblocks.window import OCBWindow +from opencodeblocks.widget import OCBWidget from tests.integration.utils import apply_function_inapp, CheckingQueue class TestBlocks: - @pytest.fixture(autouse=True) def setup(self): - """ Setup reused variables. """ + """Setup reused variables.""" self.window = OCBWindow() self.ocb_widget = OCBWidget() self.subwindow = self.window.mdiArea.addSubWindow(self.ocb_widget) @@ -30,21 +29,28 @@ def setup(self): self.block = OCBBlock(title="Testing block") def test_create_blocks(self, qtbot: QtBot): - """ can be added to the scene. """ + """can be added to the scene.""" self.ocb_widget.scene.addItem(self.block) def test_move_blocks(self, qtbot: QtBot): - """ can be dragged around with the mouse. """ + """can be dragged around with the mouse.""" self.ocb_widget.scene.addItem(self.block) + self.ocb_widget.view.horizontalScrollBar().setValue(self.block.x()) + self.ocb_widget.view.verticalScrollBar().setValue( + self.block.y() - self.ocb_widget.view.height() + self.block.height + ) def testing_drag(msgQueue: CheckingQueue): - expected_move_amount = [70, -30] + # put block1 at the bottom left + # This line works because the zoom is 1 by default. + + expected_move_amount = [20, -30] pos_block = QPointF(self.block.pos().x(), self.block.pos().y()) pos_block.setX( - pos_block.x() + self.block.title_height + self.block.edge_size + pos_block.x() + self.block.title_widget.height() + self.block.edge_size ) - pos_block.setY(pos_block.y() + self.block.title_height/2) + pos_block.setY(pos_block.y() + self.block.title_widget.height() / 2) pos_block = self.ocb_widget.view.mapFromScene(pos_block) pos_block = self.ocb_widget.view.mapToGlobal(pos_block) @@ -53,10 +59,10 @@ def testing_drag(msgQueue: CheckingQueue): pyautogui.mouseDown(button="left") iterations = 5 - for i in range(iterations+1): + for i in range(iterations + 1): pyautogui.moveTo( pos_block.x() + expected_move_amount[0] * i / iterations, - pos_block.y() + expected_move_amount[1] * i / iterations + pos_block.y() + expected_move_amount[1] * i / iterations, ) pyautogui.mouseUp(button="left") @@ -67,7 +73,8 @@ def testing_drag(msgQueue: CheckingQueue): move_amount[1] = move_amount[1] * self.ocb_widget.view.zoom msgQueue.check_equal( - move_amount, expected_move_amount, "Block moved by the correct amound") + move_amount, expected_move_amount, "Block moved by the correct amound" + ) msgQueue.stop() apply_function_inapp(self.window, testing_drag) diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index 42f3cd2f..7ea96da0 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -11,29 +11,28 @@ from PyQt5.QtCore import QPointF -from opencodeblocks.graphics.blocks.codeblock import OCBCodeBlock -from opencodeblocks.graphics.window import OCBWindow -from opencodeblocks.graphics.widget import OCBWidget +from opencodeblocks.blocks.codeblock import OCBCodeBlock +from opencodeblocks.window import OCBWindow +from opencodeblocks.widget import OCBWidget from tests.integration.utils import apply_function_inapp, CheckingQueue class TestCodeBlocks: - @pytest.fixture(autouse=True) def setup(self): - """ Setup reused variables. """ + """Setup reused variables.""" self.window = OCBWindow() self.ocb_widget = OCBWidget() self.subwindow = self.window.mdiArea.addSubWindow(self.ocb_widget) self.subwindow.show() def test_run_python(self, qtbot: QtBot): - """ run source code when run button is pressed. """ + """run source code when run button is pressed.""" # Add a block with the source to the window EXPRESSION = "3 + 5 * 2" - SOURCE_TEST = f'''print({EXPRESSION})''' + SOURCE_TEST = f"""print({EXPRESSION})""" expected_result = str(3 + 5 * 2) test_block = OCBCodeBlock(title="CodeBlock test", source=SOURCE_TEST) diff --git a/tests/integration/test_window.py b/tests/integration/test_window.py index 1ab0ecee..a988cd9a 100644 --- a/tests/integration/test_window.py +++ b/tests/integration/test_window.py @@ -9,22 +9,21 @@ import pytest from pytest_mock import MockerFixture -from opencodeblocks.graphics.window import OCBWindow +from opencodeblocks.window import OCBWindow class TestWindow: - @pytest.fixture(autouse=True) def setup(self, mocker: MockerFixture): - """ Setup reused variables. """ + """Setup reused variables.""" self.window = OCBWindow() def test_window_close(self, qtbot): - """ closes """ + """closes""" self.window.close() def test_open_file(self): - """ loads files """ + """loads files""" wnd = OCBWindow() file_example_path = "./tests/assets/example_graph1.ipyg" subwnd = wnd.createNewMdiChild(os.path.abspath(file_example_path)) diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 956a7f7f..d141449e 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -14,14 +14,13 @@ from qtpy.QtWidgets import QApplication import pytest_check as check -from opencodeblocks.graphics.window import OCBWindow +from opencodeblocks.window import OCBWindow STOP_MSG = "stop" CHECK_MSG = "check" class CheckingQueue(Queue): - def check_equal(self, a, b, msg=""): self.put([CHECK_MSG, a, b, msg]) diff --git a/tests/unit/scene/test_clipboard.py b/tests/unit/scene/test_clipboard.py index d5b682e9..d28b2087 100644 --- a/tests/unit/scene/test_clipboard.py +++ b/tests/unit/scene/test_clipboard.py @@ -7,7 +7,7 @@ from pytest_mock import MockerFixture import pytest_check as check -from opencodeblocks.graphics.scene.clipboard import SceneClipboard +from opencodeblocks.scene.clipboard import SceneClipboard class TestSerializeSelected: @@ -15,7 +15,7 @@ class TestSerializeSelected: """SceneClipboard._serializeSelected""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.view = mocker.MagicMock() @@ -44,23 +44,23 @@ def setup(self, mocker:MockerFixture): self.scene.sortedSelectedItems.return_value = self.blocks, self.edges self.clipboard = SceneClipboard(self.scene) - def test_serialize_selected_blocks(self, mocker:MockerFixture): - """ should allow for blocks serialization.""" + def test_serialize_selected_blocks(self, mocker: MockerFixture): + """should allow for blocks serialization.""" data = self.clipboard._serializeSelected() - check.equal(data['blocks'], [block.serialize() for block in self.blocks]) + check.equal(data["blocks"], [block.serialize() for block in self.blocks]) - def test_serialize_selected_edges(self, mocker:MockerFixture): - """ should allow for edges serialization.""" + def test_serialize_selected_edges(self, mocker: MockerFixture): + """should allow for edges serialization.""" data = self.clipboard._serializeSelected() - check.equal(data['edges'], [edge.serialize() for edge in self.edges]) + 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.""" + 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() - check.equal(data['edges'], [self.edges[0].serialize()]) + check.equal(data["edges"], [self.edges[0].serialize()]) - def test_serialize_delete(self, mocker:MockerFixture): - """ should allow for items deletion after serialization.""" + def test_serialize_delete(self, mocker: MockerFixture): + """should allow for items deletion after serialization.""" self.clipboard._serializeSelected(delete=True) check.is_true(self.view.deleteSelected.called) diff --git a/tests/unit/scene/test_function_parsing.py b/tests/unit/scene/test_function_parsing.py index d6e01cad..ab2bfc13 100644 --- a/tests/unit/scene/test_function_parsing.py +++ b/tests/unit/scene/test_function_parsing.py @@ -4,12 +4,15 @@ from pytest_mock import MockerFixture import pytest_check as check -from opencodeblocks.graphics.function_parsing import (find_kwarg_index, run_cell, - get_function_name, - get_signature, - extract_args, - execute_function, - find_kwarg_index) +from opencodeblocks.function_parsing import ( + find_kwarg_index, + run_cell, + get_function_name, + get_signature, + extract_args, + execute_function, + find_kwarg_index, +) class TestFunctionParsing: @@ -17,72 +20,95 @@ class TestFunctionParsing: """Testing function_parsing functions""" def test_run_cell(self, mocker: MockerFixture): - """ Test run_cell """ - check.equal(run_cell("print(10)"), '10\n') + """Test run_cell""" + check.equal(run_cell("print(10)"), "10\n") def test_get_function_name(self, mocker: MockerFixture): - """ Test get_function_name """ - check.equal(get_function_name( - "def function():\n return 'Hello'"), 'function') - check.equal(get_function_name( - "#Hello\ndef function():\n return 'Hello'\na = 10"), 'function') - check.equal(get_function_name( - "#Hello\ndef function(a,b=10):\n return 'Hello'\na = 10"), 'function') + """Test get_function_name""" + check.equal(get_function_name("def function():\n return 'Hello'"), "function") + check.equal( + get_function_name("#Hello\ndef function():\n return 'Hello'\na = 10"), + "function", + ) + check.equal( + get_function_name( + "#Hello\ndef function(a,b=10):\n return 'Hello'\na = 10" + ), + "function", + ) def test_get_function_name_error(self, mocker: MockerFixture): - """ Return ValueError if get_function_name has wrong input """ + """Return ValueError if get_function_name has wrong input""" with pytest.raises(ValueError): get_function_name("") get_function_name("#Hello") get_function_name("def function") def test_get_signature(self, mocker: MockerFixture): - """ Test get_signature """ + """Test get_signature""" mocker.patch( - 'opencodeblocks.graphics.function_parsing.run_cell', return_value="(a, b, c=10)\n") - check.equal(get_signature( - "def function(a,b, c=10):\n return None"), "(a, b, c=10)\n") + "opencodeblocks.function_parsing.run_cell", + return_value="(a, b, c=10)\n", + ) + check.equal( + get_signature("def function(a,b, c=10):\n return None"), "(a, b, c=10)\n" + ) def test_find_kwarg_index(self, mocker: MockerFixture): - """ Test find_kwarg_index """ - check.equal(find_kwarg_index(['a', 'b', 'c=10']), 2) + """Test find_kwarg_index""" + check.equal(find_kwarg_index(["a", "b", "c=10"]), 2) check.equal(find_kwarg_index([]), 0) def test_extract_args(self, mocker: MockerFixture): - """ Test extract_args """ + """Test extract_args""" mocker.patch( - 'opencodeblocks.graphics.function_parsing.get_signature', return_value="()\n") + "opencodeblocks.function_parsing.get_signature", + return_value="()\n", + ) + mocker.patch("opencodeblocks.function_parsing.find_kwarg_index", return_value=0) + check.equal(extract_args("def function():\n return 'Hello'"), ([], [])) mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=0) - check.equal(extract_args( - "def function():\n return 'Hello'"), ([], [])) - mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', - return_value="(a,b,c = 10)\n") - mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=2) - check.equal(extract_args( - "def function(a,b,c = 10):\n return 'Hello'"), (["a", "b"], ["c=10"])) + "opencodeblocks.function_parsing.get_signature", + return_value="(a,b,c = 10)\n", + ) + mocker.patch("opencodeblocks.function_parsing.find_kwarg_index", return_value=2) + check.equal( + extract_args("def function(a,b,c = 10):\n return 'Hello'"), + (["a", "b"], ["c=10"]), + ) def test_extract_args_empty(self, mocker: MockerFixture): - """ Return a couple of empty lists if signature is empty """ - mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', - return_value="()\n") + """Return a couple of empty lists if signature is empty""" + mocker.patch( + "opencodeblocks.function_parsing.get_signature", + return_value="()\n", + ) mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=None) - check.equal(extract_args( - "def function( ):\n return 'Hello'"), ([], [])) - mocker.patch('opencodeblocks.graphics.function_parsing.get_signature', - return_value="()\n") + "opencodeblocks.function_parsing.find_kwarg_index", + return_value=None, + ) + check.equal(extract_args("def function( ):\n return 'Hello'"), ([], [])) mocker.patch( - 'opencodeblocks.graphics.function_parsing.find_kwarg_index', return_value=None) - check.equal(extract_args( - "def function():\n return 'Hello'"), ([], [])) + "opencodeblocks.function_parsing.get_signature", + return_value="()\n", + ) + mocker.patch( + "opencodeblocks.function_parsing.find_kwarg_index", + return_value=None, + ) + check.equal(extract_args("def function():\n return 'Hello'"), ([], [])) def test_execute_function(self, mocker: MockerFixture): - """ Test execute_function """ - mocker.patch('opencodeblocks.graphics.function_parsing.get_function_name', - return_value="function") + """Test execute_function""" + mocker.patch( + "opencodeblocks.function_parsing.get_function_name", + return_value="function", + ) mocker.patch( - 'opencodeblocks.graphics.function_parsing.run_cell', return_value="Out[1]: 25\n") - check.equal(execute_function( - "def function(a,b,c=10):\n return a+b+c", 10, 5), "Out[1]: 25\n") + "opencodeblocks.function_parsing.run_cell", + return_value="Out[1]: 25\n", + ) + check.equal( + execute_function("def function(a,b,c=10):\n return a+b+c", 10, 5), + "Out[1]: 25\n", + ) diff --git a/tests/unit/scene/test_history.py b/tests/unit/scene/test_history.py index 52367d34..30fe0964 100644 --- a/tests/unit/scene/test_history.py +++ b/tests/unit/scene/test_history.py @@ -7,7 +7,7 @@ from pytest_mock import MockerFixture import pytest_check as check -from opencodeblocks.graphics.scene.history import SceneHistory +from opencodeblocks.scene.history import SceneHistory class TestUndo: @@ -15,28 +15,28 @@ class TestUndo: """Undo""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.history = SceneHistory(self.scene, max_stack=6) - self.history.history_stack = ['A', 'B', 'C', 'D'] + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 3 - def test_undo(self, mocker:MockerFixture): - """ should allow for undo without breaking the history stack.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + def test_undo(self, mocker: MockerFixture): + """should allow for undo without breaking the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'D') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "D") self.history.undo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'C') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "C") check.is_true(self.history.restore.called) - def test_undo_nostack(self, mocker:MockerFixture): - """ should allow to undo without any change if the history stack is empty.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + def test_undo_nostack(self, mocker: MockerFixture): + """should allow to undo without any change if the history stack is empty.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.history_stack = [] self.history.current = -1 @@ -47,18 +47,18 @@ def test_undo_nostack(self, mocker:MockerFixture): check.equal(self.history.current, -1) check.is_false(self.history.restore.called) - def test_undo_end_of_stack(self, mocker:MockerFixture): - """ should allow to undo without any change if at the end of the history stack.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + def test_undo_end_of_stack(self, mocker: MockerFixture): + """should allow to undo without any change if at the end of the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.current = 0 - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'A') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "A") self.history.undo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'A') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "A") check.is_false(self.history.restore.called) @@ -67,28 +67,28 @@ class TestRedo: """Redo""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.history = SceneHistory(self.scene, max_stack=6) - self.history.history_stack = ['A', 'B', 'C', 'D'] + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 1 - def test_redo(self, mocker:MockerFixture): - """ should allow for redo without changing the history stack.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + def test_redo(self, mocker: MockerFixture): + """should allow for redo without changing the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'B') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "B") self.history.redo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'C') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "C") check.is_true(self.history.restore.called) - def test_redo_nostack(self, mocker:MockerFixture): - """ should allow to redo without any change if the history stack is empty.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + def test_redo_nostack(self, mocker: MockerFixture): + """should allow to redo without any change if the history stack is empty.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.history_stack = [] self.history.current = -1 @@ -99,18 +99,18 @@ def test_redo_nostack(self, mocker:MockerFixture): check.equal(self.history.current, -1) check.is_false(self.history.restore.called) - def test_redo_end_of_stack(self, mocker:MockerFixture): - """ should allow to redo without any change if at the beggining of the history stack.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + def test_redo_end_of_stack(self, mocker: MockerFixture): + """should allow to redo without any change if at the beggining of the history stack.""" + mocker.patch("opencodeblocks.scene.history.SceneHistory.restore") self.history.current = 3 - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'D') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "D") self.history.redo() - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) - check.equal(self.history.history_stack[self.history.current], 'D') + check.equal(self.history.history_stack, ["A", "B", "C", "D"]) + check.equal(self.history.history_stack[self.history.current], "D") check.is_false(self.history.restore.called) @@ -119,39 +119,37 @@ class TestStore: """Store""" @pytest.fixture(autouse=True) - def setup(self, mocker:MockerFixture): + def setup(self, mocker: MockerFixture): self.scene = mocker.MagicMock() self.history = SceneHistory(self.scene, max_stack=6) - def test_store(self): - """ should update current while storing new data.""" - self.history.history_stack = ['A', 'B', 'C', 'D'] + """should update current while storing new data.""" + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 3 - self.history.store('E') + self.history.store("E") - check.equal(self.history.history_stack, ['A', 'B', 'C', 'D', 'E']) - check.equal(self.history.history_stack[self.history.current], 'E') + check.equal(self.history.history_stack, ["A", "B", "C", "D", "E"]) + check.equal(self.history.history_stack[self.history.current], "E") def test_store_cut(self): - """ should cut upper stack when storing new data.""" - self.history.history_stack = ['A', 'B', 'C', 'D'] + """should cut upper stack when storing new data.""" + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 2 - self.history.store('E') - - check.equal(self.history.history_stack, ['A', 'B', 'C', 'E']) - check.equal(self.history.history_stack[self.history.current], 'E') + self.history.store("E") + check.equal(self.history.history_stack, ["A", "B", "C", "E"]) + check.equal(self.history.history_stack[self.history.current], "E") def test_store_max_stack(self): - """ should forget oldests checkpoint when storing new data at maximum stack size.""" - self.history.history_stack = ['A', 'B', 'C', 'D'] + """should forget oldests checkpoint when storing new data at maximum stack size.""" + self.history.history_stack = ["A", "B", "C", "D"] self.history.current = 3 self.history.max_stack = 4 - self.history.store('E') + self.history.store("E") - check.equal(self.history.history_stack, ['B', 'C', 'D', 'E']) - check.equal(self.history.history_stack[self.history.current], 'E') + check.equal(self.history.history_stack, ["B", "C", "D", "E"]) + check.equal(self.history.history_stack[self.history.current], "E")