Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
83d955c
:tada: The title of the blocks is editable by clicking on it.
vanyle Nov 28, 2021
08eca81
:umbrella: Fix the move test by making the cursor inside the window.
vanyle Nov 28, 2021
1919a99
:umbrella: More work to make the move test work
vanyle Nov 28, 2021
31370ed
:umbrella: Narrowing down the issue
vanyle Nov 28, 2021
be288e0
:beetle: Block is now centered on screen.
vanyle Nov 28, 2021
9476fb7
:tada: The title is wider
vanyle Nov 28, 2021
7939e5a
:tada: To edit the title a double click is required. The dragging tes…
vanyle Nov 29, 2021
60580db
:beetle: Fix issue with test. A boolean expression was previously inc…
vanyle Nov 29, 2021
e7f14bd
:beetle: The cursor moves at the correct position when clicking the t…
vanyle Nov 29, 2021
8150366
:beetle: This line seems to change without any reason. I will investi…
vanyle Nov 29, 2021
9cca452
Remove comment
vanyle Nov 29, 2021
ee4a6ec
:twisted_rightwards_arrows: Merge master into feature/editable_title
MathisFederico Nov 30, 2021
b8e4003
:hammer: Moved widgets into their own files.
vanyle Nov 30, 2021
e3fb45e
:beetle: The content of the title is aligned to the left when the tit…
vanyle Nov 30, 2021
cf5f95a
:sparkles: Applied black code style
MathisFederico Nov 30, 2021
5104973
:wrench: Refactor OCBTitle
MathisFederico Nov 30, 2021
71d58fc
:beetle: Fix test_block
MathisFederico Nov 30, 2021
b39e41c
:sparkles: Remove false positive linting
MathisFederico Nov 30, 2021
4fca931
:wrench: Make titles background transparent
MathisFederico Nov 30, 2021
1ffaf92
:memo: Add a lot of docstrings
vanyle Nov 30, 2021
73f736f
:truck: Renamed a lot of files to make the graphics directory less c…
vanyle Nov 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ confidence=
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=inconsistent-return-statements,
unidiomatic-typecheck,
attribute-defined-outside-init,
pointless-statement,
no-self-use,
Expand Down
58 changes: 29 additions & 29 deletions examples/mnist.ipyg

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

""" Module for the OCB Blocks of different types. """

from opencodeblocks.graphics.blocks.block import OCBBlock
from opencodeblocks.graphics.blocks.codeblock import OCBCodeBlock
from opencodeblocks.blocks.block import OCBBlock
from opencodeblocks.blocks.codeblock import OCBCodeBlock

BLOCKS = {
'base': OCBBlock,
Expand Down
317 changes: 317 additions & 0 deletions opencodeblocks/blocks/block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# OpenCodeBlock an open-source tool for modular visual programing in python
# Copyright (C) 2021 Mathïs FEDERICO <https://www.gnu.org/licenses/>
# pylint:disable=unused-argument

""" Module for the base OCB Block. """

from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple, Union

from PyQt5.QtCore import QPointF, QRectF, Qt
from PyQt5.QtGui import QBrush, QPen, QColor, QPainter, QPainterPath
from PyQt5.QtWidgets import (
QGraphicsItem,
QGraphicsProxyWidget,
QGraphicsSceneMouseEvent,
QStyleOptionGraphicsItem,
QWidget,
)

from opencodeblocks.core.serializable import Serializable
from opencodeblocks.graphics.socket import OCBSocket
from opencodeblocks.blocks.widgets import OCBSplitter, OCBSizeGrip, OCBTitle

if TYPE_CHECKING:
from opencodeblocks.graphics.scene.scene import OCBScene

BACKGROUND_COLOR = QColor("#E3212121")


class OCBBlock(QGraphicsItem, Serializable):

"""Base class for blocks in OpenCodeBlocks."""

def __init__(
self,
block_type: str = "base",
source: str = "",
position: tuple = (0, 0),
width: int = 300,
height: int = 200,
edge_size: float = 10.0,
title: Union[OCBTitle, str] = "New block",
parent: Optional["QGraphicsItem"] = None,
):
"""Base class for blocks in OpenCodeBlocks.

Args:
block_type: Block type.
source: Block source text.
position: Block position in the scene.
width: Block width.
height: Block height.
edge_size: Block edges size.
title: Block title.
parent: Parent of the block.

"""
QGraphicsItem.__init__(self, parent=parent)
Serializable.__init__(self)

self.block_type = block_type
self.source = source
self.stdout = ""
self.setPos(QPointF(*position))
self.sockets_in = []
self.sockets_out = []

self._pen_outline = QPen(QColor("#7F000000"))
self._pen_outline_selected = QPen(QColor("#FFFFA637"))
self._brush_background = QBrush(BACKGROUND_COLOR)

self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)

self.setAcceptHoverEvents(True)

self.holder = QGraphicsProxyWidget(self)
self.root = QWidget()
self.root.setAttribute(Qt.WA_TranslucentBackground)
self.root.setGeometry(0, 0, int(width), int(height))

self.title_widget = OCBTitle(title, parent=self.root)
self.title_widget.setAttribute(Qt.WA_TranslucentBackground)

self.splitter = OCBSplitter(self, Qt.Vertical, self.root)

self.size_grip = OCBSizeGrip(self, self.root)

if type(self) == OCBBlock:
# This has to be called at the end of the constructor of
# every class inheriting this.
self.holder.setWidget(self.root)

self.edge_size = edge_size
self.min_width = 300
self.min_height = 100
self.width = width
self.height = height

self.moved = False
self.metadata = {}

def scene(self) -> "OCBScene":
"""Get the current OCBScene containing the block."""
return super().scene()

