From 8b4f4a355157670468d8907ecc5fe407ba8081bd Mon Sep 17 00:00:00 2001 From: leon-thomm Date: Mon, 4 Dec 2023 21:45:35 +0100 Subject: [PATCH] rework inspector mechanism --- .../inspector_example/__init__.py | 0 .../example_nodes/inspector_example/gui.py | 22 +++ .../example_nodes/inspector_example/nodes.py | 14 ++ ryven-editor/ryven/gui/flow_ui.py | 4 +- ryven-editor/ryven/main/packages/gui_env.py | 32 +--- ryvencore-qt/ryvencore_qt/__init__.py | 3 +- .../ryvencore_qt/src/flows/FlowCommands.py | 6 +- .../src/flows/nodes/InspectorGUI.py | 172 ------------------ .../ryvencore_qt/src/flows/nodes/NodeGUI.py | 19 +- .../src/flows/nodes/NodeInspector.py | 109 +++++++++++ .../src/flows/nodes/WidgetBaseClasses.py | 33 ++++ 11 files changed, 206 insertions(+), 208 deletions(-) create mode 100644 ryven-editor/ryven/example_nodes/inspector_example/__init__.py create mode 100644 ryven-editor/ryven/example_nodes/inspector_example/gui.py create mode 100644 ryven-editor/ryven/example_nodes/inspector_example/nodes.py delete mode 100644 ryvencore-qt/ryvencore_qt/src/flows/nodes/InspectorGUI.py create mode 100644 ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeInspector.py diff --git a/ryven-editor/ryven/example_nodes/inspector_example/__init__.py b/ryven-editor/ryven/example_nodes/inspector_example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ryven-editor/ryven/example_nodes/inspector_example/gui.py b/ryven-editor/ryven/example_nodes/inspector_example/gui.py new file mode 100644 index 00000000..5f68ebc4 --- /dev/null +++ b/ryven-editor/ryven/example_nodes/inspector_example/gui.py @@ -0,0 +1,22 @@ +from ryven.gui_env import * +from qtpy.QtWidgets import ( + QWidget, + QVBoxLayout, + QLabel, +) +from .nodes import MyNode + + +class MyNodeInspector(NodeInspectorWidget, QWidget): + def __init__(self, params): + NodeInspectorWidget.__init__(self, params) + QWidget.__init__(self) + + self.setLayout(QVBoxLayout()) + self.layout().addWidget(QLabel('This is a custom inspector for MyNode.')) + + +@node_gui(MyNode) +class MyNodeGui(NodeGUI): + inspector_widget_class = MyNodeInspector + wrap_inspector_in_default = True diff --git a/ryven-editor/ryven/example_nodes/inspector_example/nodes.py b/ryven-editor/ryven/example_nodes/inspector_example/nodes.py new file mode 100644 index 00000000..a41d3868 --- /dev/null +++ b/ryven-editor/ryven/example_nodes/inspector_example/nodes.py @@ -0,0 +1,14 @@ +from ryven.node_env import * + + +class MyNode(Node): + title = 'My Node' + + +export_nodes( + [MyNode], +) + +@on_gui_load +def on_load(): + from . import gui diff --git a/ryven-editor/ryven/gui/flow_ui.py b/ryven-editor/ryven/gui/flow_ui.py index ee9f61c8..43201fca 100644 --- a/ryven-editor/ryven/gui/flow_ui.py +++ b/ryven-editor/ryven/gui/flow_ui.py @@ -19,7 +19,7 @@ from ryven.gui.code_editor.CodePreviewWidget import CodePreviewWidget from ryven.gui.uic.ui_flow_window import Ui_FlowWindow from ryvencore_qt.src.flows.FlowView import FlowView -from ryvencore_qt.src.flows.nodes.InspectorGUI import BaseNodeInspector, InspectorWidget +from ryvencore_qt.src.flows.nodes.NodeInspector import InspectorView from typing import List @@ -94,7 +94,7 @@ def __init__(self, main_window, flow: Flow, flow_view: FlowView): self.ui.source_dock.setWidget(self.code_preview_widget) # inspector widget - self.inspector_widget = InspectorWidget(self.flow_view) + self.inspector_widget = InspectorView(self.flow_view) self.ui.inspector_dock.setWidget(self.inspector_widget) #undo history widget diff --git a/ryven-editor/ryven/main/packages/gui_env.py b/ryven-editor/ryven/main/packages/gui_env.py index ce3e2368..db107940 100644 --- a/ryven-editor/ryven/main/packages/gui_env.py +++ b/ryven-editor/ryven/main/packages/gui_env.py @@ -2,19 +2,18 @@ This module automatically imports all requirements for Gui definitions of a nodes package. """ -import inspect from typing import Type -import ryven.gui.std_input_widgets as inp_widgets - -from ryvencore_qt import NodeInputWidget, NodeMainWidget, NodeGUI, BaseNodeInspector - from ryvencore import Data, Node, serialize, deserialize from ryvencore.InfoMsgs import InfoMsgs + +from ryvencore_qt import NodeInputWidget, NodeMainWidget, NodeGUI, NodeInspectorWidget + +import ryven.gui.std_input_widgets as inp_widgets from ryven.main.utils import in_gui_mode + __explicit_nodes: set = set() # for protection against setting the gui twice on the same node -__explicit_inspectors: set = set() # same as above def init_node_guis_env(): pass @@ -63,24 +62,3 @@ def register_gui(gui_cls: Type[NodeGUI]): return gui_cls return register_gui - - - -def inspector_gui(node_cls: Type[Node]): - """ - Registers an inspector for a node class. - """ - if not issubclass(node_cls, Node): - raise Exception(f"{node_cls} is not of type {Node}") - - def register_inspector(inspect_cls: Type[BaseNodeInspector]): - if node_cls in __explicit_inspectors: - InfoMsgs.write(f'{node_cls.__name__} has defined an explicit inspector {node_cls.inspector.__name__}') - return - - node_cls.inspector = inspect_cls - __explicit_inspectors.add(inspect_cls) - InfoMsgs.write(f"Registered node inspector: {node_cls} for {inspect_cls}") - return inspect_cls - - return register_inspector \ No newline at end of file diff --git a/ryvencore-qt/ryvencore_qt/__init__.py b/ryvencore-qt/ryvencore_qt/__init__.py index 1854d7cf..c5f38e06 100644 --- a/ryvencore-qt/ryvencore_qt/__init__.py +++ b/ryvencore-qt/ryvencore_qt/__init__.py @@ -11,11 +11,10 @@ from .src.SessionGUI import SessionGUI from .src.flows.nodes.NodeGUI import NodeGUI -from .src.flows.nodes.InspectorGUI import BaseNodeInspector, NodeInspector # customer base classes from ryvencore import Node -from .src.flows.nodes.WidgetBaseClasses import NodeMainWidget, NodeInputWidget +from .src.flows.nodes.WidgetBaseClasses import NodeMainWidget, NodeInputWidget, NodeInspectorWidget # gui classes from .src.widgets import * diff --git a/ryvencore-qt/ryvencore_qt/src/flows/FlowCommands.py b/ryvencore-qt/ryvencore_qt/src/flows/FlowCommands.py index 46bf036e..ea0d9da0 100644 --- a/ryvencore-qt/ryvencore_qt/src/flows/FlowCommands.py +++ b/ryvencore-qt/ryvencore_qt/src/flows/FlowCommands.py @@ -2,15 +2,15 @@ This file contains the implementations of undoable actions for FlowView. """ +from typing import Tuple from qtpy.QtCore import QObject, QPointF from qtpy.QtWidgets import QUndoCommand +from ryvencore.Flow import Flow + from .drawings.DrawingObject import DrawingObject from .nodes.NodeItem import NodeItem -from typing import Tuple -from ryvencore.NodePort import NodePort, NodeInput, NodeOutput -from ryvencore.Flow import Flow from .connections.ConnectionItem import ConnectionItem def undo_text_multi(items:list, command: str, get_text=None): diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/InspectorGUI.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/InspectorGUI.py deleted file mode 100644 index 2dee349b..00000000 --- a/ryvencore-qt/ryvencore_qt/src/flows/nodes/InspectorGUI.py +++ /dev/null @@ -1,172 +0,0 @@ -from abc import ABC, abstractmethod -from qtpy.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit, QScrollArea -from ryvencore import Node -from typing import Union -from ryvencore_qt.src.flows.FlowView import FlowView -from ryvencore_qt.src.flows.nodes.NodeItem import NodeItem -from typing import Type, List - - -class InspectorWidget(QWidget): - """The widget that handles the inspecting""" - - def __init__(self, flow_view: FlowView, parent: QWidget = None): - super().__init__(parent=parent) - self.node: Node = None - self.inspector: BaseNodeInspector = None - self.flow_view = flow_view - - self.setup_ui() - self.flow_view.nodes_selection_changed.connect(self.set_selected_nodes) - - def setup_ui(self): - self.setLayout(QVBoxLayout()) - - def set_selected_nodes(self, nodes: List[Node]): - if len(nodes) == 0: - self.set_node(None) - else: - self.set_node(nodes[-1]) - - def set_node(self, node: Node): - """Sets a node for inspection, if it exists. Otherwise clears the inspector""" - if self.node == node: - return - - if self.inspector: - self.inspector.unload() - self.node = None - self.inspector = None - self.clear() - - node_cls = type(node) - if not node: - return - - self.node = node - if hasattr(node_cls, 'inspector'): - inspect_cls: Type[BaseNodeInspector] = node_cls.inspector - self.inspector = inspect_cls(node, self.flow_view) - else: - self.inspector = NodeInspector(node, self.flow_view) - - inspect_gui = self.inspector.create_inspector(self) - if inspect_gui: - self.layout().addWidget(inspect_gui) - - def clear(self): - """ - A typical method for clearing a layout. This does not - delete the widgets. This is left to the InspectorGUI. - Ideally, one would want to handle the removal there as - well, if the widget is not deleted. - """ - layout = self.layout() - while layout.count(): - item = layout.takeAt(0) - widget = item.widget() - if widget: - widget.setParent(None) - else: - layout.removeItem(item) - - -class BaseNodeInspector(ABC): - """Abstract class for an inspector. Requires the node.""" - - def __init__(self, node: Node, flow_view: FlowView): - self.node: Node = node - self.flow_view: FlowView = flow_view - - @abstractmethod - def create_inspector( - self, parent: QWidget - ) -> Union[QWidget, None]: # should be QWidget | None in 3.10+ - """ - Creates the inspector. The parent is also provided. If the - function returns None, it means parenting has been applied - here. Otherwise, it must be applied externally within the - corresponding application. - - The implementation is left for the user - """ - pass - - def unload(self): - """ - VIRTUAL - This is called when the inspector widget changes the internal - GUI. Should be used for clearing up any Widgets in order to - free up memory. - """ - pass - - def node_item(self): - return self.flow_view.node_items__cache.get(self.node) - - -class NodeInspector(BaseNodeInspector): - """ - This is a basic implementation. Will be used if there is no inspector - provided. Can also be subclassed for easier inspector building. - """ - - def __init__(self, node: Node, flow_view: FlowView): - super().__init__(node, flow_view) - self.inspection_widget = NodeInspectorWidget() - self.inspection_widget.populate(node, self.node_item()) - - def create_inspector(self, parent: QWidget): - parent.layout().addWidget(self.inspection_widget) - self.attach_inspector(self.inspection_widget.inspect_area) - - def attach_inspector(self, parent: QScrollArea): - pass - - def unload(self): - self.inspection_widget.deleteLater() - pass - - -class NodeInspectorWidget(QWidget): - """The basic implementation widget""" - - @staticmethod - def __big_bold_text(txt: str): - return f'{txt}' - - def __init__(self): - super().__init__() - self.setLayout(QVBoxLayout()) - - self.title_label: QLabel = QLabel() - self.layout().addWidget(self.title_label) - - self.inspect_area: QWidget = QWidget() - self.inspect_area.setLayout(QVBoxLayout()) - self.layout().addWidget(self.inspect_area) - - self.description_area: QTextEdit = QTextEdit() - self.description_area.setReadOnly(True) - self.layout().addWidget(self.description_area) - - def populate(self, node: Node, node_item: NodeItem): - # self.title_label.setText(node_item.widget.title_label.title_str) - self.title_label.setText(f'

