diff --git a/.pylintrc b/.pylintrc index a45db43c..c58e25a3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -61,6 +61,7 @@ confidence= # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=inconsistent-return-statements, + unidiomatic-typecheck, attribute-defined-outside-init, pointless-statement, no-self-use, diff --git a/examples/mnist.ipyg b/examples/mnist.ipyg index 35c2b1f9..43b367db 100644 --- a/examples/mnist.ipyg +++ b/examples/mnist.ipyg @@ -9,7 +9,7 @@ "stdout": "Epoch 1/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 10s 4ms/step - loss: 0.2116 - accuracy: 0.9367\nEpoch 2/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 7s 4ms/step - loss: 0.0853 - accuracy: 0.9738\nEpoch 3/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 7s 4ms/step - loss: 0.0597 - accuracy: 0.9813\nEpoch 4/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 7s 4ms/step - loss: 0.0472 - accuracy: 0.9848\n", "splitter_pos": [ 85, - 259 + 261 ], "position": [ 1062.374999999999, @@ -21,7 +21,7 @@ "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, + "size": 12, "padding": 4.0 } }, @@ -31,7 +31,7 @@ "type": "input", "position": [ 0.0, - 42.0 + 40.0 ], "metadata": { "color": "#e02c2c", @@ -45,7 +45,7 @@ "type": "output", "position": [ 1064.0, - 42.0 + 40.0 ], "metadata": { "color": "#35bc31", @@ -64,7 +64,7 @@ "stdout": "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\n", "splitter_pos": [ 0, - 278 + 280 ], "position": [ 2330.066406249998, @@ -76,7 +76,7 @@ "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, + "size": 12, "padding": 4.0 } }, @@ -86,7 +86,7 @@ "type": "input", "position": [ 0.0, - 42.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -104,8 +104,8 @@ "source": "metrics = model.evaluate(x_test, y_test)\r\nprint(f\"mean_loss:{metrics[0]:.2f}, mean_acc:{metrics[1]:.2f}\")\r\n", "stdout": "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r313/313 [==============================] - 1s 2ms/step - loss: 0.0552 - accuracy: 0.9823\nmean_loss:0.06, mean_acc:0.98\n", "splitter_pos": [ - 76, - 76 + 77, + 77 ], "position": [ 2295.17578125, @@ -117,7 +117,7 @@ "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, + "size": 12, "padding": 4.0 } }, @@ -127,7 +127,7 @@ "type": "input", "position": [ 0.0, - 42.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -145,7 +145,7 @@ "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, + 88, 0 ], "position": [ @@ -158,7 +158,7 @@ "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, + "size": 12, "padding": 4.0 } }, @@ -168,7 +168,7 @@ "type": "output", "position": [ 850.0, - 42.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -186,8 +186,8 @@ "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, + 208 ], "position": [ 44.48828125000068, @@ -199,7 +199,7 @@ "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, + "size": 12, "padding": 4.0 } }, @@ -209,7 +209,7 @@ "type": "input", "position": [ 0.0, - 42.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -223,7 +223,7 @@ "type": "output", "position": [ 855.0, - 42.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -241,7 +241,7 @@ "source": "import tensorflow as tf\r\nfrom tensorflow.keras.layers import (Dense, Flatten,\r\nConv2D, MaxPooling2D, Dropout)\r\nfrom tensorflow.keras.models import Sequential\r\n\r\nmodel = Sequential()\r\nmodel.add(Conv2D(28, kernel_size=(3,3), input_shape=x_train.shape[1:]))\r\nmodel.add(MaxPooling2D(pool_size=(2, 2)))\r\nmodel.add(Flatten())\r\nmodel.add(Dense(128, activation=tf.nn.relu))\r\nmodel.add(Dropout(0.2))\r\nmodel.add(Dense(10,activation=tf.nn.softmax))\r\n\r\nmodel.compile(optimizer='adam', \r\n loss='sparse_categorical_crossentropy', \r\n metrics=['accuracy'])\r\n", "stdout": "", "splitter_pos": [ - 418, + 420, 0 ], "position": [ @@ -254,7 +254,7 @@ "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, + "size": 12, "padding": 4.0 } }, @@ -264,7 +264,7 @@ "type": "output", "position": [ 1002.0, - 42.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", @@ -277,25 +277,25 @@ }, { "id": 2828158533848, - "title": "Plot Image Dataset Example", + "title": "Plot Dataset Example", "block_type": "code", "source": "import matplotlib.pyplot as plt\r\nimport numpy as np\r\n\r\n# Display an example from the dataset\r\nrd_index = np.random.randint(len(x_train))\r\nplt.imshow(x_train[rd_index], cmap='gray')\r\nplt.title('Class '+ str(y_train[rd_index]))\r\n", "stdout": "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\n", "splitter_pos": [ 0, - 274 + 284 ], "position": [ - 103.60937500000011, - -734.9375 + 95.60937500000011, + -728.9375 ], "width": 300, - "height": 329, + "height": 337, "metadata": { "title_metadata": { "color": "white", "font": "Ubuntu", - "size": 10, + "size": 12, "padding": 4.0 } }, @@ -305,7 +305,7 @@ "type": "input", "position": [ 0.0, - 42.0 + 40.0 ], "metadata": { "color": "#FF55FFF0", diff --git a/opencodeblocks/graphics/blocks/__init__.py b/opencodeblocks/blocks/__init__.py similarity index 67% rename from opencodeblocks/graphics/blocks/__init__.py rename to opencodeblocks/blocks/__init__.py index 893604f8..f88fde65 100644 --- a/opencodeblocks/graphics/blocks/__init__.py +++ b/opencodeblocks/blocks/__init__.py @@ -3,8 +3,8 @@ """ 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, diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py new file mode 100644 index 00000000..e6f3384a --- /dev/null +++ b/opencodeblocks/blocks/block.py @@ -0,0 +1,317 @@ +# OpenCodeBlock an open-source tool for modular visual programing in python +# Copyright (C) 2021 Mathïs FEDERICO +# pylint:disable=unused-argument + +""" 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.core.serializable import Serializable +from opencodeblocks.graphics.socket import OCBSocket +from opencodeblocks.blocks.widgets import OCBSplitter, OCBSizeGrip, OCBTitle + +if TYPE_CHECKING: + from opencodeblocks.graphics.scene.scene import OCBScene + +BACKGROUND_COLOR = QColor("#E3212121") + + +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(BACKGROUND_COLOR) + + 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 = OCBTitle(title, parent=self.root) + self.title_widget.setAttribute(Qt.WA_TranslucentBackground) + + self.splitter = OCBSplitter(self, Qt.Vertical, self.root) + + self.size_grip = OCBSizeGrip(self, self.root) + + 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 = {} + + 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, + widget: Optional[QWidget] = None, + ): + """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_splitter(self): + """ Change the geometry of the splitter to match the block """ + # 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_title(self): + """ Change the geometry of the title to match the block """ + self.title_widget.setGeometry( + int(self.edge_size), + int(self.edge_size / 2), + int(self.width - self.edge_size * 3), + int(self.title_widget.height()), + ) + + def update_size_grip(self): + """ Change the geometry of the size grip to match the block""" + 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_all(self): + """ Update sockets and title.""" + self.update_sockets() + self.update_splitter() + self.update_title() + self.update_size_grip() + + @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: + """ Return a serialized version of this widget """ + self.metadata.update({"title_metadata": self.title_widget.serialize()}) + 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: + """ Restore the block from serialized data """ + 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.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 75% rename from opencodeblocks/graphics/blocks/codeblock.py rename to opencodeblocks/blocks/codeblock.py index aba18bb7..d77a8ce1 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -9,7 +9,7 @@ from ansi2html import Ansi2HTMLConverter -from opencodeblocks.graphics.blocks.block import OCBBlock +from opencodeblocks.blocks.block import OCBBlock from opencodeblocks.graphics.pyeditor import PythonEditor from opencodeblocks.graphics.worker import Worker @@ -29,10 +29,13 @@ class OCBCodeBlock(OCBBlock): """ def __init__(self, **kwargs): - + """ + Create a new OCBCodeBlock. + Initialize all the child widgets specific to this block type + """ 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 +58,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.move(int(self.edge_size), int(self.edge_size / 2)) + run_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) run_button.clicked.connect(self.run_code) - run_button.raise_() return run_button def run_code(self): @@ -81,26 +84,29 @@ 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): + self.title_widget.setGeometry( + int(self.edge_size) + self.run_button.width(), + int(self.edge_size / 2), + int(self.width - self.edge_size * 3 - self.run_button.width()), + int(self.title_widget.height()), + ) + def update_output_panel(self): # 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 +120,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 +144,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 +167,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..d62d3431 --- /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.blocksplitter import OCBSplitter +from opencodeblocks.blocks.widgets.blocktitle import OCBTitle +from opencodeblocks.blocks.widgets.blocksizegrip import OCBSizeGrip diff --git a/opencodeblocks/graphics/blocks/blocksizegrip.py b/opencodeblocks/blocks/widgets/blocksizegrip.py similarity index 98% rename from opencodeblocks/graphics/blocks/blocksizegrip.py rename to opencodeblocks/blocks/widgets/blocksizegrip.py index 2c8eb4ed..c08ee4b9 100644 --- a/opencodeblocks/graphics/blocks/blocksizegrip.py +++ b/opencodeblocks/blocks/widgets/blocksizegrip.py @@ -11,7 +11,7 @@ from PyQt5.QtGui import QMouseEvent -class BlockSizeGrip(QSizeGrip): +class OCBSizeGrip(QSizeGrip): """ A grip to resize a block """ def __init__(self, block: QGraphicsItem, parent: QWidget = None): diff --git a/opencodeblocks/blocks/widgets/blocksplitter.py b/opencodeblocks/blocks/widgets/blocksplitter.py new file mode 100644 index 00000000..31137fb0 --- /dev/null +++ b/opencodeblocks/blocks/widgets/blocksplitter.py @@ -0,0 +1,31 @@ +""" +Module defining a Splitter, the widget that contains multiple areas inside +a block and allows the user to resize those areas. +""" + +from PyQt5.QtGui import QMouseEvent +from PyQt5.QtWidgets import QSplitter, QSplitterHandle, QWidget + + +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: QWidget, 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) \ No newline at end of file diff --git a/opencodeblocks/blocks/widgets/blocktitle.py b/opencodeblocks/blocks/widgets/blocktitle.py new file mode 100644 index 00000000..95c1fd3a --- /dev/null +++ b/opencodeblocks/blocks/widgets/blocktitle.py @@ -0,0 +1,92 @@ +# pylint:disable=unused-argument +""" +Module defining the widget for the title of blocks. +It's a QLineEdit modified so that editing it requires a double click. +""" + +import time +from typing import OrderedDict +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent +from PyQt5.QtWidgets import QLineEdit, QWidget + +from opencodeblocks.core.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, + parent: QWidget = None, + ): + """ Create a new title for an OCBBlock """ + Serializable.__init__(self) + QLineEdit.__init__(self, text, parent) + self.clickTime = None + self.init_ui(color, font, size) + self.setReadOnly(True) + self.setCursorPosition(0) + + def init_ui(self, color: str, font: str, size: int): + """ Apply the style given to the title """ + self.color = color + self.setStyleSheet( + f""" + QLineEdit {{ + color : {self.color}; + background-color: transparent; + border:none; + }}""" + ) + self.setFont(QFont(font, size)) + + 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.setCursorPosition(0) + 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.MouseFocusReason) + + def serialize(self) -> OrderedDict: + """ Return a serialized version of this widget """ + return OrderedDict( + [ + ("color", self.color), + ("font", self.font().family()), + ("size", self.font().pointSize()), + ] + ) + + def deserialize( + self, data: OrderedDict, hashmap: dict = None, restore_id=True + ): + """ Restore a title from serialized data """ + if restore_id: + self.id = data.get("id", id(self)) + self.init_ui(data["color"], data["font"], data["size"]) 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/pyeditor.py b/opencodeblocks/graphics/pyeditor.py index 0ab39a94..d7db6b62 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor.py @@ -9,7 +9,7 @@ from PyQt5.Qsci import QsciScintilla, QsciLexerPython from opencodeblocks.graphics.theme_manager import theme_manager -from opencodeblocks.graphics.blocks.block import OCBBlock +from opencodeblocks.blocks.block import OCBBlock from opencodeblocks.graphics.kernel import Kernel kernel = Kernel() diff --git a/opencodeblocks/graphics/socket.py b/opencodeblocks/graphics/socket.py index 484ad501..d4fbd152 100644 --- a/opencodeblocks/graphics/socket.py +++ b/opencodeblocks/graphics/socket.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from opencodeblocks.graphics.edge import OCBEdge - from opencodeblocks.graphics.blocks.block import OCBBlock + from opencodeblocks.blocks.block import OCBBlock class OCBSocket(QGraphicsItem, Serializable): diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index c76ffd91..be48822d 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -13,10 +13,10 @@ from PyQt5.sip import isdeleted -from opencodeblocks.graphics.scene import OCBScene +from opencodeblocks.scene import OCBScene from opencodeblocks.graphics.socket import OCBSocket from opencodeblocks.graphics.edge import OCBEdge -from opencodeblocks.graphics.blocks import OCBBlock +from opencodeblocks.blocks import OCBBlock class OCBView(QGraphicsView): diff --git a/opencodeblocks/graphics/widget.py b/opencodeblocks/graphics/widget.py index 761b5ebf..0a847e04 100644 --- a/opencodeblocks/graphics/widget.py +++ b/opencodeblocks/graphics/widget.py @@ -8,7 +8,7 @@ from PyQt5.QtWidgets import QVBoxLayout, QWidget from PyQt5.QtCore import Qt -from opencodeblocks.graphics.scene import OCBScene +from opencodeblocks.scene import OCBScene from opencodeblocks.graphics.view import OCBView diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index d50f1b7e..e344ea70 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -13,7 +13,7 @@ from opencodeblocks.graphics.widget import OCBWidget from opencodeblocks.graphics.theme_manager import theme_manager -from opencodeblocks.graphics.qss import loadStylesheets +from opencodeblocks.qss import loadStylesheets class OCBWindow(QMainWindow): @@ -24,9 +24,9 @@ def __init__(self): super().__init__() self.stylesheet_filename = os.path.join( - os.path.dirname(__file__), 'qss', 'ocb.qss') + os.path.dirname(__file__),'..', 'qss', 'ocb.qss') loadStylesheets(( - os.path.join(os.path.dirname(__file__), 'qss', 'ocb_dark.qss'), + os.path.join(os.path.dirname(__file__),'..', 'qss', 'ocb_dark.qss'), self.stylesheet_filename )) diff --git a/opencodeblocks/graphics/qss/__init__.py b/opencodeblocks/qss/__init__.py similarity index 91% rename from opencodeblocks/graphics/qss/__init__.py rename to opencodeblocks/qss/__init__.py index a44a510c..16024f9c 100644 --- a/opencodeblocks/graphics/qss/__init__.py +++ b/opencodeblocks/qss/__init__.py @@ -8,7 +8,7 @@ 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]): 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 98% rename from opencodeblocks/graphics/scene/clipboard.py rename to opencodeblocks/scene/clipboard.py index c8e02fd6..9e6290fd 100644 --- a/opencodeblocks/graphics/scene/clipboard.py +++ b/opencodeblocks/scene/clipboard.py @@ -9,7 +9,7 @@ import json from PyQt5.QtWidgets import QApplication -from opencodeblocks.graphics.blocks import OCBBlock, OCBCodeBlock +from opencodeblocks.blocks import OCBBlock, OCBCodeBlock from opencodeblocks.graphics.edge import OCBEdge if TYPE_CHECKING: diff --git a/opencodeblocks/graphics/scene/history.py b/opencodeblocks/scene/history.py similarity index 97% rename from opencodeblocks/graphics/scene/history.py rename to opencodeblocks/scene/history.py index a6baabe0..9d82a8a1 100644 --- a/opencodeblocks/graphics/scene/history.py +++ b/opencodeblocks/scene/history.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from opencodeblocks.graphics.scene import OCBScene + from opencodeblocks.scene import OCBScene class SceneHistory(): diff --git a/opencodeblocks/graphics/scene/scene.py b/opencodeblocks/scene/scene.py similarity index 96% rename from opencodeblocks/graphics/scene/scene.py rename to opencodeblocks/scene/scene.py index fa99e84b..772ba2b2 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -13,11 +13,11 @@ 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.blocks.block import OCBBlock +from opencodeblocks.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.scene.clipboard import SceneClipboard +from opencodeblocks.scene.history import SceneHistory class OCBScene(QGraphicsScene, Serializable): diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index 3c741b46..02e6253e 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -11,7 +11,7 @@ from PyQt5.QtCore import QPointF -from opencodeblocks.graphics.blocks.codeblock import OCBBlock +from opencodeblocks.blocks.codeblock import OCBBlock from opencodeblocks.graphics.window import OCBWindow from opencodeblocks.graphics.widget import OCBWidget @@ -19,10 +19,9 @@ 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..7ab6b67b 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -11,7 +11,7 @@ from PyQt5.QtCore import QPointF -from opencodeblocks.graphics.blocks.codeblock import OCBCodeBlock +from opencodeblocks.blocks.codeblock import OCBCodeBlock from opencodeblocks.graphics.window import OCBWindow from opencodeblocks.graphics.widget import OCBWidget diff --git a/tests/unit/scene/test_clipboard.py b/tests/unit/scene/test_clipboard.py index d5b682e9..99739f20 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: diff --git a/tests/unit/scene/test_history.py b/tests/unit/scene/test_history.py index 52367d34..756e39be 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: @@ -23,7 +23,7 @@ def setup(self, mocker:MockerFixture): def test_undo(self, mocker:MockerFixture): """ should allow for undo without breaking the history stack.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + 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') @@ -36,7 +36,7 @@ def test_undo(self, mocker:MockerFixture): 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') + mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') self.history.history_stack = [] self.history.current = -1 @@ -49,7 +49,7 @@ def test_undo_nostack(self, mocker:MockerFixture): 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') + mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') self.history.current = 0 check.equal(self.history.history_stack, ['A', 'B', 'C', 'D']) @@ -75,7 +75,7 @@ def setup(self, mocker:MockerFixture): def test_redo(self, mocker:MockerFixture): """ should allow for redo without changing the history stack.""" - mocker.patch('opencodeblocks.graphics.scene.history.SceneHistory.restore') + 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') @@ -88,7 +88,7 @@ def test_redo(self, mocker:MockerFixture): 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') + mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') self.history.history_stack = [] self.history.current = -1 @@ -101,7 +101,7 @@ def test_redo_nostack(self, mocker:MockerFixture): 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') + mocker.patch('opencodeblocks.scene.history.SceneHistory.restore') self.history.current = 3 check.equal(self.history.history_stack, ['A', 'B', 'C', 'D'])