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.