{node.title}

id: {node.global_id}, pyid: {id(node)}

') - - desc = node.__doc__ if node.__doc__ and node.__doc__ != "" else "No description given" - bbt = NodeInspectorWidget.__big_bold_text - - self.description_area.setText( - f""" - - - {bbt('Title:')} {node.title}
- {bbt('Version:')} {node.version}

- {bbt('Description:')}

- {desc} - - -""" - ) - diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeGUI.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeGUI.py index fc9a1a75..b0c4e0b7 100644 --- a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeGUI.py +++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeGUI.py @@ -3,6 +3,9 @@ from qtpy.QtCore import QObject, Signal +from .WidgetBaseClasses import NodeMainWidget, NodeInputWidget, NodeInspectorWidget +from .NodeInspector import NodeInspectorDefaultWidget + class NodeGUI(QObject): """ @@ -11,9 +14,11 @@ class NodeGUI(QObject): # customizable gui attributes description_html: str = None - main_widget_class: list = None + main_widget_class: Optional[NodeMainWidget] = None main_widget_pos: str = 'below ports' - input_widget_classes: dict = {} + input_widget_classes: Dict[str, NodeInputWidget] = {} + inspector_widget_class: NodeInspectorWidget = NodeInspectorDefaultWidget + wrap_inspector_in_default: bool = False init_input_widgets: dict = {} style: str = 'normal' color: str = '#c69a15' @@ -62,6 +67,16 @@ def __init__(self, params): self.node.input_removed.sub(self._on_input_removed) self.node.output_removed.sub(self._on_output_removed) + # create the inspector widget + inspector_params = (self.node, self) + if self.wrap_inspector_in_default: + self.inspector_widget = NodeInspectorDefaultWidget( + child=self.inspector_widget_class((self.node, self)), + params=inspector_params, + ) + else: + self.inspector_widget = self.inspector_widget_class(inspector_params) + def initialized(self): """ *VIRTUAL* diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeInspector.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeInspector.py new file mode 100644 index 00000000..29059e09 --- /dev/null +++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeInspector.py @@ -0,0 +1,109 @@ +from typing import Union, Type, List, Optional, Tuple + +from qtpy.QtWidgets import ( + QWidget, + QVBoxLayout, + QLabel, + QTextEdit, +) + +from ryvencore import Node + +from .WidgetBaseClasses import NodeInspectorWidget + + +class InspectorView(QWidget): + """ + A widget that can display the inspector of the currently selected node. + """ + + def __init__(self, flow_view, parent: QWidget = None): + super().__init__(parent=parent) + self.node: Node = None + self.inspector_widget: NodeInspectorWidget = None + self.flow_view = flow_view + + self.setup_ui() + self.flow_view.nodes_selection_changed.connect(self.set_selected_nodes) + + def setup_ui(self): + self.setLayout(QVBoxLayout()) + + def set_selected_nodes(self, nodes: List[Node]): + if len(nodes) == 0: + self.set_node(None) + else: + self.set_node(nodes[-1]) + + def set_node(self, node: Node): + """Sets a node for inspection, if it exists. Otherwise clears the inspector""" + + if self.node == node: + return + + if self.inspector_widget: + self.inspector_widget.unload() + self.node = None + self.inspector_widget = None + self.clear() + + if node is not None: + self.node = node + self.layout().addWidget(self.node.gui.inspector_widget) + + def clear(self): + """Clears the layout. This does not delete the widgets.""" + + layout = self.layout() + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget: + widget.setParent(None) + else: + layout.removeItem(item) + + +class NodeInspectorDefaultWidget(NodeInspectorWidget, QWidget): + """ + Default node inspector widget implementation. + Can also be extended by embedding a custom widget. + """ + + @staticmethod + def _big_bold_text(txt: str): + return f'{txt}' + + def __init__(self, params, child: Optional[NodeInspectorWidget] = None): + QWidget.__init__(self) + NodeInspectorWidget.__init__(self, params) + + self.setLayout(QVBoxLayout()) + + self.title_label: QLabel = QLabel() + self.title_label.setText( + f'

