From 83d955cdf5270e82fb4ee237894ee802d9293e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Sun, 28 Nov 2021 23:27:50 +0100 Subject: [PATCH 01/19] :tada: The title of the blocks is editable by clicking on it. Changed the behavior of the move test so that the mouse clicks further right outside the title area. --- opencodeblocks/graphics/blocks/block.py | 33 ++++++++++++++----------- tests/integration/test_blocks.py | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 0d13dd0f..26398a40 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -8,7 +8,7 @@ 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, \ + QGraphicsSceneMouseEvent, QLineEdit, QSplitter, QSplitterHandle, \ QStyleOptionGraphicsItem, QWidget from opencodeblocks.core.serializable import Serializable @@ -18,6 +18,7 @@ if TYPE_CHECKING: from opencodeblocks.graphics.scene.scene import OCBScene +BACKGROUND_COLOR = QColor("#E3212121") class OCBBlock(QGraphicsItem, Serializable): @@ -55,15 +56,12 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = self.sockets_in = [] self.sockets_out = [] - self.title_height = 3 * title_size - self.title = title + self.title_height = 3.5 * title_size 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._brush_background = QBrush(BACKGROUND_COLOR) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) @@ -79,8 +77,8 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = int(height) ) - self.title_widget = QLabel(self.title, self.root) - self.title_widget.setAttribute(Qt.WA_TransparentForMouseEvents) + self.title_widget = QLineEdit(title, self.root) + # self.title_widget.setAttribute(Qt.WA_TransparentForMouseEvents) self.title_widget.setAttribute(Qt.WA_TranslucentBackground) self.setTitleGraphics( title_color, @@ -133,8 +131,16 @@ def setTitleGraphics(self, color: str, font: str, padding: title padding. """ - self.title_widget.setMargin(int(padding)) - self.title_widget.setStyleSheet(f"QLabel {{ color : {color} }}") + # self.title_widget.setMargin(int(padding)) + self.title_widget.setStyleSheet( + f""" + QLineEdit {{ + color : {color}; + background-color: #E3212121; + border:none; + padding: {padding}px; + }}""" + ) self.title_widget.setFont(QFont(font, size)) def paint(self, painter: QPainter, @@ -247,7 +253,7 @@ def update_all(self): 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.width / 3), int(self.title_height) ) self.size_grip.setGeometry( @@ -260,13 +266,12 @@ def update_all(self): @property def title(self): """ Block title. """ - return self._title + return self.title_widget.text() @title.setter def title(self, value: str): - self._title = value if hasattr(self, 'title_widget'): - self.title_widget.setText(self._title) + self.title_widget.setText(value) @property def width(self): diff --git a/tests/integration/test_blocks.py b/tests/integration/test_blocks.py index 449f7763..d9273c74 100644 --- a/tests/integration/test_blocks.py +++ b/tests/integration/test_blocks.py @@ -55,7 +55,7 @@ def testing_drag(msgQueue): pos_block = QPointF(self.block1.pos().x(), self.block1.pos().y()) pos_block.setX( - pos_block.x() + self.block1.title_height + self.block1.edge_size + pos_block.x() + self.block1.width - self.block1.edge_size * 2 ) pos_block.setY(pos_block.y() + self.block1.title_height/2) From 08eca81b09f822c6b57ff81067fc1a4f10cea25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 00:02:25 +0100 Subject: [PATCH 02/19] :umbrella: Fix the move test by making the cursor inside the window. Reduced the move amount to prevent failure due to moving the block outside the window. The goal of this test is to check that the block moves at the same pace as the mouse --- tests/integration/test_blocks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_blocks.py b/tests/integration/test_blocks.py index d9273c74..6dc368e5 100644 --- a/tests/integration/test_blocks.py +++ b/tests/integration/test_blocks.py @@ -45,7 +45,7 @@ def test_move_blocks(self, qtbot): QApplication.processEvents() - expected_move_amount = [70, -30] + expected_move_amount = [-20, -30] STOP_MSG = "stop" CHECK_MSG = "check" @@ -55,7 +55,7 @@ def testing_drag(msgQueue): pos_block = QPointF(self.block1.pos().x(), self.block1.pos().y()) pos_block.setX( - pos_block.x() + self.block1.width - self.block1.edge_size * 2 + pos_block.x() + self.block1.width - self.block1.edge_size * 3 ) pos_block.setY(pos_block.y() + self.block1.title_height/2) From 1919a99adc4864832351d374dbed7f16c2f185c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 00:08:53 +0100 Subject: [PATCH 03/19] :umbrella: More work to make the move test work --- tests/integration/test_blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_blocks.py b/tests/integration/test_blocks.py index 6dc368e5..4ab691b3 100644 --- a/tests/integration/test_blocks.py +++ b/tests/integration/test_blocks.py @@ -45,7 +45,7 @@ def test_move_blocks(self, qtbot): QApplication.processEvents() - expected_move_amount = [-20, -30] + expected_move_amount = [20, -30] STOP_MSG = "stop" CHECK_MSG = "check" From 31370ed8198a30114d4f9198cd3836b5f09e28b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 00:12:39 +0100 Subject: [PATCH 04/19] :umbrella: Narrowing down the issue --- tests/integration/test_blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_blocks.py b/tests/integration/test_blocks.py index 4ab691b3..1f343e1b 100644 --- a/tests/integration/test_blocks.py +++ b/tests/integration/test_blocks.py @@ -55,7 +55,7 @@ def testing_drag(msgQueue): pos_block = QPointF(self.block1.pos().x(), self.block1.pos().y()) pos_block.setX( - pos_block.x() + self.block1.width - self.block1.edge_size * 3 + pos_block.x() + self.block1.width/2 ) pos_block.setY(pos_block.y() + self.block1.title_height/2) From be288e0c7f7f4916bd4ef7f9095162edba135d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 00:21:32 +0100 Subject: [PATCH 05/19] :beetle: Block is now centered on screen. --- tests/integration/test_blocks.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/integration/test_blocks.py b/tests/integration/test_blocks.py index 1f343e1b..c6122f36 100644 --- a/tests/integration/test_blocks.py +++ b/tests/integration/test_blocks.py @@ -43,6 +43,13 @@ def test_move_blocks(self, qtbot): self.ocb_widget.scene.addItem(self.block1) self.subwindow.show() + # put block1 at the bottom left + # This line works because the zoom is 1 by default. + self.ocb_widget.view.horizontalScrollBar().setValue(self.block1.x()) + self.ocb_widget.view.verticalScrollBar().setValue( + self.block1.y() - self.ocb_widget.view.height() + self.block1.height + ) + QApplication.processEvents() expected_move_amount = [20, -30] From 9476fb77013188e6db59b10cc0d219c53c9b830f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 00:50:10 +0100 Subject: [PATCH 06/19] :tada: The title is wider --- opencodeblocks/graphics/blocks/block.py | 2 +- tests/integration/test_blocks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 26398a40..d83b554a 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -253,7 +253,7 @@ def update_all(self): self.title_widget.setGeometry( int(self.edge_size + self.title_left_offset), int(self.edge_size / 2), - int(self.width / 3), + int(self.width / 2), int(self.title_height) ) self.size_grip.setGeometry( diff --git a/tests/integration/test_blocks.py b/tests/integration/test_blocks.py index c6122f36..9f44fcf3 100644 --- a/tests/integration/test_blocks.py +++ b/tests/integration/test_blocks.py @@ -62,7 +62,7 @@ def testing_drag(msgQueue): pos_block = QPointF(self.block1.pos().x(), self.block1.pos().y()) pos_block.setX( - pos_block.x() + self.block1.width/2 + pos_block.x() + self.block1.width*2/3 ) pos_block.setY(pos_block.y() + self.block1.title_height/2) From 7939e5aecf7128dee08aeb529ad476b7b9de24fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 01:30:24 +0100 Subject: [PATCH 07/19] :tada: To edit the title a double click is required. The dragging test fails for unknown reasons. --- opencodeblocks/graphics/blocks/block.py | 40 ++++++++++++++++++++++--- tests/integration/test_blocks.py | 2 +- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index d83b554a..86b4dd16 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -5,8 +5,10 @@ from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple +import time + from PyQt5.QtCore import QPointF, QRectF, Qt -from PyQt5.QtGui import QBrush, QMouseEvent, QPen, QColor, QFont, QPainter, QPainterPath +from PyQt5.QtGui import QBrush, QFocusEvent, QMouseEvent, QPen, QColor, QFont, QPainter, QPainterPath from PyQt5.QtWidgets import QGraphicsItem, QGraphicsProxyWidget, \ QGraphicsSceneMouseEvent, QLineEdit, QSplitter, QSplitterHandle, \ QStyleOptionGraphicsItem, QWidget @@ -20,6 +22,35 @@ BACKGROUND_COLOR = QColor("#E3212121") + +class OCBTitle(QLineEdit): + """ The title of an OCBBlock. Needs to be double clicked to interact """ + + def __init__(self, content: str, parent: QWidget = None): + """ Create a new title for an OCBBlock """ + super().__init__(content, parent) + self.clickTime = None + self.setReadOnly(True) + + def mousePressEvent(self, event: QMouseEvent): + if self.clickTime is not None and self.isReadOnly() and time.time() - self.clickTime > 0.3: + self.parent().mousePressEvent(event) + else: + self.mouseDoubleClickEvent(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.MouseFocusReason) + + class OCBBlock(QGraphicsItem, Serializable): """ Base class for blocks in OpenCodeBlocks. """ @@ -77,7 +108,7 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = int(height) ) - self.title_widget = QLineEdit(title, self.root) + self.title_widget = OCBTitle(title, self.root) # self.title_widget.setAttribute(Qt.WA_TransparentForMouseEvents) self.title_widget.setAttribute(Qt.WA_TranslucentBackground) self.setTitleGraphics( @@ -91,7 +122,8 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = self.size_grip = BlockSizeGrip(self, self.root) - if type(self) == OCBBlock: # DO NOT TRUST codacy !!! isinstance != type + if isinstance( + 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) @@ -253,7 +285,7 @@ def update_all(self): self.title_widget.setGeometry( int(self.edge_size + self.title_left_offset), int(self.edge_size / 2), - int(self.width / 2), + int(self.width - self.edge_size * 3), int(self.title_height) ) self.size_grip.setGeometry( diff --git a/tests/integration/test_blocks.py b/tests/integration/test_blocks.py index 9f44fcf3..632d7b33 100644 --- a/tests/integration/test_blocks.py +++ b/tests/integration/test_blocks.py @@ -62,7 +62,7 @@ def testing_drag(msgQueue): pos_block = QPointF(self.block1.pos().x(), self.block1.pos().y()) pos_block.setX( - pos_block.x() + self.block1.width*2/3 + pos_block.x() + self.block1.title_height + self.block1.edge_size ) pos_block.setY(pos_block.y() + self.block1.title_height/2) From 60580db16a1b5aa63ca4a6ddc7fc7bc1cca13f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 02:00:15 +0100 Subject: [PATCH 08/19] :beetle: Fix issue with test. A boolean expression was previously incorrect --- opencodeblocks/graphics/blocks/block.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 86b4dd16..d9100d2c 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -8,7 +8,8 @@ import time from PyQt5.QtCore import QPointF, QRectF, Qt -from PyQt5.QtGui import QBrush, QFocusEvent, QMouseEvent, QPen, QColor, QFont, QPainter, QPainterPath +from PyQt5.QtGui import QBrush, QFocusEvent, QMouseEvent, QPen, QColor, QFont, \ + QPainter, QPainterPath from PyQt5.QtWidgets import QGraphicsItem, QGraphicsProxyWidget, \ QGraphicsSceneMouseEvent, QLineEdit, QSplitter, QSplitterHandle, \ QStyleOptionGraphicsItem, QWidget @@ -33,7 +34,12 @@ def __init__(self, content: str, parent: QWidget = None): self.setReadOnly(True) def mousePressEvent(self, event: QMouseEvent): - if self.clickTime is not None and self.isReadOnly() and time.time() - self.clickTime > 0.3: + """ + 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) else: self.mouseDoubleClickEvent(event) From e7f14bd8c69f1fc436e6059a7a155c3e26302740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 02:08:30 +0100 Subject: [PATCH 09/19] :beetle: The cursor moves at the correct position when clicking the title of a block --- opencodeblocks/graphics/blocks/block.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index d9100d2c..c98fece6 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -41,8 +41,11 @@ def mousePressEvent(self, event: QMouseEvent): if self.clickTime is None or ( self.isReadOnly() and time.time() - self.clickTime > 0.3): self.parent().mousePressEvent(event) - else: + elif self.isReadOnly(): self.mouseDoubleClickEvent(event) + super().mousePressEvent(event) + else: + super().mousePressEvent(event) self.clickTime = time.time() def focusOutEvent(self, event: QFocusEvent): From 8150366e93a54785fc8327bd93fe35cbeea735d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 02:16:14 +0100 Subject: [PATCH 10/19] :beetle: This line seems to change without any reason. I will investigate this. Can Codacy edit it automatically ? --- opencodeblocks/graphics/blocks/block.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index c98fece6..4b09db29 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -131,8 +131,7 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = self.size_grip = BlockSizeGrip(self, self.root) - if isinstance( - self, OCBBlock): # DO NOT TRUST codacy !!! isinstance != type + if type(self) == OCBBlock: # DO NOT TRUST codacy !!! type(self) should be used, not isinstance. # This has to be called at the end of the constructor of # every class inheriting this. self.holder.setWidget(self.root) From 9cca4521780faa3d4e5b4638f4ac0f9fea071b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Del=C3=A8gue?= Date: Mon, 29 Nov 2021 21:25:09 +0100 Subject: [PATCH 11/19] Remove comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathïs Fédérico <60117466+MathisFederico@users.noreply.github.com> --- opencodeblocks/graphics/blocks/block.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 4b09db29..a99ae9ef 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -118,7 +118,6 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = ) self.title_widget = OCBTitle(title, self.root) - # self.title_widget.setAttribute(Qt.WA_TransparentForMouseEvents) self.title_widget.setAttribute(Qt.WA_TranslucentBackground) self.setTitleGraphics( title_color, From 5eb5c5f19ad4a990c8c545be40b4fef45c0ed319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= <60117466+MathisFederico@users.noreply.github.com> Date: Tue, 30 Nov 2021 14:14:10 +0100 Subject: [PATCH 12/19] :wrench: Make titles background transparent --- opencodeblocks/graphics/blocks/block.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 74b4c0ec..778820f2 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -174,7 +174,7 @@ def setTitleGraphics(self, color: str, font: str, f""" QLineEdit {{ color : {color}; - background-color: #E3212121; + background-color: transparent; border:none; padding: {padding}px; }}""" From d408cf4212531e668ee4eb799ef952822772bfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 14:57:19 +0100 Subject: [PATCH 13/19] :hammer: Refactor OCBTitle & OCBBlock Structured Blocks widgets --- opencodeblocks/graphics/blocks/block.py | 192 ++++-------------- .../graphics/blocks/widgets/__init__.py | 7 + .../graphics/blocks/widgets/splitter.py | 36 ++++ .../graphics/blocks/widgets/title.py | 79 +++++++ tests/integration/blocks/test_block.py | 4 +- 5 files changed, 169 insertions(+), 149 deletions(-) create mode 100644 opencodeblocks/graphics/blocks/widgets/__init__.py create mode 100644 opencodeblocks/graphics/blocks/widgets/splitter.py create mode 100644 opencodeblocks/graphics/blocks/widgets/title.py diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 74b4c0ec..01f583ae 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -3,71 +3,33 @@ """ Module for the base OCB Block. """ -from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple - -import time +from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple, Union from PyQt5.QtCore import QPointF, QRectF, Qt -from PyQt5.QtGui import QBrush, QFocusEvent, QMouseEvent, QPen, QColor, QFont, \ - QPainter, QPainterPath +from PyQt5.QtGui import QBrush, QPen, QColor, \ + QPainter, QPainterPath from PyQt5.QtWidgets import QGraphicsItem, QGraphicsProxyWidget, \ - QGraphicsSceneMouseEvent, QLineEdit, QSplitter, QSplitterHandle, \ - QStyleOptionGraphicsItem, QWidget + QGraphicsSceneMouseEvent, QStyleOptionGraphicsItem, QWidget from opencodeblocks.core.serializable import Serializable from opencodeblocks.graphics.socket import OCBSocket from opencodeblocks.graphics.blocks.blocksizegrip import BlockSizeGrip +from opencodeblocks.graphics.blocks.widgets import OCBTitle, OCBSplitter if TYPE_CHECKING: from opencodeblocks.graphics.scene.scene import OCBScene -BACKGROUND_COLOR = QColor("#E3212121") - - -class OCBTitle(QLineEdit): - """ The title of an OCBBlock. Needs to be double clicked to interact """ - - def __init__(self, content: str, parent: QWidget = None): - """ Create a new title for an OCBBlock """ - super().__init__(content, parent) - self.clickTime = None - self.setReadOnly(True) - - 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.MouseFocusReason) - 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): + 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: @@ -77,11 +39,6 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = 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. """ @@ -95,12 +52,9 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = self.sockets_in = [] self.sockets_out = [] - self.title_height = 3.5 * title_size - self.title_left_offset = 0 - self._pen_outline = QPen(QColor("#7F000000")) self._pen_outline_selected = QPen(QColor("#FFFFA637")) - self._brush_background = QBrush(BACKGROUND_COLOR) + self._brush_background = QBrush(QColor("#E3212121")) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) @@ -109,27 +63,27 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = self.holder = QGraphicsProxyWidget(self) self.root = QWidget() - self.root.setAttribute(Qt.WA_TranslucentBackground) + self.root.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.root.setGeometry( 0, 0, int(width), int(height) ) - self.title_widget = OCBTitle(title, self.root) - self.title_widget.setAttribute(Qt.WA_TranslucentBackground) - self.setTitleGraphics( - title_color, - title_font, - title_size, - title_padding - ) + 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.Vertical, self.root) + self.splitter = OCBSplitter(self, Qt.Orientation.Vertical, self.root) self.size_grip = BlockSizeGrip(self, self.root) - if type(self) == OCBBlock: # DO NOT TRUST codacy !!! type(self) should be used, not isinstance. + # 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) @@ -142,12 +96,7 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = self.moved = False self.metadata = { - 'title_metadata': { - 'color': title_color, - 'font': title_font, - 'size': title_size, - 'padding': title_padding, - }, + 'title_metadata': self.title_widget.metadatas, } def scene(self) -> 'OCBScene': @@ -158,29 +107,6 @@ 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""" - QLineEdit {{ - color : {color}; - background-color: #E3212121; - border:none; - padding: {padding}px; - }}""" - ) - 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 @@ -218,7 +144,7 @@ def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]: x = self.width sockets = self.sockets_out - y_offset = self.title_height + 2 * socket.radius + y_offset = self.title_widget.height() + 2 * socket.radius if len(sockets) < 2: y = y_offset else: @@ -272,33 +198,28 @@ def remove(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) + # 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) + + if hasattr(self, 'title_widget'): self.title_widget.setGeometry( - int(self.edge_size + self.title_left_offset), + int(self.edge_size + self.title_widget.left_offset), int(self.edge_size / 2), int(self.width - self.edge_size * 3), - 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) + int(self.title_widget.height()) ) @property @@ -357,7 +278,8 @@ def deserialize(self, data: dict, hashmap: dict = None, self.setPos(QPointF(*data['position'])) self.metadata = dict(data['metadata']) - self.setTitleGraphics(**self.metadata['title_metadata']) + self.title_widget = OCBTitle( + data['title'], **self.metadata['title_metadata']) if 'splitter_pos' in data: self.splitter.setSizes(data['splitter_pos']) @@ -370,27 +292,3 @@ def deserialize(self, data: dict, hashmap: dict = 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/blocks/widgets/__init__.py b/opencodeblocks/graphics/blocks/widgets/__init__.py new file mode 100644 index 00000000..33d9d228 --- /dev/null +++ b/opencodeblocks/graphics/blocks/widgets/__init__.py @@ -0,0 +1,7 @@ +# 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.graphics.blocks.widgets.title import OCBTitle +from opencodeblocks.graphics.blocks.widgets.splitter import OCBSplitter diff --git a/opencodeblocks/graphics/blocks/widgets/splitter.py b/opencodeblocks/graphics/blocks/widgets/splitter.py new file mode 100644 index 00000000..72beaa0f --- /dev/null +++ b/opencodeblocks/graphics/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.graphics.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/graphics/blocks/widgets/title.py b/opencodeblocks/graphics/blocks/widgets/title.py new file mode 100644 index 00000000..4f86ca25 --- /dev/null +++ b/opencodeblocks/graphics/blocks/widgets/title.py @@ -0,0 +1,79 @@ +# 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 PyQt5.QtCore import Qt + +from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent +from PyQt5.QtWidgets import QLineEdit + + +class OCBTitle(QLineEdit): + """ 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, padding=4.0, left_offset=4): + """ 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. + padding: Padding of the block title. + + """ + super().__init__(text, None) + self.setFixedHeight(int(3.5 * size)) + self.setFont(QFont(font, size)) + self.color = color + self.padding = padding + self.left_offset = left_offset + self.setStyleSheet( + f""" + QLineEdit {{ + color : {self.color}; + background-color: #E3212121; + border:none; + padding: {self.padding}px; + }}""" + ) + self.clickTime = None + self.setReadOnly(True) + + @property + def metadatas(self) -> dict: + return { + 'color': self.color, + 'font': self.font().family(), + 'size': self.font().pointSize(), + 'padding': self.padding, + } + + 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) diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index c302a0fb..ed4a6b34 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -49,9 +49,9 @@ def testing_drag(msgQueue: CheckingQueue): 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) From ee74d43ce13fb94b43ee367f54690b01a6f5b8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 15:46:36 +0100 Subject: [PATCH 14/19] :wrench: Refactor blocksizegrip --- opencodeblocks/graphics/blocks/block.py | 32 +++++++++---------- .../graphics/blocks/widgets/__init__.py | 1 + .../blocks/{ => widgets}/blocksizegrip.py | 19 +++++++---- opencodeblocks/graphics/scene/scene.py | 8 ++++- 4 files changed, 36 insertions(+), 24 deletions(-) rename opencodeblocks/graphics/blocks/{ => widgets}/blocksizegrip.py (86%) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 01f583ae..437006a6 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -13,8 +13,7 @@ from opencodeblocks.core.serializable import Serializable from opencodeblocks.graphics.socket import OCBSocket -from opencodeblocks.graphics.blocks.blocksizegrip import BlockSizeGrip -from opencodeblocks.graphics.blocks.widgets import OCBTitle, OCBSplitter +from opencodeblocks.graphics.blocks.widgets import OCBTitle, OCBSplitter, BlockSizeGrip if TYPE_CHECKING: from opencodeblocks.graphics.scene.scene import OCBScene @@ -39,6 +38,7 @@ def __init__(self, block_type: str = 'base', width: Block width. height: Block height. edge_size: Block edges size. + title: Block Title. parent: Parent of the block. """ @@ -79,8 +79,7 @@ def __init__(self, block_type: str = 'base', Qt.WidgetAttribute.WA_TranslucentBackground) self.splitter = OCBSplitter(self, Qt.Orientation.Vertical, self.root) - - self.size_grip = BlockSizeGrip(self, self.root) + self.size_grip = BlockSizeGrip(self) # DO NOT TRUST codacy !!! type(self) should be used, not isinstance. if type(self) == OCBBlock: @@ -130,11 +129,6 @@ def paint(self, painter: QPainter, 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': @@ -214,13 +208,19 @@ def update_all(self): sizes[-1] += height_delta self.splitter.setSizes(sizes) - if hasattr(self, 'title_widget'): - 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()) - ) + 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()) + ) + + 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): diff --git a/opencodeblocks/graphics/blocks/widgets/__init__.py b/opencodeblocks/graphics/blocks/widgets/__init__.py index 33d9d228..84042141 100644 --- a/opencodeblocks/graphics/blocks/widgets/__init__.py +++ b/opencodeblocks/graphics/blocks/widgets/__init__.py @@ -5,3 +5,4 @@ from opencodeblocks.graphics.blocks.widgets.title import OCBTitle from opencodeblocks.graphics.blocks.widgets.splitter import OCBSplitter +from opencodeblocks.graphics.blocks.widgets.blocksizegrip import BlockSizeGrip diff --git a/opencodeblocks/graphics/blocks/blocksizegrip.py b/opencodeblocks/graphics/blocks/widgets/blocksizegrip.py similarity index 86% rename from opencodeblocks/graphics/blocks/blocksizegrip.py rename to opencodeblocks/graphics/blocks/widgets/blocksizegrip.py index 2c8eb4ed..1a2f60ee 100644 --- a/opencodeblocks/graphics/blocks/blocksizegrip.py +++ b/opencodeblocks/graphics/blocks/widgets/blocksizegrip.py @@ -6,22 +6,27 @@ 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.graphics.blocks.block import OCBBlock + class BlockSizeGrip(QSizeGrip): """ 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 diff --git a/opencodeblocks/graphics/scene/scene.py b/opencodeblocks/graphics/scene/scene.py index fa99e84b..228e5636 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/graphics/scene/scene.py @@ -6,7 +6,7 @@ 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 @@ -19,6 +19,9 @@ from opencodeblocks.graphics.scene.clipboard import SceneClipboard from opencodeblocks.graphics.scene.history import SceneHistory +if TYPE_CHECKING: + from opencodeblocks.graphics.view import OCBView + class OCBScene(QGraphicsScene, Serializable): @@ -188,6 +191,9 @@ def create_block_from_file( data["sockets"] = {} self.create_block(data, None, False) + 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 """ From e7a3e40187410e0718237373ccac7a0b74e0e9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 16:00:53 +0100 Subject: [PATCH 15/19] :hammer: :sparkles: :fire: Restructured opencodeblocks Applied `black` code style --- README.md | 6 +- opencodeblocks/__init__.py | 6 +- opencodeblocks/__main__.py | 6 +- .../{graphics => }/blocks/__init__.py | 9 +- opencodeblocks/{graphics => }/blocks/block.py | 184 ++++++------ .../{graphics => }/blocks/codeblock.py | 46 +-- opencodeblocks/blocks/widgets/__init__.py | 8 + .../blocks/widgets/blocksizegrip.py | 31 +- .../{graphics => }/blocks/widgets/splitter.py | 16 +- .../{graphics => }/blocks/widgets/title.py | 30 +- opencodeblocks/core/__init__.py | 2 - opencodeblocks/{graphics => }/edge.py | 135 +++++---- .../{graphics => }/function_parsing.py | 11 +- opencodeblocks/graphics/__init__.py | 2 - .../graphics/blocks/widgets/__init__.py | 8 - opencodeblocks/{graphics => }/kernel.py | 37 ++- opencodeblocks/{graphics => }/pyeditor.py | 36 ++- opencodeblocks/{graphics => }/qss/__init__.py | 6 +- .../{graphics => }/qss/dark_resources.py | 0 opencodeblocks/{graphics => }/qss/ocb.qss | 0 .../{graphics => }/qss/ocb_dark.qss | 0 .../{graphics => }/scene/__init__.py | 2 +- .../{graphics => }/scene/clipboard.py | 80 ++--- .../{graphics => }/scene/history.py | 33 +-- opencodeblocks/{graphics => }/scene/scene.py | 122 ++++---- opencodeblocks/{core => }/serializable.py | 12 +- opencodeblocks/{graphics => }/socket.py | 106 ++++--- opencodeblocks/{graphics => }/theme.py | 16 +- .../{graphics => }/theme_manager.py | 14 +- opencodeblocks/{graphics => }/view.py | 186 +++++++----- opencodeblocks/{graphics => }/widget.py | 14 +- opencodeblocks/{graphics => }/window.py | 279 +++++++++++------- opencodeblocks/{graphics => }/worker.py | 15 +- requirements-dev.txt | 2 +- 34 files changed, 816 insertions(+), 644 deletions(-) rename opencodeblocks/{graphics => }/blocks/__init__.py (52%) rename opencodeblocks/{graphics => }/blocks/block.py (62%) rename opencodeblocks/{graphics => }/blocks/codeblock.py (81%) create mode 100644 opencodeblocks/blocks/widgets/__init__.py rename opencodeblocks/{graphics => }/blocks/widgets/blocksizegrip.py (73%) rename opencodeblocks/{graphics => }/blocks/widgets/splitter.py (64%) rename opencodeblocks/{graphics => }/blocks/widgets/title.py (75%) delete mode 100644 opencodeblocks/core/__init__.py rename opencodeblocks/{graphics => }/edge.py (58%) rename opencodeblocks/{graphics => }/function_parsing.py (94%) delete mode 100644 opencodeblocks/graphics/__init__.py delete mode 100644 opencodeblocks/graphics/blocks/widgets/__init__.py rename opencodeblocks/{graphics => }/kernel.py (77%) rename opencodeblocks/{graphics => }/pyeditor.py (79%) rename opencodeblocks/{graphics => }/qss/__init__.py (80%) rename opencodeblocks/{graphics => }/qss/dark_resources.py (100%) rename opencodeblocks/{graphics => }/qss/ocb.qss (100%) rename opencodeblocks/{graphics => }/qss/ocb_dark.qss (100%) rename opencodeblocks/{graphics => }/scene/__init__.py (78%) rename opencodeblocks/{graphics => }/scene/clipboard.py (58%) rename opencodeblocks/{graphics => }/scene/history.py (63%) rename opencodeblocks/{graphics => }/scene/scene.py (65%) rename opencodeblocks/{core => }/serializable.py (69%) rename opencodeblocks/{graphics => }/socket.py (50%) rename opencodeblocks/{graphics => }/theme.py (83%) rename opencodeblocks/{graphics => }/theme_manager.py (81%) rename opencodeblocks/{graphics => }/view.py (68%) rename opencodeblocks/{graphics => }/widget.py (79%) rename opencodeblocks/{graphics => }/window.py (58%) rename opencodeblocks/{graphics => }/worker.py (77%) diff --git a/README.md b/README.md index 9164c457..e29c549d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # OpenCodeBlocks +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Pytest badge](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-tests.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/ddd03302fd7c4849b452959753bc0939)](https://www.codacy.com/gh/MathisFederico/OpenCodeBlocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/OpenCodeBlocks&utm_campaign=Badge_Grade) -[![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml) [![Total coverage Codacy Badge](https://app.codacy.com/project/badge/Coverage/ddd03302fd7c4849b452959753bc0939)](https://www.codacy.com/gh/MathisFederico/OpenCodeBlocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/OpenCodeBlocks&utm_campaign=Badge_Coverage) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-pylint.yml) [![Unit coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_unit_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) [![Integration coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_integration_coverage_badge.json)](https://github.com/MathisFederico/OpenCodeBlocks/actions/workflows/python-coverage.yml) [![Licence - GPLv3](https://img.shields.io/github/license/MathisFederico/Crafting?style=plastic)](https://www.gnu.org/licenses/) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) + OpenCodeBlocks is an open-source tool for modular visual programing in python. diff --git a/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/graphics/blocks/block.py b/opencodeblocks/blocks/block.py similarity index 62% rename from opencodeblocks/graphics/blocks/block.py rename to opencodeblocks/blocks/block.py index 437006a6..a5e93725 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -6,30 +6,39 @@ from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple, Union from PyQt5.QtCore import QPointF, QRectF, Qt -from PyQt5.QtGui import QBrush, QPen, QColor, \ - QPainter, QPainterPath -from PyQt5.QtWidgets import QGraphicsItem, QGraphicsProxyWidget, \ - QGraphicsSceneMouseEvent, QStyleOptionGraphicsItem, QWidget - -from opencodeblocks.core.serializable import Serializable -from opencodeblocks.graphics.socket import OCBSocket -from opencodeblocks.graphics.blocks.widgets import OCBTitle, OCBSplitter, BlockSizeGrip +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.graphics.scene.scene import OCBScene + 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. + """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. @@ -64,19 +73,14 @@ def __init__(self, block_type: str = 'base', self.holder = QGraphicsProxyWidget(self) self.root = QWidget() self.root.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - self.root.setGeometry( - 0, 0, - int(width), - int(height) - ) + 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.title_widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.splitter = OCBSplitter(self, Qt.Orientation.Vertical, self.root) self.size_grip = BlockSizeGrip(self) @@ -95,43 +99,49 @@ def __init__(self, block_type: str = 'base', self.moved = False self.metadata = { - 'title_metadata': self.title_widget.metadatas, + "title_metadata": self.title_widget.metadatas, } - def scene(self) -> 'OCBScene': - """ Get the current OCBScene containing the block. """ + def scene(self) -> "OCBScene": + """Get the current OCBScene containing the block.""" return super().scene() def boundingRect(self) -> QRectF: - """ Get the the block bounding box. """ + """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. """ + 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) + 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) + 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) + 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': + """Get a socket position to place them on the block sides.""" + if socket.socket_type == "input": x = 0 sockets = self.sockets_in else: @@ -143,26 +153,25 @@ def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]: 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) + y = y_offset + side_lenght * sockets.index(socket) / (len(sockets) - 1) return x, y def update_sockets(self): - """ Update the sockets positions. """ + """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': + """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': + """Remove a socket from the block.""" + if socket.socket_type == "input": self.sockets_in.remove(socket) else: self.sockets_out.remove(socket) @@ -170,19 +179,19 @@ def remove_socket(self, socket: OCBSocket): self.update_sockets() def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): - """ OCBBlock reaction to a mouseReleaseEvent. """ + """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. """ + """OCBBlock reaction to a mouseMoveEvent.""" super().mouseMoveEvent(event) self.moved = True def remove(self): - """ Remove the block from the scene containing it. """ + """Remove the block from the scene containing it.""" scene = self.scene() for socket in self.sockets_in + self.sockets_out: self.remove_socket(socket) @@ -190,7 +199,7 @@ def remove(self): scene.removeItem(self) def update_all(self): - """ Update sockets and title. """ + """Update sockets and title.""" self.update_sockets() # We make the resizing of splitter only affect @@ -201,7 +210,7 @@ def update_all(self): 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()) + int(self.height - self.edge_size * 2 - self.title_widget.height()), ) if len(sizes) > 1: height_delta = self.splitter.height() - old_height @@ -212,29 +221,29 @@ def update_all(self): 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()) + int(self.title_widget.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) + int(self.edge_size * 1.7), ) @property def title(self): - """ Block title. """ + """Block title.""" return self.title_widget.text() @title.setter def title(self, value: str): - if hasattr(self, 'title_widget'): + if hasattr(self, "title_widget"): self.title_widget.setText(value) @property def width(self): - """ Block width. """ + """Block width.""" return self.root.width() @width.setter @@ -244,7 +253,7 @@ def width(self, value: float): @property def height(self): - """ Block height. """ + """Block height.""" return self.root.height() @height.setter @@ -254,41 +263,46 @@ def height(self, value: float): 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: + 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'): + 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.metadata['title_metadata']) + self.setPos(QPointF(*data["position"])) + self.metadata = dict(data["metadata"]) + self.title_widget = OCBTitle(data["title"], **self.metadata["title_metadata"]) - if 'splitter_pos' in data: - self.splitter.setSizes(data['splitter_pos']) + if "splitter_pos" in data: + self.splitter.setSizes(data["splitter_pos"]) - for socket_data in data['sockets']: + 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}) + hashmap.update({socket_data["id"]: socket}) self.update_all() diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py similarity index 81% rename from opencodeblocks/graphics/blocks/codeblock.py rename to opencodeblocks/blocks/codeblock.py index aba18bb7..ef4f10a6 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,14 +54,14 @@ 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.clicked.connect(self.run_code) @@ -82,14 +81,14 @@ def run_code(self): self.source_editor.threadpool.start(worker) def update_all(self): - """ Update the code block parts. """ + """Update the code block parts.""" super().update_all() - if hasattr(self, 'run_button'): + 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) + int(2.5 * self.edge_size), ) # Close output panel if no output @@ -100,7 +99,7 @@ def update_all(self): @property def source(self) -> str: - """ Source code. """ + """Source code.""" return self.source_editor.text() @source.setter @@ -114,8 +113,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 +137,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 +160,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/widgets/blocksizegrip.py b/opencodeblocks/blocks/widgets/blocksizegrip.py similarity index 73% rename from opencodeblocks/graphics/blocks/widgets/blocksizegrip.py rename to opencodeblocks/blocks/widgets/blocksizegrip.py index 1a2f60ee..9edeb40b 100644 --- a/opencodeblocks/graphics/blocks/widgets/blocksizegrip.py +++ b/opencodeblocks/blocks/widgets/blocksizegrip.py @@ -1,4 +1,3 @@ - """ Implements the SizeGrip Widget for the Blocks. @@ -13,14 +12,14 @@ from PyQt5.QtGui import QMouseEvent if TYPE_CHECKING: - from opencodeblocks.graphics.blocks.block import OCBBlock + 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: 'OCBBlock'): - """ Constructor for BlockSizeGrip + def __init__(self, block: "OCBBlock"): + """Constructor for BlockSizeGrip Args: block: OCBBlock holding the QSizeGrip. @@ -33,23 +32,25 @@ def __init__(self, block: 'OCBBlock'): 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)) @@ -63,14 +64,8 @@ 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() diff --git a/opencodeblocks/graphics/blocks/widgets/splitter.py b/opencodeblocks/blocks/widgets/splitter.py similarity index 64% rename from opencodeblocks/graphics/blocks/widgets/splitter.py rename to opencodeblocks/blocks/widgets/splitter.py index 72beaa0f..b8a213ba 100644 --- a/opencodeblocks/graphics/blocks/widgets/splitter.py +++ b/opencodeblocks/blocks/widgets/splitter.py @@ -9,14 +9,14 @@ from PyQt5.QtGui import QMouseEvent if TYPE_CHECKING: - from opencodeblocks.graphics.blocks import OCBBlock + from opencodeblocks.blocks import OCBBlock class OCBSplitterHandle(QSplitterHandle): - """ A handle for splitters with undoable events """ + """A handle for splitters with undoable events""" - def mouseReleaseEvent(self, event: 'QMouseEvent'): - """ When releasing the handle, save the state to history """ + 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) @@ -24,13 +24,13 @@ def mouseReleaseEvent(self, event: 'QMouseEvent'): class OCBSplitter(QSplitter): - """ A spliter with undoable events """ + """A spliter with undoable events""" - def __init__(self, block: 'OCBBlock', orientation: int, parent: QWidget): - """ Create a new OCBSplitter """ + 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 the middle handle of the splitter""" return OCBSplitterHandle(self.orientation(), self) diff --git a/opencodeblocks/graphics/blocks/widgets/title.py b/opencodeblocks/blocks/widgets/title.py similarity index 75% rename from opencodeblocks/graphics/blocks/widgets/title.py rename to opencodeblocks/blocks/widgets/title.py index 8fb177c9..fb008735 100644 --- a/opencodeblocks/graphics/blocks/widgets/title.py +++ b/opencodeblocks/blocks/widgets/title.py @@ -11,11 +11,18 @@ class OCBTitle(QLineEdit): - """ The title of an OCBBlock. Needs to be double clicked to interact """ + """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, padding=4.0, left_offset=4): - """ Create a new title for an OCBBlock + def __init__( + self, + text: str, + color: str = "white", + font: str = "Ubuntu", + size: int = 12, + padding=4.0, + left_offset=4, + ): + """Create a new title for an OCBBlock Args: text: Block title. @@ -46,10 +53,10 @@ def __init__(self, text: str, color: str = 'white', font: str = "Ubuntu", @property def metadatas(self) -> dict: return { - 'color': self.color, - 'font': self.font().family(), - 'size': self.font().pointSize(), - 'padding': self.padding, + "color": self.color, + "font": self.font().family(), + "size": self.font().pointSize(), + "padding": self.padding, } def mousePressEvent(self, event: QMouseEvent): @@ -58,7 +65,8 @@ def mousePressEvent(self, event: QMouseEvent): 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.isReadOnly() and time.time() - self.clickTime > 0.3 + ): self.parent().mousePressEvent(event) elif self.isReadOnly(): self.mouseDoubleClickEvent(event) @@ -68,12 +76,12 @@ def mousePressEvent(self, event: QMouseEvent): self.clickTime = time.time() def focusOutEvent(self, event: QFocusEvent): - """ The title is read-only when focused is lost """ + """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 """ + """Toggle readonly mode when double clicking""" self.setReadOnly(not self.isReadOnly()) if not self.isReadOnly(): self.setFocus(Qt.FocusReason.MouseFocusReason) 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/widgets/__init__.py b/opencodeblocks/graphics/blocks/widgets/__init__.py deleted file mode 100644 index 84042141..00000000 --- a/opencodeblocks/graphics/blocks/widgets/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# 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.graphics.blocks.widgets.title import OCBTitle -from opencodeblocks.graphics.blocks.widgets.splitter import OCBSplitter -from opencodeblocks.graphics.blocks.widgets.blocksizegrip import BlockSizeGrip 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 65% rename from opencodeblocks/graphics/scene/scene.py rename to opencodeblocks/scene/scene.py index 228e5636..0a8003a9 100644 --- a/opencodeblocks/graphics/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -12,26 +12,32 @@ 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.graphics.view import OCBView + from opencodeblocks.view import OCBView class OCBScene(QGraphicsScene, Serializable): - """ Scene for the OCB Window. """ - - def __init__(self, parent=None, - background_color: str = "#393939", - grid_color: str = "#292929", grid_light_color: str = "#2f2f2f", - width: int = 64000, height: int = 64000, - grid_size: int = 20, grid_squares: int = 5): + """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) @@ -42,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 @@ -54,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 @@ -64,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): @@ -78,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())) @@ -118,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() @@ -176,55 +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 views(self) -> List['OCBView']: + 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 """ + 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 From 9072c23dfc6395de131c2ecb072c2178feaaee22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 16:05:20 +0100 Subject: [PATCH 16/19] :wrench: Updated tests with new structure --- tests/integration/blocks/test_block.py | 22 ++-- tests/integration/blocks/test_codeblock.py | 13 +-- tests/integration/test_window.py | 9 +- tests/integration/utils.py | 3 +- tests/unit/scene/test_clipboard.py | 26 ++--- tests/unit/scene/test_function_parsing.py | 126 +++++++++++++-------- tests/unit/scene/test_history.py | 112 +++++++++--------- 7 files changed, 166 insertions(+), 145 deletions(-) diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index ed4a6b34..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,11 +29,11 @@ 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( @@ -51,7 +50,7 @@ def testing_drag(msgQueue: CheckingQueue): pos_block.setX( pos_block.x() + self.block.title_widget.height() + self.block.edge_size ) - pos_block.setY(pos_block.y() + self.block.title_widget.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) @@ -60,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") @@ -74,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") From b7d51f9053e7a4cfc5cfe99562ea8832253d7e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 18:33:35 +0100 Subject: [PATCH 17/19] :wrench: Refactor block updates Refactor title serialization --- examples/mnist.ipyg | 34 ++++----- opencodeblocks/blocks/block.py | 51 ++++++++----- opencodeblocks/blocks/codeblock.py | 31 ++++++-- .../blocks/widgets/blocksizegrip.py | 1 + opencodeblocks/blocks/widgets/title.py | 74 +++++++++++++------ 5 files changed, 128 insertions(+), 63 deletions(-) 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/blocks/block.py b/opencodeblocks/blocks/block.py index a5e93725..fc7d2896 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -99,7 +99,7 @@ def __init__( self.moved = False self.metadata = { - "title_metadata": self.title_widget.metadatas, + "title_metadata": self.title_widget.serialize(), } def scene(self) -> "OCBScene": @@ -198,10 +198,26 @@ def remove(self): if scene is not None: scene.removeItem(self) - def update_all(self): - """Update sockets and title.""" - self.update_sockets() + 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() @@ -217,19 +233,12 @@ def update_all(self): sizes[-1] += height_delta self.splitter.setSizes(sizes) - 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()), - ) - - self.size_grip.setGeometry( - int(self.width - self.edge_size * 2), - int(self.height - self.edge_size * 2), - int(self.edge_size * 1.7), - int(self.edge_size * 1.7), - ) + def update_all(self): + """Update everything.""" + self.update_sockets() + self.update_splitter() + self.update_grip() + self.update_title() @property def title(self): @@ -249,7 +258,6 @@ def width(self): @width.setter def width(self, value: float): self.root.setGeometry(0, 0, int(value), self.root.height()) - self.update_all() @property def height(self): @@ -259,7 +267,6 @@ def height(self): @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())) @@ -293,7 +300,11 @@ def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None self.setPos(QPointF(*data["position"])) self.metadata = dict(data["metadata"]) - self.title_widget = OCBTitle(data["title"], **self.metadata["title_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"]) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index ef4f10a6..27dfcb18 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -63,9 +63,9 @@ def init_output_panel(self): def init_run_button(self): """Initialize the run button""" run_button = QPushButton(">", self.root) - run_button.setMinimumWidth(int(self.edge_size)) + run_button.setFixedSize(int(2.5 * self.edge_size), int(2.5 * self.edge_size)) + run_button.move(self.edge_size // 2, self.edge_size // 2) run_button.clicked.connect(self.run_code) - run_button.raise_() return run_button def run_code(self): @@ -80,9 +80,22 @@ 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() + def update_title(self): + """Update the block title position.""" + button_pos = self.run_button.mapToParent(self.run_button.pos()).x() + offset = self.edge_size + button_pos + self.run_button.width() + # self.title_widget.setMaximumWidth(self.width) + self.title_widget.move(offset, 0) + self.title_widget.setFixedWidth(self.width - 2 * offset) + # self.title_widget.setGeometry( + # int(offset + self.run_button.widthMM()), + # int(self.edge_size / 2), + # int(self.width), + # int(self.title_widget.height()), + # ) + + def update_run_button(self): + """Update the run button""" if hasattr(self, "run_button"): self.run_button.setGeometry( int(self.edge_size), @@ -91,12 +104,20 @@ def update_all(self): int(2.5 * self.edge_size), ) + 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_run_button() + self.update_output_panel() + @property def source(self) -> str: """Source code.""" diff --git a/opencodeblocks/blocks/widgets/blocksizegrip.py b/opencodeblocks/blocks/widgets/blocksizegrip.py index 9edeb40b..bb85a4fe 100644 --- a/opencodeblocks/blocks/widgets/blocksizegrip.py +++ b/opencodeblocks/blocks/widgets/blocksizegrip.py @@ -68,6 +68,7 @@ def mouseMoveEvent(self, mouseEvent: QMouseEvent): 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/title.py b/opencodeblocks/blocks/widgets/title.py index fb008735..bded55e2 100644 --- a/opencodeblocks/blocks/widgets/title.py +++ b/opencodeblocks/blocks/widgets/title.py @@ -4,13 +4,16 @@ """ 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): + +class OCBTitle(QLineEdit, Serializable): """The title of an OCBBlock. Needs to be double clicked to interact""" def __init__( @@ -19,8 +22,6 @@ def __init__( color: str = "white", font: str = "Ubuntu", size: int = 12, - padding=4.0, - left_offset=4, ): """Create a new title for an OCBBlock @@ -29,35 +30,39 @@ def __init__( color: Color of the block title. font: Font of the block title. size: Size of the block title. - padding: Padding of the block title. """ - super().__init__(text, None) - self.setFixedHeight(int(3.5 * size)) + 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.padding = padding - self.left_offset = left_offset self.setStyleSheet( f""" QLineEdit {{ color : {self.color}; - background-color: transparent; + background-color: #FF0000; border:none; - padding: {self.padding}px; }}""" ) - self.clickTime = None - self.setReadOnly(True) - - @property - def metadatas(self) -> dict: - return { - "color": self.color, - "font": self.font().family(), - "size": self.font().pointSize(), - "padding": self.padding, - } def mousePressEvent(self, event: QMouseEvent): """ @@ -85,3 +90,30 @@ def mouseDoubleClickEvent(self, event: QMouseEvent): 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.""" + 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"]) From 7eb1c8522d0af7dd48e876696549a7b37fddf55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 18:58:12 +0100 Subject: [PATCH 18/19] :beetle: Fixed OCBTitle.serialize --- opencodeblocks/blocks/widgets/title.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencodeblocks/blocks/widgets/title.py b/opencodeblocks/blocks/widgets/title.py index bded55e2..a0541906 100644 --- a/opencodeblocks/blocks/widgets/title.py +++ b/opencodeblocks/blocks/widgets/title.py @@ -93,7 +93,7 @@ def mouseDoubleClickEvent(self, event: QMouseEvent): def serialize(self) -> OrderedDict: """Serialize the object as an ordered dictionary.""" - OrderedDict( + return OrderedDict( [ ("id", self.id), ("color", self.color), From 3a39beb5b6ce4e37363078d5cd81c887f29139f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 18:58:52 +0100 Subject: [PATCH 19/19] :fire: Remove update_run_button --- opencodeblocks/blocks/codeblock.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index 27dfcb18..0a3142bb 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -63,8 +63,8 @@ def init_output_panel(self): def init_run_button(self): """Initialize the run button""" run_button = QPushButton(">", self.root) - run_button.setFixedSize(int(2.5 * self.edge_size), int(2.5 * self.edge_size)) - run_button.move(self.edge_size // 2, self.edge_size // 2) + 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) return run_button @@ -83,26 +83,18 @@ def run_code(self): def update_title(self): """Update the block title position.""" button_pos = self.run_button.mapToParent(self.run_button.pos()).x() - offset = self.edge_size + button_pos + self.run_button.width() + offset = button_pos + self.run_button.width() # self.title_widget.setMaximumWidth(self.width) - self.title_widget.move(offset, 0) + self.title_widget.move(offset, int(self.edge_size / 4)) self.title_widget.setFixedWidth(self.width - 2 * offset) # self.title_widget.setGeometry( - # int(offset + self.run_button.widthMM()), - # int(self.edge_size / 2), + # int(offset), + # int(self.edge_size / 4), # int(self.width), # int(self.title_widget.height()), # ) - - def update_run_button(self): - """Update the run button""" - 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), - ) + # print(self.width, self.title_widget.width()) + # print(self.title_widget.getTextMargins()) def update_output_panel(self): """Update the output panel""" @@ -115,7 +107,6 @@ def update_output_panel(self): def update_all(self): """Update the code block parts.""" super().update_all() - self.update_run_button() self.update_output_panel() @property