Exemple #1
0
    def __init__(self, parameter):
        self._tunneled_parameter = parameter
        super(TunnelParameter, self).__init__(node=parameter.node)

        self.tunneled_parameterChanged = Signal()
        self.sink_changed = Signal()
        self.source_changed = Signal()
Exemple #2
0
    def __init__(self, nodegraph, sourceParam, sinkParam, *args, **kwargs):
        # Infer scene
        if sourceParam is not None and sourceParam.node.parent is not None:
            kwargs['scene'] = nodegraph.getNodeUI(sourceParam.node).scene()
        elif sinkParam is not None and sinkParam.node.parent is not None:
            kwargs['scene'] = nodegraph.getNodeUI(sinkParam.node).scene()
        super(Connection, self).__init__(*args, **kwargs)
        # nodegraph/ramen stuff
        self.nodegraph = nodegraph
        self.source = sourceParam
        self.sink = sinkParam

        # signals
        self.updatedGeo = Signal()
        self.posChanged = Signal()

        # ui stuff
        self.setZValue(-1)
        pen = QtGui.QPen()
        pen.setWidth(0)
        pen.setColor(QtGui.QColor(0, 0, 0, 0))
        self.setPen(pen)

        self._curvePath = QtGui.QPainterPath()
        self._stroker = QtGui.QPainterPathStroker()
        self._selectStroker = QtGui.QPainterPathStroker()
        self._path = QtGui.QPainterPath()
        self._selectPath = QtGui.QPainterPath()
        self.width = 3
        self.registerRamenCallbacks()
        self.registerNodegraphCallbacks()
        self.updateGeo()
Exemple #3
0
    def __init__(self):
        self._parent = None
        self._children = set()
        self._accepts_children = True

        self.accepts_children_changed = Signal()
        self.parent_changed = Signal()
        self.child_added = Signal()
        self.child_removed = Signal()
        self.children_changed = Signal()
Exemple #4
0
    def __init__(self, nodegraph, ramenParameter, *args, **kwargs):
        kwargs['scene'] = nodegraph.getNodeUI(ramenParameter.node).scene()
        super(Parameter, self).__init__(*args, **kwargs)
        self._nodegraph = nodegraph
        self._ramenParameter = ramenParameter
        self._label = QtGui.QGraphicsTextItem(self)

        self._connectionsOut = {}

        # TODO consider wiring the node's updatedGeo to this
        self.updatedGeo = Signal()

        self.registerRamenCallbacks()
        self.syncConnections()
        self.updateGeo()
Exemple #5
0
    def __init__(self, nodegraph, ramenNode, *args, **kwargs):
        if ramenNode.parent is not None:
            kwargs['scene'] = nodegraph.getNodeUI(ramenNode.parent).scene
        super(Node, self).__init__(*args, **kwargs)
        self._nodegraph = nodegraph
        self._ramenNode = ramenNode
        self._parameterUIs = {}

        self._backdrop = QtGui.QGraphicsRectItem(self)
        self._label = QtGui.QGraphicsTextItem(self)

        self.updatedGeo = Signal()

        self.registerRamenCallbacks()
        self.syncParameters()
        self.updateGeo()
