diff --git a/README.md b/README.md
index b894556..cbae040 100644
--- a/README.md
+++ b/README.md
@@ -49,9 +49,11 @@ It also aims to prevent re-duplication and re-writing of code inside of a projec
✅ Deactivation of nodes
+
✅ Annotations
+
diff --git a/constants.py b/constants.py
index 28f16f2..0a9bb0b 100644
--- a/constants.py
+++ b/constants.py
@@ -51,9 +51,12 @@
# Naming
GRAPHIC_NODE = "GRAPHIC_NODE"
GRAPHIC_ATTRIBUTE = "GRAPHIC_ATTRIBUTE"
+
PLUG = "PLUG"
CONNECTOR_LINE = "CONNECTOR_LINE"
+GRAPHIC_ANNOTATION = "GRAPHIC_ANNOTATION"
+
# -------------------------------- LOGIC -------------------------------- #
# Naming
@@ -95,9 +98,6 @@ class PreviewsGUI(Enum):
IMAGE_PREVIEW = "Image preview"
-# -------------------------------- KEYBOARD SHORTCUTS -------------------------------- #
-
-
# -------------------------------- LOGGING PREFS -------------------------------- #
LOGGING_LEVEL = logging.INFO
CONSOLE_LOG_FORMATTER = logging.Formatter(
diff --git a/docs/annotations.gif b/docs/annotations.gif
new file mode 100644
index 0000000..036ad28
Binary files /dev/null and b/docs/annotations.gif differ
diff --git a/docs/preview.png b/docs/preview.png
index ee45e72..921c2e4 100644
Binary files a/docs/preview.png and b/docs/preview.png differ
diff --git a/graphic/general_graphics/gears.svg b/graphic/general_graphics/gears.svg
new file mode 100644
index 0000000..d144415
--- /dev/null
+++ b/graphic/general_graphics/gears.svg
@@ -0,0 +1,58 @@
+
+
+
diff --git a/graphic/general_graphics/hand.svg b/graphic/general_graphics/hand.svg
new file mode 100644
index 0000000..f49e7e7
--- /dev/null
+++ b/graphic/general_graphics/hand.svg
@@ -0,0 +1,48 @@
+
+
+
diff --git a/graphic/general_graphics/long_tag.svg b/graphic/general_graphics/long_tag.svg
new file mode 100644
index 0000000..0d6c4f8
--- /dev/null
+++ b/graphic/general_graphics/long_tag.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/graphic/general_graphics/note.svg b/graphic/general_graphics/note.svg
new file mode 100644
index 0000000..bd42a4a
--- /dev/null
+++ b/graphic/general_graphics/note.svg
@@ -0,0 +1,31 @@
+
+
+
diff --git a/graphic/general_graphics/notepad.svg b/graphic/general_graphics/notepad.svg
new file mode 100644
index 0000000..2a851b9
--- /dev/null
+++ b/graphic/general_graphics/notepad.svg
@@ -0,0 +1,76 @@
+
+
+
diff --git a/graphic/general_graphics/pencil.svg b/graphic/general_graphics/pencil.svg
new file mode 100644
index 0000000..a23f388
--- /dev/null
+++ b/graphic/general_graphics/pencil.svg
@@ -0,0 +1,521 @@
+
+
+
diff --git a/graphic/general_graphics/post_it.svg b/graphic/general_graphics/post_it.svg
new file mode 100644
index 0000000..2316452
--- /dev/null
+++ b/graphic/general_graphics/post_it.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/graphic/general_graphics/separator.svg b/graphic/general_graphics/separator.svg
new file mode 100644
index 0000000..2231856
--- /dev/null
+++ b/graphic/general_graphics/separator.svg
@@ -0,0 +1,26 @@
+
+
+
diff --git a/graphic/general_graphics/short_tag.svg b/graphic/general_graphics/short_tag.svg
new file mode 100644
index 0000000..22ac4ea
--- /dev/null
+++ b/graphic/general_graphics/short_tag.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/graphic/general_graphics/warning.svg b/graphic/general_graphics/warning.svg
new file mode 100644
index 0000000..8dac57d
--- /dev/null
+++ b/graphic/general_graphics/warning.svg
@@ -0,0 +1,37 @@
+
+
+
diff --git a/graphic/general_icons/back.svg b/graphic/general_icons/back.svg
new file mode 100644
index 0000000..c7c88fc
--- /dev/null
+++ b/graphic/general_icons/back.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/graphic/general_icons/front.svg b/graphic/general_icons/front.svg
new file mode 100644
index 0000000..648d0d3
--- /dev/null
+++ b/graphic/general_icons/front.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/graphic/graphic_annotation.py b/graphic/graphic_annotation.py
new file mode 100644
index 0000000..511a17f
--- /dev/null
+++ b/graphic/graphic_annotation.py
@@ -0,0 +1,102 @@
+__author__ = "Jaime Rivera "
+__copyright__ = "Copyright 2022, Jaime Rivera"
+__credits__ = []
+__license__ = "MIT License"
+
+
+from PySide2 import QtCore
+from PySide2 import QtGui
+from PySide2 import QtSvg
+from PySide2 import QtWidgets
+
+from all_nodes import constants
+from all_nodes import utils
+
+
+LOGGER = utils.get_logger(__name__)
+
+
+ANNOTATION_TYPES_WITH_TEXT = [
+ "note",
+ "paper",
+ "long_tag",
+ "short_tag",
+ "notepad",
+ "post_it",
+]
+
+
+# -------------------------------- ANNOTATION CLASS -------------------------------- #
+class GeneralGraphicAnnotation(QtWidgets.QGraphicsPathItem):
+ def __init__(self, annotation_type: str):
+ # INIT
+ QtWidgets.QGraphicsPathItem.__init__(self)
+ self.setData(0, constants.GRAPHIC_ANNOTATION)
+ self.setAcceptHoverEvents(True)
+ self.setFlags(
+ QtWidgets.QGraphicsPathItem.ItemIsMovable
+ | QtWidgets.QGraphicsPathItem.ItemIsSelectable
+ | QtWidgets.QGraphicsPathItem.ItemSendsScenePositionChanges
+ )
+
+ self.setPen(QtCore.Qt.NoPen)
+
+ # Setup graphics
+ self.annotation_type = annotation_type
+ self.width = 0
+ self.height = 0
+ self.setup_graphics()
+
+ def setup_graphics(self):
+ # Main graphics
+ self.renderer = QtSvg.QSvgRenderer(f"graphics:{self.annotation_type}.svg")
+ main_graphics = QtSvg.QGraphicsSvgItem(parentItem=self)
+ main_graphics.setSharedRenderer(self.renderer)
+
+ self.width = main_graphics.boundingRect().width()
+ self.height = main_graphics.boundingRect().height()
+
+ padding = 40
+
+ # Text input
+ self.proxy_edit = QtWidgets.QGraphicsProxyWidget(parent=self)
+ self.note_text_edit = QtWidgets.QPlainTextEdit()
+ self.note_text_edit.setFont(QtGui.QFont("arial", 8))
+ self.note_text_edit.setStyleSheet(
+ "color:black; background-color:transparent; border:1px dotted rgba(100,100,100,120);"
+ )
+ self.note_text_edit.setFixedSize(self.width - padding, self.height - padding)
+ self.note_text_edit.setPlainText("Write here...")
+ self.proxy_edit.setWidget(self.note_text_edit)
+ self.proxy_edit.setPos(
+ self.width,
+ self.height,
+ )
+
+ if self.annotation_type not in ANNOTATION_TYPES_WITH_TEXT:
+ self.proxy_edit.hide()
+
+ # Main shape
+ path = QtGui.QPainterPath()
+ path.addRect(
+ QtCore.QRect(
+ 0,
+ 0,
+ self.width + padding,
+ self.height + padding,
+ ),
+ )
+ main_graphics.setPos(padding / 2, padding / 2)
+ self.proxy_edit.setPos(padding, padding)
+ self.setPath(path)
+
+ def get_type(self):
+ return self.annotation_type
+
+ def get_text(self):
+ if self.annotation_type in ANNOTATION_TYPES_WITH_TEXT:
+ return self.note_text_edit.toPlainText()
+ return None
+
+ def set_text(self, text: str):
+ self.note_text_edit.setPlainText(text)
diff --git a/graphic/graphic_scene.py b/graphic/graphic_scene.py
index 4f681d6..307fc41 100644
--- a/graphic/graphic_scene.py
+++ b/graphic/graphic_scene.py
@@ -21,6 +21,7 @@
import yaml
from all_nodes import constants
+from all_nodes.graphic.graphic_annotation import GeneralGraphicAnnotation
from all_nodes.graphic.graphic_node import GeneralGraphicNode, GeneralGraphicAttribute
from all_nodes.logic.class_registry import CLASS_REGISTRY as CR
from all_nodes.logic.logic_node import GeneralLogicNode
@@ -58,6 +59,16 @@ def __init__(self):
self.feedback_line.setFont(QtGui.QFont("arial", 12))
self.feedback_line.hide()
+ self.run_btn = QtWidgets.QPushButton("Run scene", parent=self)
+ self.run_btn.setFixedSize(160, 35)
+ self.run_btn.setFont(QtGui.QFont("arial", 10))
+ self.run_btn.setIcon(QtGui.QIcon("icons:brain.png"))
+
+ self.reset_btn = QtWidgets.QPushButton("Reset scene", parent=self)
+ self.reset_btn.setFixedSize(160, 35)
+ self.reset_btn.setFont(QtGui.QFont("arial", 10))
+ self.reset_btn.setIcon(QtGui.QIcon("icons:reset.png"))
+
self.hourglass_animation = QtWidgets.QLabel(parent=self)
self.hourglass_animation.setAlignment(QtCore.Qt.AlignCenter)
ag_file = "ui:hourglass.gif"
@@ -75,12 +86,32 @@ def __init__(self):
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
# Signals connection
+ self.reset_btn.clicked.connect(self.reset)
+ self.run_btn.clicked.connect(self.run)
+
GS.signals.execution_started.connect(self.hourglass_animation.show)
GS.signals.execution_finished.connect(self.hourglass_animation.hide)
GS.signals.class_scanning_finished.connect(
lambda: self.show_feedback("Finished scanning classes", level=logging.INFO)
)
+ # RUN AND REFRESH ----------------------
+ def run(self):
+ """
+ Run the current scene.
+ """
+ if self.scene():
+ self.scene().run_graphic_scene()
+ GS.signals.attribute_editor_global_refresh_requested.emit()
+
+ def reset(self):
+ """
+ Reset the current scene.
+ """
+ if self.scene():
+ self.scene().reset_graphic_scene()
+ GS.signals.attribute_editor_global_refresh_requested.emit()
+
# UTILITY ----------------------
def show_feedback(self, message, level=logging.INFO):
"""
@@ -138,14 +169,18 @@ def move_search_bar(self, x, y):
# RESIZE EVENTS ----------------------
def resizeEvent(self, event):
- QtWidgets.QGraphicsView.resizeEvent(self, event)
- self.feedback_line.move(25, self.height() - 50)
- self.feedback_line.setFixedSize(self.width(), 30)
-
self.hourglass_animation.move(self.width() - 200, self.height() - 200)
self.hourglass_animation.setFixedSize(200, 200)
self.movie.setScaledSize(QtCore.QSize(200, 200))
+ self.feedback_line.move(25, self.height() - 50)
+ self.feedback_line.setFixedSize(self.width(), 30)
+
+ self.reset_btn.move(self.width() - 380, self.height() - 55)
+ self.run_btn.move(self.width() - 200, self.height() - 55)
+
+ QtWidgets.QGraphicsView.resizeEvent(self, event)
+
# MOUSE EVENTS ----------------------
def mousePressEvent(self, event):
self.middle_pressed = False
@@ -213,7 +248,10 @@ def __init__(self, context=None):
# Nodes
self.all_graphic_nodes = set()
- # PATH TESTS
+ # Annotations
+ self.all_graphic_annotations = set()
+
+ # Path tests
self.testing_graphic_attr = None
self.testing_path = ConnectorLine()
@@ -231,6 +269,16 @@ def drawBackground(self, painter, rect):
if i > rect.y() and i < rect.y() + rect.height():
painter.drawLine(QtCore.QLine(-20_000, i, 20_000, i))
+ # ANNOTATIONS ----------------------
+ def add_annotation_by_type(
+ self, annotation_type: str, x: int = 0, y: int = 0
+ ) -> GeneralGraphicAnnotation:
+ new_annotation = GeneralGraphicAnnotation(annotation_type)
+ new_annotation.setPos(x, y)
+ self.all_graphic_annotations.add(new_annotation)
+ self.addItem(new_annotation)
+ return new_annotation
+
# ADD AND DELETE NODES ----------------------
def add_graphic_node_by_class_name(
self, node_classname: str, x: int = 0, y: int = 0
@@ -244,7 +292,7 @@ def add_graphic_node_by_class_name(
y (int): optional, coords to place the node at
Returns:
- GeneralGraphicNode: newly create node
+ GeneralGraphicNode: newly created node
"""
all_classes = CR.get_all_classes()
for lib in sorted(all_classes):
@@ -265,6 +313,12 @@ def add_graphic_node_by_class_name(
)
return new_graph_node
+ GS.signals.main_screen_feedback.emit(
+ "Could not create graphic node {}".format(node_classname),
+ logging.ERROR,
+ )
+ return None
+
def add_graphic_node_from_logic_node(
self, logic_node, x: int = 0, y: int = 0
) -> GeneralGraphicNode:
@@ -314,6 +368,11 @@ def delete_node(self, graphic_node: GeneralGraphicNode):
del graphic_node
+ def delete_annotation(self, graphic_annotation: GeneralGraphicAnnotation):
+ self.all_graphic_annotations.remove(graphic_annotation)
+ self.removeItem(graphic_annotation)
+ del graphic_annotation
+
# NODE CONNECTIONS/LINES ----------------------
def connect_graphic_attrs(
self,
@@ -380,7 +439,7 @@ def redraw_node_lines(self, node: GeneralGraphicNode):
graphic_attrs = [
c
for c in self.items()
- if c.data(0) == "GRAPHIC_ATTRIBUTE" and c.parent_node == node
+ if c.data(0) == constants.GRAPHIC_ATTRIBUTE and c.parent_node == node
]
for g in graphic_attrs:
@@ -468,6 +527,20 @@ def save_to_file(self, filepath=None):
node_dict[node_name]["y_pos"] = int(g_node.scenePos().y())
break
+ # Add annotations
+ if self.all_graphic_annotations:
+ the_dict["annotations"] = list()
+ annotation_count = 0
+ for ann in self.all_graphic_annotations:
+ annotation_dict = dict()
+ annotation_dict["annotation_type"] = ann.get_type()
+ annotation_dict["x_pos"] = int(ann.scenePos().x())
+ annotation_dict["y_pos"] = int(ann.scenePos().y())
+ if ann.get_text():
+ annotation_dict["text"] = str(ann.get_text())
+ the_dict["annotations"].append(annotation_dict)
+ annotation_count += 1
+
# Save logic scene
self.logic_scene.save_to_file(target_file, the_dict)
@@ -502,6 +575,14 @@ def load_from_file(self, source_file: str, create_logic_nodes=True):
)
break
+ # Create annotations
+ if "annotations" in scene_dict:
+ for ann_dict in scene_dict["annotations"]:
+ new_annotation = self.add_annotation_by_type(
+ ann_dict["annotation_type"], ann_dict["x_pos"], ann_dict["y_pos"]
+ )
+ new_annotation.set_text(ann_dict.get("text", ""))
+
# Connections
all_graphic_attrs = []
for g_node in self.all_graphic_nodes:
@@ -648,16 +729,25 @@ def run_nodes(self, graphic_node_list: list = None):
Args:
graphic_node_list (list): nodes to execute
"""
+ # Check nodes to execute
+ if not graphic_node_list:
+ graphic_node_list = self.selected_nodes()
+
+ if not graphic_node_list:
+ GS.signals.main_screen_feedback.emit(
+ "No nodes selected, nothing to execute", logging.WARNING
+ )
+ return
+
+ # Execution
GS.signals.main_screen_feedback.emit(
"Running only selected node(s)", logging.INFO
)
GS.signals.execution_started.emit()
- if not graphic_node_list:
- graphic_node_list = self.selected_nodes()
- for graphic_node in graphic_node_list:
- logic_node = graphic_node.logic_node
- self.logic_scene.run_list_of_nodes([logic_node])
+ self.logic_scene.run_list_of_nodes(
+ [graphic_node.logic_node for graphic_node in graphic_node_list]
+ )
def reset_nodes(self, graphic_node_list: list = None):
"""
@@ -711,7 +801,8 @@ def toggle_activated_nodes(self, graphic_node_list: list = None):
def add_attr_to_node(self, graphic_node: GeneralGraphicNode):
a_picker = AttributePicker()
- graphic_node.add_single_graphic_attribute(*a_picker.get_results())
+ if a_picker.get_results():
+ graphic_node.add_single_graphic_attribute(*a_picker.get_results())
def export_nodes_code(self, graphic_node_list: list):
if not graphic_node_list:
@@ -771,6 +862,21 @@ def deselect_all(self):
for n in self.all_graphic_nodes:
n.setSelected(False)
+ # ANNOTATION SPECIFIC ----------------------
+ def bring_annotation_to_front(self, annotation_list: list):
+ if not annotation_list:
+ annotation_list = self.selected_annotations()
+
+ for annotation in annotation_list:
+ annotation.setZValue(500)
+
+ def move_annotation_backward(self, annotation_list: list):
+ if not annotation_list:
+ annotation_list = self.selected_annotations()
+
+ for annotation in annotation_list:
+ annotation.setZValue(-500)
+
# SCENE EXECUTION ----------------------
def reset_all_graphic_nodes(self):
"""
@@ -817,21 +923,30 @@ def selected_nodes(self) -> list:
sel_nodes.append(n)
return sel_nodes
+ def selected_annotations(self) -> list:
+ sel_annotations = []
+ for a in self.all_graphic_annotations:
+ if a.isSelected():
+ sel_annotations.append(a)
+ return sel_annotations
+
def fit_in_view(self):
"""
If selected nodes, fit all of them in the view. Otherwise, fit all nodes.
"""
self.parent().resetMatrix()
- nodes = list(self.all_graphic_nodes)
- if self.selected_nodes():
- nodes = self.selected_nodes()
+ nodes = list(self.all_graphic_nodes) + list(self.all_graphic_annotations)
+ if self.selected_nodes() or self.selected_annotations():
+ nodes = list(self.selected_nodes()) + list(self.selected_annotations())
if not nodes:
return
rect = nodes[0].sceneBoundingRect()
for n in nodes:
rect = rect.united(n.sceneBoundingRect())
- self.parent().fitInView(rect, QtCore.Qt.KeepAspectRatio)
+ self.parent().fitInView(
+ rect + QtCore.QMarginsF(20.0, 20.0, 20.0, 20.0), QtCore.Qt.KeepAspectRatio
+ )
# CONTEXT EVENTS ----------------------
def contextMenuEvent(self, event):
@@ -847,6 +962,9 @@ def contextMenuEvent(self, event):
if item and item.data(0) == constants.GRAPHIC_NODE:
self.menu_node(event, item)
break
+ elif item and item.data(0) == constants.GRAPHIC_ANNOTATION:
+ self.menu_annotation(event, item)
+ break
else:
self.menu_scene(event)
@@ -926,6 +1044,29 @@ def menu_node(self, event, node):
# Exec
menu.exec_(event.screenPos(), parent=self)
+ def menu_annotation(self, event, node):
+ menu = QtWidgets.QMenu() # TODO add tooltips
+
+ bring_forward_action = menu.addAction(" Bring annotation to front")
+ bring_forward_action.setIcon(QtGui.QIcon("icons:front.svg"))
+ bring_forward_action.triggered.connect(
+ lambda: self.bring_annotation_to_front([node])
+ )
+
+ move_backward_action = menu.addAction(" Move annotation back")
+ move_backward_action.setIcon(QtGui.QIcon("icons:back.svg"))
+ move_backward_action.triggered.connect(
+ lambda: self.move_annotation_backward([node])
+ )
+
+ # Style
+ f = QtCore.QFile(r"ui:stylesheet.qss") # TODO not ideal, maybe a reduced qss?
+ with open(f.fileName(), "r") as s:
+ menu.setStyleSheet(s.read())
+
+ # Exec
+ menu.exec_(event.screenPos(), parent=self)
+
def menu_scene(self, event):
menu = QtWidgets.QMenu() # TODO add tooltips
@@ -1005,6 +1146,9 @@ def event(self, event: QtWidgets.QGraphicsScene.event):
)
self.delete_node(n)
+ for a in self.selected_annotations():
+ self.delete_annotation(a)
+
# --- Fit in view
elif event.key() == QtCore.Qt.Key_F:
self.fit_in_view()
@@ -1203,9 +1347,9 @@ def dragLeaveEvent(self, event):
event.acceptProposedAction()
def dropEvent(self, event):
+ # TODO make sure this can only happen after GS emits signal of all classes scanned
event_data = event.mimeData()
if event_data.hasUrls():
- # TODO make sure this can only happen after GS emits signal of all classes scanned
event_urls = event_data.urls()
for url in event_urls:
if os.path.isfile(url.toLocalFile()):
diff --git a/graphic/ui/all_nodes.ui b/graphic/ui/all_nodes.ui
index 459e21b..6b9f717 100644
--- a/graphic/ui/all_nodes.ui
+++ b/graphic/ui/all_nodes.ui
@@ -18,50 +18,102 @@
-
+ Qt::Horizontal
-
- 12
-
-
-
-
-
-
-
- 10
-
-
-
- Filter nodes by name...
-
-
-
-
-
-
-
- Arial
- 10
-
-
-
- false
-
-
+
+
+ Qt::Vertical
+
+
+ 8
+
+
+
+
+
+
+
+ 10
+
+
+
+ Filter nodes by name...
+
+
+
+
+
+
+
+ Arial
+ 10
+
+
+
+ false
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 20
+
+
+
+
+
+
+
+
+ 12
+
+
- 1
+ Annotations:
-
-
-
-
+
+
+
+
+
+ 2
+
+
+ true
+
+
+
+ 1
+
+
+
+
+
+
-
+
@@ -74,50 +126,6 @@
-
-
-
-
- 150
- 30
-
-
-
-
- 10
-
-
-
- Run current scene
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
- 150
- 30
-
-
-
- Reset current scene
-
-
-
diff --git a/graphic/ui/stylesheet.qss b/graphic/ui/stylesheet.qss
index 6c4aec7..7b62d0a 100644
--- a/graphic/ui/stylesheet.qss
+++ b/graphic/ui/stylesheet.qss
@@ -74,7 +74,7 @@ QLineEdit
/* ---------------------------- QPushButton ---------------------------- */
QPushButton
{
- background-color: qlineargradient(spread:repeat, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255,255,255,20), stop:0.2 rgba(255,255,255,40), stop:1 rgba(0,0,0,40));
+ background-color: qlineargradient(spread:repeat, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(60,60,60,255), stop:0.2 rgba(100,100,110,255), stop:1 rgba(20,20,20,255));
border:1px solid black;
border-radius: 4px;
padding: 5px;
diff --git a/graphic/widgets/attribute_picker.py b/graphic/widgets/attribute_picker.py
index 8861453..cf96c3e 100644
--- a/graphic/widgets/attribute_picker.py
+++ b/graphic/widgets/attribute_picker.py
@@ -63,26 +63,28 @@ def __init__(self, *args, **kwargs):
self.attribute_types_layout = QtWidgets.QHBoxLayout()
basic_attrs_layout = QtWidgets.QVBoxLayout()
- basic_attrs_layout.addWidget(QtWidgets.QLabel("basic types:"))
+ basic_attrs_layout.addWidget(QtWidgets.QLabel("Basic types:"))
self.basic_attrs_list = QtWidgets.QListWidget()
self.basic_attrs_list.addItems(BASIC_DATATYPES)
basic_attrs_layout.addWidget(self.basic_attrs_list)
+ in_out_layout = QtWidgets.QHBoxLayout()
self.in_checkbox = QtWidgets.QCheckBox(constants.INPUT)
self.in_checkbox.setChecked(True)
- basic_attrs_layout.addWidget(self.in_checkbox)
+ in_out_layout.addWidget(self.in_checkbox)
self.out_checkbox = QtWidgets.QCheckBox(constants.OUTPUT)
- basic_attrs_layout.addWidget(self.out_checkbox)
+ in_out_layout.addWidget(self.out_checkbox)
+ basic_attrs_layout.addLayout(in_out_layout)
self.attribute_types_layout.addLayout(basic_attrs_layout)
inputs_layout = QtWidgets.QVBoxLayout()
- inputs_layout.addWidget(QtWidgets.QLabel("Inputs:"))
+ inputs_layout.addWidget(QtWidgets.QLabel("GUI Inputs:"))
self.inputs_list = QtWidgets.QListWidget()
self.inputs_list.addItems(INPUTS_GUI)
inputs_layout.addWidget(self.inputs_list)
self.attribute_types_layout.addLayout(inputs_layout)
previews_layout = QtWidgets.QVBoxLayout()
- previews_layout.addWidget(QtWidgets.QLabel("Previews:"))
+ previews_layout.addWidget(QtWidgets.QLabel("GUI Previews:"))
self.previews_list = QtWidgets.QListWidget()
self.previews_list.addItems(OUTPUTS_GUI)
previews_layout.addWidget(self.previews_list)
diff --git a/graphic/widgets/main_window.py b/graphic/widgets/main_window.py
index a4b9d62..4594f67 100644
--- a/graphic/widgets/main_window.py
+++ b/graphic/widgets/main_window.py
@@ -7,6 +7,8 @@
import os
from functools import partial
+from pathlib import Path
+
from PySide2 import QtCore
from PySide2 import QtGui
@@ -18,9 +20,7 @@
from all_nodes.graphic.widgets.attribute_editor import AttributeEditor
from all_nodes.graphic.widgets.global_signaler import GlobalSignaler
from all_nodes.graphic.widgets.shortcuts_help import ShortcutsHelp
-
from all_nodes.logic.class_registry import CLASS_REGISTRY as CR
-
from all_nodes import utils
@@ -39,6 +39,9 @@ def __init__(self):
# Add search paths
root_dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
QtCore.QDir.addSearchPath("icons", os.path.join(root_dir_path, "general_icons"))
+ QtCore.QDir.addSearchPath(
+ "graphics", os.path.join(root_dir_path, "general_graphics")
+ )
QtCore.QDir.addSearchPath("ui", os.path.join(root_dir_path, "ui"))
QtCore.QDir.addSearchPath(
"resources", os.path.join(root_dir_path, "../logic/resources")
@@ -70,13 +73,15 @@ def __init__(self):
self.the_process_window.setWindowTitle("Execution feedback")
# ELEMENTS OF THE UI
- self.ui.nodes_tree.setMinimumWidth(260)
+ self.ui.nodes_tree.setMinimumWidth(300)
self.ui.nodes_tree.setDragEnabled(True)
+ self.ui.annotations_tree.setDragEnabled(True)
- self.add_scene()
+ self.ui.splitter_libs.setStretchFactor(0, 8)
+ self.ui.splitter_libs.setStretchFactor(1, 2)
- self.ui.reset_current_btn.setIcon(QtGui.QIcon("icons:reset.png"))
- self.ui.run_current_btn.setIcon(QtGui.QIcon("icons:brain.png"))
+ self.add_scene()
+ self.create_dock_windows()
# STYLESHEET
f = QtCore.QFile(r"ui:stylesheet.qss")
@@ -87,7 +92,6 @@ def __init__(self):
self.make_connections()
# INITIALIZE
- self.create_dock_windows()
self.show()
LOGGER.debug("all_nodes main window created")
@@ -107,8 +111,8 @@ def make_connections(self):
self.ui.tabWidget.currentChanged.connect(self.show_scene_results)
- self.ui.reset_current_btn.clicked.connect(self.reset_current_scene)
- self.ui.run_current_btn.clicked.connect(self.run_current_scene)
+ self.ui.nodes_tree.itemEntered.connect(self.ui.annotations_tree.clearSelection)
+ self.ui.annotations_tree.itemEntered.connect(self.ui.nodes_tree.clearSelection)
# Classes scanning / populating
for worker in CR.get_workers():
@@ -118,6 +122,8 @@ def make_connections(self):
lambda: self.menuBar().setEnabled(True)
)
+ GS.signals.class_scanning_finished.connect(self.populate_annotations)
+
# Global signaler
GS.signals.node_creation_requested.connect(self.add_node_to_current)
@@ -159,6 +165,7 @@ def add_scenes_recursive(entries_dict: dict, menu: QtWidgets.QMenu):
nice_name = key.replace("scene_lib", "").title().replace("_", " ")
libs_menu = menu.addMenu(nice_name)
libs_menu.setIcon(QtGui.QIcon("icons:folder.svg"))
+ libs_menu.setToolTipsVisible(True)
scenes_list = entries_dict[key]
for elem in scenes_list:
if isinstance(elem, dict):
@@ -222,6 +229,23 @@ def create_dock_windows(self):
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.attr_editr_dock)
self.attr_editr_dock.hide()
+ def populate_annotations(self):
+ annotation_items_folder = QtCore.QDir.searchPaths("graphics")[0]
+
+ for note_type_elem in os.listdir(annotation_items_folder):
+ note_type = Path(note_type_elem).stem
+ annotation_item = QtWidgets.QTreeWidgetItem()
+ annotation_item.setIcon(0, QtGui.QIcon(f"graphics:{note_type}.svg"))
+ annotation_item.setText(0, note_type.replace("_", " ").title())
+ annotation_item.setData(0, QtCore.Qt.UserRole, note_type)
+ annotation_item.setData(
+ 0, QtCore.Qt.UserRole + 1, constants.GRAPHIC_ANNOTATION
+ )
+ annotation_item.setFont(0, QtGui.QFont("arial", 14))
+ self.ui.annotations_tree.addTopLevelItem(annotation_item)
+
+ self.ui.annotations_tree.sortByColumn(0, QtCore.Qt.AscendingOrder)
+
def populate_tree(self):
"""
Populate the tree where all nodes are displayed.
@@ -268,6 +292,9 @@ def populate_tree(self):
),
)
class_item.setData(0, QtCore.Qt.UserRole, name)
+ class_item.setData(
+ 0, QtCore.Qt.UserRole + 1, constants.GRAPHIC_NODE
+ )
if (
hasattr(cls, "NICE_NAME") and cls.NICE_NAME
): # TODO inheritance not working here?
@@ -337,6 +364,7 @@ def reload_classes(self):
# Clear the UI
self.menuBar().setEnabled(False)
self.ui.nodes_tree.clear()
+ self.ui.annotations_tree.clear()
self.libraries_added.clear()
# Re-scan classes
@@ -398,9 +426,7 @@ def add_scene(self, context=None):
graphics_scene.dropped_node.connect(
self.add_node_to_current
) # TODO use the GS?
- GS.signals.main_screen_feedback.connect(
- graphics_view.show_feedback
- ) # TODO use the GS?
+ GS.signals.main_screen_feedback.connect(graphics_view.show_feedback)
if context:
graphics_scene.load_from_file(context.CONTEXT_DEFINITION_FILE, False)
@@ -477,14 +503,32 @@ def add_node_to_current(self, pos, class_name=None):
current_gw = self.ui.tabWidget.widget(self.ui.tabWidget.currentIndex())
current_scene = current_gw.scene()
node_class_name = class_name
+ item_type = constants.GRAPHIC_NODE
+
if class_name is None:
- selected = self.ui.nodes_tree.selectedItems()[0]
+ selected_item = (
+ self.ui.nodes_tree.selectedItems()
+ or self.ui.annotations_tree.selectedItems()
+ )
+ if not selected_item:
+ return
+ selected = selected_item[0]
node_class_name = str(selected.data(0, QtCore.Qt.UserRole)).strip()
- current_scene.add_graphic_node_by_class_name(
- node_class_name,
- current_gw.mapToScene(pos).x(),
- current_gw.mapToScene(pos).y(),
- )
+ item_type = str(selected.data(0, QtCore.Qt.UserRole + 1)).strip()
+
+ if item_type == constants.GRAPHIC_NODE:
+ current_scene.add_graphic_node_by_class_name(
+ node_class_name,
+ current_gw.mapToScene(pos).x() - 50,
+ current_gw.mapToScene(pos).y() - 20,
+ )
+
+ elif item_type == constants.GRAPHIC_ANNOTATION:
+ current_scene.add_annotation_by_type(
+ node_class_name,
+ current_gw.mapToScene(pos).x() - 50,
+ current_gw.mapToScene(pos).y() - 50,
+ )
# ATTRIBUTE EDITOR ----------------------
def refresh_node_in_attribute_editor_by_uuid(self, uuid):
@@ -558,6 +602,9 @@ def load_scene(self, source_file):
# Clear scenes
self.clear_tabs()
+ # Clear attr editor
+ self.attr_editor.clear_all()
+
# Add scene and load
self.add_scene()
current_gw = self.ui.tabWidget.widget(self.ui.tabWidget.currentIndex())
diff --git a/graphic/widgets/shortcuts_help.py b/graphic/widgets/shortcuts_help.py
index e99bc63..20444a7 100644
--- a/graphic/widgets/shortcuts_help.py
+++ b/graphic/widgets/shortcuts_help.py
@@ -26,6 +26,7 @@
"Soft-reset selected nodes": "S",
"Delete selected nodes": "Del",
"Fit to view": "F",
+ "Search node class": "Tab",
}
diff --git a/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx b/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx
index 3d4a6f3..56b2d10 100644
--- a/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx
+++ b/lib/base_node_lib/contexts_general_library/EnvironToYmlCtx.ctx
@@ -18,8 +18,8 @@ nodes:
y_pos: -367
- SetStrOutputToCtx_1:
class_name: SetStrOutputToCtx
- x_pos: -176
- y_pos: -27
+ x_pos: -57
+ y_pos: -34
- StrInput_2:
class_name: StrInput
node_attributes:
@@ -42,6 +42,13 @@ connections:
- StrInput_2.out_str -> SetStrOutputToCtx_1.out_parent_attr_name
- TextFileExtensionSelect_2.out_str -> CreateTempFile_1.suffix
+# Annotations section: list of annotations in the scene
+annotations:
+- annotation_type: short_tag
+ text: OUTPUTS
+ x_pos: -77
+ y_pos: 62
-# Context modified at: 2024-07-09 10:04:08.759331
-# Modified by: Jaime
\ No newline at end of file
+
+# Context modified at: 2024-07-24 13:28:38.903619
+# Modified by: jaime.rvq
\ No newline at end of file
diff --git a/lib/basic_examples_scene_lib/fail_scene.yml b/lib/basic_examples_scene_lib/fail_scene.yml
index 83a5d6c..f43e2e5 100644
--- a/lib/basic_examples_scene_lib/fail_scene.yml
+++ b/lib/basic_examples_scene_lib/fail_scene.yml
@@ -49,6 +49,18 @@ connections:
- StrInput_1.out_str -> GetEnvVariable_1.env_variable_name
- TimedNode_1.COMPLETED -> ErrorNode_1.START
+# Annotations section: list of annotations in the scene
+annotations:
+- annotation_type: notepad
+ text: '
-# Scene modified at: 2024-07-02 19:29:35.295502
-# Modified by: Jaime
\ No newline at end of file
+ This var we know that should not work, we will have an error in the next node.'
+ x_pos: -1822
+ y_pos: -409
+- annotation_type: hand
+ x_pos: -675
+ y_pos: -124
+
+
+# Scene modified at: 2024-07-24 13:33:27.841690
+# Modified by: jaime.rvq
\ No newline at end of file
diff --git a/lib/basic_examples_scene_lib/loop_example.yml b/lib/basic_examples_scene_lib/loop_example.yml
index ca560ea..bcb671b 100644
--- a/lib/basic_examples_scene_lib/loop_example.yml
+++ b/lib/basic_examples_scene_lib/loop_example.yml
@@ -21,8 +21,8 @@ nodes:
y_pos: -622
- ForEachBegin_1:
class_name: ForEachBegin
- x_pos: -1986
- y_pos: -475
+ x_pos: -2078
+ y_pos: -490
- ForEachEnd_1:
class_name: ForEachEnd
x_pos: -34
@@ -34,8 +34,8 @@ nodes:
- A
- B
- C
- x_pos: -2393
- y_pos: -550
+ x_pos: -2485
+ y_pos: -555
- PrintToConsole_1:
class_name: PrintToConsole
x_pos: -1543
@@ -66,6 +66,19 @@ connections:
- PrintToConsole_2.COMPLETED -> EmptyNode_1.START
- StrInput_1.out_str -> PrintToConsole_2.in_object_1
+# Annotations section: list of annotations in the scene
+annotations:
+- annotation_type: separator
+ x_pos: -1760
+ y_pos: -734
+- annotation_type: separator
+ x_pos: -176
+ y_pos: -741
+- annotation_type: long_tag
+ text: LOOP BODY
+ x_pos: -1562
+ y_pos: -731
-# Scene modified at: 2024-06-30 22:36:12.141286
-# Modified by: Jaime
\ No newline at end of file
+
+# Scene modified at: 2024-07-25 15:53:52.208486
+# Modified by: jaime.rvq
\ No newline at end of file
diff --git a/logic/class_registry.py b/logic/class_registry.py
index 95da49b..78adb2a 100644
--- a/logic/class_registry.py
+++ b/logic/class_registry.py
@@ -17,6 +17,7 @@
from all_nodes import constants
from all_nodes import utils
+from all_nodes.logic.logic_node import GeneralLogicNode
from all_nodes.graphic.widgets.global_signaler import GlobalSignaler
@@ -93,14 +94,18 @@ def register_node_lib(lib_path):
classes_dict[node_library_name][module_name] = dict()
module_classes = list()
class_counter = 0
- for name, cls in class_members:
- if name in CLASSES_TO_SKIP:
+ for name, cls_object in class_members:
+ if (
+ not issubclass(cls_object, GeneralLogicNode)
+ or cls_object == GeneralLogicNode
+ ):
continue
+
# Icon for this class # TODO Refactor this out
default_icon = node_styles.get(module_name, dict()).get("default_icon")
icon_path = "icons:nodes.svg"
if (
- hasattr(cls, "IS_CONTEXT") and cls.IS_CONTEXT
+ hasattr(cls_object, "IS_CONTEXT") and cls_object.IS_CONTEXT
): # TODO inheritance not working here?
icon_path = "icons:cubes.svg"
if QtCore.QFile.exists(f"icons:{name}.png"):
@@ -112,11 +117,11 @@ def register_node_lib(lib_path):
icon_path = f"icons:{default_icon}.png"
elif QtCore.QFile.exists("icons:" + default_icon + ".svg"):
icon_path = f"icons:{default_icon}.svg"
- setattr(cls, "ICON_PATH", icon_path)
+ setattr(cls_object, "ICON_PATH", icon_path)
# Class name and object
- setattr(cls, "FILEPATH", py_path) # TODO not ideal?
- module_classes.append((name, cls))
+ setattr(cls_object, "FILEPATH", py_path) # TODO not ideal?
+ module_classes.append((name, cls_object))
class_counter += 1
classes_dict[node_library_name][module_name][
@@ -308,7 +313,7 @@ def scan_for_classes(cls):
for future in concurrent.futures.as_completed(futures):
cls._all_classes.update(future.result())
- LOGGER.info(f"Total time scanning classes: {time.time() -t1} s.")
+ LOGGER.info(f"Total time scanning classes: {time.time() -t1}s.")
GS.signals.class_scanning_finished.emit()
def get_all_classes(cls):
@@ -363,7 +368,7 @@ def update_classes_dict(cls):
if not len(cls._lib_workers):
LOGGER.info(
- f"Total time scanning classes: {time.time() - cls._time_start} s."
+ f"Total time scanning classes: {time.time() - cls._time_start}s."
)
GS.signals.class_scanning_finished.emit()
diff --git a/logic/logic_scene.py b/logic/logic_scene.py
index 0b3d8d1..c3869a1 100644
--- a/logic/logic_scene.py
+++ b/logic/logic_scene.py
@@ -237,14 +237,15 @@ def convert_scene_to_dict(self):
"""
scene_dict = dict()
- scene_dict["nodes"] = list()
all_node_names = sorted([n.node_name for n in self.all_logic_nodes])
- for name in all_node_names:
- for node in self.all_logic_nodes:
- if node.node_name == name:
- node_dict = node.get_node_basic_dict()
- scene_dict["nodes"].append(node_dict)
- break
+ if all_node_names:
+ scene_dict["nodes"] = list()
+ for name in all_node_names:
+ for node in self.all_logic_nodes:
+ if node.node_name == name:
+ node_dict = node.get_node_basic_dict()
+ scene_dict["nodes"].append(node_dict)
+ break
connections = set()
for node in self.all_logic_nodes:
@@ -252,7 +253,8 @@ def convert_scene_to_dict(self):
if out_conns:
for c in out_conns:
connections.add(" -> ".join(c))
- scene_dict["connections"] = sorted(list(connections))
+ if connections:
+ scene_dict["connections"] = sorted(list(connections))
return scene_dict
@@ -285,20 +287,29 @@ def save_to_file(self, filepath: str, scene_dict: dict = None) -> None:
)
file.write(header)
file.write("\n# " + "-" * (len(header) - 2))
- file.write("\n# Description: ")
+ file.write("\n# Description: \n")
- file.write(
- "\n\n# Nodes section: overall list of nodes to be created\n" "nodes:\n"
- )
- yaml.dump(scene_dict["nodes"], file, sort_keys=True)
+ if "nodes" in scene_dict:
+ file.write(
+ "\n# Nodes section: overall list of nodes to be created\n"
+ "nodes:\n"
+ )
+ yaml.dump(scene_dict["nodes"], file, sort_keys=True)
- if scene_dict["connections"]:
+ if "connections" in scene_dict:
file.write(
"\n# Connections section: connections to be done between nodes\n"
"connections:\n"
)
yaml.dump(scene_dict["connections"], file)
+ if "annotations" in scene_dict:
+ file.write(
+ "\n# Annotations section: list of annotations in the scene\n"
+ "annotations:\n"
+ )
+ yaml.dump(scene_dict["annotations"], file, sort_keys=True)
+
file.write(
f"\n\n# {file_type.capitalize()} {save_type} at: {datetime.datetime.now()}"
)
@@ -458,7 +469,7 @@ def run_all_nodes_batch(self):
def run_list_of_nodes(self, nodes_to_execute: list, spawn_thread: bool = True):
"""
- Execute a list of nodes.y.
+ Execute a list of nodes.
Parameters:
nodes_to_execute (list): A list of nodes to be executed.