From bf4adc969fea573d07de782ba768df657890e5cf Mon Sep 17 00:00:00 2001 From: jchan Date: Sun, 27 Mar 2022 18:16:35 +1300 Subject: [PATCH 1/6] doc string update --- NodeGraphQt/nodes/group_node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NodeGraphQt/nodes/group_node.py b/NodeGraphQt/nodes/group_node.py index 595c7891..8e03050a 100644 --- a/NodeGraphQt/nodes/group_node.py +++ b/NodeGraphQt/nodes/group_node.py @@ -10,6 +10,8 @@ class GroupNode(BaseNode): """ + `Implemented in` ``v0.2.0`` + The ``NodeGraphQt.GroupNode`` class extends from the :class:``NodeGraphQt.BaseNode`` class with the ability to nest other nodes inside of it. From eaa8acb8ed51947300b81ae1c494caedab3f66a2 Mon Sep 17 00:00:00 2001 From: jchan Date: Sun, 3 Apr 2022 07:38:06 +1200 Subject: [PATCH 2/6] wip dynamic layout switching. --- NodeGraphQt/base/graph.py | 43 +++++-- NodeGraphQt/constants.py | 37 +++--- NodeGraphQt/qgraphics/node_base.py | 186 ++++++++++++++++++++++++----- NodeGraphQt/qgraphics/pipe.py | 12 +- NodeGraphQt/widgets/viewer.py | 22 +++- 5 files changed, 244 insertions(+), 56 deletions(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index caa27ad6..92fb0c6f 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -17,9 +17,12 @@ from NodeGraphQt.base.node import NodeObject from NodeGraphQt.base.port import Port from NodeGraphQt.constants import ( - NODE_LAYOUT_DIRECTION, NODE_LAYOUT_HORIZONTAL, NODE_LAYOUT_VERTICAL, + NODE_LAYOUT_DIRECTION, + NODE_LAYOUT_HORIZONTAL, + NODE_LAYOUT_VERTICAL, + URI_SCHEME, + URN_SCHEME, PipeLayoutEnum, - URI_SCHEME, URN_SCHEME, PortTypeEnum, ViewerEnum ) @@ -786,6 +789,26 @@ def set_pipe_style(self, style=PipeLayoutEnum.CURVED.value): style = style if 0 <= style <= pipe_max else PipeLayoutEnum.CURVED.value self._viewer.set_pipe_layout(style) + def set_layout_direction(self, direction): + """ + Sets the node graph layout direction to horizontal or vertical. + + Note: + By default node graph direction is set to "NODE_LAYOUT_HORIZONTAL". + + Node Graph Layout Types: + + * :attr:`NodeGraphQt.constants.NODE_LAYOUT_HORIZONTAL` + * :attr:`NodeGraphQt.constants.NODE_LAYOUT_VERTICAL` + + Args: + direction (int): layout direction. + """ + direction_types = [NODE_LAYOUT_HORIZONTAL, NODE_LAYOUT_VERTICAL] + if direction not in direction_types: + direction = NODE_LAYOUT_HORIZONTAL + self._viewer.set_layout_direction(direction) + def fit_to_selection(self): """ Sets the zoom level to fit selected nodes. @@ -1672,7 +1695,9 @@ def auto_layout_nodes(self, nodes=None, down_stream=True, start_nodes=None): else: rank_map[rank] = [node] - if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: + node_layout_direction = self._viewer.get_layout_direction() + + if node_layout_direction is NODE_LAYOUT_HORIZONTAL: current_x = 0 node_height = 120 for rank in sorted(range(len(rank_map)), reverse=not down_stream): @@ -1687,7 +1712,7 @@ def auto_layout_nodes(self, nodes=None, down_stream=True, start_nodes=None): current_y += dy * 0.5 + 10 current_x += max_width * 0.5 + 100 - elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: + elif node_layout_direction is NODE_LAYOUT_VERTICAL: current_y = 0 node_width = 250 for rank in sorted(range(len(rank_map)), reverse=not down_stream): @@ -1953,6 +1978,8 @@ def _build_port_nodes(self): Returns: tuple(dict, dict): input nodes, output nodes. """ + node_layout_direction = self._viewer.get_layout_direction() + # build the parent input port nodes. input_nodes = {n.name(): n for n in self.get_nodes_by_type(PortInputNode.type_)} @@ -1965,9 +1992,9 @@ def _build_port_nodes(self): input_nodes[port.name()] = input_node self.add_node(input_node, selected=False, push_undo=False) x, y = input_node.pos() - if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: + if node_layout_direction is NODE_LAYOUT_HORIZONTAL: x -= 100 - elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: + elif node_layout_direction is NODE_LAYOUT_VERTICAL: y -= 100 input_node.set_property('pos', [x, y], push_undo=False) @@ -1983,9 +2010,9 @@ def _build_port_nodes(self): output_nodes[port.name()] = output_node self.add_node(output_node, selected=False, push_undo=False) x, y = output_node.pos() - if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: + if node_layout_direction is NODE_LAYOUT_HORIZONTAL: x += 100 - elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: + elif node_layout_direction is NODE_LAYOUT_VERTICAL: y += 100 output_node.set_property('pos', [x, y], push_undo=False) diff --git a/NodeGraphQt/constants.py b/NodeGraphQt/constants.py index ca70c7be..ff7b91a4 100644 --- a/NodeGraphQt/constants.py +++ b/NodeGraphQt/constants.py @@ -17,38 +17,24 @@ URI_SCHEME = 'nodegraphqt://' URN_SCHEME = 'nodegraphqt::' -# === PATHS === - +# PATHS BASE_PATH = os.path.dirname(os.path.abspath(__file__)) ICON_PATH = os.path.join(BASE_PATH, 'widgets', 'icons') ICON_DOWN_ARROW = os.path.join(ICON_PATH, 'down_arrow.png') ICON_NODE_BASE = os.path.join(ICON_PATH, 'node_base.png') -# === DRAW STACK ORDER === - +# DRAW STACK ORDER Z_VAL_PIPE = -1 Z_VAL_NODE = 1 Z_VAL_PORT = 2 Z_VAL_NODE_WIDGET = 3 -# === ITEM CACHE MODE === - +# ITEM CACHE MODE # QGraphicsItem.NoCache # QGraphicsItem.DeviceCoordinateCache # QGraphicsItem.ItemCoordinateCache - ITEM_CACHE_MODE = QtWidgets.QGraphicsItem.DeviceCoordinateCache -# === NODE LAYOUT DIRECTION === - -#: Mode for vertical node layout. -NODE_LAYOUT_VERTICAL = 0 -#: Mode for horizontal node layout. -NODE_LAYOUT_HORIZONTAL = 1 -#: Variable for setting the node layout direction. -# NODE_LAYOUT_DIRECTION = NODE_LAYOUT_VERTICAL -NODE_LAYOUT_DIRECTION = NODE_LAYOUT_HORIZONTAL - # =================================== GLOBAL =================================== @@ -66,6 +52,23 @@ class VersionEnum(Enum): #: PATCH = int(_v.split('.')[2]) + +class LayoutDirectionEnum(Enum): + """ + Node graph nodes layout direction: + :py:mod:`NodeGraphQt.constants.ViewerLayoutEnum` + """ + #: layout nodes top to bottom. + VERTICAL = 0 + #: layout nodes left to right. + HORIZONTAL = 1 + + +#: Variable for setting the node layout direction. +# NODE_LAYOUT_DIRECTION = LayoutDirectionEnum.VERTICAL.value +NODE_LAYOUT_DIRECTION = LayoutDirectionEnum.HORIZONTAL.value + + # =================================== VIEWER =================================== diff --git a/NodeGraphQt/qgraphics/node_base.py b/NodeGraphQt/qgraphics/node_base.py index 9bb8dc4c..ae781a31 100644 --- a/NodeGraphQt/qgraphics/node_base.py +++ b/NodeGraphQt/qgraphics/node_base.py @@ -253,17 +253,7 @@ def reset_pipes(self): for pipe in port.connected_pipes: pipe.reset() - def calc_size(self, add_w=0.0, add_h=0.0): - """ - Calculates the minimum node size. - - Args: - add_w (float): additional width. - add_h (float): additional height. - - Returns: - tuple(float, float): width, height. - """ + def _calc_size_horizontal(self): # width, height from node name text. text_w = self._text_item.boundingRect().width() text_h = self._text_item.boundingRect().height() @@ -321,46 +311,119 @@ def calc_size(self, add_w=0.0, add_h=0.0): # add bottom margin for node widget. height += 4.0 height *= 1.05 + return width, height - # additional width, height. - width += add_w - height += add_h + def _calc_size_vertical(self): + p_input_width = 0.0 + p_output_width = 0.0 + p_input_height = 0.0 + p_output_height = 0.0 + for port in self._input_items.keys(): + if port.isVisible(): + p_input_width += port.boundingRect().width() + if not p_input_height: + p_input_height = port.boundingRect().height() + for port in self._output_items.keys(): + if port.isVisible(): + p_output_width += port.boundingRect().width() + if not p_output_height: + p_output_height = port.boundingRect().height() + + widget_width = 0.0 + widget_height = 0.0 + for widget in self._widgets.values(): + if widget.boundingRect().width() > widget_width: + widget_width = widget.boundingRect().width() + widget_height += widget.boundingRect().height() + + width = max([p_input_width, p_output_width, widget_width]) + add_w + height = p_input_height + p_output_height + widget_height + add_h return width, height - def align_icon(self, h_offset=0.0, v_offset=0.0): + def calc_size(self, add_w=0.0, add_h=0.0): """ - Align node icon to the default top left of the node. + Calculates the minimum node size. Args: - v_offset (float): additional vertical offset. - h_offset (float): additional horizontal offset. + add_w (float): additional width. + add_h (float): additional height. + + Returns: + tuple(float, float): width, height. """ + layout_direction = self.viewer().get_layout_direction() + if layout_direction is NODE_LAYOUT_HORIZONTAL: + width, height = self._calc_size_horizontal() + elif layout_direction is NODE_LAYOUT_VERTICAL: + width, height = self._calc_size_vertical() + else: + raise RuntimeError('Node graph layout direction not valid!') + + # additional width, height. + width += add_w + height += add_h + return width, height + + def _align_icon_horizontal(self, h_offset, v_offset): icon_rect = self._icon_item.boundingRect() text_rect = self._text_item.boundingRect() x = self.boundingRect().left() + 2.0 y = text_rect.center().y() - (icon_rect.height() / 2) self._icon_item.setPos(x + h_offset, y + v_offset) - def align_label(self, h_offset=0.0, v_offset=0.0): + def _align_icon_vertical(self, h_offset, v_offset): + center_y = self.boundingRect().center().y() + icon_rect = self._icon_item.boundingRect() + text_rect = self._text_item.boundingRect() + x = self.boundingRect().right() + h_offset + y = center_y - text_rect.height() - (icon_rect.height() / 2) + v_offset + self._icon_item.setPos(x, y) + + def align_icon(self, h_offset=0.0, v_offset=0.0): """ - Center node label text to the top of the node. + Align node icon to the default top left of the node. Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. + v_offset (float): additional vertical offset. + h_offset (float): additional horizontal offset. """ + layout_direction = self.viewer().get_layout_direction() + if layout_direction is NODE_LAYOUT_HORIZONTAL: + self._align_icon_horizontal(h_offset, v_offset) + elif layout_direction is NODE_LAYOUT_VERTICAL: + self._align_icon_vertical(h_offset, v_offset) + else: + raise RuntimeError('Node graph layout direction not valid!') + + def _align_label_horizontal(self, h_offset, v_offset): rect = self.boundingRect() text_rect = self._text_item.boundingRect() x = rect.center().x() - (text_rect.width() / 2) self._text_item.setPos(x + h_offset, rect.y() + v_offset) - def align_widgets(self, v_offset=0.0): + def _align_label_vertical(self, h_offset, v_offset): + rect = self._text_item.boundingRect() + x = self.boundingRect().right() + h_offset + y = self.boundingRect().center().y() - (rect.height() / 2) + v_offset + self.text_item.setPos(x, y) + + def align_label(self, h_offset=0.0, v_offset=0.0): """ - Align node widgets to the default center of the node. + Center node label text to the top of the node. Args: v_offset (float): vertical offset. + h_offset (float): horizontal offset. """ + layout_direction = self.viewer().get_layout_direction() + if layout_direction is NODE_LAYOUT_HORIZONTAL: + self._align_label_horizontal(h_offset, v_offset) + elif layout_direction is NODE_LAYOUT_VERTICAL: + self._align_label_vertical(h_offset, v_offset) + else: + raise RuntimeError('Node graph layout direction not valid!') + + def _align_widgets_horizontal(self, v_offset): if not self._widgets: return rect = self.boundingRect() @@ -381,13 +444,40 @@ def align_widgets(self, v_offset=0.0): widget.setPos(x, y) y += widget_rect.height() - def align_ports(self, v_offset=0.0): + def _align_widgets_vertical(self, v_offset): + if not self._widgets: + return + rect = self.boundingRect() + y = rect.center().y() + v_offset + widget_height = 0.0 + for widget in self._widgets.values(): + widget_rect = widget.boundingRect() + widget_height += widget_rect.height() + y -= widget_height / 2 + + for widget in self._widgets.values(): + widget_rect = widget.boundingRect() + x = rect.center().x() - (widget_rect.width() / 2) + widget.widget().setTitleAlign('center') + widget.setPos(x, y) + y += widget_rect.height() + + def align_widgets(self, v_offset=0.0): """ - Align input, output ports in the node layout. + Align node widgets to the default center of the node. Args: - v_offset (float): port vertical offset. + v_offset (float): vertical offset. """ + layout_direction = self.viewer().get_layout_direction() + if layout_direction is NODE_LAYOUT_HORIZONTAL: + self._align_widget_horizontal(v_offset) + elif layout_direction is NODE_LAYOUT_VERTICAL: + self._align_widget_vertical(v_offset) + else: + raise RuntimeError('Node graph layout direction not valid!') + + def _align_ports_horizontal(self, v_offset): width = self._width txt_offset = PortEnum.CLICK_FALLOFF.value - 2 spacing = 1 @@ -425,6 +515,48 @@ def align_ports(self, v_offset=0.0): txt_x = port.x() - txt_width text.setPos(txt_x, port.y() - 1.5) + def _align_ports_vertical(self, v_offset): + # adjust input position + inputs = [p for p in self.inputs if p.isVisible()] + if inputs: + port_width = inputs[0].boundingRect().width() + port_height = inputs[0].boundingRect().height() + half_width = port_width / 2 + delta = self._width / (len(inputs) + 1) + port_x = delta + port_y = (port_height / 2) * -1 + for port in inputs: + port.setPos(port_x - half_width, port_y) + port_x += delta + + # adjust output position + outputs = [p for p in self.outputs if p.isVisible()] + if outputs: + port_width = outputs[0].boundingRect().width() + port_height = outputs[0].boundingRect().height() + half_width = port_width / 2 + delta = self._width / (len(outputs) + 1) + port_x = delta + port_y = self._height - (port_height / 2) + for port in outputs: + port.setPos(port_x - half_width, port_y) + port_x += delta + + def align_ports(self, v_offset=0.0): + """ + Align input, output ports in the node layout. + + Args: + v_offset (float): port vertical offset. + """ + layout_direction = self.viewer().get_layout_direction() + if layout_direction is NODE_LAYOUT_HORIZONTAL: + self._align_ports_horizontal(v_offset) + elif layout_direction is NODE_LAYOUT_VERTICAL: + self._align_ports_vertical(v_offset) + else: + raise RuntimeError('Node graph layout direction not valid!') + def draw_node(self): """ Re-draw the node item in the scene. diff --git a/NodeGraphQt/qgraphics/pipe.py b/NodeGraphQt/qgraphics/pipe.py index 29beada0..902f1079 100644 --- a/NodeGraphQt/qgraphics/pipe.py +++ b/NodeGraphQt/qgraphics/pipe.py @@ -7,7 +7,8 @@ PipeEnum, PipeLayoutEnum, PortTypeEnum, Z_VAL_PIPE, Z_VAL_NODE_WIDGET, ITEM_CACHE_MODE, - NODE_LAYOUT_VERTICAL, NODE_LAYOUT_HORIZONTAL, + NODE_LAYOUT_VERTICAL, + NODE_LAYOUT_HORIZONTAL, NODE_LAYOUT_DIRECTION ) from NodeGraphQt.qgraphics.port import PortItem @@ -262,9 +263,9 @@ def draw_path(self, start_port, end_port=None, cursor_pos=None): self.setPath(path) return else: - if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: + if self.viewer_layout_direction() is NODE_LAYOUT_VERTICAL: self.__draw_path_vertical(start_port, pos1, pos2, path) - elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: + elif self.viewer_layout_direction() is NODE_LAYOUT_HORIZONTAL: self.__draw_path_horizontal(start_port, pos1, pos2, path) def reset_path(self): @@ -293,6 +294,11 @@ def viewer_pipe_layout(self): viewer = self.scene().viewer() return viewer.get_pipe_layout() + def viewer_layout_direction(self): + if self.scene(): + viewer = self.scene().viewer() + return viewer.get_layout_direction() + def activate(self): self._active = True color = QtGui.QColor(*PipeEnum.ACTIVE_COLOR.value) diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index 6fc3e744..43642ce4 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -71,6 +71,8 @@ def __init__(self, parent=None, undo_stack=None): self._update_scene() self._last_size = self.size() + self._layout_direction = NODE_LAYOUT_HORIZONTAL + self._pipe_layout = PipeLayoutEnum.CURVED.value self._detached_port = None self._start_port = None @@ -1153,12 +1155,30 @@ def set_pipe_layout(self, layout): Sets the pipe layout mode and redraw all pipe items in the scene. Args: - layout (int): pipe layout mode. (see the contants module) + layout (int): pipe layout mode. (see the constants module) """ self._pipe_layout = layout for pipe in self.all_pipes(): pipe.draw_path(pipe.input_port, pipe.output_port) + def get_layout_direction(self): + """ + Get the layout direction of the node graph. + + Returns: + int: graph layout mode. + """ + return self._layout_direction + + def set_layout_direction(self, direction): + """ + Sets the node graph layout direction. + + Args: + direction (int): graph layout direction. (see the constants module) + """ + self._layout_direction = direction + def reset_zoom(self, cent=None): """ Reset the viewer zoom level. From d75dff2d18cea9b9985c921793f65dfa15b7bdca Mon Sep 17 00:00:00 2001 From: jchan Date: Fri, 7 Oct 2022 22:10:44 +1300 Subject: [PATCH 3/6] Dynamic graph layout switching. --- NodeGraphQt/base/commands.py | 5 + NodeGraphQt/base/graph.py | 42 ++- NodeGraphQt/base/graph_actions.py | 36 +- NodeGraphQt/base/model.py | 5 + NodeGraphQt/base/node.py | 61 ++-- NodeGraphQt/constants.py | 11 +- NodeGraphQt/nodes/backdrop_node.py | 10 +- NodeGraphQt/nodes/base_node.py | 44 +-- NodeGraphQt/nodes/group_node.py | 14 +- NodeGraphQt/nodes/port_node.py | 31 +- NodeGraphQt/pkg_info.py | 2 +- NodeGraphQt/qgraphics/node_abstract.py | 16 +- NodeGraphQt/qgraphics/node_base.py | 452 +++++++++---------------- NodeGraphQt/qgraphics/node_group.py | 270 +++++---------- NodeGraphQt/qgraphics/node_port_in.py | 179 ++++------ NodeGraphQt/qgraphics/node_port_out.py | 188 ++++------ NodeGraphQt/qgraphics/pipe.py | 21 +- NodeGraphQt/widgets/viewer.py | 18 +- 18 files changed, 583 insertions(+), 822 deletions(-) diff --git a/NodeGraphQt/base/commands.py b/NodeGraphQt/base/commands.py index 184245b6..0fd3c4dd 100644 --- a/NodeGraphQt/base/commands.py +++ b/NodeGraphQt/base/commands.py @@ -121,6 +121,11 @@ def redo(self): self.model.nodes[self.node.id] = self.node self.viewer.add_node(self.node.view, self.pos) + # node width & height is calculated when its added to the scene + # so we have to update the node model here. + self.node.model.width = self.node.view.width + self.node.model.height = self.node.view.height + class NodeRemovedCmd(QtWidgets.QUndoCommand): """ diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index 92fb0c6f..6ceca248 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -17,11 +17,9 @@ from NodeGraphQt.base.node import NodeObject from NodeGraphQt.base.port import Port from NodeGraphQt.constants import ( - NODE_LAYOUT_DIRECTION, - NODE_LAYOUT_HORIZONTAL, - NODE_LAYOUT_VERTICAL, URI_SCHEME, URN_SCHEME, + LayoutDirectionEnum, PipeLayoutEnum, PortTypeEnum, ViewerEnum @@ -789,9 +787,20 @@ def set_pipe_style(self, style=PipeLayoutEnum.CURVED.value): style = style if 0 <= style <= pipe_max else PipeLayoutEnum.CURVED.value self._viewer.set_pipe_layout(style) + def layout_direction(self): + """ + Return the current node graph layout direction. + + Returns: + int: layout direction. + """ + return self.model.layout_direction + def set_layout_direction(self, direction): """ Sets the node graph layout direction to horizontal or vertical. + This function will also override the layout direction on all + nodes in the current node graph. Note: By default node graph direction is set to "NODE_LAYOUT_HORIZONTAL". @@ -804,9 +813,12 @@ def set_layout_direction(self, direction): Args: direction (int): layout direction. """ - direction_types = [NODE_LAYOUT_HORIZONTAL, NODE_LAYOUT_VERTICAL] + direction_types = [e.value for e in LayoutDirectionEnum] if direction not in direction_types: - direction = NODE_LAYOUT_HORIZONTAL + direction = LayoutDirectionEnum.HORIZONTAL.value + self._model.layout_direction = direction + for node in self.all_nodes(): + node.set_layout_direction(direction) self._viewer.set_layout_direction(direction) def fit_to_selection(self): @@ -945,6 +957,9 @@ def format_color(clr): if pos: node.model.pos = [float(pos[0]), float(pos[1])] + # initial node direction layout. + node.model.layout_direction = self.layout_direction() + node.update() if push_undo: @@ -987,6 +1002,11 @@ def add_node(self, node, pos=None, selected=True, push_undo=True): node.NODE_NAME = self.get_unique_name(node.NODE_NAME) node.model._graph_model = self.model node.model.name = node.NODE_NAME + + # initial node direction layout. + node.model.layout_direction = self.layout_direction() + + # update method must be called before it's been added to the viewer. node.update() if push_undo: @@ -1697,7 +1717,7 @@ def auto_layout_nodes(self, nodes=None, down_stream=True, start_nodes=None): node_layout_direction = self._viewer.get_layout_direction() - if node_layout_direction is NODE_LAYOUT_HORIZONTAL: + if node_layout_direction is LayoutDirectionEnum.HORIZONTAL.value: current_x = 0 node_height = 120 for rank in sorted(range(len(rank_map)), reverse=not down_stream): @@ -1712,7 +1732,7 @@ def auto_layout_nodes(self, nodes=None, down_stream=True, start_nodes=None): current_y += dy * 0.5 + 10 current_x += max_width * 0.5 + 100 - elif node_layout_direction is NODE_LAYOUT_VERTICAL: + elif node_layout_direction is LayoutDirectionEnum.VERTICAL.value: current_y = 0 node_width = 250 for rank in sorted(range(len(rank_map)), reverse=not down_stream): @@ -1992,9 +2012,9 @@ def _build_port_nodes(self): input_nodes[port.name()] = input_node self.add_node(input_node, selected=False, push_undo=False) x, y = input_node.pos() - if node_layout_direction is NODE_LAYOUT_HORIZONTAL: + if node_layout_direction is LayoutDirectionEnum.HORIZONTAL.value: x -= 100 - elif node_layout_direction is NODE_LAYOUT_VERTICAL: + elif node_layout_direction is LayoutDirectionEnum.VERTICAL.value: y -= 100 input_node.set_property('pos', [x, y], push_undo=False) @@ -2010,9 +2030,9 @@ def _build_port_nodes(self): output_nodes[port.name()] = output_node self.add_node(output_node, selected=False, push_undo=False) x, y = output_node.pos() - if node_layout_direction is NODE_LAYOUT_HORIZONTAL: + if node_layout_direction is LayoutDirectionEnum.HORIZONTAL.value: x += 100 - elif node_layout_direction is NODE_LAYOUT_VERTICAL: + elif node_layout_direction is LayoutDirectionEnum.VERTICAL.value: y += 100 output_node.set_property('pos', [x, y], push_undo=False) diff --git a/NodeGraphQt/base/graph_actions.py b/NodeGraphQt/base/graph_actions.py index b77fe3d6..7970db08 100644 --- a/NodeGraphQt/base/graph_actions.py +++ b/NodeGraphQt/base/graph_actions.py @@ -8,11 +8,11 @@ def build_context_menu(graph): graph (NodeGraphQt.NodeGraph): node graph controller. """ from Qt import QtGui, QtCore - graph_menu = graph.get_context_menu('graph') + context_menu = graph.get_context_menu('graph') # "File" menu. # -------------------------------------------------------------------------- - file_menu = graph_menu.add_menu('&File') + file_menu = context_menu.add_menu('&File') file_menu.add_command('Open...', _open_session, QtGui.QKeySequence.Open) file_menu.add_command('Import...', _import_session) @@ -22,7 +22,7 @@ def build_context_menu(graph): # "Edit" menu. # -------------------------------------------------------------------------- - edit_menu = graph_menu.add_menu('&Edit') + edit_menu = context_menu.add_menu('&Edit') edit_menu.add_separator() edit_menu.add_command('Clear Undo History', _clear_undo) @@ -49,20 +49,24 @@ def build_context_menu(graph): edit_menu.add_command('Zoom Out', _zoom_out, '-') edit_menu.add_command('Reset Zoom', _reset_zoom, 'H') - edit_menu.add_separator() + context_menu.add_separator() - # "Grid Mode" submenu. + # "Node" menu. # -------------------------------------------------------------------------- - bg_menu = edit_menu.add_menu('&Grid Mode') + graph_menu = context_menu.add_menu('&Graph') + + bg_menu = graph_menu.add_menu('&Background') bg_menu.add_command('None', _bg_grid_none) bg_menu.add_command('Lines', _bg_grid_lines) bg_menu.add_command('Dots', _bg_grid_dots) - edit_menu.add_separator() + layout_menu = graph_menu.add_menu('&Layout') + layout_menu.add_command('Horizontal', _layout_h_mode) + layout_menu.add_command('Vertical', _layout_v_mode) # "Node" menu. # -------------------------------------------------------------------------- - node_menu = graph_menu.add_menu('&Nodes') + node_menu = context_menu.add_menu('&Nodes') node_menu.add_command('Node Search', _toggle_node_search, 'Tab') node_menu.add_separator() node_menu.add_command( @@ -76,7 +80,7 @@ def build_context_menu(graph): # "Pipe" menu. # -------------------------------------------------------------------------- - pipe_menu = graph_menu.add_menu('&Pipes') + pipe_menu = context_menu.add_menu('&Pipes') pipe_menu.add_command('Curved', _curved_pipe) pipe_menu.add_command('Straight', _straight_pipe) pipe_menu.add_command('Angle', _angle_pipe) @@ -109,6 +113,20 @@ def _reset_zoom(graph): graph.reset_zoom() +def _layout_h_mode(graph): + """ + Set node graph layout direction to horizontal. + """ + graph.set_layout_direction(0) + + +def _layout_v_mode(graph): + """ + Set node graph layout direction to vertical. + """ + graph.set_layout_direction(1) + + def _open_session(graph): """ Prompts a file open dialog to load a session. diff --git a/NodeGraphQt/base/model.py b/NodeGraphQt/base/model.py index ac1d8710..5bd59e04 100644 --- a/NodeGraphQt/base/model.py +++ b/NodeGraphQt/base/model.py @@ -3,6 +3,7 @@ from collections import defaultdict from NodeGraphQt.constants import ( + LayoutDirectionEnum, NODE_PROP, NODE_PROP_QLABEL, NODE_PROP_QLINEEDIT, @@ -73,6 +74,7 @@ def __init__(self): self.width = 100.0 self.height = 80.0 self.pos = [0.0, 0.0] + self.layout_direction = LayoutDirectionEnum.HORIZONTAL.value # BaseNode attrs. self.inputs = {} @@ -107,6 +109,7 @@ def __init__(self): 'width': NODE_PROP, 'height': NODE_PROP, 'pos': NODE_PROP, + 'layout_direction': NODE_PROP, 'inputs': NODE_PROP, 'outputs': NODE_PROP, } @@ -230,6 +233,7 @@ def to_dict(self): 'width': 0.0, 'height: 0.0, 'pos': (0.0, 0.0), + 'layout_direction': 0, 'custom': {}, 'inputs': { : {: [, ]} @@ -317,6 +321,7 @@ def __init__(self): self.session = '' self.acyclic = True self.pipe_collision = False + self.layout_direction = LayoutDirectionEnum.HORIZONTAL.value def common_properties(self): """ diff --git a/NodeGraphQt/base/node.py b/NodeGraphQt/base/node.py index 57fb4340..520ab25f 100644 --- a/NodeGraphQt/base/node.py +++ b/NodeGraphQt/base/node.py @@ -1,10 +1,7 @@ #!/usr/bin/python from NodeGraphQt.base.commands import PropertyChangedCmd from NodeGraphQt.base.model import NodeModel -from NodeGraphQt.constants import (NODE_PROP, - NODE_LAYOUT_DIRECTION, - NODE_LAYOUT_VERTICAL, - NODE_LAYOUT_HORIZONTAL) +from NodeGraphQt.constants import NODE_PROP class _ClassProperty(object): @@ -26,9 +23,7 @@ class NodeObject(object): :class:`NodeGraphQt.BackdropNode` Args: - qgraphics_views (dict): Dictionary with the node layout type as the key - and a custom graphics item subclassed from the ``AbstractNodeItem`` - as the value. + qgraphics_item (AbstractNodeItem): QGraphicsItem item used for drawing. .. code-block:: python @@ -36,11 +31,8 @@ class NodeObject(object): class BaseNode(NodeObject): - def __init__(self, qgraphics_views=None): - qgraphics_views = qgraphics_views or { - NodeGraphQt.constants.NODE_LAYOUT_HORIZONTAL: NodeItem, - NodeGraphQt.constants.NODE_LAYOUT_VERTICAL: NodeItemVertical - } + def __init__(self, qgraphics_item=None): + qgraphics_item = qgraphics_item or NodeItem super(BaseNode, self).__init__(qgraphics_views) """ @@ -51,29 +43,27 @@ def __init__(self, qgraphics_views=None): # Base node name. NODE_NAME = None - def __init__(self, qgraphics_views=None): + def __init__(self, qgraphics_item=None): + """ + Args: + qgraphics_item (AbstractNodeItem): QGraphicsItem used for drawing. + """ self._graph = None self._model = NodeModel() self._model.type_ = self.type_ self._model.name = self.NODE_NAME - _NodeItem = None - if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL: - _NodeItem = qgraphics_views.get(NODE_LAYOUT_VERTICAL) - elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL: - _NodeItem = qgraphics_views.get(NODE_LAYOUT_HORIZONTAL) - + _NodeItem = qgraphics_item if _NodeItem is None: - raise ValueError( - 'qgraphics item for the {} node layout can\'t be None!'.format({ - NODE_LAYOUT_VERTICAL: 'vertical', - NODE_LAYOUT_HORIZONTAL: 'horizontal' - }[NODE_LAYOUT_DIRECTION])) + raise RuntimeError( + 'No qgraphics item specified for the node object!' + ) self._view = _NodeItem() self._view.type_ = self.type_ self._view.name = self.model.name self._view.id = self._model.id + self._view.layout_direction = self._model.layout_direction def __repr__(self): return '<{}("{}") object at {}>'.format( @@ -217,6 +207,7 @@ def serialize(self): 'width': 0.0, 'height: 0.0, 'pos': (0.0, 0.0), + 'layout_direction': 0, 'custom': {}, } } @@ -469,3 +460,25 @@ def pos(self): self.model.pos = self.view.xy_pos return self.model.pos + + def layout_direction(self): + """ + Returns the current node layout direction. + + Returns: + int: node layout direction. + """ + return self.model.layout_direction + + def set_layout_direction(self, value=0): + """ + Sets the node layout direction to either horizontal or vertical. + + Note: + This function does not register to the undo stack. + + Args: + value (int): layout direction mode. + """ + self.model.layout_direction = value + self.view.layout_direction = value diff --git a/NodeGraphQt/constants.py b/NodeGraphQt/constants.py index ff7b91a4..47f53954 100644 --- a/NodeGraphQt/constants.py +++ b/NodeGraphQt/constants.py @@ -58,15 +58,10 @@ class LayoutDirectionEnum(Enum): Node graph nodes layout direction: :py:mod:`NodeGraphQt.constants.ViewerLayoutEnum` """ - #: layout nodes top to bottom. - VERTICAL = 0 #: layout nodes left to right. - HORIZONTAL = 1 - - -#: Variable for setting the node layout direction. -# NODE_LAYOUT_DIRECTION = LayoutDirectionEnum.VERTICAL.value -NODE_LAYOUT_DIRECTION = LayoutDirectionEnum.HORIZONTAL.value + HORIZONTAL = 0 + #: layout nodes top to bottom. + VERTICAL = 1 # =================================== VIEWER =================================== diff --git a/NodeGraphQt/nodes/backdrop_node.py b/NodeGraphQt/nodes/backdrop_node.py index 9da573c0..628cd570 100644 --- a/NodeGraphQt/nodes/backdrop_node.py +++ b/NodeGraphQt/nodes/backdrop_node.py @@ -1,8 +1,6 @@ #!/usr/bin/python from NodeGraphQt.base.node import NodeObject -from NodeGraphQt.constants import (NODE_PROP_QTEXTEDIT, - NODE_LAYOUT_HORIZONTAL, - NODE_LAYOUT_VERTICAL) +from NodeGraphQt.constants import NODE_PROP_QTEXTEDIT from NodeGraphQt.qgraphics.node_backdrop import BackdropNodeItem @@ -22,11 +20,7 @@ class BackdropNode(NodeObject): NODE_NAME = 'Backdrop' def __init__(self, qgraphics_views=None): - qgraphics_views = qgraphics_views or { - NODE_LAYOUT_HORIZONTAL: BackdropNodeItem, - NODE_LAYOUT_VERTICAL: BackdropNodeItem - } - super(BackdropNode, self).__init__(qgraphics_views) + super(BackdropNode, self).__init__(qgraphics_views or BackdropNodeItem) # override base default color. self.model.color = (5, 129, 138, 255) self.create_property('backdrop_text', '', diff --git a/NodeGraphQt/nodes/base_node.py b/NodeGraphQt/nodes/base_node.py index 8ad5a84e..9a50c772 100644 --- a/NodeGraphQt/nodes/base_node.py +++ b/NodeGraphQt/nodes/base_node.py @@ -3,17 +3,16 @@ from NodeGraphQt.base.node import NodeObject from NodeGraphQt.base.port import Port -from NodeGraphQt.constants import (NODE_PROP_QLABEL, +from NodeGraphQt.constants import (LayoutDirectionEnum, + NODE_PROP_QLABEL, NODE_PROP_QLINEEDIT, NODE_PROP_QCOMBO, NODE_PROP_QCHECKBOX, - PortTypeEnum, - NODE_LAYOUT_VERTICAL, - NODE_LAYOUT_HORIZONTAL) + PortTypeEnum) from NodeGraphQt.errors import (PortError, PortRegistrationError, NodeWidgetError) -from NodeGraphQt.qgraphics.node_base import NodeItem, NodeItemVertical +from NodeGraphQt.qgraphics.node_base import NodeItem from NodeGraphQt.widgets.node_widgets import (NodeBaseWidget, NodeComboBox, NodeLineEdit, @@ -57,21 +56,11 @@ def __init__(self): NODE_NAME = 'Node' - def __init__(self, qgraphics_views=None): - qgraphics_views = qgraphics_views or { - NODE_LAYOUT_HORIZONTAL: NodeItem, - NODE_LAYOUT_VERTICAL: NodeItemVertical - } - super(BaseNode, self).__init__(qgraphics_views) + def __init__(self, qgraphics_item=None): + super(BaseNode, self).__init__(qgraphics_item or NodeItem) self._inputs = [] self._outputs = [] - def draw(self): - """ - Redraws the node in the scene. - """ - self.view.draw_node() - def update_model(self): """ Update the node model from view. @@ -84,6 +73,21 @@ def update_model(self): for name, widget in self.view.widgets.items(): self.model.set_property(name, widget.value) + def set_layout_direction(self, value=0): + """ + Sets the node layout direction to either horizontal or vertical. + + Note: + This function does not register to the undo stack. + + Args: + value (int): layout direction mode. + """ + # base logic to update the model and view attributes only. + super(BaseNode, self).set_layout_direction(value) + # redraw the node. + self._view.draw_node() + def set_icon(self, icon=None): """ Set the node icon. @@ -372,7 +376,7 @@ def delete_input(self, port): self._model.inputs.pop(port.name()) self._view.delete_input(port.view) port.model.node = None - self.draw() + self._view.draw_node() def delete_output(self, port): """ @@ -402,7 +406,7 @@ def delete_output(self, port): self._model.outputs.pop(port.name()) self._view.delete_output(port.view) port.model.node = None - self.draw() + self._view.draw_node() def set_port_deletion_allowed(self, mode=False): """ @@ -490,7 +494,7 @@ def set_ports(self, port_data): display_name=port['display_name'], locked=port.get('locked') or False) for port in port_data['output_ports']] - self.draw() + self._view.draw_node() def inputs(self): """ diff --git a/NodeGraphQt/nodes/group_node.py b/NodeGraphQt/nodes/group_node.py index 8e03050a..c9b78d88 100644 --- a/NodeGraphQt/nodes/group_node.py +++ b/NodeGraphQt/nodes/group_node.py @@ -1,11 +1,7 @@ #!/usr/bin/python -from NodeGraphQt.constants import (NODE_LAYOUT_VERTICAL, - NODE_LAYOUT_HORIZONTAL) - from NodeGraphQt.nodes.base_node import BaseNode from NodeGraphQt.nodes.port_node import PortInputNode, PortOutputNode -from NodeGraphQt.qgraphics.node_group import (GroupNodeItem, - GroupNodeVerticalItem) +from NodeGraphQt.qgraphics.node_group import GroupNodeItem class GroupNode(BaseNode): @@ -26,12 +22,8 @@ class GroupNode(BaseNode): NODE_NAME = 'Group' - def __init__(self, qgraphics_views=None): - qgraphics_views = qgraphics_views or { - NODE_LAYOUT_HORIZONTAL: GroupNodeItem, - NODE_LAYOUT_VERTICAL: GroupNodeVerticalItem - } - super(GroupNode, self).__init__(qgraphics_views) + def __init__(self, qgraphics_item=None): + super(GroupNode, self).__init__(qgraphics_item or GroupNodeItem) self._input_port_nodes = {} self._output_port_nodes = {} diff --git a/NodeGraphQt/nodes/port_node.py b/NodeGraphQt/nodes/port_node.py index f7ddc2f2..fed4d3bc 100644 --- a/NodeGraphQt/nodes/port_node.py +++ b/NodeGraphQt/nodes/port_node.py @@ -1,13 +1,8 @@ #!/usr/bin/python -from NodeGraphQt.constants import (NODE_LAYOUT_VERTICAL, - NODE_LAYOUT_HORIZONTAL) - from NodeGraphQt.errors import PortRegistrationError from NodeGraphQt.nodes.base_node import BaseNode -from NodeGraphQt.qgraphics.node_port_in import (PortInputNodeItem, - PortInputNodeVerticalItem) -from NodeGraphQt.qgraphics.node_port_out import (PortOutputNodeItem, - PortOutputNodeVerticalItem) +from NodeGraphQt.qgraphics.node_port_in import PortInputNodeItem +from NodeGraphQt.qgraphics.node_port_out import PortOutputNodeItem class PortInputNode(BaseNode): @@ -26,12 +21,8 @@ class PortInputNode(BaseNode): NODE_NAME = 'InputPort' - def __init__(self, qgraphics_views=None, parent_port=None): - qgraphics_views = qgraphics_views or { - NODE_LAYOUT_HORIZONTAL: PortInputNodeItem, - NODE_LAYOUT_VERTICAL: PortInputNodeVerticalItem - } - super(PortInputNode, self).__init__(qgraphics_views) + def __init__(self, qgraphics_item=None, parent_port=None): + super(PortInputNode, self).__init__(qgraphics_item or PortInputNodeItem) self._parent_port = parent_port @property @@ -73,8 +64,8 @@ def add_output(self, name='output', multi_output=True, display_name=True, class PortOutputNode(BaseNode): """ - The ``PortOutputNode`` class is the node object that represents a port from a - :class:`NodeGraphQt.GroupNode` when expanded in a + The ``PortOutputNode`` class is the node object that represents a port + from a :class:`NodeGraphQt.GroupNode` when expanded in a :class:`NodeGraphQt.SubGraph`. **Inherited from:** :class:`NodeGraphQt.BaseNode` @@ -87,12 +78,10 @@ class PortOutputNode(BaseNode): NODE_NAME = 'OutputPort' - def __init__(self, qgraphics_views=None, parent_port=None): - qgraphics_views = qgraphics_views or { - NODE_LAYOUT_HORIZONTAL: PortOutputNodeItem, - NODE_LAYOUT_VERTICAL: PortOutputNodeVerticalItem - } - super(PortOutputNode, self).__init__(qgraphics_views) + def __init__(self, qgraphics_item=None, parent_port=None): + super(PortOutputNode, self).__init__( + qgraphics_item or PortOutputNodeItem + ) self._parent_port = parent_port @property diff --git a/NodeGraphQt/pkg_info.py b/NodeGraphQt/pkg_info.py index 4f361c88..4e7a22a8 100644 --- a/NodeGraphQt/pkg_info.py +++ b/NodeGraphQt/pkg_info.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -__version__ = '0.2.2' +__version__ = '0.3.0' __status__ = 'Work in Progress' __license__ = 'MIT' diff --git a/NodeGraphQt/qgraphics/node_abstract.py b/NodeGraphQt/qgraphics/node_abstract.py index ca996da3..29dfcf18 100644 --- a/NodeGraphQt/qgraphics/node_abstract.py +++ b/NodeGraphQt/qgraphics/node_abstract.py @@ -1,7 +1,12 @@ #!/usr/bin/python from Qt import QtCore, QtWidgets -from NodeGraphQt.constants import Z_VAL_NODE, NodeEnum, ITEM_CACHE_MODE +from NodeGraphQt.constants import ( + Z_VAL_NODE, + ITEM_CACHE_MODE, + LayoutDirectionEnum, + NodeEnum +) class AbstractNodeItem(QtWidgets.QGraphicsItem): @@ -24,6 +29,7 @@ def __init__(self, name='node', parent=None): 'selected': False, 'disabled': False, 'visible': False, + 'layout_direction': LayoutDirectionEnum.HORIZONTAL.value, } self._width = NodeEnum.WIDTH.value self._height = NodeEnum.HEIGHT.value @@ -85,6 +91,14 @@ def type_(self): def type_(self, node_type='NODE'): self._properties['type_'] = node_type + @property + def layout_direction(self): + return self._properties['layout_direction'] + + @layout_direction.setter + def layout_direction(self, value=0): + self._properties['layout_direction'] = value + @property def size(self): return self._width, self._height diff --git a/NodeGraphQt/qgraphics/node_base.py b/NodeGraphQt/qgraphics/node_base.py index ae781a31..ada4ae9f 100644 --- a/NodeGraphQt/qgraphics/node_base.py +++ b/NodeGraphQt/qgraphics/node_base.py @@ -6,6 +6,7 @@ from NodeGraphQt.constants import ( ITEM_CACHE_MODE, ICON_NODE_BASE, + LayoutDirectionEnum, NodeEnum, PortEnum, PortTypeEnum, @@ -46,18 +47,27 @@ def __init__(self, name='node', parent=None): self._proxy_mode = False self._proxy_mode_threshold = 70 - def paint(self, painter, option, widget): + def post_init(self, viewer, pos=None): """ - Draws the node base not the ports or text. + Called after node has been added into the scene. Args: - painter (QtGui.QPainter): painter used for drawing the item. - option (QtGui.QStyleOptionGraphicsItem): - used to describe the parameters needed to draw. - widget (QtWidgets.QWidget): not used. - """ - self.auto_switch_mode() - + viewer (NodeGraphQt.widgets.viewer.NodeViewer): main viewer + pos (tuple): the cursor pos if node is called with tab search. + """ + if self.layout_direction == LayoutDirectionEnum.VERTICAL.value: + font = QtGui.QFont() + font.setPointSize(15) + self.text_item.setFont(font) + + # hide port text items for vertical layout. + if self.layout_direction is LayoutDirectionEnum.VERTICAL.value: + for text_item in self._input_items.values(): + text_item.setVisible(False) + for text_item in self._output_items.values(): + text_item.setVisible(False) + + def _paint_horizontal(self, painter, option, widget): painter.save() painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtCore.Qt.NoBrush) @@ -115,6 +125,79 @@ def paint(self, painter, option, widget): painter.restore() + def _paint_vertical(self, painter, option, widget): + painter.save() + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(QtCore.Qt.NoBrush) + + # base background. + margin = 1.0 + rect = self.boundingRect() + rect = QtCore.QRectF(rect.left() + margin, + rect.top() + margin, + rect.width() - (margin * 2), + rect.height() - (margin * 2)) + + radius = 4.0 + painter.setBrush(QtGui.QColor(*self.color)) + painter.drawRoundedRect(rect, radius, radius) + + # light overlay on background when selected. + if self.selected: + painter.setBrush( + QtGui.QColor(*NodeEnum.SELECTED_COLOR.value) + ) + painter.drawRoundedRect(rect, radius, radius) + + # top & bottom edge background. + padding = 2.0 + height = 10 + if self.selected: + painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) + else: + painter.setBrush(QtGui.QColor(0, 0, 0, 80)) + for y in [rect.y() + padding, rect.height() - height - 1]: + edge_rect = QtCore.QRectF(rect.x() + padding, y, + rect.width() - (padding * 2), height) + painter.drawRoundedRect(edge_rect, 3.0, 3.0) + + # node border + border_width = 0.8 + border_color = QtGui.QColor(*self.border_color) + if self.selected: + border_width = 1.2 + border_color = QtGui.QColor( + *NodeEnum.SELECTED_BORDER_COLOR.value + ) + border_rect = QtCore.QRectF(rect.left(), rect.top(), + rect.width(), rect.height()) + + pen = QtGui.QPen(border_color, border_width) + pen.setCosmetic(self.viewer().get_zoom() < 0.0) + painter.setBrush(QtCore.Qt.NoBrush) + painter.setPen(pen) + painter.drawRoundedRect(border_rect, radius, radius) + + painter.restore() + + def paint(self, painter, option, widget): + """ + Draws the node base not the ports. + + Args: + painter (QtGui.QPainter): painter used for drawing the item. + option (QtGui.QStyleOptionGraphicsItem): + used to describe the parameters needed to draw. + widget (QtWidgets.QWidget): not used. + """ + self.auto_switch_mode() + if self.layout_direction is LayoutDirectionEnum.HORIZONTAL.value: + self._paint_horizontal(painter, option, widget) + elif self.layout_direction is LayoutDirectionEnum.VERTICAL.value: + self._paint_vertical(painter, option, widget) + else: + raise RuntimeError('Node graph layout direction not valid!') + def mousePressEvent(self, event): """ Re-implemented to ignore event if LMB is over port collision area. @@ -336,8 +419,8 @@ def _calc_size_vertical(self): widget_width = widget.boundingRect().width() widget_height += widget.boundingRect().height() - width = max([p_input_width, p_output_width, widget_width]) + add_w - height = p_input_height + p_output_height + widget_height + add_h + width = max([p_input_width, p_output_width, widget_width]) + height = p_input_height + p_output_height + widget_height return width, height def calc_size(self, add_w=0.0, add_h=0.0): @@ -351,10 +434,9 @@ def calc_size(self, add_w=0.0, add_h=0.0): Returns: tuple(float, float): width, height. """ - layout_direction = self.viewer().get_layout_direction() - if layout_direction is NODE_LAYOUT_HORIZONTAL: + if self.layout_direction is LayoutDirectionEnum.HORIZONTAL.value: width, height = self._calc_size_horizontal() - elif layout_direction is NODE_LAYOUT_VERTICAL: + elif self.layout_direction is LayoutDirectionEnum.VERTICAL.value: width, height = self._calc_size_vertical() else: raise RuntimeError('Node graph layout direction not valid!') @@ -387,10 +469,9 @@ def align_icon(self, h_offset=0.0, v_offset=0.0): v_offset (float): additional vertical offset. h_offset (float): additional horizontal offset. """ - layout_direction = self.viewer().get_layout_direction() - if layout_direction is NODE_LAYOUT_HORIZONTAL: + if self.layout_direction is LayoutDirectionEnum.HORIZONTAL.value: self._align_icon_horizontal(h_offset, v_offset) - elif layout_direction is NODE_LAYOUT_VERTICAL: + elif self.layout_direction is LayoutDirectionEnum.VERTICAL.value: self._align_icon_vertical(h_offset, v_offset) else: raise RuntimeError('Node graph layout direction not valid!') @@ -415,10 +496,9 @@ def align_label(self, h_offset=0.0, v_offset=0.0): v_offset (float): vertical offset. h_offset (float): horizontal offset. """ - layout_direction = self.viewer().get_layout_direction() - if layout_direction is NODE_LAYOUT_HORIZONTAL: + if self.layout_direction is LayoutDirectionEnum.HORIZONTAL.value: self._align_label_horizontal(h_offset, v_offset) - elif layout_direction is NODE_LAYOUT_VERTICAL: + elif self.layout_direction is LayoutDirectionEnum.VERTICAL.value: self._align_label_vertical(h_offset, v_offset) else: raise RuntimeError('Node graph layout direction not valid!') @@ -469,11 +549,10 @@ def align_widgets(self, v_offset=0.0): Args: v_offset (float): vertical offset. """ - layout_direction = self.viewer().get_layout_direction() - if layout_direction is NODE_LAYOUT_HORIZONTAL: - self._align_widget_horizontal(v_offset) - elif layout_direction is NODE_LAYOUT_VERTICAL: - self._align_widget_vertical(v_offset) + if self.layout_direction is LayoutDirectionEnum.HORIZONTAL.value: + self._align_widgets_horizontal(v_offset) + elif self.layout_direction is LayoutDirectionEnum.VERTICAL.value: + self._align_widgets_vertical(v_offset) else: raise RuntimeError('Node graph layout direction not valid!') @@ -549,21 +628,22 @@ def align_ports(self, v_offset=0.0): Args: v_offset (float): port vertical offset. """ - layout_direction = self.viewer().get_layout_direction() - if layout_direction is NODE_LAYOUT_HORIZONTAL: + if self.layout_direction is LayoutDirectionEnum.HORIZONTAL.value: self._align_ports_horizontal(v_offset) - elif layout_direction is NODE_LAYOUT_VERTICAL: + elif self.layout_direction is LayoutDirectionEnum.VERTICAL.value: self._align_ports_vertical(v_offset) else: raise RuntimeError('Node graph layout direction not valid!') - def draw_node(self): - """ - Re-draw the node item in the scene. - (re-implemented for vertical layout design) - """ + def _draw_node_horizontal(self): height = self._text_item.boundingRect().height() + 4.0 + # update port text items in visibility. + for port, text in self._input_items.items(): + text.setVisible(port.display_name) + for port, text in self._output_items.items(): + text.setVisible(port.display_name) + # setup initial base size. self._set_base_size(add_h=height) # set text color when node is initialized. @@ -585,6 +665,46 @@ def draw_node(self): self.update() + def _draw_node_vertical(self): + # hide the port text items in vertical layout. + for port, text in self._input_items.items(): + text.setVisible(False) + for port, text in self._output_items.items(): + text.setVisible(False) + + # setup initial base size. + self._set_base_size() + # set text color when node is initialized. + self._set_text_color(self.text_color) + # set the tooltip + self._tooltip_disable(self.disabled) + + # --- setup node layout --- + # (do all the graphic item layout offsets here) + + # align label text + self.align_label(h_offset=6) + # align icon + self.align_icon(h_offset=6, v_offset=4) + # arrange input and output ports. + self.align_ports() + # arrange node widgets + self.align_widgets() + + self.update() + + def draw_node(self): + """ + Re-draw the node item in the scene with proper + calculated size and widgets aligned. + """ + if self.layout_direction is LayoutDirectionEnum.HORIZONTAL.value: + self._draw_node_horizontal() + elif self.layout_direction is LayoutDirectionEnum.VERTICAL.value: + self._draw_node_vertical() + else: + raise RuntimeError('Node graph layout direction not valid!') + def post_init(self, viewer=None, pos=None): """ Called after node has been added into the scene. @@ -672,6 +792,11 @@ def icon(self, path=None): self.update() + @AbstractNodeItem.layout_direction.setter + def layout_direction(self, value=0): + AbstractNodeItem.layout_direction.fset(self, value) + self.draw_node() + @AbstractNodeItem.width.setter def width(self, width=0.0): w, h = self.calc_size() @@ -900,264 +1025,3 @@ def from_dict(self, node_dict): for name, value in widgets.items(): if self._widgets.get(name): self._widgets[name].value = value - - -class NodeItemVertical(NodeItem): - """ - Vertical Node item. - - Args: - name (str): name displayed on the node. - parent (QtWidgets.QGraphicsItem): parent item. - """ - - def __init__(self, name='node', parent=None): - super(NodeItemVertical, self).__init__(name, parent) - font = QtGui.QFont() - font.setPointSize(15) - self.text_item.setFont(font) - - def paint(self, painter, option, widget): - """ - Draws the node base not the ports. - - Args: - painter (QtGui.QPainter): painter used for drawing the item. - option (QtGui.QStyleOptionGraphicsItem): - used to describe the parameters needed to draw. - widget (QtWidgets.QWidget): not used. - """ - self.auto_switch_mode() - - painter.save() - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(QtCore.Qt.NoBrush) - - # base background. - margin = 1.0 - rect = self.boundingRect() - rect = QtCore.QRectF(rect.left() + margin, - rect.top() + margin, - rect.width() - (margin * 2), - rect.height() - (margin * 2)) - - radius = 4.0 - painter.setBrush(QtGui.QColor(*self.color)) - painter.drawRoundedRect(rect, radius, radius) - - # light overlay on background when selected. - if self.selected: - painter.setBrush( - QtGui.QColor(*NodeEnum.SELECTED_COLOR.value) - ) - painter.drawRoundedRect(rect, radius, radius) - - # top & bottom edge background. - padding = 2.0 - height = 10 - if self.selected: - painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) - else: - painter.setBrush(QtGui.QColor(0, 0, 0, 80)) - for y in [rect.y() + padding, rect.height() - height - 1]: - edge_rect = QtCore.QRectF(rect.x() + padding, y, - rect.width() - (padding * 2), height) - painter.drawRoundedRect(edge_rect, 3.0, 3.0) - - # node border - border_width = 0.8 - border_color = QtGui.QColor(*self.border_color) - if self.selected: - border_width = 1.2 - border_color = QtGui.QColor( - *NodeEnum.SELECTED_BORDER_COLOR.value - ) - border_rect = QtCore.QRectF(rect.left(), rect.top(), - rect.width(), rect.height()) - - pen = QtGui.QPen(border_color, border_width) - pen.setCosmetic(self.viewer().get_zoom() < 0.0) - painter.setBrush(QtCore.Qt.NoBrush) - painter.setPen(pen) - painter.drawRoundedRect(border_rect, radius, radius) - - painter.restore() - - def align_icon(self, h_offset=0.0, v_offset=0.0): - """ - Align node icon to the right side of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ - center_y = self.boundingRect().center().y() - icon_rect = self._icon_item.boundingRect() - text_rect = self._text_item.boundingRect() - x = self.boundingRect().right() + h_offset - y = center_y - text_rect.height() - (icon_rect.height() / 2) + v_offset - self._icon_item.setPos(x, y) - - def align_label(self, h_offset=0.0, v_offset=0.0): - """ - Align node label to the right side of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ - rect = self._text_item.boundingRect() - x = self.boundingRect().right() + h_offset - y = self.boundingRect().center().y() - (rect.height() / 2) + v_offset - self.text_item.setPos(x, y) - - def align_ports(self, v_offset=0.0): - """ - Align input, output ports in the node layout. - """ - # adjust input position - inputs = [p for p in self.inputs if p.isVisible()] - if inputs: - port_width = inputs[0].boundingRect().width() - port_height = inputs[0].boundingRect().height() - half_width = port_width/2 - delta = self._width / (len(inputs)+1) - port_x = delta - port_y = (port_height / 2) * -1 - for port in inputs: - port.setPos(port_x - half_width, port_y) - port_x += delta - - # adjust output position - outputs = [p for p in self.outputs if p.isVisible()] - if outputs: - port_width = outputs[0].boundingRect().width() - port_height = outputs[0].boundingRect().height() - half_width = port_width / 2 - delta = self._width / (len(outputs)+1) - port_x = delta - port_y = self._height - (port_height / 2) - for port in outputs: - port.setPos(port_x-half_width, port_y) - port_x += delta - - def align_widgets(self, v_offset=0.0): - """ - Align node widgets to the default center of the node. - - Args: - v_offset (float): vertical offset. - """ - if not self._widgets: - return - rect = self.boundingRect() - y = rect.center().y() + v_offset - widget_height = 0.0 - for widget in self._widgets.values(): - widget_rect = widget.boundingRect() - widget_height += widget_rect.height() - y -= widget_height / 2 - - for widget in self._widgets.values(): - widget_rect = widget.boundingRect() - x = rect.center().x() - (widget_rect.width() / 2) - widget.widget().setTitleAlign('center') - widget.setPos(x, y) - y += widget_rect.height() - - def draw_node(self): - """ - Re-draw the node item in the scene. - """ - # setup initial base size. - self._set_base_size() - # set text color when node is initialized. - self._set_text_color(self.text_color) - # set the tooltip - self._tooltip_disable(self.disabled) - - # --- setup node layout --- - # (do all the graphic item layout offsets here) - - # align label text - self.align_label(h_offset=6) - # align icon - self.align_icon(h_offset=6, v_offset=4) - # arrange input and output ports. - self.align_ports() - # arrange node widgets - self.align_widgets() - - self.update() - - def calc_size(self, add_w=0.0, add_h=0.0): - """ - Calculate minimum node size. - - Args: - add_w (float): additional width. - add_h (float): additional height. - """ - p_input_width = 0.0 - p_output_width = 0.0 - p_input_height = 0.0 - p_output_height = 0.0 - for port in self._input_items.keys(): - if port.isVisible(): - p_input_width += port.boundingRect().width() - if not p_input_height: - p_input_height = port.boundingRect().height() - for port in self._output_items.keys(): - if port.isVisible(): - p_output_width += port.boundingRect().width() - if not p_output_height: - p_output_height = port.boundingRect().height() - - widget_width = 0.0 - widget_height = 0.0 - for widget in self._widgets.values(): - if widget.boundingRect().width() > widget_width: - widget_width = widget.boundingRect().width() - widget_height += widget.boundingRect().height() - - width = max([p_input_width, p_output_width, widget_width]) + add_w - height = p_input_height + p_output_height + widget_height + add_h - return width, height - - def add_input(self, name='input', multi_port=False, display_name=True, - locked=False, painter_func=None): - """ - Adds a port qgraphics item into the node with the "port_type" set as - IN_PORT - - Args: - name (str): name for the port. - multi_port (bool): allow multiple connections. - display_name (bool): (not used). - locked (bool): locked state. - painter_func (function): custom paint function. - - Returns: - PortItem: port qgraphics item. - """ - return super(NodeItemVertical, self).add_input( - name, multi_port, False, locked, painter_func) - - def add_output(self, name='output', multi_port=False, display_name=True, - locked=False, painter_func=None): - """ - Adds a port qgraphics item into the node with the "port_type" set as - OUT_PORT - - Args: - name (str): name for the port. - multi_port (bool): allow multiple connections. - display_name (bool): (not used). - locked (bool): locked state. - painter_func (function): custom paint function. - - Returns: - PortItem: port qgraphics item. - """ - return super(NodeItemVertical, self).add_output( - name, multi_port, False, locked, painter_func) diff --git a/NodeGraphQt/qgraphics/node_group.py b/NodeGraphQt/qgraphics/node_group.py index fa4e91dd..5b6a69e0 100644 --- a/NodeGraphQt/qgraphics/node_group.py +++ b/NodeGraphQt/qgraphics/node_group.py @@ -17,18 +17,7 @@ class GroupNodeItem(NodeItem): def __init__(self, name='group', parent=None): super(GroupNodeItem, self).__init__(name, parent) - def paint(self, painter, option, widget): - """ - Draws the node base not the ports or text. - - Args: - painter (QtGui.QPainter): painter used for drawing the item. - option (QtGui.QStyleOptionGraphicsItem): - used to describe the parameters needed to draw. - widget (QtWidgets.QWidget): not used. - """ - self.auto_switch_mode() - + def _paint_horizontal(self, painter, option, widget): painter.save() painter.setBrush(QtCore.Qt.NoBrush) painter.setPen(QtCore.Qt.NoPen) @@ -106,109 +95,7 @@ def paint(self, painter, option, widget): painter.restore() - def align_ports(self, v_offset=0.0): - """ - Align input, output ports in the node layout. - - Args: - v_offset (float): port vertical offset. - """ - width = self._width - txt_offset = PortEnum.CLICK_FALLOFF.value - 2 - spacing = 1 - - # adjust input position - inputs = [p for p in self.inputs if p.isVisible()] - if inputs: - port_width = inputs[0].boundingRect().width() - port_height = inputs[0].boundingRect().height() - port_x = port_width / 2 * -1 - port_x += 3.0 - port_y = v_offset - for port in inputs: - port.setPos(port_x, port_y) - port_y += port_height + spacing - # adjust input text position - for port, text in self._input_items.items(): - if port.isVisible(): - txt_x = port.boundingRect().width() / 2 - txt_offset - txt_x += 3.0 - text.setPos(txt_x, port.y() - 1.5) - - # adjust output position - outputs = [p for p in self.outputs if p.isVisible()] - if outputs: - port_width = outputs[0].boundingRect().width() - port_height = outputs[0].boundingRect().height() - port_x = width - (port_width / 2) - port_x -= 9.0 - port_y = v_offset - for port in outputs: - port.setPos(port_x, port_y) - port_y += port_height + spacing - # adjust output text position - for port, text in self._output_items.items(): - if port.isVisible(): - txt_width = text.boundingRect().width() - txt_offset - txt_x = port.x() - txt_width - text.setPos(txt_x, port.y() - 1.5) - - def draw_node(self): - """ - Re-draw the node item in the scene. - (re-implemented for vertical layout design) - """ - height = self._text_item.boundingRect().height() - - # setup initial base size. - self._set_base_size(add_w=8.0, add_h=height + 10) - # set text color when node is initialized. - self._set_text_color(self.text_color) - # set the tooltip - self._tooltip_disable(self.disabled) - - # --- set the initial node layout --- - # (do all the graphic item layout offsets here) - - # align label text - self.align_label() - # arrange icon - self.align_icon(h_offset=2.0, v_offset=3.0) - # arrange input and output ports. - self.align_ports(v_offset=height) - # arrange node widgets - self.align_widgets(v_offset=height) - - self.update() - - -class GroupNodeVerticalItem(NodeItem): - """ - Vertical Group Node item. - - Args: - name (str): name displayed on the node. - parent (QtWidgets.QGraphicsItem): parent item. - """ - - def __init__(self, name='group', parent=None): - super(GroupNodeVerticalItem, self).__init__(name, parent) - font = QtGui.QFont() - font.setPointSize(15) - self.text_item.setFont(font) - - def paint(self, painter, option, widget): - """ - Draws the node base not the ports or text. - - Args: - painter (QtGui.QPainter): painter used for drawing the item. - option (QtGui.QStyleOptionGraphicsItem): - used to describe the parameters needed to draw. - widget (QtWidgets.QWidget): not used. - """ - self.auto_switch_mode() - + def _paint_vertical(self, painter, option, widget): painter.save() painter.setBrush(QtCore.Qt.NoBrush) painter.setPen(QtCore.Qt.NoPen) @@ -284,38 +171,65 @@ def paint(self, painter, option, widget): painter.drawRect(rect_2) painter.restore() - - def align_icon(self, h_offset=0.0, v_offset=0.0): - """ - Align node icon to the default top left of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ + + def _align_icon_horizontal(self, h_offset, v_offset): + super(GroupNodeItem, self)._align_icon_horizontal(h_offset, v_offset) + + def _align_icon_vertical(self, h_offset, v_offset): y = self._height / 2 y -= self._icon_item.boundingRect().height() self._icon_item.setPos(self._width + h_offset, y + v_offset) - def align_label(self, h_offset=0.0, v_offset=0.0): - """ - Center node label text to the top of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ + def _align_label_horizontal(self, h_offset, v_offset): + super(GroupNodeItem, self)._align_label_horizontal(h_offset, v_offset) + + def _align_label_vertical(self, h_offset, v_offset): y = self._height / 2 y -= self.text_item.boundingRect().height() / 2 self._text_item.setPos(self._width + h_offset, y + v_offset) - def align_ports(self, v_offset=0.0): - """ - Align input, output ports in the node layout. + def _align_ports_horizontal(self, v_offset): + width = self._width + txt_offset = PortEnum.CLICK_FALLOFF.value - 2 + spacing = 1 - Args: - v_offset (float): port vertical offset. - """ + # adjust input position + inputs = [p for p in self.inputs if p.isVisible()] + if inputs: + port_width = inputs[0].boundingRect().width() + port_height = inputs[0].boundingRect().height() + port_x = port_width / 2 * -1 + port_x += 3.0 + port_y = v_offset + for port in inputs: + port.setPos(port_x, port_y) + port_y += port_height + spacing + # adjust input text position + for port, text in self._input_items.items(): + if port.isVisible(): + txt_x = port.boundingRect().width() / 2 - txt_offset + txt_x += 3.0 + text.setPos(txt_x, port.y() - 1.5) + + # adjust output position + outputs = [p for p in self.outputs if p.isVisible()] + if outputs: + port_width = outputs[0].boundingRect().width() + port_height = outputs[0].boundingRect().height() + port_x = width - (port_width / 2) + port_x -= 9.0 + port_y = v_offset + for port in outputs: + port.setPos(port_x, port_y) + port_y += port_height + spacing + # adjust output text position + for port, text in self._output_items.items(): + if port.isVisible(): + txt_width = text.boundingRect().width() - txt_offset + txt_x = port.x() - txt_width + text.setPos(txt_x, port.y() - 1.5) + + def _align_ports_vertical(self, v_offset): # adjust input position inputs = [p for p in self.inputs if p.isVisible()] if inputs: @@ -342,13 +256,45 @@ def align_ports(self, v_offset=0.0): port.setPos(port_x - half_width, port_y) port_x += delta - def draw_node(self): - """ - Re-draw the node item in the scene. - (re-implemented for vertical layout design) - """ + def _draw_node_horizontal(self): + height = self._text_item.boundingRect().height() + + # update port text items in visibility. + for port, text in self._input_items.items(): + text.setVisible(port.display_name) + for port, text in self._output_items.items(): + text.setVisible(port.display_name) + + # setup initial base size. + self._set_base_size(add_w=8.0, add_h=height + 10) + # set text color when node is initialized. + self._set_text_color(self.text_color) + # set the tooltip + self._tooltip_disable(self.disabled) + + # --- set the initial node layout --- + # (do all the graphic item layout offsets here) + + # align label text + self.align_label() + # arrange icon + self.align_icon(h_offset=2.0, v_offset=3.0) + # arrange input and output ports. + self.align_ports(v_offset=height) + # arrange node widgets + self.align_widgets(v_offset=height) + + self.update() + + def _draw_node_vertical(self): height = self._text_item.boundingRect().height() + # hide the port text items in vertical layout. + for port, text in self._input_items.items(): + text.setVisible(False) + for port, text in self._output_items.items(): + text.setVisible(False) + # setup initial base size. self._set_base_size(add_w=8.0) # set text color when node is initialized. @@ -369,41 +315,3 @@ def draw_node(self): self.align_widgets(v_offset=height / 2) self.update() - - def add_input(self, name='input', multi_port=False, display_name=True, - locked=False, painter_func=None): - """ - Adds a port qgraphics item into the node with the "port_type" set as - IN_PORT - - Args: - name (str): name for the port. - multi_port (bool): allow multiple connections. - display_name (bool): (not used). - locked (bool): locked state. - painter_func (function): custom paint function. - - Returns: - PortItem: port qgraphics item. - """ - return super(GroupNodeVerticalItem, self).add_input( - name, multi_port, False, locked, painter_func) - - def add_output(self, name='output', multi_port=False, display_name=True, - locked=False, painter_func=None): - """ - Adds a port qgraphics item into the node with the "port_type" set as - OUT_PORT - - Args: - name (str): name for the port. - multi_port (bool): allow multiple connections. - display_name (bool): (not used). - locked (bool): locked state. - painter_func (function): custom paint function. - - Returns: - PortItem: port qgraphics item. - """ - return super(GroupNodeVerticalItem, self).add_output( - name, multi_port, False, locked, painter_func) diff --git a/NodeGraphQt/qgraphics/node_port_in.py b/NodeGraphQt/qgraphics/node_port_in.py index e76e5d73..b34d44d1 100644 --- a/NodeGraphQt/qgraphics/node_port_in.py +++ b/NodeGraphQt/qgraphics/node_port_in.py @@ -2,7 +2,7 @@ from Qt import QtCore, QtGui, QtWidgets from NodeGraphQt.constants import NodeEnum -from NodeGraphQt.qgraphics.node_base import NodeItem, NodeItemVertical +from NodeGraphQt.qgraphics.node_base import NodeItem class PortInputNodeItem(NodeItem): @@ -25,16 +25,7 @@ def _set_base_size(self, add_w=0.0, add_h=0.0): self._width = width + 60 self._height = height if height >= 60 else 60 - def paint(self, painter, option, widget): - """ - Draws the node base not the ports or text. - - Args: - painter (QtGui.QPainter): painter used for drawing the item. - option (QtGui.QStyleOptionGraphicsItem): - used to describe the parameters needed to draw. - widget (QtWidgets.QWidget): not used. - """ + def _paint_horizontal(self, painter, option, widget): self.auto_switch_mode() painter.save() @@ -94,6 +85,66 @@ def paint(self, painter, option, widget): painter.restore() + def _paint_vertical(self, painter, option, widget): + self.auto_switch_mode() + + painter.save() + painter.setBrush(QtCore.Qt.NoBrush) + painter.setPen(QtCore.Qt.NoPen) + + margin = 2.0 + rect = self.boundingRect() + rect = QtCore.QRectF(rect.left() + margin, + rect.top() + margin, + rect.width() - (margin * 2), + rect.height() - (margin * 2)) + + text_rect = self._text_item.boundingRect() + text_rect = QtCore.QRectF( + rect.center().x() - (text_rect.width() / 2) - 5, + rect.top() + margin, + text_rect.width() + 10, + text_rect.height() + ) + + painter.setBrush(QtGui.QColor(255, 255, 255, 20)) + painter.drawRoundedRect(rect, 20, 20) + + painter.setBrush(QtGui.QColor(0, 0, 0, 100)) + painter.drawRoundedRect(text_rect, 3, 3) + + size = int(rect.height() / 4) + triangle = QtGui.QPolygonF() + triangle.append(QtCore.QPointF(-size, size)) + triangle.append(QtCore.QPointF(0.0, 0.0)) + triangle.append(QtCore.QPointF(size, size)) + + transform = QtGui.QTransform() + transform.translate(rect.center().x(), rect.bottom() - (size / 3)) + transform.rotate(180) + poly = transform.map(triangle) + + if self.selected: + pen = QtGui.QPen( + QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 + ) + painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) + else: + pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) + painter.setBrush(QtGui.QColor(0, 0, 0, 50)) + + pen.setJoinStyle(QtCore.Qt.MiterJoin) + painter.setPen(pen) + painter.drawPolygon(poly) + + edge_size = 30 + edge_rect = QtCore.QRectF(rect.center().x() - (edge_size / 2), + rect.bottom() - (size * 1.9), + edge_size, 4) + painter.drawRect(edge_rect) + + painter.restore() + def set_proxy_mode(self, mode): """ Set whether to draw the node with proxy mode. @@ -127,21 +178,21 @@ def set_proxy_mode(self, mode): self._text_item.setVisible(visible) - def align_label(self, h_offset=0.0, v_offset=0.0): - """ - Center node label text to the top of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ + def _align_label_horizontal(self, h_offset, v_offset): rect = self.boundingRect() text_rect = self._text_item.boundingRect() x = rect.center().x() - (text_rect.width() / 2) y = rect.center().y() - (text_rect.height() / 2) self._text_item.setPos(x + h_offset, y + v_offset) - def align_ports(self, v_offset=0.0): + def _align_label_vertical(self, h_offset, v_offset): + rect = self.boundingRect() + text_rect = self._text_item.boundingRect() + x = rect.center().x() - (text_rect.width() / 2) + y = rect.center().y() - text_rect.height() - 2.0 + self._text_item.setPos(x + h_offset, y + v_offset) + + def _align_ports_horizontal(self, v_offset): """ Align input, output ports in the node layout. """ @@ -153,7 +204,10 @@ def align_ports(self, v_offset=0.0): break super(PortInputNodeItem, self).align_ports(v_offset=v_offset) - def draw_node(self): + def _align_ports_vertical(self, v_offset): + super(PortInputNodeItem, self)._align_ports_vertical(v_offset) + + def _draw_node_horizontal(self): """ Re-draw the node item in the scene. (re-implemented for vertical layout design) @@ -178,86 +232,3 @@ def draw_node(self): self.align_widgets() self.update() - - -class PortInputNodeVerticalItem(PortInputNodeItem): - - def paint(self, painter, option, widget): - self.auto_switch_mode() - - painter.save() - painter.setBrush(QtCore.Qt.NoBrush) - painter.setPen(QtCore.Qt.NoPen) - - margin = 2.0 - rect = self.boundingRect() - rect = QtCore.QRectF(rect.left() + margin, - rect.top() + margin, - rect.width() - (margin * 2), - rect.height() - (margin * 2)) - - text_rect = self._text_item.boundingRect() - text_rect = QtCore.QRectF( - rect.center().x() - (text_rect.width() / 2) - 5, - rect.top() + margin, - text_rect.width() + 10, - text_rect.height() - ) - - painter.setBrush(QtGui.QColor(255, 255, 255, 20)) - painter.drawRoundedRect(rect, 20, 20) - - painter.setBrush(QtGui.QColor(0, 0, 0, 100)) - painter.drawRoundedRect(text_rect, 3, 3) - - size = int(rect.height() / 4) - triangle = QtGui.QPolygonF() - triangle.append(QtCore.QPointF(-size, size)) - triangle.append(QtCore.QPointF(0.0, 0.0)) - triangle.append(QtCore.QPointF(size, size)) - - transform = QtGui.QTransform() - transform.translate(rect.center().x(), rect.bottom() - (size / 3)) - transform.rotate(180) - poly = transform.map(triangle) - - if self.selected: - pen = QtGui.QPen( - QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 - ) - painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) - else: - pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) - painter.setBrush(QtGui.QColor(0, 0, 0, 50)) - - pen.setJoinStyle(QtCore.Qt.MiterJoin) - painter.setPen(pen) - painter.drawPolygon(poly) - - edge_size = 30 - edge_rect = QtCore.QRectF(rect.center().x() - (edge_size / 2), - rect.bottom() - (size * 1.9), - edge_size, 4) - painter.drawRect(edge_rect) - - painter.restore() - - def align_label(self, h_offset=0.0, v_offset=0.0): - """ - Center node label text to the center of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ - rect = self.boundingRect() - text_rect = self._text_item.boundingRect() - x = rect.center().x() - (text_rect.width() / 2) - y = rect.center().y() - text_rect.height() - 2.0 - self._text_item.setPos(x + h_offset, y + v_offset) - - def align_ports(self, v_offset=0.0): - """ - Align input, output ports in the node layout. - """ - NodeItemVertical.align_ports(self, v_offset=v_offset) diff --git a/NodeGraphQt/qgraphics/node_port_out.py b/NodeGraphQt/qgraphics/node_port_out.py index a692c74a..0982c873 100644 --- a/NodeGraphQt/qgraphics/node_port_out.py +++ b/NodeGraphQt/qgraphics/node_port_out.py @@ -2,7 +2,7 @@ from Qt import QtCore, QtGui, QtWidgets from NodeGraphQt.constants import NodeEnum -from NodeGraphQt.qgraphics.node_base import NodeItem, NodeItemVertical +from NodeGraphQt.qgraphics.node_base import NodeItem class PortOutputNodeItem(NodeItem): @@ -25,16 +25,7 @@ def _set_base_size(self, add_w=0.0, add_h=0.0): self._width = width + 60 self._height = height if height >= 60 else 60 - def paint(self, painter, option, widget): - """ - Draws the node base not the ports or text. - - Args: - painter (QtGui.QPainter): painter used for drawing the item. - option (QtGui.QStyleOptionGraphicsItem): - used to describe the parameters needed to draw. - widget (QtWidgets.QWidget): not used. - """ + def _paint_horizontal(self, painter, option, widget): self.auto_switch_mode() painter.save() @@ -94,6 +85,66 @@ def paint(self, painter, option, widget): painter.restore() + def _paint_vertical(self, painter, option, widget): + self.auto_switch_mode() + + painter.save() + painter.setBrush(QtCore.Qt.NoBrush) + painter.setPen(QtCore.Qt.NoPen) + + margin = 2.0 + rect = self.boundingRect() + rect = QtCore.QRectF(rect.left() + margin, + rect.top() + margin, + rect.width() - (margin * 2), + rect.height() - (margin * 2)) + + text_rect = self._text_item.boundingRect() + text_rect = QtCore.QRectF( + rect.center().x() - (text_rect.width() / 2) - 5, + rect.height() - text_rect.height(), + text_rect.width() + 10, + text_rect.height() + ) + + painter.setBrush(QtGui.QColor(255, 255, 255, 20)) + painter.drawRoundedRect(rect, 20, 20) + + painter.setBrush(QtGui.QColor(0, 0, 0, 100)) + painter.drawRoundedRect(text_rect, 3, 3) + + size = int(rect.height() / 4) + triangle = QtGui.QPolygonF() + triangle.append(QtCore.QPointF(-size, size)) + triangle.append(QtCore.QPointF(0.0, 0.0)) + triangle.append(QtCore.QPointF(size, size)) + + transform = QtGui.QTransform() + transform.translate(rect.center().x(), rect.y() + (size / 3)) + # transform.rotate(-90) + poly = transform.map(triangle) + + if self.selected: + pen = QtGui.QPen( + QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 + ) + painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) + else: + pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) + painter.setBrush(QtGui.QColor(0, 0, 0, 50)) + + pen.setJoinStyle(QtCore.Qt.MiterJoin) + painter.setPen(pen) + painter.drawPolygon(poly) + + edge_size = 30 + edge_rect = QtCore.QRectF(rect.center().x() - (edge_size / 2), + rect.y() + (size * 1.6), + edge_size, 4) + painter.drawRect(edge_rect) + + painter.restore() + def set_proxy_mode(self, mode): """ Set whether to draw the node with proxy mode. @@ -127,21 +178,21 @@ def set_proxy_mode(self, mode): self._text_item.setVisible(visible) - def align_label(self, h_offset=0.0, v_offset=0.0): - """ - Center node label text to the center of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ + def _align_label_horizontal(self, h_offset, v_offset): rect = self.boundingRect() text_rect = self._text_item.boundingRect() x = rect.center().x() - (text_rect.width() / 2) y = rect.center().y() - (text_rect.height() / 2) self._text_item.setPos(x + h_offset, y + v_offset) - def align_ports(self, v_offset=0.0): + def _align_label_vertical(self, h_offset, v_offset): + rect = self.boundingRect() + text_rect = self._text_item.boundingRect() + x = rect.center().x() - (text_rect.width() / 2) + y = rect.height() - text_rect.height() - 4.0 + self._text_item.setPos(x + h_offset, y + v_offset) + + def _align_ports_horizontal(self, v_offset): """ Align input, output ports in the node layout. """ @@ -153,7 +204,10 @@ def align_ports(self, v_offset=0.0): break super(PortOutputNodeItem, self).align_ports(v_offset=v_offset) - def draw_node(self): + def _align_ports_vertical(self, v_offset): + super(PortOutputNodeItem, self)._align_ports_vertical(v_offset) + + def _draw_node_horizontal(self): """ Re-draw the node item in the scene. (re-implemented for vertical layout design) @@ -178,95 +232,3 @@ def draw_node(self): self.align_widgets() self.update() - - -class PortOutputNodeVerticalItem(PortOutputNodeItem): - - def paint(self, painter, option, widget): - """ - Draws the node base not the ports or text. - - Args: - painter (QtGui.QPainter): painter used for drawing the item. - option (QtGui.QStyleOptionGraphicsItem): - used to describe the parameters needed to draw. - widget (QtWidgets.QWidget): not used. - """ - self.auto_switch_mode() - - painter.save() - painter.setBrush(QtCore.Qt.NoBrush) - painter.setPen(QtCore.Qt.NoPen) - - margin = 2.0 - rect = self.boundingRect() - rect = QtCore.QRectF(rect.left() + margin, - rect.top() + margin, - rect.width() - (margin * 2), - rect.height() - (margin * 2)) - - text_rect = self._text_item.boundingRect() - text_rect = QtCore.QRectF( - rect.center().x() - (text_rect.width() / 2) - 5, - rect.height() - text_rect.height(), - text_rect.width() + 10, - text_rect.height() - ) - - painter.setBrush(QtGui.QColor(255, 255, 255, 20)) - painter.drawRoundedRect(rect, 20, 20) - - painter.setBrush(QtGui.QColor(0, 0, 0, 100)) - painter.drawRoundedRect(text_rect, 3, 3) - - size = int(rect.height() / 4) - triangle = QtGui.QPolygonF() - triangle.append(QtCore.QPointF(-size, size)) - triangle.append(QtCore.QPointF(0.0, 0.0)) - triangle.append(QtCore.QPointF(size, size)) - - transform = QtGui.QTransform() - transform.translate(rect.center().x(), rect.y() + (size / 3)) - # transform.rotate(-90) - poly = transform.map(triangle) - - if self.selected: - pen = QtGui.QPen( - QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 - ) - painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) - else: - pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) - painter.setBrush(QtGui.QColor(0, 0, 0, 50)) - - pen.setJoinStyle(QtCore.Qt.MiterJoin) - painter.setPen(pen) - painter.drawPolygon(poly) - - edge_size = 30 - edge_rect = QtCore.QRectF(rect.center().x() - (edge_size / 2), - rect.y() + (size * 1.6), - edge_size, 4) - painter.drawRect(edge_rect) - - painter.restore() - - def align_label(self, h_offset=0.0, v_offset=0.0): - """ - Center node label text to the center of the node. - - Args: - v_offset (float): vertical offset. - h_offset (float): horizontal offset. - """ - rect = self.boundingRect() - text_rect = self._text_item.boundingRect() - x = rect.center().x() - (text_rect.width() / 2) - y = rect.height() - text_rect.height() - 4.0 - self._text_item.setPos(x + h_offset, y + v_offset) - - def align_ports(self, v_offset=0.0): - """ - Align input, output ports in the node layout. - """ - NodeItemVertical.align_ports(self, v_offset=v_offset) diff --git a/NodeGraphQt/qgraphics/pipe.py b/NodeGraphQt/qgraphics/pipe.py index 902f1079..2795210b 100644 --- a/NodeGraphQt/qgraphics/pipe.py +++ b/NodeGraphQt/qgraphics/pipe.py @@ -4,12 +4,13 @@ from Qt import QtCore, QtGui, QtWidgets from NodeGraphQt.constants import ( - PipeEnum, PipeLayoutEnum, PortTypeEnum, Z_VAL_PIPE, - Z_VAL_NODE_WIDGET, + LayoutDirectionEnum, + PipeEnum, + PipeLayoutEnum, + PortTypeEnum, ITEM_CACHE_MODE, - NODE_LAYOUT_VERTICAL, - NODE_LAYOUT_HORIZONTAL, - NODE_LAYOUT_DIRECTION + Z_VAL_PIPE, + Z_VAL_NODE_WIDGET ) from NodeGraphQt.qgraphics.port import PortItem @@ -262,11 +263,11 @@ def draw_path(self, start_port, end_port=None, cursor_pos=None): path.lineTo(pos2) self.setPath(path) return - else: - if self.viewer_layout_direction() is NODE_LAYOUT_VERTICAL: - self.__draw_path_vertical(start_port, pos1, pos2, path) - elif self.viewer_layout_direction() is NODE_LAYOUT_HORIZONTAL: - self.__draw_path_horizontal(start_port, pos1, pos2, path) + + if self.viewer_layout_direction() is LayoutDirectionEnum.VERTICAL.value: + self.__draw_path_vertical(start_port, pos1, pos2, path) + elif self.viewer_layout_direction() is LayoutDirectionEnum.HORIZONTAL.value: + self.__draw_path_horizontal(start_port, pos1, pos2, path) def reset_path(self): path = QtGui.QPainterPath(QtCore.QPointF(0.0, 0.0)) diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index 43642ce4..5e899145 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -6,7 +6,9 @@ from Qt import QtGui, QtCore, QtWidgets from NodeGraphQt.base.menu import BaseMenu -from NodeGraphQt.constants import PortTypeEnum, PipeLayoutEnum +from NodeGraphQt.constants import ( + LayoutDirectionEnum, PortTypeEnum, PipeLayoutEnum +) from NodeGraphQt.qgraphics.node_abstract import AbstractNodeItem from NodeGraphQt.qgraphics.node_backdrop import BackdropNodeItem from NodeGraphQt.qgraphics.pipe import PipeItem, LivePipeItem @@ -71,7 +73,7 @@ def __init__(self, parent=None, undo_stack=None): self._update_scene() self._last_size = self.size() - self._layout_direction = NODE_LAYOUT_HORIZONTAL + self._layout_direction = LayoutDirectionEnum.HORIZONTAL.value self._pipe_layout = PipeLayoutEnum.CURVED.value self._detached_port = None @@ -1001,7 +1003,7 @@ def all_pipes(self): Returns all pipe qgraphic items. Returns: - list[Pipe]: instances of pipe items. + list[PipeItem]: instances of pipe items. """ excl = [self._LIVE_PIPE, self._SLICER_PIPE] return [i for i in self.scene().items() @@ -1163,7 +1165,8 @@ def set_pipe_layout(self, layout): def get_layout_direction(self): """ - Get the layout direction of the node graph. + Returns the layout direction set on the the node graph viewer + used by the pipe items for drawing. Returns: int: graph layout mode. @@ -1172,12 +1175,15 @@ def get_layout_direction(self): def set_layout_direction(self, direction): """ - Sets the node graph layout direction. + Sets the node graph viewer layout direction for re-drawing + the pipe items. Args: - direction (int): graph layout direction. (see the constants module) + direction (int): graph layout direction. """ self._layout_direction = direction + for pipe_item in self.all_pipes(): + pipe_item.draw_path(pipe_item.input_port, pipe_item.output_port) def reset_zoom(self, cent=None): """ From 9b138e1aaa955dbd7b82f80d9ea8e1397190d893 Mon Sep 17 00:00:00 2001 From: jchan Date: Sat, 8 Oct 2022 23:57:52 +1300 Subject: [PATCH 4/6] layout switching tweaks. --- NodeGraphQt/base/graph.py | 30 ++++++++++++++++++++++---- NodeGraphQt/base/graph_actions.py | 10 ++++----- NodeGraphQt/base/node.py | 3 +++ NodeGraphQt/nodes/base_node.py | 5 +++-- NodeGraphQt/qgraphics/node_port_in.py | 4 ++-- NodeGraphQt/qgraphics/node_port_out.py | 4 ++-- 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index 6ceca248..c7925863 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -126,6 +126,15 @@ def __init__(self, parent=None, **kwargs): self.setObjectName('NodeGraph') self._model = ( kwargs.get('model') or NodeGraphModel()) + + layout_direction = kwargs.get('layout_direction') + if layout_direction: + if layout_direction not in [e.value for e in LayoutDirectionEnum]: + layout_direction = LayoutDirectionEnum.HORIZONTAL.value + self._model.layout_direction = layout_direction + else: + layout_direction = self._model.layout_direction + self._node_factory = ( kwargs.get('node_factory') or NodeFactory()) @@ -139,6 +148,7 @@ def __init__(self, parent=None, **kwargs): self._viewer = ( kwargs.get('viewer') or NodeViewer(undo_stack=self._undo_stack)) + self._viewer.set_layout_direction(layout_direction) self._build_context_menu() self._register_builtin_nodes() @@ -802,6 +812,8 @@ def set_layout_direction(self, direction): This function will also override the layout direction on all nodes in the current node graph. + `Implemented in` ``v0.3.0`` + Note: By default node graph direction is set to "NODE_LAYOUT_HORIZONTAL". @@ -1906,7 +1918,11 @@ def expand_group_node(self, node): # build new sub graph. node_factory = copy.deepcopy(self.node_factory) - sub_graph = SubGraph(self, node=node, node_factory=node_factory) + layout_direction = self.layout_direction() + sub_graph = SubGraph(self, + node=node, + node_factory=node_factory, + layout_direction=layout_direction) # populate the sub graph. session = node.get_sub_graph_session() @@ -1958,14 +1974,17 @@ class SubGraph(NodeGraph): - """ - def __init__(self, parent=None, node=None, node_factory=None): + def __init__(self, parent=None, node=None, node_factory=None, **kwargs): """ Args: parent (object): object parent. node (GroupNode): group node related to this sub graph. node_factory (NodeFactory): override node factory. + **kwargs (dict): additional kwargs. """ - super(SubGraph, self).__init__(parent, node_factory=node_factory) + super(SubGraph, self).__init__( + parent, node_factory=node_factory, **kwargs + ) # sub graph attributes. self._node = node @@ -2329,7 +2348,10 @@ def expand_group_node(self, node): # build new sub graph. node_factory = copy.deepcopy(self.node_factory) - sub_graph = SubGraph(self, node=node, node_factory=node_factory) + sub_graph = SubGraph(self, + node=node, + node_factory=node_factory, + layout_direction=self.layout_direction()) # populate the sub graph. serialized_session = node.get_sub_graph_session() diff --git a/NodeGraphQt/base/graph_actions.py b/NodeGraphQt/base/graph_actions.py index 7970db08..18259627 100644 --- a/NodeGraphQt/base/graph_actions.py +++ b/NodeGraphQt/base/graph_actions.py @@ -56,13 +56,13 @@ def build_context_menu(graph): graph_menu = context_menu.add_menu('&Graph') bg_menu = graph_menu.add_menu('&Background') - bg_menu.add_command('None', _bg_grid_none) - bg_menu.add_command('Lines', _bg_grid_lines) - bg_menu.add_command('Dots', _bg_grid_dots) + bg_menu.add_command('None', _bg_grid_none, 'Alt+1') + bg_menu.add_command('Lines', _bg_grid_lines, 'Alt+2') + bg_menu.add_command('Dots', _bg_grid_dots, 'Alt+3') layout_menu = graph_menu.add_menu('&Layout') - layout_menu.add_command('Horizontal', _layout_h_mode) - layout_menu.add_command('Vertical', _layout_v_mode) + layout_menu.add_command('Horizontal', _layout_h_mode, 'Shift+1') + layout_menu.add_command('Vertical', _layout_v_mode, 'Shift+2') # "Node" menu. # -------------------------------------------------------------------------- diff --git a/NodeGraphQt/base/node.py b/NodeGraphQt/base/node.py index 520ab25f..10aa7557 100644 --- a/NodeGraphQt/base/node.py +++ b/NodeGraphQt/base/node.py @@ -474,6 +474,8 @@ def set_layout_direction(self, value=0): """ Sets the node layout direction to either horizontal or vertical. + `Implemented in` ``v0.3.0`` + Note: This function does not register to the undo stack. @@ -482,3 +484,4 @@ def set_layout_direction(self, value=0): """ self.model.layout_direction = value self.view.layout_direction = value + diff --git a/NodeGraphQt/nodes/base_node.py b/NodeGraphQt/nodes/base_node.py index 9a50c772..db2e6f84 100644 --- a/NodeGraphQt/nodes/base_node.py +++ b/NodeGraphQt/nodes/base_node.py @@ -3,8 +3,7 @@ from NodeGraphQt.base.node import NodeObject from NodeGraphQt.base.port import Port -from NodeGraphQt.constants import (LayoutDirectionEnum, - NODE_PROP_QLABEL, +from NodeGraphQt.constants import (NODE_PROP_QLABEL, NODE_PROP_QLINEEDIT, NODE_PROP_QCOMBO, NODE_PROP_QCHECKBOX, @@ -77,6 +76,8 @@ def set_layout_direction(self, value=0): """ Sets the node layout direction to either horizontal or vertical. + `Implemented in` ``v0.3.0`` + Note: This function does not register to the undo stack. diff --git a/NodeGraphQt/qgraphics/node_port_in.py b/NodeGraphQt/qgraphics/node_port_in.py index b34d44d1..b5c23618 100644 --- a/NodeGraphQt/qgraphics/node_port_in.py +++ b/NodeGraphQt/qgraphics/node_port_in.py @@ -188,7 +188,7 @@ def _align_label_horizontal(self, h_offset, v_offset): def _align_label_vertical(self, h_offset, v_offset): rect = self.boundingRect() text_rect = self._text_item.boundingRect() - x = rect.center().x() - (text_rect.width() / 2) + x = rect.center().x() - (text_rect.width() / 1.5) - 2.0 y = rect.center().y() - text_rect.height() - 2.0 self._text_item.setPos(x + h_offset, y + v_offset) @@ -202,7 +202,7 @@ def _align_ports_horizontal(self, v_offset): if ports: v_offset -= ports[0].boundingRect().height() / 2 break - super(PortInputNodeItem, self).align_ports(v_offset=v_offset) + super(PortInputNodeItem, self)._align_ports_horizontal(v_offset) def _align_ports_vertical(self, v_offset): super(PortInputNodeItem, self)._align_ports_vertical(v_offset) diff --git a/NodeGraphQt/qgraphics/node_port_out.py b/NodeGraphQt/qgraphics/node_port_out.py index 0982c873..733b0043 100644 --- a/NodeGraphQt/qgraphics/node_port_out.py +++ b/NodeGraphQt/qgraphics/node_port_out.py @@ -188,7 +188,7 @@ def _align_label_horizontal(self, h_offset, v_offset): def _align_label_vertical(self, h_offset, v_offset): rect = self.boundingRect() text_rect = self._text_item.boundingRect() - x = rect.center().x() - (text_rect.width() / 2) + x = rect.center().x() - (text_rect.width() / 1.5) - 2.0 y = rect.height() - text_rect.height() - 4.0 self._text_item.setPos(x + h_offset, y + v_offset) @@ -202,7 +202,7 @@ def _align_ports_horizontal(self, v_offset): if ports: v_offset -= ports[0].boundingRect().height() / 2 break - super(PortOutputNodeItem, self).align_ports(v_offset=v_offset) + super(PortOutputNodeItem, self)._align_ports_horizontal(v_offset) def _align_ports_vertical(self, v_offset): super(PortOutputNodeItem, self)._align_ports_vertical(v_offset) From 94359e2bfea4bc0d81b17b52cb6ecfd39a9c8446 Mon Sep 17 00:00:00 2001 From: jchan Date: Sun, 9 Oct 2022 00:18:24 +1300 Subject: [PATCH 5/6] doc updates --- NodeGraphQt/base/graph.py | 20 ++++++++++++++++---- NodeGraphQt/base/node.py | 14 +++++++++++--- NodeGraphQt/nodes/base_node.py | 10 ++++++++-- docs/nodes/BaseNode.rst | 2 +- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index c7925863..f3048b9e 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -801,6 +801,11 @@ def layout_direction(self): """ Return the current node graph layout direction. + `Implemented in` ``v0.3.0`` + + See Also: + :meth:`NodeGraph.set_layout_direction` + Returns: int: layout direction. """ @@ -814,13 +819,20 @@ def set_layout_direction(self, direction): `Implemented in` ``v0.3.0`` + See Also: + :meth:`NodeGraph.layout_direction`, + :meth:`NodeObject.set_layout_direction` + Note: - By default node graph direction is set to "NODE_LAYOUT_HORIZONTAL". + By default node graph direction is set to horizontal. Node Graph Layout Types: - * :attr:`NodeGraphQt.constants.NODE_LAYOUT_HORIZONTAL` - * :attr:`NodeGraphQt.constants.NODE_LAYOUT_VERTICAL` + * :attr:`NodeGraphQt.constants.LayoutDirectionEnum.HORIZONTAL` + * :attr:`NodeGraphQt.constants.LayoutDirectionEnum.VERTICAL` + + Warnings: + This function does not register to the undo stack. Args: direction (int): layout direction. @@ -900,7 +912,7 @@ def register_node(self, node, alias=None): Register the node to the :meth:`NodeGraph.node_factory` Args: - node (_NodeGraphQt.NodeObject): node object. + node (NodeGraphQt.NodeObject): node object. alias (str): custom alias name for the node type. """ self._node_factory.register_node(node, alias) diff --git a/NodeGraphQt/base/node.py b/NodeGraphQt/base/node.py index 10aa7557..d48fdc46 100644 --- a/NodeGraphQt/base/node.py +++ b/NodeGraphQt/base/node.py @@ -463,7 +463,10 @@ def pos(self): def layout_direction(self): """ - Returns the current node layout direction. + Returns layout direction for this node. + + See Also: + :meth:`NodeObject.set_layout_direction` Returns: int: node layout direction. @@ -472,11 +475,16 @@ def layout_direction(self): def set_layout_direction(self, value=0): """ - Sets the node layout direction to either horizontal or vertical. + Sets the node layout direction to either horizontal or vertical on + the current node only. `Implemented in` ``v0.3.0`` - Note: + See Also: + :meth:`NodeGraph.set_layout_direction` + :meth:`NodeObject.layout_direction` + + Warnings: This function does not register to the undo stack. Args: diff --git a/NodeGraphQt/nodes/base_node.py b/NodeGraphQt/nodes/base_node.py index db2e6f84..f8c04d17 100644 --- a/NodeGraphQt/nodes/base_node.py +++ b/NodeGraphQt/nodes/base_node.py @@ -74,11 +74,17 @@ def update_model(self): def set_layout_direction(self, value=0): """ - Sets the node layout direction to either horizontal or vertical. + Sets the node layout direction to either horizontal or vertical on + the current node only. `Implemented in` ``v0.3.0`` - Note: + See Also: + :meth:`NodeGraph.set_layout_direction`, + :meth:`NodeObject.layout_direction` + + + Warnings: This function does not register to the undo stack. Args: diff --git a/docs/nodes/BaseNode.rst b/docs/nodes/BaseNode.rst index 7d23ea5e..6d3fcc2b 100644 --- a/docs/nodes/BaseNode.rst +++ b/docs/nodes/BaseNode.rst @@ -3,4 +3,4 @@ BaseNode .. autoclass:: NodeGraphQt.BaseNode :members: - :exclude-members: update_model + :exclude-members: update_model, set_layout_direction From 04038d3f93bdcd2a4791abe90b8fba52e860a25a Mon Sep 17 00:00:00 2001 From: jchan Date: Sun, 9 Oct 2022 15:49:01 +1300 Subject: [PATCH 6/6] layout switching doc updates --- NodeGraphQt/base/graph.py | 2 -- docs/_images/layout_direction_switch.gif | Bin 0 -> 93471 bytes 2 files changed, 2 deletions(-) create mode 100644 docs/_images/layout_direction_switch.gif diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index f3048b9e..d790f955 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -824,8 +824,6 @@ def set_layout_direction(self, direction): :meth:`NodeObject.set_layout_direction` Note: - By default node graph direction is set to horizontal. - Node Graph Layout Types: * :attr:`NodeGraphQt.constants.LayoutDirectionEnum.HORIZONTAL` diff --git a/docs/_images/layout_direction_switch.gif b/docs/_images/layout_direction_switch.gif new file mode 100644 index 0000000000000000000000000000000000000000..f7a338fef3b7fd7daa9c3922a71fb3ad1aa6b4f5 GIT binary patch literal 93471 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 literal 0 HcmV?d00001