diff --git a/NodeGraphQt/base/commands.py b/NodeGraphQt/base/commands.py index 9385c665..25f6b4be 100644 --- a/NodeGraphQt/base/commands.py +++ b/NodeGraphQt/base/commands.py @@ -1,7 +1,7 @@ #!/usr/bin/python +from .utils import minimize_node_ref_count from .. import QtWidgets from ..constants import IN_PORT, OUT_PORT -from .utils import minimize_node_ref_count class PropertyChangedCmd(QtWidgets.QUndoCommand): diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index 59eafe6b..3dbef2fa 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -1,11 +1,12 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +import gc import json import os import re + import copy -import gc -from .. import QtCore, QtWidgets, QtGui + from .commands import (NodeAddedCmd, NodeRemovedCmd, NodeMovedCmd, @@ -15,14 +16,15 @@ from .model import NodeGraphModel from .node import NodeObject, BaseNode from .port import Port +from .. import QtCore, QtWidgets, QtGui from ..constants import (DRAG_DROP_ID, PIPE_LAYOUT_CURVED, PIPE_LAYOUT_STRAIGHT, PIPE_LAYOUT_ANGLE, IN_PORT, OUT_PORT, VIEWER_GRID_LINES) -from ..widgets.viewer import NodeViewer from ..widgets.node_space_bar import node_space_bar +from ..widgets.viewer import NodeViewer class QWidgetDrops(QtWidgets.QWidget): @@ -179,9 +181,10 @@ def _wire_signals(self): self._viewer.connection_changed.connect(self._on_connection_changed) self._viewer.moved_nodes.connect(self._on_nodes_moved) self._viewer.node_double_clicked.connect(self._on_node_double_clicked) + self._viewer.node_name_changed.connect(self._on_node_name_changed) self._viewer.insert_node.connect(self._insert_node) - # pass through signals. + # pass through translated signals. self._viewer.node_selected.connect(self._on_node_selected) self._viewer.node_selection_changed.connect( self._on_node_selection_changed) @@ -254,6 +257,21 @@ def _on_property_bin_changed(self, node_id, prop_name, prop_value): value = copy.deepcopy(prop_value) node.set_property(prop_name, value) + def _on_node_name_changed(self, node_id, name): + """ + called when a node text qgraphics item in the viewer is edited. + (sets the name through the node object so undo commands are registered.) + + Args: + node_id (str): node id emitted by the viewer. + name (str): new node name. + """ + node = self.get_node_by_id(node_id) + node.set_name(name) + + # TODO: not sure about redrawing the node here. + node.view.draw_node() + def _on_node_double_clicked(self, node_id): """ called when a node in the viewer is double click. diff --git a/NodeGraphQt/base/node.py b/NodeGraphQt/base/node.py index 9f16df7f..a3ccd5bf 100644 --- a/NodeGraphQt/base/node.py +++ b/NodeGraphQt/base/node.py @@ -2,6 +2,7 @@ from .commands import PropertyChangedCmd from .model import NodeModel from .port import Port +from .utils import update_node_down_stream from ..constants import (NODE_PROP, NODE_PROP_QLINEEDIT, NODE_PROP_QTEXTEDIT, @@ -23,7 +24,6 @@ NodeIntEdit, NodeCheckBox, NodeFilePath) -from .utils import update_node_down_stream class classproperty(object): @@ -528,7 +528,6 @@ def __init__(self): self._inputs = [] self._outputs = [] self._has_draw = False - self._view.text_item.editingFinished.connect(self.set_name) def draw(self, force=True): """ diff --git a/NodeGraphQt/pkg_info.py b/NodeGraphQt/pkg_info.py index bf8d8663..b2ef4608 100644 --- a/NodeGraphQt/pkg_info.py +++ b/NodeGraphQt/pkg_info.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -__version__ = '0.1.2' +__version__ = '0.1.1' __status__ = 'Work in Progress' __license__ = 'MIT' diff --git a/NodeGraphQt/qgraphics/node_abstract.py b/NodeGraphQt/qgraphics/node_abstract.py index c74b222c..bae71863 100644 --- a/NodeGraphQt/qgraphics/node_abstract.py +++ b/NodeGraphQt/qgraphics/node_abstract.py @@ -162,7 +162,8 @@ def visible(self, visible=False): def xy_pos(self): """ return the item scene postion. - ("node.pos" conflicted with "QGraphicsItem.pos()" so it was refactored to "xy_pos".) + ("node.pos" conflicted with "QGraphicsItem.pos()" + so it was refactored to "xy_pos".) Returns: list[float]: x, y scene position. @@ -173,7 +174,8 @@ def xy_pos(self): def xy_pos(self, pos=None): """ set the item scene postion. - ("node.pos" conflicted with "QGraphicsItem.pos()" so it was refactored to "xy_pos".) + ("node.pos" conflicted with "QGraphicsItem.pos()" + so it was refactored to "xy_pos".) Args: pos (list[float]): x, y scene position. @@ -231,7 +233,8 @@ def from_dict(self, node_dict): node_attrs = list(self._properties.keys()) + ['width', 'height', 'pos'] for name, value in node_dict.items(): if name in node_attrs: - # "node.pos" conflicted with "QGraphicsItem.pos()" so it's refactored to "xy_pos". + # "node.pos" conflicted with "QGraphicsItem.pos()" + # so it's refactored to "xy_pos". if name == 'pos': name = 'xy_pos' setattr(self, name, value) diff --git a/NodeGraphQt/qgraphics/node_base.py b/NodeGraphQt/qgraphics/node_base.py index d02af940..95266596 100644 --- a/NodeGraphQt/qgraphics/node_base.py +++ b/NodeGraphQt/qgraphics/node_base.py @@ -1,5 +1,8 @@ #!/usr/bin/python +from .node_abstract import AbstractNodeItem +from .node_text_item import NodeTextItem +from .port import PortItem, CustomPortItem from .. import QtGui, QtCore, QtWidgets from ..constants import (IN_PORT, OUT_PORT, NODE_WIDTH, NODE_HEIGHT, @@ -8,9 +11,6 @@ PORT_FALLOFF, Z_VAL_NODE, Z_VAL_NODE_WIDGET, ITEM_CACHE_MODE) from ..errors import NodeWidgetError -from .node_abstract import AbstractNodeItem -from .port import PortItem, CustomPortItem -from .node_text_item import NodeTextItem class XDisabledItem(QtWidgets.QGraphicsItem): @@ -128,7 +128,7 @@ def __init__(self, name='node', parent=None): self._properties['icon'] = ICON_NODE_BASE self._icon_item = QtWidgets.QGraphicsPixmapItem(pixmap, self) self._icon_item.setTransformationMode(QtCore.Qt.SmoothTransformation) - self.text_item = NodeTextItem(self.name, self) + self._text_item = NodeTextItem(self.name, self) self._x_item = XDisabledItem(self, 'DISABLED') self._input_items = {} self._output_items = {} @@ -207,12 +207,10 @@ def mousePressEvent(self, event): for p in self._input_items.keys(): if p.hovered: event.ignore() - super(NodeItem, self).mousePressEvent(event) return for p in self._output_items.keys(): if p.hovered: event.ignore() - super(NodeItem, self).mousePressEvent(event) return super(NodeItem, self).mousePressEvent(event) @@ -223,9 +221,25 @@ def mouseReleaseEvent(self, event): super(NodeItem, self).mouseReleaseEvent(event) def mouseDoubleClickEvent(self, event): - viewer = self.viewer() - if viewer: - viewer.node_double_clicked.emit(self.id) + """ + Re-implemented to emit "node_double_clicked" signal. + + Args: + event (QtWidgets.QGraphicsSceneMouseEvent): mouse event. + """ + if event.button() == QtCore.Qt.LeftButton: + + # enable text item edit mode. + items = self.scene().items(event.scenePos()) + if self._text_item in items: + self._text_item.set_editable(True) + self._text_item.setFocus() + event.ignore() + return + + viewer = self.viewer() + if viewer: + viewer.node_double_clicked.emit(self.id) super(NodeItem, self).mouseDoubleClickEvent(event) def itemChange(self, change, value): @@ -280,7 +294,7 @@ def _set_text_color(self, color): text.setDefaultTextColor(text_color) for port, text in self._output_items.items(): text.setDefaultTextColor(text_color) - self.text_item.setDefaultTextColor(text_color) + self._text_item.setDefaultTextColor(text_color) def activate_pipes(self): """ @@ -302,7 +316,7 @@ def highlight_pipes(self): def reset_pipes(self): """ - reset the pipe color. + Reset all the pipe colors. """ ports = self.inputs + self.outputs for port in ports: @@ -311,15 +325,17 @@ def reset_pipes(self): def calc_size(self, add_w=0.0, add_h=0.0): """ - calculate minimum node size. - + Calculates the minimum node size. Args: add_w (float): additional width. add_h (float): additional height. + + Returns: + tuple(float, float): width, height. """ width = 0 - height = self.text_item.boundingRect().height() + height = self._text_item.boundingRect().height() if self._widgets: wid_width = max([ @@ -330,6 +346,7 @@ def calc_size(self, add_w=0.0, add_h=0.0): port_height = 0.0 if self._input_items: + port = None input_widths = [] for port, text in self._input_items.items(): input_width = port.boundingRect().width() - PORT_FALLOFF @@ -337,9 +354,11 @@ def calc_size(self, add_w=0.0, add_h=0.0): input_width += text.boundingRect().width() / 1.5 input_widths.append(input_width) width += max(input_widths) - port_height = port.boundingRect().height() + if port: + port_height = port.boundingRect().height() if self._output_items: + port = None output_widths = [] for port, text in self._output_items.items(): output_width = port.boundingRect().width() @@ -347,7 +366,8 @@ def calc_size(self, add_w=0.0, add_h=0.0): output_width += text.boundingRect().width() / 1.5 output_widths.append(output_width) width += max(output_widths) - port_height = port.boundingRect().height() + if port: + port_height = port.boundingRect().height() in_count = len([p for p in self.inputs if p.isVisible()]) out_count = len([p for p in self.outputs if p.isVisible()]) @@ -385,12 +405,12 @@ def align_label(self, h_offset=0.0, v_offset=0.0): v_offset (float): vertical offset. h_offset (float): horizontal offset. """ - text_rect = self.text_item.boundingRect() + text_rect = self._text_item.boundingRect() text_x = (self._width / 2) - (text_rect.width() / 2) - text_y = text_rect.height() * -1 + text_y = 2.0 text_x += h_offset text_y += v_offset - self.text_item.setPos(text_x, text_y) + self._text_item.setPos(text_x, text_y) def align_widgets(self, v_offset=0.0): """ @@ -464,16 +484,16 @@ def offset_label(self, x=0.0, y=0.0): x (float): horizontal x offset y (float): vertical y offset """ - icon_x = self.text_item.pos().x() + x - icon_y = self.text_item.pos().y() + y - self.text_item.setPos(icon_x, icon_y) + icon_x = self._text_item.pos().x() + x + icon_y = self._text_item.pos().y() + y + self._text_item.setPos(icon_x, icon_y) def draw_node(self): """ Re-draw the node item in the scene. (re-implemented for vertical layout design) """ - height = self.text_item.boundingRect().height() + height = self._text_item.boundingRect().height() # setup initial base size. self._set_base_size(add_w=0.0, add_h=height) # set text color when node is initialized. @@ -550,7 +570,7 @@ def set_proxy_mode(self, mode): if text.visible_: text.setVisible(visible) - self.text_item.setVisible(visible) + self._text_item.setVisible(visible) self._icon_item.setVisible(visible) @property @@ -601,7 +621,9 @@ def selected(self, selected=False): @AbstractNodeItem.name.setter def name(self, name=''): AbstractNodeItem.name.fset(self, name) - self.text_item.setPlainText(name) + if name == self._text_item.toPlainText(): + return + self._text_item.setPlainText(name) if self.scene(): self.align_label() self.update() @@ -619,6 +641,16 @@ def text_color(self, color=(100, 100, 100, 255)): self._set_text_color(color) self.update() + @property + def text_item(self): + """ + Get the node name text qgraphics item. + + Returns: + NodeTextItem: node text object. + """ + return self._text_item + @property def inputs(self): """ @@ -637,11 +669,13 @@ def outputs(self): def _add_port(self, port): """ + Adds a port qgraphics item into the node. + Args: port (PortItem): port item. Returns: - PortItem: input item widget + PortItem: port qgraphics item. """ text = QtWidgets.QGraphicsTextItem(port.name, self) text.font().setPointSize(8) @@ -660,6 +694,9 @@ def _add_port(self, port): def add_input(self, name='input', multi_port=False, display_name=True, 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. @@ -667,7 +704,7 @@ def add_input(self, name='input', multi_port=False, display_name=True, painter_func (function): custom paint function. Returns: - PortItem: input item widget + PortItem: input port qgraphics item. """ if painter_func: port = CustomPortItem(self, painter_func) @@ -682,6 +719,9 @@ def add_input(self, name='input', multi_port=False, display_name=True, def add_output(self, name='output', multi_port=False, display_name=True, 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. @@ -689,7 +729,7 @@ def add_output(self, name='output', multi_port=False, display_name=True, painter_func (function): custom paint function. Returns: - PortItem: output item widget + PortItem: output port qgraphics item. """ if painter_func: port = CustomPortItem(self, painter_func) @@ -703,9 +743,11 @@ def add_output(self, name='output', multi_port=False, display_name=True, def _delete_port(self, port, text): """ + Removes port item and port text from node. + Args: port (PortItem): port object. - text (QGraphicsTextItem): port text object. + text (QtWidgets.QGraphicsTextItem): port text object. """ port.delete() port.setParentItem(None) @@ -717,6 +759,8 @@ def _delete_port(self, port, text): def delete_input(self, port): """ + Remove input port from node. + Args: port (PortItem): port object. """ @@ -724,6 +768,8 @@ def delete_input(self, port): def delete_output(self, port): """ + Remove output port from node. + Args: port (PortItem): port object. """ @@ -938,11 +984,12 @@ def draw_node(self): self.align_ports() # arrange node widgets self.align_widgets(v_offset=0.0) + self.update() def calc_size(self, add_w=0.0, add_h=0.0): """ - calculate minimum node size. + Calculate minimum node size. Args: add_w (float): additional width. @@ -987,6 +1034,9 @@ def calc_size(self, add_w=0.0, add_h=0.0): def add_input(self, name='input', multi_port=False, display_name=True, 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. @@ -994,7 +1044,7 @@ def add_input(self, name='input', multi_port=False, display_name=True, painter_func (function): custom paint function. Returns: - PortItem: output item widget + PortItem: port qgraphics item. """ return super(NodeItemVertical, self).add_input( name, multi_port, False, painter_func) @@ -1002,6 +1052,9 @@ def add_input(self, name='input', multi_port=False, display_name=True, def add_output(self, name='output', multi_port=False, display_name=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. @@ -1009,7 +1062,7 @@ def add_output(self, name='output', multi_port=False, display_name=False, painter_func (function): custom paint function. Returns: - PortItem: output item widget + PortItem: port qgraphics item. """ return super(NodeItemVertical, self).add_output( name, multi_port, False, painter_func) diff --git a/NodeGraphQt/qgraphics/node_text_item.py b/NodeGraphQt/qgraphics/node_text_item.py index 913a5835..b01cc0de 100644 --- a/NodeGraphQt/qgraphics/node_text_item.py +++ b/NodeGraphQt/qgraphics/node_text_item.py @@ -1,31 +1,98 @@ -from .. import QtWidgets, QtCore +from .. import QtWidgets, QtCore, QtGui class NodeTextItem(QtWidgets.QGraphicsTextItem): - editingFinished = QtCore.Signal(str) + """ + NodeTextItem class used to display and edit the name of a NodeItem. + """ def __init__(self, text, parent=None): super(NodeTextItem, self).__init__(text, parent) self.setFlags(QtWidgets.QGraphicsItem.ItemIsFocusable) - self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.isEditing = False + self.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + self.setToolTip('double-click to edit node name.') + self.set_editable(False) - def _editingFinished(self): - if self.isEditing: - self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.isEditing = False - self.editingFinished.emit(self.toPlainText()) + def mouseDoubleClickEvent(self, event): + """ + Re-implemented to jump into edit mode when user clicks on node text. + + Args: + event (QtWidgets.QGraphicsSceneMouseEvent): mouse event. + """ + if event.button() == QtCore.Qt.LeftButton: + self.set_editable(True) + event.ignore() + return + super(NodeTextItem, self).mouseDoubleClickEvent(event) + + def keyPressEvent(self, event): + """ + Re-implemented to catch the Return & Escape keys when in edit mode. - def mousePressEvent(self, event): - self.setTextInteractionFlags(QtCore.Qt.TextEditable) - self.isEditing = True - super(NodeTextItem, self).mousePressEvent(event) + Args: + event (QtGui.QKeyEvent): key event. + """ + if event.key() == QtCore.Qt.Key_Return: + current_text = self.toPlainText() + self.set_node_name(current_text) + self.set_editable(False) + elif event.key() == QtCore.Qt.Key_Escape: + self.setPlainText(self.node.name) + self.set_editable(False) + super(NodeTextItem, self).keyPressEvent(event) def focusOutEvent(self, event): - self._editingFinished() + """ + Re-implemented to jump out of edit mode. + + Args: + event (QtGui.QFocusEvent): + """ + current_text = self.toPlainText() + self.set_node_name(current_text) + self.set_editable(False) super(NodeTextItem, self).focusOutEvent(event) - def keyPressEvent(self, event): - if event.key() is QtCore.Qt.Key_Return: - self._editingFinished() - super(NodeTextItem, self).keyPressEvent(event) \ No newline at end of file + def set_editable(self, value=False): + """ + Set the edit mode for the text item. + + Args: + value (bool): true in edit mode. + """ + if value: + self.setTextInteractionFlags( + QtCore.Qt.TextEditable | + QtCore.Qt.TextSelectableByMouse | + QtCore.Qt.TextSelectableByKeyboard + ) + else: + self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + cursor = self.textCursor() + cursor.clearSelection() + self.setTextCursor(cursor) + + def set_node_name(self, name): + """ + Updates the node name through the node "NodeViewer().node_name_changed" + signal which then updates the node name through the BaseNode object this + will register it as an undo command. + + Args: + name (str): new node name. + """ + if name == self.node.name: + return + viewer = self.node.viewer() + viewer.node_name_changed.emit(self.node.id, name) + + @property + def node(self): + """ + Get the parent node item. + + Returns: + NodeItem: parent node qgraphics item. + """ + return self.parentItem() diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index 428c1da7..7e5cb0fb 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -27,14 +27,18 @@ class NodeViewer(QtWidgets.QGraphicsView): class:`NodeGraphQt.NodeGraph` class. """ + # node viewer signals. + # (some of these signals are called by port & node items and connected + # to the node graph slot functions) moved_nodes = QtCore.Signal(dict) search_triggered = QtCore.Signal(str, tuple) connection_sliced = QtCore.Signal(list) connection_changed = QtCore.Signal(list, list) insert_node = QtCore.Signal(object, str, dict) need_show_tab_search = QtCore.Signal() + node_name_changed = QtCore.Signal(str, str) - # pass through signals + # pass through signals that are translated into "NodeGraph()" signals. node_selected = QtCore.Signal(str) node_selection_changed = QtCore.Signal(list, list) node_double_clicked = QtCore.Signal(str) @@ -229,8 +233,8 @@ def mousePressEvent(self, event): self._origin_pos = event.pos() self._previous_pos = event.pos() - self._prev_selection_nodes, \ - self._prev_selection_pipes = self.selected_items() + self._prev_selection_nodes, self._prev_selection_pipes = \ + self.selected_items() # close tab search if self._search_widget.isVisible():