diff --git a/opencodeblocks/blocks/block.py b/opencodeblocks/blocks/block.py index 754c330d..9974bc0c 100644 --- a/opencodeblocks/blocks/block.py +++ b/opencodeblocks/blocks/block.py @@ -25,6 +25,16 @@ BACKGROUND_COLOR = QColor("#E3212121") +DEFAULT_BLOCK_DATA = { + "title": "_", + "splitter_pos": [88, 41], + "width": 618, + "height": 184, + "metadata": {"title_metadata": {"color": "white", "font": "Ubuntu", "size": 10}}, + "sockets": [], +} +NONE_OPTIONAL_FIELDS = {"block_type", "position"} + class OCBBlock(QGraphicsItem, Serializable): @@ -291,8 +301,11 @@ def serialize(self) -> OrderedDict: def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None: """Restore the block from serialized data""" - if restore_id: + if restore_id and "id" in data: self.id = data["id"] + + self.complete_with_default(data) + for dataname in ("title", "block_type", "width", "height"): setattr(self, dataname, data[dataname]) @@ -319,3 +332,13 @@ def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None hashmap.update({socket_data["id"]: socket}) self.update_all() + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_BLOCK_DATA: + if key not in data: + data[key] = DEFAULT_BLOCK_DATA[key] diff --git a/opencodeblocks/blocks/codeblock.py b/opencodeblocks/blocks/codeblock.py index fec56fce..a2b1cf40 100644 --- a/opencodeblocks/blocks/codeblock.py +++ b/opencodeblocks/blocks/codeblock.py @@ -15,6 +15,9 @@ conv = Ansi2HTMLConverter() +DEFAULT_CODE_BLOCK_DATA = {"source": "", "output": ""} +NONE_OPTIONAL_FIELDS = {} + class OCBCodeBlock(OCBBlock): @@ -197,7 +200,22 @@ def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): """Restore a codeblock from it's serialized state""" + + self.complete_with_default(data) + for dataname in ("source", "stdout"): if dataname in data: setattr(self, dataname, data[dataname]) super().deserialize(data, hashmap, restore_id) + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_CODE_BLOCK_DATA: + if key not in data: + data[key] = DEFAULT_CODE_BLOCK_DATA[key] + + super().complete_with_default(data) diff --git a/opencodeblocks/graphics/edge.py b/opencodeblocks/graphics/edge.py index e4b33722..5f2de4a8 100644 --- a/opencodeblocks/graphics/edge.py +++ b/opencodeblocks/graphics/edge.py @@ -14,17 +14,26 @@ from opencodeblocks.core.serializable import Serializable from opencodeblocks.graphics.socket import OCBSocket +DEFAULT_EDGE_DATA = {"path_type": "bezier"} +NONE_OPTIONAL_FIELDS = {"source", "destination"} -class OCBEdge(QGraphicsPathItem, Serializable): - """ Base class for directed edges in OpenCodeBlocks. """ +class OCBEdge(QGraphicsPathItem, Serializable): - 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 +71,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 +110,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 +140,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 +152,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 +167,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,33 +178,72 @@ 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'] + if restore_id and "id" in data: + self.id = data["id"] + + self.complete_with_default(data) + + 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: self.remove() + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_EDGE_DATA: + if key not in data: + data[key] = DEFAULT_EDGE_DATA[key] diff --git a/opencodeblocks/graphics/socket.py b/opencodeblocks/graphics/socket.py index d567c54f..6d0d2a9b 100644 --- a/opencodeblocks/graphics/socket.py +++ b/opencodeblocks/graphics/socket.py @@ -17,6 +17,17 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.blocks.block import OCBBlock +DEFAULT_SOCKET_DATA = { + "type": "input", + "metadata": { + "color": "#FF55FFF0", + "linecolor": "#FF000000", + "linewidth": 1.0, + "radius": 6.0, + }, +} +NONE_OPTIONAL_FIELDS = {"position"} + class OCBSocket(QGraphicsItem, Serializable): @@ -133,13 +144,25 @@ def serialize(self) -> OrderedDict: ) def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): - if restore_id: + if restore_id and "id" in data: self.id = data["id"] + + self.complete_with_default(data) + self.socket_type = data["type"] self.setPos(QPointF(*data["position"])) self.metadata = dict(data["metadata"]) - self.radius = self.metadata["radius"] self._pen.setColor(QColor(self.metadata["linecolor"])) self._pen.setWidth(int(self.metadata["linewidth"])) self._brush.setColor(QColor(self.metadata["color"])) + + def complete_with_default(self, data: OrderedDict) -> None: + """Add default data in place when fields are missing""" + for key in NONE_OPTIONAL_FIELDS: + if key not in data: + raise ValueError(f"{key} of the socket is missing") + + for key in DEFAULT_SOCKET_DATA: + if key not in data: + data[key] = DEFAULT_SOCKET_DATA[key] diff --git a/opencodeblocks/scene/scene.py b/opencodeblocks/scene/scene.py index e61c2d8c..34adff0b 100644 --- a/opencodeblocks/scene/scene.py +++ b/opencodeblocks/scene/scene.py @@ -218,7 +218,7 @@ 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: + if restore_id and 'id' in data: self.id = data['id'] # Create blocks