{self.node.title}

' + f'

id: {self.node.global_id}, pyid: {id(self.node)}

' + ) + self.layout().addWidget(self.title_label) + + if child: + self.layout().addWidget(child) + + desc = self.node.__doc__ if self.node.__doc__ and self.node.__doc__ != "" else "No description given" + bbt = NodeInspectorDefaultWidget._big_bold_text + + self.description_area: QTextEdit = QTextEdit() + self.description_area.setReadOnly(True) + self.description_area.setText(f""" + + + {bbt('Title:')} {self.node.title}
+ {bbt('Version:')} {self.node.version}

+ {bbt('Description:')}

+ {desc} + + + """) + + self.layout().addWidget(self.description_area) diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py index 0e66b69c..f31abfe8 100644 --- a/ryvencore-qt/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py +++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py @@ -83,3 +83,36 @@ def update_node(self): def update_node_shape(self): self.node_gui.update_shape() + + +class NodeInspectorWidget: + """Base class for the inspector widget of a node.""" + + def __init__(self, params): + self.node, self.node_gui = params + + def get_state(self) -> dict: + """ + *VIRTUAL* + + Return the state of the widget, in a (pickle) serializable format. + """ + data = {} + return data + + def set_state(self, data: dict): + """ + *VIRTUAL* + + Set the state of the widget, where data corresponds to the dict + returned by get_state(). + """ + pass + + def load(self): + """Called when the inspector is loaded into the inspector view in the editor.""" + pass + + def unload(self): + """Called when the inspector is removed from the inspector view in the editor.""" + pass