Exemple #6
0
class Parameter(QtGui.QGraphicsItem):
    def __init__(self, nodegraph, ramenParameter, *args, **kwargs):
        kwargs['scene'] = nodegraph.getNodeUI(ramenParameter.node).scene()
        super(Parameter, self).__init__(*args, **kwargs)
        self._nodegraph = nodegraph
        self._ramenParameter = ramenParameter
        self._label = QtGui.QGraphicsTextItem(self)

        self._connectionsOut = {}

        # TODO consider wiring the node's updatedGeo to this
        self.updatedGeo = Signal()

        self.registerRamenCallbacks()
        self.syncConnections()
        self.updateGeo()

    @property
    def nodegraph(self):
        return self._nodegraph

    @property
    def ramenParameter(self):
        return self._ramenParameter

    def registerRamenCallbacks(self):
        self.ramenParameter.connection_added.connect(
            self._connectionAddedCallback)
        self.ramenParameter.connection_removed.connect(
            self._connectionRemovedCallback)
        self.ramenParameter.source_changed.connect(self.updateGeo)
        self.ramenParameter.sink_changed.connect(self.updateGeo)

    def deregisterRamenCallbacks(self):
        self.ramenParameter.connection_added.disconnect(
            self._connectionAddedCallback)
        self.ramenParameter.connection_removed.disconnect(
            self._connectionRemovedCallback)
        self.ramenParameter.source_changed.disconnect(self.updateGeo)
        self.ramenParameter.sink_changed.disconnect(self.updateGeo)

    def _connectionAddedCallback(self, source, sink):
        # Only create the connection if we're the source
        # (prevent double-creating the connection)
        if source == self.ramenParameter:
            return self._addConnectionOut(sink)

    def _connectionRemovedCallback(self, source, sink):
        if source == self.ramenParameter:
            return self._removeConnectionOut(sink)

    def updateGeo(self):
        nodeUI = self.nodegraph.getNodeUI(self.ramenParameter.node)

        boundingRect = QtCore.QRectF()
        self._label.setDefaultTextColor(QtGui.QColor(255, 255, 255, 150))
        if self.ramenParameter.parent is None:
            self._label.setPlainText('')
        else:
            labelText = self.ramenParameter.label
            # XXX quick way to show sources/sinks; unicode for arrow
            if self.ramenParameter.sink:
                labelText = u'\u25b6 ' + labelText
            if self.ramenParameter.source:
                labelText = labelText + u' \u25b6'
            self._label.setPlainText(labelText)
            boundingRect |= self._label.boundingRect().translated(
                self._label.pos())
        for childParameter in self.ramenParameter.children:
            childParameterUI = nodeUI.getParameterUI(childParameter)
            childParameterUI.setPos(0, boundingRect.height())
            boundingRect |= childParameterUI.boundingRect().translated(
                childParameterUI.pos())
        self.updatedGeo.emit()

    def boundingRect(self):
        return self.childrenBoundingRect()

    def paint(self, *args):
        pass

    def syncConnections(self):
        connectionsOut = set(self.ramenParameter.connections_out)
        knownConnectionsOut = set(self._connectionsOut.keys())

        connectionsOutToRemove = knownConnectionsOut.difference(connectionsOut)
        connectionsOutToAdd = connectionsOut.difference(knownConnectionsOut)

        for sinkParam in connectionsOutToRemove:
            self._removeConnectionOut(sinkParam)
        for sinkParam in connectionsOutToAdd:
            self._addConnectionOut(sinkParam)

    def _addConnectionOut(self, sinkParam):
        self._connectionsOut[sinkParam] = Connection(self.nodegraph,
                                                     self.ramenParameter,
                                                     sinkParam)

    def _removeConnectionOut(self, sinkParam):
        connUI = self.getConnectionOutUI(sinkParam)
        if connUI is not None:
            connUI.delete()

    def getConnectionOutUI(self, sinkParam):
        return self._connectionsOut.get(sinkParam, None)

    def getConnectionInUI(self, sourceParam):
        # We actually need to go to the other UI item for this
        nodeUI = self.nodegraph.getNodeUI(sourceParam.node)
        paramUI = nodeUI.getParameterUI(sourceParam)
        connUI = paramUI.getConnectionOutUI(sourceParam)
        return connUI
Exemple #7
0
    def __init__(self):
        # ID -> node
        self._nodes = {}
        self._root_node_id = None

        self._selected_nodes = set()

        # Nodes
        self.node_added = Signal()
        self.node_removed = Signal()
        self.node_parent_changed = Signal()

        # Parameters
        self.parameter_added = Signal()
        self.parameter_removed = Signal()
        self.parameter_sink_changed = Signal()
        self.parameter_source_changed = Signal()

        # Connections
        self.connection_added = Signal()
        self.connection_removed = Signal()

        # node attributes
        self.node_id_changed = Signal()
        self.node_label_changed = Signal()
        self.node_attributes_changed = Signal()
        self.node_pos_changed = Signal()
        self.node_selected_changed = Signal()
        self.node_name_changed = Signal()

        self.node_added.connect(self._node_added_callback)
        self.node_removed.connect(self._node_removed_callback)
        self.node_selected_changed.connect(
            self._node_selected_changed_callback)