def boundingRect(self) -> QRectF:
"""Get the the block bounding box."""
return QRectF(0, 0, self.width, self.height).normalized()

def paint(
self,
painter: QPainter,
option: QStyleOptionGraphicsItem,
widget: Optional[QWidget] = None,
):
"""Paint the block."""

# content
path_content = QPainterPath()
path_content.setFillRule(Qt.FillRule.WindingFill)
path_content.addRoundedRect(
0, 0, self.width, self.height, self.edge_size, self.edge_size
)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self._brush_background)
painter.drawPath(path_content.simplified())

# outline
path_outline = QPainterPath()
path_outline.addRoundedRect(
0, 0, self.width, self.height, self.edge_size, self.edge_size
)
painter.setPen(
self._pen_outline_selected if self.isSelected() else self._pen_outline
)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawPath(path_outline.simplified())

def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]:
"""Get a socket position to place them on the block sides."""
if socket.socket_type == "input":
x = 0
sockets = self.sockets_in
else:
x = self.width
sockets = self.sockets_out

y_offset = self.title_widget.height() + 2 * socket.radius
if len(sockets) < 2:
y = y_offset
else:
side_lenght = self.height - y_offset - 2 * socket.radius - self.edge_size
y = y_offset + side_lenght * sockets.index(socket) / (len(sockets) - 1)
return x, y

def update_sockets(self):
"""Update the sockets positions."""
for socket in self.sockets_in + self.sockets_out:
socket.setPos(*self.get_socket_pos(socket))

def add_socket(self, socket: OCBSocket):
"""Add a socket to the block."""
if socket.socket_type == "input":
self.sockets_in.append(socket)
else:
self.sockets_out.append(socket)
self.update_sockets()

def remove_socket(self, socket: OCBSocket):
"""Remove a socket from the block."""
if socket.socket_type == "input":
self.sockets_in.remove(socket)
else:
self.sockets_out.remove(socket)
socket.remove()
self.update_sockets()

def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent):
"""OCBBlock reaction to a mouseReleaseEvent."""
if self.moved:
self.moved = False
self.scene().history.checkpoint("Moved block", set_modified=True)
super().mouseReleaseEvent(event)

def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
"""OCBBlock reaction to a mouseMoveEvent."""
super().mouseMoveEvent(event)
self.moved = True

def remove(self):
"""Remove the block from the scene containing it."""
scene = self.scene()
for socket in self.sockets_in + self.sockets_out:
self.remove_socket(socket)
if scene is not None:
scene.removeItem(self)

def update_splitter(self):
""" Change the geometry of the splitter to match the block """
# We make the resizing of splitter only affect
# the last element of the split view
sizes = self.splitter.sizes()
old_height = self.splitter.height()
self.splitter.setGeometry(
int(self.edge_size),
int(self.edge_size + self.title_widget.height()),
int(self.width - self.edge_size * 2),
int(self.height - self.edge_size * 2 - self.title_widget.height()),
)
if len(sizes) > 1:
height_delta = self.splitter.height() - old_height
sizes[-1] += height_delta
self.splitter.setSizes(sizes)

def update_title(self):
""" Change the geometry of the title to match the block """
self.title_widget.setGeometry(
int(self.edge_size),
int(self.edge_size / 2),
int(self.width - self.edge_size * 3),
int(self.title_widget.height()),
)

def update_size_grip(self):
""" Change the geometry of the size grip to match the block"""
self.size_grip.setGeometry(
int(self.width - self.edge_size * 2),
int(self.height - self.edge_size * 2),
int(self.edge_size * 1.7),
int(self.edge_size * 1.7),
)

def update_all(self):
""" Update sockets and title."""
self.update_sockets()
self.update_splitter()
self.update_title()
self.update_size_grip()

@property
def title(self):
"""Block title."""
return self.title_widget.text()

@title.setter
def title(self, value: str):
if hasattr(self, "title_widget"):
self.title_widget.setText(value)

@property
def width(self):
"""Block width."""
return self.root.width()

@width.setter
def width(self, value: float):
self.root.setGeometry(0, 0, int(value), self.root.height())

@property
def height(self):
"""Block height."""
return self.root.height()

@height.setter
def height(self, value: float):
self.root.setGeometry(0, 0, self.root.width(), int(value))

def serialize(self) -> OrderedDict:
""" Return a serialized version of this widget """
self.metadata.update({"title_metadata": self.title_widget.serialize()})
metadata = OrderedDict(sorted(self.metadata.items()))
return OrderedDict(
[
("id", self.id),
("title", self.title),
("block_type", self.block_type),
("source", self.source),
("stdout", self.stdout),
("splitter_pos", self.splitter.sizes()),
("position", [self.pos().x(), self.pos().y()]),
("width", self.width),
("height", self.height),
("metadata", metadata),
(
"sockets",
[
socket.serialize()
for socket in self.sockets_in + self.sockets_out
],
),
]
)

def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None:
""" Restore the block from serialized data """
if restore_id:
self.id = data["id"]
for dataname in ("title", "block_type", "source", "stdout", "width", "height"):
setattr(self, dataname, data[dataname])

self.setPos(QPointF(*data["position"]))
self.metadata = dict(data["metadata"])
self.title_widget.deserialize(
self.metadata["title_metadata"], hashmap, restore_id
)

if "splitter_pos" in data:
self.splitter.setSizes(data["splitter_pos"])

for socket_data in data["sockets"]:
socket = OCBSocket(block=self)
socket.deserialize(socket_data, hashmap, restore_id)
self.add_socket(socket)
if hashmap is not None:
hashmap.update({socket_data["id"]: socket})

self.update_all()
Loading