diff --git a/README.md b/README.md
index 9164c457..e29c549d 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,15 @@
# OpenCodeBlocks
+[](CONTRIBUTING.md)
[](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml)
[](https://www.codacy.com/gh/MathisFederico/OpenCodeBlocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/OpenCodeBlocks&utm_campaign=Badge_Grade)
-[](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml)
[](https://www.codacy.com/gh/MathisFederico/OpenCodeBlocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/OpenCodeBlocks&utm_campaign=Badge_Coverage)
+[](https://github.com/psf/black)
+[](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml)
[](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml)
[](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml)
[](https://www.gnu.org/licenses/)
-[](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")