Exemple #8
0
class Graph(object):
    '''The graph. Contains all our nodes.'''
    def __init__(self):
        # ID -> node
        self._nodes = {}
        self._root_node_id = None

        self._selected_nodes = set()

        # Nodes
        self.node_added = Signal()
        self.node_removed = Signal()
        self.node_parent_changed = Signal()

        # Parameters
        self.parameter_added = Signal()
        self.parameter_removed = Signal()
        self.parameter_sink_changed = Signal()
        self.parameter_source_changed = Signal()

        # Connections
        self.connection_added = Signal()
        self.connection_removed = Signal()

        # node attributes
        self.node_id_changed = Signal()
        self.node_label_changed = Signal()
        self.node_attributes_changed = Signal()
        self.node_pos_changed = Signal()
        self.node_selected_changed = Signal()
        self.node_name_changed = Signal()

        self.node_added.connect(self._node_added_callback)
        self.node_removed.connect(self._node_removed_callback)
        self.node_selected_changed.connect(
            self._node_selected_changed_callback)

    @property
    def nodes(self):
        return self._nodes.values()

    @nodes.setter
    def nodes(self, new_nodes):
        cur_nodes = set(self._nodes.values())
        new_nodes = set(new_nodes)
        # Don't delete the root node!
        new_nodes.add(self.root_node)
        nodes_to_add = new_nodes.difference(cur_nodes)
        nodes_to_remove = cur_nodes.difference(new_nodes)
        for node in nodes_to_add:
            node.graph = self
        for node in nodes_to_remove:
            node.graph = None

    @nodes.deleter
    def nodes(self):
        self.clear()

    def create_node(self, *args, **kwargs):
        # convenience
        kwargs['graph'] = self
        return node.Node(*args, **kwargs)

    def clear(self):
        self.nodes = []

    @property
    def selected_nodes(self):
        return set(self._selected_nodes)

    def __getitem__(self, node_id):
        return self._nodes.get(node_id, None)

    def __contains__(self, node_id):
        return node_id in self._nodes

    @property
    def selected_nodes(self):
        return set(self._selected_nodes)

    @selected_nodes.setter
    def selected_nodes(self, selected_nodes):
        selected_nodes = set(selected_nodes)
        nodes_to_select = selected_nodes.difference(self._selected_nodes)
        nodes_to_unselect = self._selected_nodes.difference(selected_nodes)
        for node in nodes_to_select:
            node.selected = True
        for node in nodes_to_unselect:
            node.selected = False

    def clear_selection(self):
        for node in self.get_selected_nodes():
            node.set_selected(False)

    def _uniquefy_node_id(self, node_id):
        if node_id not in self:
            return node_id
        # The node_id can be either a string or int (or anything else you want,
        # but we need to be able to make a unique type)
        if type(node_id) == str:
            attempt = 0
            unique_node_id = node_id
            while unique_node_id in self:
                attempt += 1
                unique_node_id = node_id + '_' + str(attempt)
            return unique_node_id

        elif type(node_id) == int or type(node_id) == float:
            while node_id in self:
                node_id += 1
            return node_id

        print('Warning! Unable to unquefy node_id type %s' %
              str(type(node_id)))
        return node_id

    @property
    def root_node(self):
        if self._root_node_id is None:
            self._root_node_id = self._uniquefy_node_id('root')
            self._root_node = node.SubgraphNode(node_id=self._root_node_id,
                                                graph=self)
            return self._root_node
        return self[self._root_node_id]

    def _node_added_callback(self, node):
        node_id = node.node_id
        if node_id in self:
            node_id = self._uniquefy_node_id(node_id)
            node.node_id = node_id
        if node.parent is None and node_id != self._root_node_id:
            node.parent = self.root_node
        self._nodes[node_id] = node

    def _node_removed_callback(self, node):
        if node.node_id not in self:
            return
        if node.node_id == self._root_node_id:
            print('Warning: deleting root node')
            self._root_node_id = None
        del self._nodes[node.node_id]

    def _node_selected_changed_callback(self, node):
        if not node.selected and node in self.selected_nodes:
            self._selected_nodes.remove(node)
        elif node.selected and node not in self.selected_nodes:
            self._selected_nodes.add(node)
