# -*- coding: utf-8 -*-
"""
A module containing NodeEditor's class for representing Edge and Edge Type Constants.
"""
from collections import OrderedDict
from node_editor.node_graphics_edge import QDMGraphicsEdge
from node_editor.node_serializable import Serializable
from node_editor.utils import dumpException
EDGE_TYPE_DIRECT = 1 #:
EDGE_TYPE_BEZIER = 2 #:
EDGE_TYPE_SQUARE = 3 #:
EDGE_TYPE_DEFAULT = EDGE_TYPE_BEZIER
DEBUG = False
[docs]class Edge(Serializable):
"""
Class for representing Edge in NodeEditor.
"""
edge_validators = [] #: class variable containing list of registered edge validators
def __init__(self, scene:'Scene', start_socket:'Socket'=None, end_socket:'Socket'=None, edge_type=EDGE_TYPE_DIRECT):
"""
:param scene: Reference to the :py:class:`~node_editor.node_scene.Scene`
:type scene: :py:class:`~nodeeditor.node_scene.Scene`
:param start_socket: Reference to the starting socket
:type start_socket: :py:class:`~nodeeditor.node_socket.Socket`
:param end_socket: Reference to the End socket or ``None``
:type end_socket: :py:class:`~nodeeditor.node_socket.Socket` or ``None``
:param edge_type: Constant determining type of edge. See :ref:`edge-type-constants`
:Instance Attributes:
- **scene** - reference to the :class:`~node_editor.node_scene.Scene`
- **grEdge** - Instance of :class:`~node_editor.node_graphics_edge.QDMGraphicsEdge` subclass handling graphical representation in the ``QGraphicsScene``.
"""
super().__init__()
self.scene = scene
# default init
self._start_socket = None
self._end_socket = None
self.start_socket = start_socket
self.end_socket = end_socket
self._edge_type = edge_type
# create Graphics Edge instance
self.grEdge = self.createEdgeClassInstance()
self.scene.addEdge(self)
def __str__(self):
return "<Edge %s..%s -- S:%s E:%s>" % (
hex(id(self))[2:5], hex(id(self))[-3:],
self.start_socket, self.end_socket
)
@property
def start_socket(self):
"""
Start socket
:getter: Returns start :class:`~node_editor.node_socket.Socket`
:setter: Sets start :class:`~node_editor.node_socket.Socket` safely
:type: :class:`~node_editor.node_socket.Socket`
"""
return self._start_socket
@start_socket.setter
def start_socket(self, value):
# if we were assigned to some socket before, delete us from the socket
if self._start_socket is not None:
self._start_socket.removeEdge(self)
# assign new start socket
self._start_socket = value
# addEdge to the Socket class
if self.start_socket is not None:
self.start_socket.addEdge(self)
@property
def end_socket(self):
"""
End socket
:getter: Returns end :class:`~node_editor.node_socket.Socket` or ``None`` if not set
:setter: Sets end :class:`~node_editor.node_socket.Socket` safely
:type: :class:`~node_editor.node_socket.Socket` or ``None``
"""
return self._end_socket
@end_socket.setter
def end_socket(self, value):
# if we were assigned to some socket before, delete us from the socket
if self._end_socket is not None:
self._end_socket.removeEdge(self)
# assign new end socket
self._end_socket= value
# addEdge to the Socket class
if self.end_socket is not None:
self.end_socket.addEdge(self)
@property
def edge_type(self):
"""
Edge type
:getter: get edge type constant for current ``Edge``. See :ref:`edge-type-constants`
:setter: sets new edge type. On background, creates new :class:`~node_editor.node_graphics_edge.QDMGraphicsEdge`
child class if necessary, adds this ``QGraphicsPathItem`` to the ``QGraphicsScene`` and updates edge sockets
positions.
"""
return self._edge_type
@edge_type.setter
def edge_type(self, value):
# assign new value
self._edge_type = value
# update the grEdge pathCalculator
self.grEdge.createEdgePathCalculator()
if self.start_socket is not None:
self.updatePositions()
[docs] @classmethod
def getEdgeValidators(cls):
"""Return the list of Edge Validator Callbacks"""
return cls.edge_validators
[docs] @classmethod
def registerEdgeValidator(cls, validator_callback: 'function'):
"""Register Edge Validator Callback
:param validator_callback: A function handle to validate Edge
:type validator_callback: `function`
"""
cls.edge_validators.append(validator_callback)
[docs] @classmethod
def validateEdge(cls, start_socket: 'Socket', end_socket: 'Socket') -> bool:
"""Validate Edge agains all registered `Edge Validator Callbacks`
:param start_socket: Starting :class:`~node_editor.node_socket.Socket` of Edge to check
:type start_socket: :class:`~nodeeditor.node_socket.Socket`
:param end_socket: Target/End :class:`~node_editor.node_socket.Socket` of Edge to check
:type end_socket: :class:`~nodeeditor.node_socket.Socket`
:return: ``True`` if the Edge is valid or ``False`` if not
:rtype: ``bool``
"""
for validator in cls.getEdgeValidators():
if not validator(start_socket, end_socket):
return False
return True
[docs] def reconnect(self, from_socket: 'Socket', to_socket: 'Socket'):
"""Helper function which reconnects edge `from_socket` to `to_socket`"""
if self.start_socket == from_socket:
self.start_socket = to_socket
elif self.end_socket == from_socket:
self.end_socket = to_socket
[docs] def getGraphicsEdgeClass(self):
"""Returns the class representing Graphics Edge"""
return QDMGraphicsEdge
[docs] def createEdgeClassInstance(self):
"""
Create instance of grEdge class
:return: Instance of `grEdge` class representing the Graphics Edge in the grScene
"""
self.grEdge = self.getGraphicsEdgeClass()(self)
self.scene.grScene.addItem(self.grEdge)
if self.start_socket is not None:
self.updatePositions()
return self.grEdge
[docs] def getOtherSocket(self, known_socket:'Socket'):
"""
Returns the opposite socket on this ``Edge``
:param known_socket: Provide known :class:`~node_editor.node_socket.Socket` to be able to determine the opposite one.
:type known_socket: :class:`~nodeeditor.node_socket.Socket`
:return: The oposite socket on this ``Edge`` or ``None``
:rtype: :class:`~nodeeditor.node_socket.Socket` or ``None``
"""
return self.start_socket if known_socket == self.end_socket else self.end_socket
[docs] def doSelect(self, new_state:bool=True):
"""
Provide the safe selecting/deselecting operation. In the background it takes care about the flags, notifications
and storing history for undo/redo.
:param new_state: ``True`` if you want to select the ``Edge``, ``False`` if you want to deselect the ``Edge``
:type new_state: ``bool``
"""
self.grEdge.doSelect(new_state)
[docs] def updatePositions(self):
"""
Updates the internal `Graphics Edge` positions according to the start and end :class:`~node_editor.node_socket.Socket`.
This should be called if you update ``Edge`` positions.
"""
source_pos = self.start_socket.getSocketPosition()
source_pos[0] += self.start_socket.node.grNode.pos().x()
source_pos[1] += self.start_socket.node.grNode.pos().y()
self.grEdge.setSource(*source_pos)
if self.end_socket is not None:
end_pos = self.end_socket.getSocketPosition()
end_pos[0] += self.end_socket.node.grNode.pos().x()
end_pos[1] += self.end_socket.node.grNode.pos().y()
self.grEdge.setDestination(*end_pos)
else:
self.grEdge.setDestination(*source_pos)
self.grEdge.update()
[docs] def remove_from_sockets(self):
"""
Helper function which sets start and end :class:`~node_editor.node_socket.Socket` to ``None``
"""
self.end_socket = None
self.start_socket = None
[docs] def remove(self, silent_for_socket:'Socket'=None, silent=False):
"""
Safely remove this Edge.
Removes `Graphics Edge` from the ``QGraphicsScene`` and it's reference to all GC to clean it up.
Notifies nodes previously connected :class:`~node_editor.node_node.Node` (s) about this event.
Triggers Nodes':
- :py:meth:`~node_editor.node_node.Node.onEdgeConnectionChanged`
- :py:meth:`~node_editor.node_node.Node.onInputChanged`
:param silent_for_socket: :class:`~node_editor.node_socket.Socket` of a :class:`~node_editor.node_node.Node` which
won't be notified, when this ``Edge`` is going to be removed
:type silent_for_socket: :class:`~nodeeditor.node_socket.Socket`
:param silent: ``True`` if no events should be triggered during removing
:type silent: ``bool``
"""
old_sockets = [self.start_socket, self.end_socket]
# ugly hack, since I noticed that even when you remove grEdge from scene,
# sometimes it stays there! How dare you Qt!
if DEBUG: print(" - hide grEdge")
self.grEdge.hide()
if DEBUG: print(" - remove grEdge", self.grEdge)
self.scene.grScene.removeItem(self.grEdge)
if DEBUG: print(" grEdge:", self.grEdge)
self.scene.grScene.update()
if DEBUG: print("# Removing Edge", self)
if DEBUG: print(" - remove edge from all sockets")
self.remove_from_sockets()
if DEBUG: print(" - remove edge from scene")
try:
self.scene.removeEdge(self)
except ValueError:
pass
if DEBUG: print(" - everything is done.")
try:
# notify nodes from old sockets
for socket in old_sockets:
if socket and socket.node:
if silent:
continue
if silent_for_socket is not None and socket == silent_for_socket:
# if we requested silence for Socket and it's this one, skip notifications
continue
# notify Socket's Node
socket.node.onEdgeConnectionChanged(self)
if socket.is_input: socket.node.onInputChanged(socket)
except Exception as e: dumpException(e)
[docs] def serialize(self) -> OrderedDict:
return OrderedDict([
('id', self.id),
('edge_type', self.edge_type),
('start', self.start_socket.id if self.start_socket is not None else None),
('end', self.end_socket.id if self.end_socket is not None else None),
])
[docs] def deserialize(self, data:dict, hashmap:dict={}, restore_id:bool=True, *args, **kwargs) -> bool:
if restore_id: self.id = data['id']
self.start_socket = hashmap[data['start']]
self.end_socket = hashmap[data['end']]
self.edge_type = data['edge_type']
# Example: using validators for Edge
# You can register edge validators wherever you want, even here...
# However if you do use overridden Edge, you should call registerEdgeValidator on that overridden class
#
# from node_editor.node_edge_validators import *
# Edge.registerEdgeValidator(edge_validator_debug)
# Edge.registerEdgeValidator(edge_cannot_connect_two_outputs_or_two_inputs)
# Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_same_node)
# Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_different_color)