Exemple #9
0
class Connection(QtGui.QGraphicsPathItem):
    def __init__(self, nodegraph, sourceParam, sinkParam, *args, **kwargs):
        # Infer scene
        if sourceParam is not None and sourceParam.node.parent is not None:
            kwargs['scene'] = nodegraph.getNodeUI(sourceParam.node).scene()
        elif sinkParam is not None and sinkParam.node.parent is not None:
            kwargs['scene'] = nodegraph.getNodeUI(sinkParam.node).scene()
        super(Connection, self).__init__(*args, **kwargs)
        # nodegraph/ramen stuff
        self.nodegraph = nodegraph
        self.source = sourceParam
        self.sink = sinkParam

        # signals
        self.updatedGeo = Signal()
        self.posChanged = Signal()

        # ui stuff
        self.setZValue(-1)
        pen = QtGui.QPen()
        pen.setWidth(0)
        pen.setColor(QtGui.QColor(0, 0, 0, 0))
        self.setPen(pen)

        self._curvePath = QtGui.QPainterPath()
        self._stroker = QtGui.QPainterPathStroker()
        self._selectStroker = QtGui.QPainterPathStroker()
        self._path = QtGui.QPainterPath()
        self._selectPath = QtGui.QPainterPath()
        self.width = 3
        self.registerRamenCallbacks()
        self.registerNodegraphCallbacks()
        self.updateGeo()

    def registerRamenCallbacks(self):
        pass

    def registerNodegraphCallbacks(self):
        # TODO create faster lighterweight self.updatePos
        self.sourceNodeUI.updatedGeo.connect(self.updateGeo)
        self.sinkNodeUI.updatedGeo.connect(self.updateGeo)

    def deregisterRamenCallbacks(self):
        pass

    def deregisterNodegraphCallbacks(self):
        self.sourceNodeUI.updatedGeo.disconnect(self.updateGeo)
        self.sinkNodeUI.updatedGeo.disconnect(self.updateGeo)

    @property
    def sourceNodeUI(self):
        return self.nodegraph.getNodeUI(self.source.node)

    @property
    def sinkNodeUI(self):
        return self.nodegraph.getNodeUI(self.sink.node)

    @property
    def sourceUI(self):
        return self.sourceNodeUI.getParameterUI(self.source)

    @property
    def sinkUI(self):
        return self.sinkNodeUI.getParameterUI(self.sink)

    @property
    def sourcePos(self):
        return self.sourceUI.mapToScene(
            QtCore.QPointF(self.sourceNodeUI.boundingRect().width(),
                           self.sourceUI.boundingRect().height() / 2))

    @property
    def sinkPos(self):
        return self.sinkUI.mapToScene(
            QtCore.QPointF(0,
                           self.sinkUI.boundingRect().height() / 2))

    @property
    def sourceColor(self):
        return QtGui.QColor(255, 255, 255)

    @property
    def sinkColor(self):
        return QtGui.QColor(255, 255, 255)

    def updateGeo(self):
        self._stroker.setWidth(self.width)
        self._selectStroker.setWidth(self.width * 3)

        gradient = QtGui.QLinearGradient(self.sourcePos, self.sinkPos)
        gradient.setColorAt(0, self.sourceColor)
        gradient.setColorAt(1, self.sinkColor)
        brush = QtGui.QBrush(gradient)
        self.setBrush(brush)

        horizDistance = abs(self.sourcePos.x() - self.sinkPos.x())
        ctrlPointDelta = QtCore.QPointF(horizDistance / 4.0, 0)

        self._curvePath = QtGui.QPainterPath(self.sourcePos)
        self._curvePath.cubicTo(self.sourcePos + ctrlPointDelta,
                                self.sinkPos - ctrlPointDelta, self.sinkPos)

        path = self._stroker.createStroke(self._curvePath)

        self.setPath(path)
        self._selectPath = self._selectStroker.createStroke(self._curvePath)
        self.updatedGeo.emit()

    def shape(self):
        return self._selectPath

    def delete(self):
        self.deregisterRamenCallbacks()
        self.deregisterNodegraphCallbacks()
        return self.scene().removeItem(self)
Exemple #10
0
class Parentable(object):
    '''An object with parent, child relationships'''
    def __init__(self):
        self._parent = None
        self._children = set()
        self._accepts_children = True

        self.accepts_children_changed = Signal()
        self.parent_changed = Signal()
        self.child_added = Signal()
        self.child_removed = Signal()
        self.children_changed = Signal()

    @property
    def parent(self):
        return self._parent

    @parent.setter
    def parent(self, parent):
        if parent == self._parent:
            return
        if parent is not None and not parent.accepts_children:
            return

        if self._parent is not None:
            self._parent.disown_child(self)
            self._parent = None

        self._parent = parent
        if parent is not None:
            parent.adopt_child(self)
        self.parent_changed.emit(parent=parent)

    @property
    def ancestors(self):
        res = []
        curItem = self
        while curItem.parent is not None:
            res.append(curItem.parent)
            curItem = curItem.parent
        return res

    @ancestors.setter
    def ancestors(self, new_ancestors):
        # This is basically calling parent.setter on everything in the list
        new_ancestors = [None] + new_ancestors + [self]

        def pairwise(iterable):
            # TODO: stick this somewhere better
            a, b = itertools.tee(iterable)
            next(b, None)
            return zip(a, b)

        for parent, child in pairwise(new_ancestors):
            child.parent = parent

    @property
    def children(self):
        # explicitly copy for now
        # TODO: make this work with .add, and etc
        return set(self._children)

    @children.setter
    def children(self, new_children):
        children_to_adopt = new_children.difference(self._children)
        children_to_disown = self._children.difference(new_children)
        if self.accepts_children:
            for child in children_to_adopt:
                self.adopt_child(child)
        for child in children_to_disown:
            self.disown_child(child)

    @property
    def accepts_children(self):
        return self._accepts_children

    @accepts_children.setter
    def accepts_children(self, accepts_children):
        self._accepts_children = accepts_children
        self.accepts_children_changed.emit(accepts_children=accepts_children)

    def adopt_child(self, child):
        if not self.accepts_children:
            return
        if child in self._children:
            return
        self._children.add(child)
        child.parent = self
        self.child_added.emit(child=child)

    def disown_child(self, child):
        if child not in self._children:
            return
        self._children.remove(child)
        child.parent = None
        self.child_removed.emit(child=child)
Exemple #11
0
class Node(QtGui.QGraphicsItem):
    def __init__(self, nodegraph, ramenNode, *args, **kwargs):
        if ramenNode.parent is not None:
            kwargs['scene'] = nodegraph.getNodeUI(ramenNode.parent).scene
        super(Node, self).__init__(*args, **kwargs)
        self._nodegraph = nodegraph
        self._ramenNode = ramenNode
        self._parameterUIs = {}

        self._backdrop = QtGui.QGraphicsRectItem(self)
        self._label = QtGui.QGraphicsTextItem(self)

        self.updatedGeo = Signal()

        self.registerRamenCallbacks()
        self.syncParameters()
        self.updateGeo()

    @property
    def nodegraph(self):
        return self._nodegraph

    @property
    def ramenNode(self):
        return self._ramenNode

    def registerRamenCallbacks(self):
        self._ramenNode.pos_changed.connect(self.updateGeo)
        self._ramenNode.selected_changed.connect(self.updateGeo)
        self._ramenNode.label_changed.connect(self.updateGeo)
        self._ramenNode.parameter_added.connect(self._addParameter)
        self._ramenNode.parameter_removed.connect(self._removeParameter)

    def deregisterRamenCallbacks(self):
        self._ramenNode.pos_changed.disconnect(self.updateGeo)
        self._ramenNode.selected_changed.disconnect(self.updateGeo)
        self._ramenNode.label_changed.disconnect(self.updateGeo)
        self._ramenNode.parameter_added.disconnect(self._addParameter)
        self._ramenNode.parameter_removed.disconnect(self._removeParameter)

    def _addParameter(self, parameter):
        for ancestor in parameter.ancestors:
            if ancestor not in self._parameterUIs:
                self._addParameter(ancestor)
        parentUI = self
        if parameter.parent is not None:
            parentUI = self._parameterUIs[parameter.parent]
        self._parameterUIs[parameter] = Parameter(self.nodegraph,
                                                  parameter,
                                                  parentUI)
        if parameter.parent is not None:
            self._parameterUIs[parameter.parent].updateGeo()

    def _removeParameter(self, parameter):
        self.scene().removeItem(self._parameterUIs[parameter])
        del self._parameterUIs[parameter]
        if parameter.parent is not None:
            self._parameterUIs[parameter.parent].updateGeo()

    def getParameterUI(self, parameter):
        return self._parameterUIs[parameter]

    def syncParameters(self):
        ramenParameters = set(self.ramenNode.parameters)
        knownParameters = set(self._parameterUIs.keys())
        parametersToRemove = knownParameters.difference(ramenParameters)
        parametersToAdd = ramenParameters.difference(knownParameters)
        for parameter in parametersToRemove:
            self._removeParameter(parameter)
        for parameter in parametersToAdd:
            self._addParameter(parameter)

    def updateGeo(self):
        # Temp colors for now -- style later
        self._label.setDefaultTextColor(QtGui.QColor(255, 255, 255, 200))
        if self.ramenNode.selected:
            self._backdrop.setPen(QtGui.QColor(200, 200, 200, 200))
        else:
            self._backdrop.setPen(QtGui.QColor(100, 100, 100, 200))
        self._backdrop.setBrush(QtGui.QColor(30, 30, 30, 200))

        backdropRect = QtCore.QRectF(0, 0, 100, 30)
        self.setPos(*self.ramenNode.pos)
        if self.ramenNode.label is not None:
            self._label.setPlainText(self.ramenNode.label)
            backdropRect |= self._label.boundingRect()

        if len(self.ramenNode.parameters) > 0:
            rootParamUI = self._parameterUIs[self.ramenNode.root_parameter]
            rootParamUIPos = self._label.boundingRect().height()
            rootParamUI.setPos(0, rootParamUIPos)
            backdropRect |= rootParamUI.boundingRect().translated(
                0, rootParamUIPos)

        self._backdrop.setRect(backdropRect)
        self.updatedGeo.emit()

    def boundingRect(self):
        return self.childrenBoundingRect()

    def paint(self, *args):
        # TODO: figure out what this does
        pass

    @property
    def node(self):
        return self._ramenNode
Exemple #12
0
    def __init__(self,
                 label=None,
                 parameter_id=0,
                 parent=None,
                 index=0,
                 node=None,
                 source=False,
                 sink=False):
        super(Parameter, self).__init__()
        if parent is not None:
            if parent.node is not node and node is not None:
                print("Warning: Specified node differs from parent's node. "
                      "Using parent's node")
            node = parent.node
        # Properties
        self._parameter_id = parameter_id
        self._label = label
        self._inbex = index

        # Attached node
        self._node = node
        # Outward and inward connections
        self._connections_out = set()
        self._connections_in = set()

        # property signals
        self.parameter_id_changed = Signal()
        self.label_changed = Signal()
        self.index_changed = Signal()

        self.connection_added = Signal()
        self.connection_removed = Signal()
        self.sink_changed = Signal()
        self.source_changed = Signal()

        self.parent = parent
        # TODO: this needs to fill in the other sink/source value
        # self.connectionModeChanged = Signal()
        # self.sink_changed.connect(self.connectionModeChanged.emit)
        # self.source_changed.connect(self.connectionModeChanged.emit)
        self.source = source
        self.sink = sink
        self.register_callbacks()
        if self._node is not None:
            self._node.parameter_added.emit(parameter=self, param=self)
Exemple #13
0
class Parameter(parentable.Parentable, connectable.Connectable):
    def __init__(self,
                 label=None,
                 parameter_id=0,
                 parent=None,
                 index=0,
                 node=None,
                 source=False,
                 sink=False):
        super(Parameter, self).__init__()
        if parent is not None:
            if parent.node is not node and node is not None:
                print("Warning: Specified node differs from parent's node. "
                      "Using parent's node")
            node = parent.node
        # Properties
        self._parameter_id = parameter_id
        self._label = label
        self._inbex = index

        # Attached node
        self._node = node
        # Outward and inward connections
        self._connections_out = set()
        self._connections_in = set()

        # property signals
        self.parameter_id_changed = Signal()
        self.label_changed = Signal()
        self.index_changed = Signal()

        self.connection_added = Signal()
        self.connection_removed = Signal()
        self.sink_changed = Signal()
        self.source_changed = Signal()

        self.parent = parent
        # TODO: this needs to fill in the other sink/source value
        # self.connectionModeChanged = Signal()
        # self.sink_changed.connect(self.connectionModeChanged.emit)
        # self.source_changed.connect(self.connectionModeChanged.emit)
        self.source = source
        self.sink = sink
        self.register_callbacks()
        if self._node is not None:
            self._node.parameter_added.emit(parameter=self, param=self)

    def delete(self):
        self.parent = None
        self.node = None

    def register_callbacks(self):
        if self._node is None:
            return
        self.connection_added.connect(self.node.connection_added.emit,
                                      parameter=self,
                                      param=self)
        self.connection_removed.connect(self.node.connection_removed.emit,
                                        parameter=self,
                                        param=self)
        self.sink_changed.connect(self.node.parameter_sink_changed.emit,
                                  parameter=self,
                                  param=self)
        self.source_changed.connect(self.node.parameter_source_changed.emit,
                                    parameter=self,
                                    param=self)

    def deregister_callbacks(self):
        if self._node is None:
            return
        self.connection_added.disconnect(self.node.connection_added.emit)
        self.connection_removed.disconnect(self.node.connection_removed.emit)
        self.sink_changed.disconnect(self.node.parameter_sink_changed.emit)
        self.source_changed.disconnect(self.node.parameter_source_changed.emit)

    def __repr__(self):
        '''fake convenience repr'''
        node_str = 'No node'
        if self.node is not None:
            node_str = 'Node: %s' % repr(self.node)

        return '<%s: %s/%s (%s)>' % (self.__class__.__name__,
                                     repr(self.parameter_id), self.label,
                                     node_str)

    @property
    def parameter_id(self):
        return self._parameter_id

    @parameter_id.setter
    def parameter_id(self, new_val):
        self._parameter_id = new_val
        self.parameter_id_changed.emit(parameter_id=self._parameter_id)

    @parameter_id.deleter
    def parameter_id(self):
        del self._parameter_id

    @property
    def label(self):
        return self._label

    @label.setter
    def label(self, new_val):
        self._label = new_val
        self.label_changed.emit(label=self._label)

    @label.deleter
    def label(self):
        del self._label

    @property
    def index(self):
        return self._index

    @index.setter
    def index(self, new_val):
        self._index = new_val
        self.index_changed.emit(index=self._index)

    @index.deleter
    def index(self):
        del self._index

    @property
    def node(self):
        return self._node

    @node.setter
    def node(self, new_node):
        if self._parent is not None:
            self.parent = None
        if self._node is not None:
            self.deregister_callbacks()
            self.parameter_removed.emit(parameter=self, param=self)
        self._node = new_node
        self.register_callbacks()
        self.parameter_added.emit(parameter=self, param=self)

    def is_transitively_connected(self, param):
        # TODO: two-sided BFS a la path tracing
        return param in self.connections

    @property
    def connection_subgraph(self):
        return self.node.parent

    def loft(self):
        # In ambiguous cases, loft the sink
        if self.sink:
            return self.loft_sink()
        if self.source:
            return self.loft_source()

    def loft_sink(self):
        if not self.sink:
            return None
        if self.node.parent is None:
            return None
        for tunnel_param in self.node.parent.tunnel_parameters:
            if self in tunnel_param.connections:
                return self.node.parent.get_parameter_for_tunnel(tunnel_param)
        lofted_param = Parameter(node=self.node.parent)
        lofted_param.sink = True
        self.node.parent.get_tunnel_parameter(lofted_param).connect(self)
        return lofted_param

    def loft_source(self):
        if not self.source:
            return None
        if self.node.parent is None:
            return None
        for tunnel_param in self.node.parent.tunnel_parameters:
            if self in tunnel_param.connections:
                return self.node.parent.get_parameter_for_tunnel(tunnel_param)
        lofted_param = Parameter(node=self.node.parent)
        lofted_param.source = True
        self.node.parent.get_tunnel_parameter(lofted_param).connect(self)
        return lofted_param

    def connect(self, param):
        # Easy case, connection in the same subgraph
        if self.connection_subgraph == param.connection_subgraph:
            return super(Parameter, self).connect(param)

        # Hard case, different parents
        # Loft up the one with more ancestors.
        # (in the case of equal length but different ancestors just choose
        # one and the other will be lofted in recursion)
        # Determine directionality
        # In ambiguous cases, prefer that we are the source
        self_is_source = True
        if self.source and param.sink:
            self_is_source = True
        elif self.sink and param.source:
            self_is_source = False
        else:
            print('unable to connect')

        deep_param = self
        surface_param = param
        deep_is_source = self_is_source
        if len(self.node.ancestors) < len(param.node.ancestors):
            deep_param = param
            surface_param = self
            deep_is_source = not self_is_source
        if deep_is_source:
            lofted_param = deep_param.loft_source()
        else:
            lofted_param = deep_param.loft_sink()
        if lofted_param is not None:
            return lofted_param.connect(surface_param)