Aleš Erjavec avatar Aleš Erjavec committed dcd1485 Merge

Merge

Comments (0)

Files changed (18)

Orange/OrangeCanvas/application/canvasmain.py

                     checkable=True,
                     checked=True,
                     shortcut=QKeySequence(Qt.ControlModifier |
-                                          Qt.ShiftModifier | Qt.Key_D),
+                                          (Qt.ShiftModifier | Qt.Key_D)),
                     triggered=self.set_tool_dock_expanded)
 
         # Gets assigned in setup_ui (the action is defined in CanvasToolDock)

Orange/OrangeCanvas/canvas/__init__.py

 Canvas
 ======
 
-The :mod:`canvas` package contains classes for visualizing the
-contents of a :class:`.scheme.Scheme`, based on the Qt's Graphics view
-framework.
+The :mod:`.canvas` package contains classes for visualizing the
+contents of a :class:`~.scheme.Scheme`, utilizing the Qt's `Graphics View
+Framework`_.
+
+.. _`Graphics View Framework`: http://qt-project.org/doc/qt-4.8/graphicsview.html
 
 """
 

Orange/OrangeCanvas/canvas/editlinksdialog.py

-"""
-An Dialog to edit links between two nodes in the scheme.
-
-"""
-
-from collections import namedtuple
-
-from xml.sax.saxutils import escape
-
-from PyQt4.QtGui import (
-    QApplication, QDialog, QVBoxLayout, QDialogButtonBox, QGraphicsScene,
-    QGraphicsView, QGraphicsWidget, QGraphicsRectItem,
-    QGraphicsLineItem, QGraphicsTextItem, QGraphicsLayoutItem,
-    QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsPixmapItem,
-    QGraphicsDropShadowEffect, QSizePolicy, QPalette, QPen,
-    QPainter
-)
-
-from PyQt4.QtCore import (
-    Qt, QObject, QSize, QSizeF, QPointF, QRectF, qVersion
-)
-
-from PyQt4.QtCore import pyqtSignal as Signal
-
-from ..scheme import SchemeNode, SchemeLink, compatible_channels
-from ..registry import InputSignal, OutputSignal
-
-from ..resources import icon_loader
-
-
-QWIDGETSIZE_MAX = ((1 << 24) - 1)
-
-
-class EditLinksDialog(QDialog):
-    """
-    A dialog for editing links.
-
-    >>> dlg = EditLinksDialog()
-    >>> dlg.setNodes(file_node, test_learners_node)
-    >>> dlg.setLinks([(file_node.output_channel("Data"),
-    ...               (test_learners_node.input_channel("Data")])
-    >>> if dlg.exec_() == EditLinksDialog.Accpeted:
-    ...     new_links = dlg.links()
-    ...
-
-    """
-    def __init__(self, *args, **kwargs):
-        QDialog.__init__(self, *args, **kwargs)
-
-        self.setModal(True)
-
-        self.__setupUi()
-
-    def __setupUi(self):
-        layout = QVBoxLayout()
-
-        # Scene with the link editor.
-        self.scene = LinksEditScene()
-        self.view = QGraphicsView(self.scene)
-        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-        self.view.setRenderHint(QPainter.Antialiasing)
-
-        self.scene.editWidget.geometryChanged.connect(self.__onGeometryChanged)
-
-        # Ok/Cancel/Clear All buttons.
-        buttons = QDialogButtonBox(QDialogButtonBox.Ok |
-                                   QDialogButtonBox.Cancel |
-                                   QDialogButtonBox.Reset,
-                                   Qt.Horizontal)
-
-        clear_button = buttons.button(QDialogButtonBox.Reset)
-        clear_button.setText(self.tr("Clear All"))
-
-        buttons.accepted.connect(self.accept)
-        buttons.rejected.connect(self.reject)
-        clear_button.clicked.connect(self.scene.editWidget.clearLinks)
-
-        layout.addWidget(self.view)
-        layout.addWidget(buttons)
-
-        self.setLayout(layout)
-        layout.setSizeConstraint(QVBoxLayout.SetFixedSize)
-
-        self.setSizeGripEnabled(False)
-
-    def setNodes(self, source_node, sink_node):
-        """Set the source/sink nodes (`SchemeNode` instances)
-        between which to edit the links.
-
-        """
-        self.scene.editWidget.setNodes(source_node, sink_node)
-
-    def setLinks(self, links):
-        """Set a list of links to display between the source and sink
-        nodes. The `links` is a list of (`OutputSignal`, `InputSignal`)
-        instances where the first element refers to the source node
-        and the second to the sink node.
-
-        """
-        self.scene.editWidget.setLinks(links)
-
-    def links(self):
-        """Return the links between the source and sink node.
-        """
-        return self.scene.editWidget.links()
-
-    def __onGeometryChanged(self):
-        size = self.scene.editWidget.size()
-        left, top, right, bottom = self.getContentsMargins()
-        self.view.setFixedSize(size.toSize() + \
-                               QSize(left + right + 4, top + bottom + 4))
-
-
-def find_item_at(scene, pos, order=Qt.DescendingOrder, type=None,
-                 name=None):
-    """Find an object in a :class:`QGraphicsScene` `scene` at `pos`.
-    If `type` is not `None` the it must specify  the type of the item.
-    I `name` is not `None` it must be a name of the object
-    (`QObject.objectName()`).
-
-    """
-    items = scene.items(pos, Qt.IntersectsItemShape, order)
-    for item in items:
-        if type is not None and \
-                not isinstance(item, type):
-            continue
-
-        if name is not None and isinstance(item, QObject) and \
-                item.objectName() != name:
-            continue
-        return item
-    else:
-        return None
-
-
-class LinksEditScene(QGraphicsScene):
-    """A :class:`QGraphicsScene` used by the :class:`LinkEditWidget`.
-
-    """
-    def __init__(self, *args, **kwargs):
-        QGraphicsScene.__init__(self, *args, **kwargs)
-
-        self.editWidget = LinksEditWidget()
-        self.addItem(self.editWidget)
-
-    findItemAt = find_item_at
-
-
-_Link = namedtuple(
-    "_Link",
-    ["output",    # OutputSignal
-     "input",     # InputSignal
-     "lineItem",  # QGraphicsLineItem connecting the input to output
-     ])
-
-
-class LinksEditWidget(QGraphicsWidget):
-    """
-    A Graphics Widget for editing the links between two nodes.
-    """
-    def __init__(self, *args, **kwargs):
-        QGraphicsWidget.__init__(self, *args, **kwargs)
-        self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton)
-
-        self.source = None
-        self.sink = None
-
-        # QGraphicsWidget/Items in the scene.
-        self.sourceNodeWidget = None
-        self.sourceNodeTitle = None
-        self.sinkNodeWidget = None
-        self.sinkNodeTitle = None
-
-        self.__links = []
-
-        self.__textItems = []
-        self.__iconItems = []
-        self.__tmpLine = None
-        self.__dragStartItem = None
-
-        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))
-        self.layout().setContentsMargins(0, 0, 0, 0)
-
-    def removeItems(self, items):
-        """
-        Remove child items form the widget and scene.
-        """
-        scene = self.scene()
-        for item in items:
-            item.setParentItem(None)
-            if scene is not None:
-                scene.removeItem(item)
-
-    def clear(self):
-        """
-        Clear the editor state (source and sink nodes, channels ...).
-        """
-        if self.layout().count():
-            widget = self.layout().takeAt(0).graphicsItem()
-            self.removeItems([widget])
-
-        self.source = None
-        self.sink = None
-
-    def setNodes(self, source, sink):
-        """
-        Set the source/sink nodes (:class:`SchemeNode` instances) between
-        which to edit the links.
-
-        .. note:: Call this before `setLinks`.
-
-        """
-        self.clear()
-
-        self.source = source
-        self.sink = sink
-
-        self.__updateState()
-
-    def setLinks(self, links):
-        """
-        Set a list of links to display between the source and sink
-        nodes. `links` must be a list of (`OutputSignal`, `InputSignal`)
-        tuples where the first element refers to the source node
-        and the second to the sink node (as set by `setNodes`).
-
-        """
-        self.clearLinks()
-        for output, input in links:
-            self.addLink(output, input)
-
-    def links(self):
-        """
-        Return the links between the source and sink node.
-        """
-        return [(link.output, link.input) for link in self.__links]
-
-    def mousePressEvent(self, event):
-        if event.button() == Qt.LeftButton:
-            startItem = find_item_at(self.scene(), event.pos(),
-                                     type=ChannelAnchor)
-            if startItem is not None:
-                # Start a connection line drag.
-                self.__dragStartItem = startItem
-                self.__tmpLine = None
-                event.accept()
-                return
-
-            lineItem = find_item_at(self.scene(), event.scenePos(),
-                                    type=QGraphicsLineItem)
-            if lineItem is not None:
-                # Remove a connection under the mouse
-                for link in self.__links:
-                    if link.lineItem == lineItem:
-                        self.removeLink(link.output, link.input)
-                event.accept()
-                return
-
-        QGraphicsWidget.mousePressEvent(self, event)
-
-    def mouseMoveEvent(self, event):
-        if event.buttons() & Qt.LeftButton:
-
-            downPos = event.buttonDownPos(Qt.LeftButton)
-            if not self.__tmpLine and self.__dragStartItem and \
-                    (downPos - event.pos()).manhattanLength() > \
-                        QApplication.instance().startDragDistance():
-                # Start a line drag
-                line = QGraphicsLineItem(self)
-                start = self.__dragStartItem.boundingRect().center()
-                start = self.mapFromItem(self.__dragStartItem, start)
-                line.setLine(start.x(), start.y(),
-                             event.pos().x(), event.pos().y())
-
-                pen = QPen(Qt.green, 4)
-                pen.setCapStyle(Qt.RoundCap)
-                line.setPen(pen)
-                line.show()
-
-                self.__tmpLine = line
-
-            if self.__tmpLine:
-                # Update the temp line
-                line = self.__tmpLine.line()
-                line.setP2(event.pos())
-                self.__tmpLine.setLine(line)
-
-        QGraphicsWidget.mouseMoveEvent(self, event)
-
-    def mouseReleaseEvent(self, event):
-        if event.button() == Qt.LeftButton and self.__tmpLine:
-            endItem = find_item_at(self.scene(), event.scenePos(),
-                                     type=ChannelAnchor)
-
-            if endItem is not None:
-                startItem = self.__dragStartItem
-                startChannel = startItem.channel()
-                endChannel = endItem.channel()
-                possible = False
-
-                # Make sure the drag was from input to output (or reversed) and
-                # not between input -> input or output -> output
-                if type(startChannel) != type(endChannel):
-                    if isinstance(startChannel, InputSignal):
-                        startChannel, endChannel = endChannel, startChannel
-
-                    possible = compatible_channels(startChannel, endChannel)
-
-                if possible:
-                    self.addLink(startChannel, endChannel)
-
-            self.scene().removeItem(self.__tmpLine)
-            self.__tmpLine = None
-            self.__dragStartItem = None
-
-        QGraphicsWidget.mouseReleaseEvent(self, event)
-
-    def addLink(self, output, input):
-        """
-        Add a link between `output` (:class:`OutputSignal`) and `input`
-        (:class:`InputSignal`).
-
-        """
-        if not compatible_channels(output, input):
-            return
-
-        if output not in self.source.output_channels():
-            raise ValueError("%r is not an output channel of %r" % \
-                             (output, self.source))
-
-        if input not in self.sink.input_channels():
-            raise ValueError("%r is not an input channel of %r" % \
-                             (input, self.sink))
-
-        if input.single:
-            # Remove existing link if it exists.
-            for s1, s2, _ in self.__links:
-                if s2 == input:
-                    self.removeLink(s1, s2)
-
-        line = QGraphicsLineItem(self)
-
-        source_anchor = self.sourceNodeWidget.anchor(output)
-        sink_anchor = self.sinkNodeWidget.anchor(input)
-
-        source_pos = source_anchor.boundingRect().center()
-        source_pos = self.mapFromItem(source_anchor, source_pos)
-
-        sink_pos = sink_anchor.boundingRect().center()
-        sink_pos = self.mapFromItem(sink_anchor, sink_pos)
-        line.setLine(source_pos.x(), source_pos.y(),
-                     sink_pos.x(), sink_pos.y())
-        pen = QPen(Qt.green, 4)
-        pen.setCapStyle(Qt.RoundCap)
-        line.setPen(pen)
-
-        self.__links.append(_Link(output, input, line))
-
-    def removeLink(self, output, input):
-        """
-        Remove a link between the `output` and `input` channels.
-        """
-        for link in list(self.__links):
-            if link.output == output and link.input == input:
-                self.scene().removeItem(link.lineItem)
-                self.__links.remove(link)
-                break
-        else:
-            raise ValueError("No such link {0.name!r} -> {1.name!r}." \
-                             .format(output, input))
-
-    def clearLinks(self):
-        """
-        Clear (remove) all the links.
-        """
-        for output, input, _ in list(self.__links):
-            self.removeLink(output, input)
-
-    def __updateState(self):
-        """
-        Update the widget with the new source/sink node signal descriptions.
-        """
-        widget = QGraphicsWidget()
-        widget.setLayout(QGraphicsGridLayout())
-
-        # Space between left and right anchors
-        widget.layout().setHorizontalSpacing(50)
-
-        left_node = EditLinksNode(self, direction=Qt.LeftToRight,
-                                  node=self.source)
-
-        left_node.setSizePolicy(QSizePolicy.MinimumExpanding,
-                                QSizePolicy.MinimumExpanding)
-
-        right_node = EditLinksNode(self, direction=Qt.RightToLeft,
-                                   node=self.sink)
-
-        right_node.setSizePolicy(QSizePolicy.MinimumExpanding,
-                                 QSizePolicy.MinimumExpanding)
-
-        left_node.setMinimumWidth(150)
-        right_node.setMinimumWidth(150)
-
-        widget.layout().addItem(left_node, 0, 0,)
-        widget.layout().addItem(right_node, 0, 1,)
-
-        title_template = "<center><b>{0}<b></center>"
-
-        left_title = GraphicsTextWidget(self)
-        left_title.setHtml(title_template.format(escape(self.source.title)))
-        left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
-
-        right_title = GraphicsTextWidget(self)
-        right_title.setHtml(title_template.format(escape(self.sink.title)))
-        right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
-
-        widget.layout().addItem(left_title, 1, 0,
-                                alignment=Qt.AlignHCenter | Qt.AlignTop)
-        widget.layout().addItem(right_title, 1, 1,
-                                alignment=Qt.AlignHCenter | Qt.AlignTop)
-
-        widget.setParentItem(self)
-
-        max_w = max(left_node.sizeHint(Qt.PreferredSize).width(),
-                    right_node.sizeHint(Qt.PreferredSize).width())
-
-        # fix same size
-        left_node.setMinimumWidth(max_w)
-        right_node.setMinimumWidth(max_w)
-        left_title.setMinimumWidth(max_w)
-        right_title.setMinimumWidth(max_w)
-
-        self.layout().addItem(widget)
-        self.layout().activate()
-
-        self.sourceNodeWidget = left_node
-        self.sinkNodeWidget = right_node
-        self.sourceNodeTitle = left_title
-        self.sinkNodeTitle = right_title
-
-    if qVersion() < "4.7":
-        geometryChanged = Signal()
-
-        def setGeometry(self, rect):
-            QGraphicsWidget.setGeometry(self, rect)
-            self.geometryChanged.emit()
-
-
-class EditLinksNode(QGraphicsWidget):
-    """
-    A Node with channel anchors.
-
-    `direction` specifies the layout (default `Qt.LeftToRight` will
-    have icon on the left and channels on the right).
-
-    """
-
-    def __init__(self, parent=None, direction=Qt.LeftToRight,
-                 node=None, icon=None, iconSize=None, **args):
-        QGraphicsWidget.__init__(self, parent, **args)
-        self.setAcceptedMouseButtons(Qt.NoButton)
-        self.__direction = direction
-
-        self.setLayout(QGraphicsLinearLayout(Qt.Horizontal))
-
-        # Set the maximum size, otherwise the layout can't grow beyond its
-        # sizeHint (and we need it to grow so the widget can grow and keep the
-        # contents centered vertically.
-        self.layout().setMaximumSize(QSizeF(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX))
-
-        self.setSizePolicy(QSizePolicy.MinimumExpanding,
-                           QSizePolicy.MinimumExpanding)
-
-        self.__iconSize = iconSize or QSize(64, 64)
-        self.__icon = icon
-
-        self.__iconItem = QGraphicsPixmapItem(self)
-        self.__iconLayoutItem = GraphicsItemLayoutItem(item=self.__iconItem)
-
-        self.__channelLayout = QGraphicsGridLayout()
-        self.__channelAnchors = []
-
-        if self.__direction == Qt.LeftToRight:
-            self.layout().addItem(self.__iconLayoutItem)
-            self.layout().addItem(self.__channelLayout)
-            channel_alignemnt = Qt.AlignRight
-
-        else:
-            self.layout().addItem(self.__channelLayout)
-            self.layout().addItem(self.__iconLayoutItem)
-            channel_alignemnt = Qt.AlignLeft
-
-        self.layout().setAlignment(self.__iconLayoutItem, Qt.AlignCenter)
-        self.layout().setAlignment(self.__channelLayout,
-                                   Qt.AlignVCenter | channel_alignemnt)
-
-        if node is not None:
-            self.setSchemeNode(node)
-
-    def setIconSize(self, size):
-        """
-        Set the icon size for the node.
-        """
-        if size != self.__iconSize:
-            self.__iconSize = size
-            if self.__icon:
-                self.__iconItem.setPixmap(self.__icon.pixmap(size))
-                self.__iconLayoutItem.updateGeometry()
-
-    def iconSize(self):
-        return self.__iconSize
-
-    def setIcon(self, icon):
-        """
-        Set the icon to display.
-        """
-        if icon != self.__icon:
-            self.__icon = icon
-            self.__iconItem.setPixmap(icon.pixmap(self.iconSize()))
-            self.__iconLayoutItem.updateGeometry()
-
-    def icon(self):
-        return self.__icon
-
-    def setSchemeNode(self, node):
-        """
-        Set an instance of `SchemeNode`. The widget will be
-        initialized with its icon and channels.
-
-        """
-        self.node = node
-
-        if self.__direction == Qt.LeftToRight:
-            channels = node.output_channels()
-        else:
-            channels = node.input_channels()
-        self.channels = channels
-
-        loader = icon_loader.from_description(node.description)
-        icon = loader.get(node.description.icon)
-
-        self.setIcon(icon)
-
-        label_template = ('<div align="{align}">'
-                          '<b class="channelname">{name}</b><br/>'
-                          '<span class="typename">{typename}</span>'
-                          '</div>')
-
-        if self.__direction == Qt.LeftToRight:
-            align = "right"
-            label_alignment = Qt.AlignVCenter | Qt.AlignRight
-            anchor_alignment = Qt.AlignVCenter | Qt.AlignLeft
-            label_row = 0
-            anchor_row = 1
-        else:
-            align = "left"
-            label_alignment = Qt.AlignVCenter | Qt.AlignLeft
-            anchor_alignment = Qt.AlignVCenter | Qt.AlignLeft
-            label_row = 1
-            anchor_row = 0
-
-        self.__channelAnchors = []
-        grid = self.__channelLayout
-
-        for i, channel in enumerate(channels):
-            text = label_template.format(align=align,
-                                         name=escape(channel.name),
-                                         typename=escape(channel.type))
-
-            text_item = GraphicsTextWidget(self)
-            text_item.setHtml(text)
-            text_item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
-
-            grid.addItem(text_item, i, label_row,
-                         alignment=label_alignment)
-
-            anchor = ChannelAnchor(self, channel=channel,
-                                   rect=QRectF(0, 0, 20, 20))
-
-            anchor.setBrush(self.palette().brush(QPalette.Mid))
-
-            layout_item = GraphicsItemLayoutItem(grid, item=anchor)
-            grid.addItem(layout_item, i, anchor_row,
-                         alignment=anchor_alignment)
-
-            if hasattr(channel, "description"):
-                text_item.setToolTip((channel.description))
-
-            self.__channelAnchors.append(anchor)
-
-    def anchor(self, channel):
-        """
-        Return the anchor item for the `channel` name.
-        """
-        for anchor in self.__channelAnchors:
-            if anchor.channel() == channel:
-                return anchor
-
-        raise ValueError(channel.name)
-
-    def paint(self, painter, option, widget=None):
-        painter.save()
-        palette = self.palette()
-        border = palette.brush(QPalette.Mid)
-        pen = QPen(border, 1)
-        pen.setCosmetic(True)
-        painter.setPen(pen)
-        painter.setBrush(palette.brush(QPalette.Window))
-        brect = self.boundingRect()
-        painter.drawRoundedRect(brect, 4, 4)
-        painter.restore()
-
-
-class GraphicsItemLayoutItem(QGraphicsLayoutItem):
-    """
-    A graphics layout that handles the position of a general QGraphicsItem
-    in a QGraphicsLayout. The items boundingRect is used as this items fixed
-    sizeHint and the item is positioned at the top left corner of the this
-    items geometry.
-
-    """
-
-    def __init__(self, parent=None, item=None, ):
-        self.__item = None
-
-        QGraphicsLayoutItem.__init__(self, parent, isLayout=False)
-
-        self.setOwnedByLayout(True)
-        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
-
-        if item is not None:
-            self.setItem(item)
-
-    def setItem(self, item):
-        self.__item = item
-        self.setGraphicsItem(item)
-
-    def setGeometry(self, rect):
-        # TODO: specifiy if the geometry should be set relative to the
-        # bounding rect top left corner
-        if self.__item:
-            self.__item.setPos(rect.topLeft())
-
-        QGraphicsLayoutItem.setGeometry(self, rect)
-
-    def sizeHint(self, which, constraint):
-        if self.__item:
-            return self.__item.boundingRect().size()
-        else:
-            return QGraphicsLayoutItem.sizeHint(self, which, constraint)
-
-
-class ChannelAnchor(QGraphicsRectItem):
-    def __init__(self, parent=None, channel=None, rect=None, **kwargs):
-        QGraphicsRectItem.__init__(self, **kwargs)
-        self.setAcceptHoverEvents(True)
-        self.setAcceptedMouseButtons(Qt.NoButton)
-        self.__channel = None
-
-        if rect is None:
-            rect = QRectF(0, 0, 20, 20)
-
-        self.setRect(rect)
-
-        if channel:
-            self.setChannel(channel)
-
-        self.__shadow = QGraphicsDropShadowEffect(blurRadius=5,
-                                                  offset=QPointF(0, 0))
-        self.setGraphicsEffect(self.__shadow)
-        self.__shadow.setEnabled(False)
-
-    def setChannel(self, channel):
-        if channel != self.__channel:
-            self.__channel = channel
-            if hasattr(channel, "description"):
-                self.setToolTip(channel.description)
-            # TODO: Should also include name, type, flags, dynamic in the
-            #       tool tip as well as add visual clues to the anchor
-
-    def channel(self):
-        return self.__channel
-
-    def hoverEnterEvent(self, event):
-        self.__shadow.setEnabled(True)
-        QGraphicsRectItem.hoverEnterEvent(self, event)
-
-    def hoverLeaveEvent(self, event):
-        self.__shadow.setEnabled(False)
-        QGraphicsRectItem.hoverLeaveEvent(self, event)
-
-
-class GraphicsTextWidget(QGraphicsWidget):
-    """A QGraphicsWidget subclass that manages a QGraphicsTextItem
-
-    """
-
-    def __init__(self, parent=None, textItem=None):
-        QGraphicsLayoutItem.__init__(self, parent)
-        if textItem is None:
-            textItem = QGraphicsTextItem()
-
-        self.__textItem = textItem
-        self.__textItem.setParentItem(self)
-        self.__textItem.setPos(0, 0)
-
-        doc_layout = self.document().documentLayout()
-        doc_layout.documentSizeChanged.connect(self._onDocumentSizeChanged)
-
-    def sizeHint(self, which, constraint=QSizeF()):
-        # TODO: More sensible size hints.
-        # If the text is a plain text or html
-        # Check how QLabel.sizeHint works.
-
-        if which == Qt.PreferredSize:
-            return self.__textItem.boundingRect().size()
-        else:
-            return QGraphicsWidget.sizeHint(self, which, constraint)
-
-    def setGeometry(self, rect):
-        QGraphicsWidget.setGeometry(self, rect)
-        self.__textItem.setTextWidth(rect.width())
-
-    def setPlainText(self, text):
-        self.__textItem.setPlainText(text)
-        self.updateGeometry()
-
-    def setHtml(self, text):
-        self.__textItem.setHtml(text)
-
-    def adjustSize(self):
-        self.__textItem.adjustSize()
-        self.updateGeometry()
-
-    def setDefaultTextColor(self, color):
-        self.__textItem.setDefaultTextColor(color)
-
-    def document(self):
-        return self.__textItem.document()
-
-    def setDocument(self, doc):
-        doc_layout = self.document().documentLayout()
-        doc_layout.documentSizeChanged.disconnect(self._onDocumentSizeChanged)
-
-        self.__textItem.setDocument(doc)
-
-        doc_layout = self.document().documentLayout()
-        doc_layout.documentSizeChanged.connect(self._onDocumentSizeChanged)
-
-        self.updateGeometry()
-
-    def _onDocumentSizeChanged(self, size):
-        """The doc size has changed"""
-        self.updateGeometry()

Orange/OrangeCanvas/canvas/items/annotationitem.py

         """
         return unicode(self.__placeholderText)
 
-    placeholderText_ = Property(unicode, placeholderText, setPlaceholderText)
+    placeholderText_ = Property(unicode, placeholderText, setPlaceholderText,
+                                doc="Placeholder text")
 
     def paint(self, painter, option, widget=None):
         QGraphicsTextItem.paint(self, painter, option, widget)
                 not (self.hasFocus() and \
                      self.textInteractionFlags() & Qt.TextEditable):
             brect = self.boundingRect()
+            painter.setFont(self.font())
             metrics = painter.fontMetrics()
             text = metrics.elidedText(self.__placeholderText, Qt.ElideRight,
                                       brect.width())

Orange/OrangeCanvas/canvas/items/linkitem.py

 """
+=========
 Link Item
+=========
 
 """
 
 
 
 class LinkCurveItem(QGraphicsPathItem):
-    """Link curve item. The main component of `LinkItem`.
+    """
+    Link curve item. The main component of a :class:`LinkItem`.
     """
     def __init__(self, parent):
         QGraphicsPathItem.__init__(self, parent)
-        assert(isinstance(parent, LinkItem))
+        if not isinstance(parent, LinkItem):
+            raise TypeError("'LinkItem' expected")
+
         self.setAcceptedMouseButtons(Qt.NoButton)
         self.__canvasLink = parent
         self.setAcceptHoverEvents(True)
         self.__hover = False
 
     def linkItem(self):
-        """Return the :class:`LinkItem` instance this curve belongs to.
-
+        """
+        Return the :class:`LinkItem` instance this curve belongs to.
         """
         return self.__canvasLink
 
 
 
 class LinkAnchorIndicator(QGraphicsEllipseItem):
-    """A visual indicator of the link anchor point at both ends
-    of the `LinkItem`.
+    """
+    A visual indicator of the link anchor point at both ends
+    of the :class:`LinkItem`.
 
     """
     def __init__(self, *args):
 
 class LinkItem(QGraphicsObject):
     """
-    A Link in the canvas.
+    A Link item in the canvas that connects two :class:`.NodeItem`\s in the
+    canvas.
+
+    The link curve connects two `Anchor` items (see :func:`setSourceItem`
+    and :func:`setSinkItem`). Once the anchors are set the curve
+    automatically adjusts its end points whenever the anchors move.
+
+    An optional source/sink text item can be displayed above the curve's
+    central point (:func:`setSourceName`, :func:`setSinkName`)
+
     """
 
+    #: Z value of the item
     Z_VALUE = 0
-    """Z value of the item"""
 
     def __init__(self, *args):
         QGraphicsObject.__init__(self, *args)
 
     def setSourceItem(self, item, anchor=None):
         """
-        Set the source `item` (:class:`NodeItem`). Use `anchor`
-        (:class:`AnchorPoint`) as the curve start point (if ``None`` a new
-        output anchor will be created).
+        Set the source `item` (:class:`.NodeItem`). Use `anchor`
+        (:class:`.AnchorPoint`) as the curve start point (if ``None`` a new
+        output anchor will be created using ``item.newOutputAnchor()``).
 
         Setting item to ``None`` and a valid anchor is a valid operation
         (for instance while mouse dragging one end of the link).
 
     def setSinkItem(self, item, anchor=None):
         """
-        Set the sink `item` (:class:`NodeItem`). Use `anchor`
-        (:class:`AnchorPoint`) as the curve end point (if ``None`` a new
-        input anchor will be created).
+        Set the sink `item` (:class:`.NodeItem`). Use `anchor`
+        (:class:`.AnchorPoint`) as the curve end point (if ``None`` a new
+        input anchor will be created using ``item.newInputAnchor()``).
 
         Setting item to ``None`` and a valid anchor is a valid operation
         (for instance while mouse dragging one and of the link).
 
     def setFont(self, font):
         """
-        Set the font for the channel names text.
+        Set the font for the channel names text item.
         """
         if font != self.font():
             self.linkTextItem.setFont(font)
 
     def setEnabled(self, enabled):
         """
+        Reimplemented from :class:`QGraphicsObject`
+
         Set link enabled state. When disabled the link is rendered with a
         dashed line.
 
 
     def setDynamic(self, dynamic):
         """
-        Mark the link as dynamic (e.i. it responds to the
-        ``setDynamicEnabled``).
+        Mark the link as dynamic (i.e. it responds to
+        :func:`setDynamicEnabled`).
 
         """
         if self.__dynamic != dynamic:

Orange/OrangeCanvas/canvas/items/nodeitem.py

 """
-NodeItem
+=========
+Node Item
+=========
 
 """
 
     """
     Create and return a default palette for a node.
     """
-    return create_palette(QColor(NAMED_COLORS["light-orange"]),
-                          QColor(NAMED_COLORS["orange"]))
+    return create_palette(QColor(NAMED_COLORS["light-yellow"]),
+                          QColor(NAMED_COLORS["yellow"]))
 
 
 def animation_restart(animation):
     A anchor indicator on the :class:`NodeAnchorItem`.
     """
 
-    # Signal emitted when the item's scene position changes.
+    #: Signal emitted when the item's scene position changes.
     scenePositionChanged = Signal(QPointF)
 
+    #: Signal emitted when the item's `anchorDirection` changes.
     anchorDirectionChanged = Signal(QPointF)
 
     def __init__(self, *args):
     An widget node item in the canvas.
     """
 
-    # Scene position of the node has changed.
+    #: Signal emitted when the scene position of the node has changed.
     positionChanged = Signal()
 
-    # Geometry of the channel anchors changed
+    #: Signal emitted when the geometry of the channel anchors changes.
     anchorGeometryChanged = Signal()
 
-    # The item has been activated (by a mouse double click or a keyboard).
+    #: Signal emitted when the item has been activated (by a mouse double
+    #: click or a keyboard)
     activated = Signal()
 
-    # The item is under the mouse.
+    #: The item is under the mouse.
     hovered = Signal()
 
     #: Span of the anchor in degrees
 
     def setIcon(self, icon):
         """
-        Set the node item's icon.
+        Set the node item's icon (:class:`QIcon`).
         """
         if isinstance(icon, QIcon):
             self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon,
 
     def newInputAnchor(self):
         """
-        Create and return a new input anchor point.
+        Create and return a new input :class:`AnchorPoint`.
         """
         if not (self.widget_description and self.widget_description.inputs):
             raise ValueError("Widget has no inputs.")
 
     def newOutputAnchor(self):
         """
-        Create a new output anchor indicator.
+        Create and return a new output :class:`AnchorPoint`.
         """
         if not (self.widget_description and self.widget_description.outputs):
             raise ValueError("Widget has no outputs.")
 
     def inputAnchors(self):
         """
-        Return a list of input anchor points.
+        Return a list of all input anchor points.
         """
         return self.inputAnchorItem.anchorPoints()
 
     def outputAnchors(self):
         """
-        Return a list of output anchor points.
+        Return a list of all output anchor points.
         """
         return self.outputAnchorItem.anchorPoints()
 

Orange/OrangeCanvas/canvas/scene.py

 
 class CanvasScene(QGraphicsScene):
     """
-    A Graphics Scene for displaying and editing an :class:`Scheme`.
+    A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance.
     """
 
-    #: An node item has been added to the scene.
+    #: Signal emitted when a :class:`NodeItem` has been added to the scene.
     node_item_added = Signal(items.NodeItem)
 
-    #: An node item has been removed from the scene
+    #: Signal emitted when a :class:`NodeItem` has been removed from the
+    #: scene.
     node_item_removed = Signal(items.LinkItem)
 
-    #: A new link item has been added to the scene
+    #: Signal emitted when a new :class:`LinkItem` has been added to the
+    #: scene.
     link_item_added = Signal(items.LinkItem)
 
-    #: Link item has been removed
+    #: Signal emitted when a :class:`LinkItem` has been removed.
     link_item_removed = Signal(items.LinkItem)
 
-    #: Annotation item has been added
+    #: Signal emitted when a :class:`Annotation` item has been added.
     annotation_added = Signal(items.annotationitem.Annotation)
 
-    #: Annotation item has been removed
+    #: Signal emitted when a :class:`Annotation` item has been removed.
     annotation_removed = Signal(items.annotationitem.Annotation)
 
-    #: The position of a node has changed
+    #: Signal emitted when the position of a :class:`NodeItem` has changed.
     node_item_position_changed = Signal(items.NodeItem, QPointF)
 
-    #: An node item has been double clicked
+    #: Signal emitted when an :class:`NodeItem` has been double clicked.
     node_item_double_clicked = Signal(items.NodeItem)
 
     #: An node item has been activated (clicked)
         log.info("'%s' intitialized." % self)
 
     def clear_scene(self):
+        """
+        Clear (reset) the scene.
+        """
         self.scheme = None
         self.__node_items = []
         self.__item_for_node = {}
         log.info("'%s' cleared." % self)
 
     def set_scheme(self, scheme):
-        """Set the scheme to display and edit. Populates the scene
-        with nodes and links already in the scheme.
+        """
+        Set the scheme to display. Populates the scene with nodes and links
+        already in the scheme. Any further change to the scheme will be
+        reflected in the scene.
+
+        Parameters
+        ----------
+        scheme : :class:`~.scheme.Scheme`
 
         """
         if self.scheme is not None:
             self.add_annotation(annot)
 
     def set_registry(self, registry):
-        """Set the widget registry.
         """
+        Set the widget registry.
+        """
+        # TODO: Remove/Deprecate. Is used only to get the category/background
+        # color. That should be part of the SchemeNode/WidgetDescription.
         log.info("Setting registry '%s on '%s'." % (registry, self))
         self.registry = registry
 
     def set_anchor_layout(self, layout):
+        """
+        Set an :class:`~.layout.AnchorLayout`
+        """
         if self.__anchor_layout != layout:
             if self.__anchor_layout:
                 self.__anchor_layout.deleteLater()
             self.__anchor_layout = layout
 
     def anchor_layout(self):
+        """
+        Return the anchor layout instance.
+        """
         return self.__anchor_layout
 
     def set_channel_names_visible(self, visible):
+        """
+        Set the channel names visibility.
+        """
         self.__channel_names_visible = visible
         for link in self.__link_items:
             link.setChannelNamesVisible(visible)
 
     def channel_names_visible(self):
+        """
+        Return the channel names visibility state.
+        """
         return self.__channel_names_visible
 
     def set_node_animation_enabled(self, enabled):
+        """
+        Set node animation enabled state.
+        """
         if self.__node_animation_enabled != enabled:
             self.__node_animation_enabled = enabled
 
                 node.setAnimationEnabled(enabled)
 
     def add_node_item(self, item):
-        """Add a :class:`NodeItem` instance to the scene.
+        """
+        Add a :class:`.NodeItem` instance to the scene.
         """
         if item in self.__node_items:
             raise ValueError("%r is already in the scene." % item)
         return item
 
     def add_node(self, node):
-        """Add and return a default constructed `NodeItem` for a
-        `SchemeNode` instance. If the node is already in the scene
-        do nothing and just return its item.
+        """
+        Add and return a default constructed :class:`.NodeItem` for a
+        :class:`SchemeNode` instance `node`. If the `node` is already in
+        the scene do nothing and just return its item.
 
         """
         if node in self.__item_for_node:
         return self.add_node_item(item)
 
     def new_node_item(self, widget_desc, category_desc=None):
-        """Construct an new `NodeItem` from a `WidgetDescription`.
+        """
+        Construct an new :class:`.NodeItem` from a `WidgetDescription`.
         Optionally also set `CategoryDescription`.
 
         """
         return item
 
     def remove_node_item(self, item):
-        """Remove `item` (:class:`NodeItem`) from the scene.
+        """
+        Remove `item` (:class:`.NodeItem`) from the scene.
         """
         self.activated_mapper.removePyMappings(item)
         self.hovered_mapper.removePyMappings(item)
         log.info("Removed item '%s' from '%s'" % (item, self))
 
     def remove_node(self, node):
-        """Remove the `NodeItem` instance that was previously constructed for
-        a `SchemeNode` node using the `add_node` method.
+        """
+        Remove the :class:`.NodeItem` instance that was previously
+        constructed for a :class:`SchemeNode` `node` using the `add_node`
+        method.
 
         """
         item = self.__item_for_node.pop(node)
         self.remove_node_item(item)
 
     def node_items(self):
-        """Return all :class:`NodeItem` instances in the scene.
+        """
+        Return all :class:`.NodeItem` instances in the scene.
         """
         return list(self.__node_items)
 
     def add_link_item(self, item):
-        """Add a link (:class:`LinkItem`)to the scene.
+        """
+        Add a link (:class:`.LinkItem`) to the scene.
         """
         if item.scene() is not self:
             self.addItem(item)
         return item
 
     def add_link(self, scheme_link):
-        """Create and add a `LinkItem` instance for a `SchemeLink`
-        instance. If the link is already in the scene do nothing
-        and just return its `LinkItem`.
+        """
+        Create and add a :class:`.LinkItem` instance for a
+        :class:`SchemeLink` instance. If the link is already in the scene
+        do nothing and just return its :class:`.LinkItem`.
 
         """
         if scheme_link in self.__item_for_link:
 
     def new_link_item(self, source_item, source_channel,
                       sink_item, sink_channel):
-        """Construct and return a new `LinkItem`
+        """
+        Construct and return a new :class:`.LinkItem`
         """
         item = items.LinkItem()
         item.setSourceItem(source_item)
         return item
 
     def remove_link_item(self, item):
-        """Remove a link (:class:`LinkItem`) from the scene.
+        """
+        Remove a link (:class:`.LinkItem`) from the scene.
         """
         # Invalidate the anchor layout.
         self.__anchor_layout.invalidateAnchorItem(
         return item
 
     def remove_link(self, scheme_link):
-        """ Remove a `LinkItem` instance that was previously constructed for
-        a `SchemeLink` node using the `add_link` method.
+        """
+        Remove a :class:`.LinkItem` instance that was previously constructed
+        for a :class:`SchemeLink` instance `link` using the `add_link` method.
 
         """
         item = self.__item_for_link.pop(scheme_link)
         self.remove_link_item(item)
 
     def link_items(self):
-        """Return all :class:`LinkItems` in the scene.
-
+        """
+        Return all :class:`.LinkItem`\s in the scene.
         """
         return list(self.__link_items)
 
     def add_annotation_item(self, annotation):
-        """Add an `Annotation` item to the scene.
-
+        """
+        Add an :class:`.Annotation` item to the scene.
         """
         self.__annotation_items.append(annotation)
         self.addItem(annotation)
         return annotation
 
     def add_annotation(self, scheme_annot):
-        """Create a new item for :class:`SchemeAnnotation` and add it
+        """
+        Create a new item for :class:`SchemeAnnotation` and add it
         to the scene. If the `scheme_annot` is already in the scene do
         nothing and just return its item.
 
         return item
 
     def remove_annotation_item(self, annotation):
-        """Remove an `Annotation` item from the scene.
+        """
+        Remove an :class:`.Annotation` instance from the scene.
 
         """
         self.__annotation_items.remove(annotation)
         self.annotation_removed.emit(annotation)
 
     def remove_annotation(self, scheme_annotation):
+        """
+        Remove an :class:`.Annotation` instance that was previously added
+        using :func:`add_anotation`.
+
+        """
         item = self.__item_for_annotation.pop(scheme_annotation)
 
         scheme_annotation.geometry_changed.disconnect(
         self.remove_annotation_item(item)
 
     def annotation_items(self):
-        """Return all `Annotation` items in the scene.
-
+        """
+        Return all :class:`.Annotation` items in the scene.
         """
         return self.__annotation_items
 
         return rev[item]
 
     def commit_scheme_node(self, node):
-        """Commit the `node` into the scheme.
+        """
+        Commit the `node` into the scheme.
         """
         if not self.editable:
             raise Exception("Scheme not editable.")
         try:
             self.scheme.add_node(node)
         except Exception:
-            log.error("An unexpected error occurred while commiting node '%s'",
+            log.error("An error occurred while committing node '%s'",
                       node, exc_info=True)
             # Cleanup (remove the node item)
             self.remove_node_item(item)
                  (node, self, self.scheme))
 
     def commit_scheme_link(self, link):
-        """Commit a scheme link.
+        """
+        Commit a scheme link.
         """
         if not self.editable:
             raise Exception("Scheme not editable")
                  (link, self, self.scheme))
 
     def node_for_item(self, item):
-        """Return the `SchemeNode` for the `item`.
+        """
+        Return the `SchemeNode` for the `item`.
         """
         rev = dict([(v, k) for k, v in self.__item_for_node.items()])
         return rev[item]
 
     def item_for_node(self, node):
-        """Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
+        """
+        Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
         """
         return self.__item_for_node[node]
 
     def link_for_item(self, item):
-        """Return the `SchemeLink for `item` (:class:`LinkItem`).
+        """
+        Return the `SchemeLink for `item` (:class:`LinkItem`).
         """
         rev = dict([(v, k) for k, v in self.__item_for_link.items()])
         return rev[item]
 
     def item_for_link(self, link):
-        """Return the :class:`LinkItem` for a :class:`SchemeLink`
+        """
+        Return the :class:`LinkItem` for a :class:`SchemeLink`
         """
         return self.__item_for_link[link]
 
     def selected_node_items(self):
-        """Return the selected :class:`NodeItem`'s.
+        """
+        Return the selected :class:`NodeItem`'s.
         """
         return [item for item in self.__node_items if item.isSelected()]
 
     def selected_annotation_items(self):
-        """Return the selected :class:`Annotation`'s
+        """
+        Return the selected :class:`Annotation`'s
         """
         return [item for item in self.__annotation_items if item.isSelected()]
 
     def node_links(self, node_item):
-        """Return all links from the `node_item` (:class:`NodeItem`).
+        """
+        Return all links from the `node_item` (:class:`NodeItem`).
         """
         return self.node_output_links(node_item) + \
                self.node_input_links(node_item)
 
     def node_output_links(self, node_item):
-        """Return a list of all output links from `node_item`.
+        """
+        Return a list of all output links from `node_item`.
         """
         return [link for link in self.__link_items
                 if link.sourceItem == node_item]
 
     def node_input_links(self, node_item):
-        """Return a list of all input links for `node_item`.
+        """
+        Return a list of all input links for `node_item`.
         """
         return [link for link in self.__link_items
                 if link.sinkItem == node_item]
 
     def neighbor_nodes(self, node_item):
-        """Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
+        """
+        Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
         """
         neighbors = map(attrgetter("sourceItem"),
                         self.node_input_links(node_item))
 
 
 def grab_svg(scene):
-    """Return a SVG rendering of the scene contents.
+    """
+    Return a SVG rendering of the scene contents.
+
+    Parameters
+    ----------
+    scene : :class:`CanvasScene`
+
     """
     from PyQt4.QtSvg import QSvgGenerator
     svg_buffer = QBuffer()

Orange/OrangeCanvas/canvas/tests/test_editlinksdialog.py

-from PyQt4.QtGui import QGraphicsScene, QGraphicsView
-from PyQt4.QtCore import Qt
-
-from ...gui import test
-from ..editlinksdialog import EditLinksDialog, EditLinksNode, \
-                              GraphicsTextWidget
-from ...scheme import SchemeNode
-
-
-class TestLinksEditDialog(test.QAppTestCase):
-    def test_links_edit(self):
-        from ...registry.tests import small_testing_registry
-
-        dlg = EditLinksDialog()
-        reg = small_testing_registry()
-        file_desc = reg.widget("Orange.OrangeWidgets.Data.OWFile.OWFile")
-        bayes_desc = reg.widget("Orange.OrangeWidgets.Classify.OWNaiveBayes."
-                                "OWNaiveBayes")
-        source_node = SchemeNode(file_desc, title="This is File")
-        sink_node = SchemeNode(bayes_desc)
-
-        source_channel = source_node.output_channel("Data")
-        sink_channel = sink_node.input_channel("Data")
-        links = [(source_channel, sink_channel)]
-
-        dlg.setNodes(source_node, sink_node)
-
-        dlg.show()
-        dlg.setLinks(links)
-
-        self.assertSequenceEqual(dlg.links(), links)
-        status = dlg.exec_()
-
-        self.assertTrue(dlg.links() == [] or \
-                        dlg.links() == links)
-
-    def test_graphicstextwidget(self):
-        scene = QGraphicsScene()
-        view = QGraphicsView(scene)
-
-        text = GraphicsTextWidget()
-        text.setHtml("<center><b>a text</b></center><p>paragraph</p>")
-        scene.addItem(text)
-        view.show()
-        view.resize(400, 300)
-
-        self.app.exec_()
-
-    def test_editlinksnode(self):
-        from ...registry.tests import small_testing_registry
-
-        reg = small_testing_registry()
-        file_desc = reg.widget("Orange.OrangeWidgets.Data.OWFile.OWFile")
-        bayes_desc = reg.widget("Orange.OrangeWidgets.Classify.OWNaiveBayes."
-                                "OWNaiveBayes")
-        source_node = SchemeNode(file_desc, title="This is File")
-        sink_node = SchemeNode(bayes_desc)
-
-        scene = QGraphicsScene()
-        view = QGraphicsView(scene)
-
-        node = EditLinksNode(node=source_node)
-        scene.addItem(node)
-
-        node = EditLinksNode(direction=Qt.RightToLeft)
-        node.setSchemeNode(sink_node)
-
-        node.setPos(300, 0)
-        scene.addItem(node)
-
-        view.show()
-        view.resize(800, 300)
-        self.app.exec_()

Orange/OrangeCanvas/document/editlinksdialog.py

+"""
+===========
+Link Editor
+===========
+
+An Dialog to edit links between two nodes in the scheme.
+
+"""
+
+from collections import namedtuple
+
+from xml.sax.saxutils import escape
+
+from PyQt4.QtGui import (
+    QApplication, QDialog, QVBoxLayout, QDialogButtonBox, QGraphicsScene,
+    QGraphicsView, QGraphicsWidget, QGraphicsRectItem,
+    QGraphicsLineItem, QGraphicsTextItem, QGraphicsLayoutItem,
+    QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsPixmapItem,
+    QGraphicsDropShadowEffect, QSizePolicy, QPalette, QPen,
+    QPainter, QIcon
+)
+
+from PyQt4.QtCore import (
+    Qt, QObject, QSize, QSizeF, QPointF, QRectF, qVersion
+)
+
+from PyQt4.QtCore import pyqtSignal as Signal
+
+from ..scheme import SchemeNode, SchemeLink, compatible_channels
+from ..registry import InputSignal, OutputSignal
+
+from ..resources import icon_loader
+
+# This is a special value defined in Qt4 but does not seem to be exported
+# by PyQt4
+QWIDGETSIZE_MAX = ((1 << 24) - 1)
+
+
+class EditLinksDialog(QDialog):
+    """
+    A dialog for editing links.
+
+    >>> dlg = EditLinksDialog()
+    >>> dlg.setNodes(file_node, test_learners_node)
+    >>> dlg.setLinks([(file_node.output_channel("Data"),
+    ...               (test_learners_node.input_channel("Data")])
+    >>> if dlg.exec_() == EditLinksDialog.Accpeted:
+    ...     new_links = dlg.links()
+    ...
+
+    """
+    def __init__(self, *args, **kwargs):
+        QDialog.__init__(self, *args, **kwargs)
+
+        self.setModal(True)
+
+        self.__setupUi()
+
+    def __setupUi(self):
+        layout = QVBoxLayout()
+
+        # Scene with the link editor.
+        self.scene = LinksEditScene()
+        self.view = QGraphicsView(self.scene)
+        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.view.setRenderHint(QPainter.Antialiasing)
+
+        self.scene.editWidget.geometryChanged.connect(self.__onGeometryChanged)
+
+        # Ok/Cancel/Clear All buttons.
+        buttons = QDialogButtonBox(QDialogButtonBox.Ok |
+                                   QDialogButtonBox.Cancel |
+                                   QDialogButtonBox.Reset,
+                                   Qt.Horizontal)
+
+        clear_button = buttons.button(QDialogButtonBox.Reset)
+        clear_button.setText(self.tr("Clear All"))
+
+        buttons.accepted.connect(self.accept)
+        buttons.rejected.connect(self.reject)
+        clear_button.clicked.connect(self.scene.editWidget.clearLinks)
+
+        layout.addWidget(self.view)
+        layout.addWidget(buttons)
+
+        self.setLayout(layout)
+        layout.setSizeConstraint(QVBoxLayout.SetFixedSize)
+
+        self.setSizeGripEnabled(False)
+
+    def setNodes(self, source_node, sink_node):
+        """
+        Set the source/sink nodes (:class:`.SchemeNode` instances)
+        between which to edit the links.
+
+        .. note:: This should be called before :func:`setLinks`.
+
+        """
+        self.scene.editWidget.setNodes(source_node, sink_node)
+
+    def setLinks(self, links):
+        """
+        Set a list of links to display between the source and sink
+        nodes. The `links` is a list of (`OutputSignal`, `InputSignal`)
+        tuples where the first element is an output signal of the source
+        node and the second an input signal of the sink node.
+
+        """
+        self.scene.editWidget.setLinks(links)
+
+    def links(self):
+        """
+        Return the links between the source and sink node.
+        """
+        return self.scene.editWidget.links()
+
+    def __onGeometryChanged(self):
+        size = self.scene.editWidget.size()
+        left, top, right, bottom = self.getContentsMargins()
+        self.view.setFixedSize(size.toSize() + \
+                               QSize(left + right + 4, top + bottom + 4))
+
+
+def find_item_at(scene, pos, order=Qt.DescendingOrder, type=None,
+                 name=None):
+    """
+    Find an object in a :class:`QGraphicsScene` `scene` at `pos`.
+    If `type` is not `None` the it must specify  the type of the item.
+    I `name` is not `None` it must be a name of the object
+    (`QObject.objectName()`).
+
+    """
+    items = scene.items(pos, Qt.IntersectsItemShape, order)
+    for item in items:
+        if type is not None and \
+                not isinstance(item, type):
+            continue
+
+        if name is not None and isinstance(item, QObject) and \
+                item.objectName() != name:
+            continue
+        return item
+    else:
+        return None
+
+
+class LinksEditScene(QGraphicsScene):
+    """
+    A :class:`QGraphicsScene` used by the :class:`LinkEditWidget`.
+    """
+    def __init__(self, *args, **kwargs):
+        QGraphicsScene.__init__(self, *args, **kwargs)
+
+        self.editWidget = LinksEditWidget()
+        self.addItem(self.editWidget)
+
+    findItemAt = find_item_at
+
+
+_Link = namedtuple(
+    "_Link",
+    ["output",    # OutputSignal
+     "input",     # InputSignal
+     "lineItem",  # QGraphicsLineItem connecting the input to output
+     ])
+
+
+class LinksEditWidget(QGraphicsWidget):
+    """
+    A Graphics Widget for editing the links between two nodes.
+    """
+    def __init__(self, *args, **kwargs):
+        QGraphicsWidget.__init__(self, *args, **kwargs)
+        self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton)
+
+        self.source = None
+        self.sink = None
+
+        # QGraphicsWidget/Items in the scene.
+        self.sourceNodeWidget = None
+        self.sourceNodeTitle = None
+        self.sinkNodeWidget = None
+        self.sinkNodeTitle = None
+
+        self.__links = []
+
+        self.__textItems = []
+        self.__iconItems = []
+        self.__tmpLine = None
+        self.__dragStartItem = None
+
+        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))
+        self.layout().setContentsMargins(0, 0, 0, 0)
+
+    def removeItems(self, items):
+        """
+        Remove child items from the widget and scene.
+        """
+        scene = self.scene()
+        for item in items:
+            item.setParentItem(None)
+            if scene is not None:
+                scene.removeItem(item)
+
+    def clear(self):
+        """
+        Clear the editor state (source and sink nodes, channels ...).
+        """
+        if self.layout().count():
+            widget = self.layout().takeAt(0).graphicsItem()
+            self.removeItems([widget])
+
+        self.source = None
+        self.sink = None
+
+    def setNodes(self, source, sink):
+        """
+        Set the source/sink nodes (:class:`SchemeNode` instances) between
+        which to edit the links.
+
+        .. note:: Call this before :func:`setLinks`.
+
+        """
+        self.clear()
+
+        self.source = source
+        self.sink = sink
+
+        self.__updateState()
+
+    def setLinks(self, links):
+        """
+        Set a list of links to display between the source and sink
+        nodes. `links` must be a list of (`OutputSignal`, `InputSignal`)
+        tuples where the first element refers to the source node
+        and the second to the sink node (as set by `setNodes`).
+
+        """
+        self.clearLinks()
+        for output, input in links:
+            self.addLink(output, input)
+
+    def links(self):
+        """
+        Return the links between the source and sink node.
+        """
+        return [(link.output, link.input) for link in self.__links]
+
+    def mousePressEvent(self, event):
+        if event.button() == Qt.LeftButton:
+            startItem = find_item_at(self.scene(), event.pos(),
+                                     type=ChannelAnchor)
+            if startItem is not None:
+                # Start a connection line drag.
+                self.__dragStartItem = startItem
+                self.__tmpLine = None
+                event.accept()
+                return
+
+            lineItem = find_item_at(self.scene(), event.scenePos(),
+                                    type=QGraphicsLineItem)
+            if lineItem is not None:
+                # Remove a connection under the mouse
+                for link in self.__links:
+                    if link.lineItem == lineItem:
+                        self.removeLink(link.output, link.input)
+                event.accept()
+                return
+
+        QGraphicsWidget.mousePressEvent(self, event)
+
+    def mouseMoveEvent(self, event):
+        if event.buttons() & Qt.LeftButton:
+
+            downPos = event.buttonDownPos(Qt.LeftButton)
+            if not self.__tmpLine and self.__dragStartItem and \
+                    (downPos - event.pos()).manhattanLength() > \
+                        QApplication.instance().startDragDistance():
+                # Start a line drag
+                line = QGraphicsLineItem(self)
+                start = self.__dragStartItem.boundingRect().center()
+                start = self.mapFromItem(self.__dragStartItem, start)
+                line.setLine(start.x(), start.y(),
+                             event.pos().x(), event.pos().y())
+
+                pen = QPen(Qt.green, 4)
+                pen.setCapStyle(Qt.RoundCap)
+                line.setPen(pen)
+                line.show()
+
+                self.__tmpLine = line
+
+            if self.__tmpLine:
+                # Update the temp line
+                line = self.__tmpLine.line()
+                line.setP2(event.pos())
+                self.__tmpLine.setLine(line)
+
+        QGraphicsWidget.mouseMoveEvent(self, event)
+
+    def mouseReleaseEvent(self, event):
+        if event.button() == Qt.LeftButton and self.__tmpLine:
+            endItem = find_item_at(self.scene(), event.scenePos(),
+                                     type=ChannelAnchor)
+
+            if endItem is not None:
+                startItem = self.__dragStartItem
+                startChannel = startItem.channel()
+                endChannel = endItem.channel()
+                possible = False
+
+                # Make sure the drag was from input to output (or reversed) and
+                # not between input -> input or output -> output
+                if type(startChannel) != type(endChannel):
+                    if isinstance(startChannel, InputSignal):
+                        startChannel, endChannel = endChannel, startChannel
+
+                    possible = compatible_channels(startChannel, endChannel)
+
+                if possible:
+                    self.addLink(startChannel, endChannel)
+
+            self.scene().removeItem(self.__tmpLine)
+            self.__tmpLine = None
+            self.__dragStartItem = None
+
+        QGraphicsWidget.mouseReleaseEvent(self, event)
+
+    def addLink(self, output, input):
+        """
+        Add a link between `output` (:class:`OutputSignal`) and `input`
+        (:class:`InputSignal`).
+
+        """
+        if not compatible_channels(output, input):
+            return
+
+        if output not in self.source.output_channels():
+            raise ValueError("%r is not an output channel of %r" % \
+                             (output, self.source))
+
+        if input not in self.sink.input_channels():
+            raise ValueError("%r is not an input channel of %r" % \
+                             (input, self.sink))
+
+        if input.single:
+            # Remove existing link if it exists.
+            for s1, s2, _ in self.__links:
+                if s2 == input:
+                    self.removeLink(s1, s2)
+
+        line = QGraphicsLineItem(self)
+
+        source_anchor = self.sourceNodeWidget.anchor(output)
+        sink_anchor = self.sinkNodeWidget.anchor(input)
+
+        source_pos = source_anchor.boundingRect().center()
+        source_pos = self.mapFromItem(source_anchor, source_pos)
+
+        sink_pos = sink_anchor.boundingRect().center()
+        sink_pos = self.mapFromItem(sink_anchor, sink_pos)
+        line.setLine(source_pos.x(), source_pos.y(),
+                     sink_pos.x(), sink_pos.y())
+        pen = QPen(Qt.green, 4)
+        pen.setCapStyle(Qt.RoundCap)
+        line.setPen(pen)
+
+        self.__links.append(_Link(output, input, line))
+
+    def removeLink(self, output, input):
+        """
+        Remove a link between the `output` and `input` channels.
+        """
+        for link in list(self.__links):
+            if link.output == output and link.input == input:
+                self.scene().removeItem(link.lineItem)
+                self.__links.remove(link)
+                break
+        else:
+            raise ValueError("No such link {0.name!r} -> {1.name!r}." \
+                             .format(output, input))
+
+    def clearLinks(self):
+        """
+        Clear (remove) all the links.
+        """
+        for output, input, _ in list(self.__links):
+            self.removeLink(output, input)
+
+    def __updateState(self):
+        """
+        Update the widget with the new source/sink node signal descriptions.
+        """
+        widget = QGraphicsWidget()
+        widget.setLayout(QGraphicsGridLayout())
+
+        # Space between left and right anchors
+        widget.layout().setHorizontalSpacing(50)
+
+        left_node = EditLinksNode(self, direction=Qt.LeftToRight,
+                                  node=self.source)
+
+        left_node.setSizePolicy(QSizePolicy.MinimumExpanding,
+                                QSizePolicy.MinimumExpanding)
+
+        right_node = EditLinksNode(self, direction=Qt.RightToLeft,
+                                   node=self.sink)
+
+        right_node.setSizePolicy(QSizePolicy.MinimumExpanding,
+                                 QSizePolicy.MinimumExpanding)
+
+        left_node.setMinimumWidth(150)
+        right_node.setMinimumWidth(150)
+
+        widget.layout().addItem(left_node, 0, 0,)
+        widget.layout().addItem(right_node, 0, 1,)
+
+        title_template = "<center><b>{0}<b></center>"
+
+        left_title = GraphicsTextWidget(self)
+        left_title.setHtml(title_template.format(escape(self.source.title)))
+        left_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+        right_title = GraphicsTextWidget(self)
+        right_title.setHtml(title_template.format(escape(self.sink.title)))
+        right_title.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+        widget.layout().addItem(left_title, 1, 0,
+                                alignment=Qt.AlignHCenter | Qt.AlignTop)
+        widget.layout().addItem(right_title, 1, 1,
+                                alignment=Qt.AlignHCenter | Qt.AlignTop)
+
+        widget.setParentItem(self)
+
+        max_w = max(left_node.sizeHint(Qt.PreferredSize).width(),
+                    right_node.sizeHint(Qt.PreferredSize).width())
+
+        # fix same size
+        left_node.setMinimumWidth(max_w)
+        right_node.setMinimumWidth(max_w)
+        left_title.setMinimumWidth(max_w)
+        right_title.setMinimumWidth(max_w)
+
+        self.layout().addItem(widget)
+        self.layout().activate()
+
+        self.sourceNodeWidget = left_node
+        self.sinkNodeWidget = right_node
+        self.sourceNodeTitle = left_title
+        self.sinkNodeTitle = right_title
+
+    if qVersion() < "4.7":
+        geometryChanged = Signal()
+
+        def setGeometry(self, rect):
+            QGraphicsWidget.setGeometry(self, rect)
+            self.geometryChanged.emit()
+
+
+class EditLinksNode(QGraphicsWidget):
+    """
+    A Node representation with channel anchors.
+
+    `direction` specifies the layout (default `Qt.LeftToRight` will
+    have icon on the left and channels on the right).
+
+    """
+
+    def __init__(self, parent=None, direction=Qt.LeftToRight,
+                 node=None, icon=None, iconSize=None, **args):
+        QGraphicsWidget.__init__(self, parent, **args)
+        self.setAcceptedMouseButtons(Qt.NoButton)
+        self.__direction = direction
+
+        self.setLayout(QGraphicsLinearLayout(Qt.Horizontal))
+
+        # Set the maximum size, otherwise the layout can't grow beyond its
+        # sizeHint (and we need it to grow so the widget can grow and keep the
+        # contents centered vertically.
+        self.layout().setMaximumSize(QSizeF(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX))
+
+        self.setSizePolicy(QSizePolicy.MinimumExpanding,
+                           QSizePolicy.MinimumExpanding)
+
+        self.__iconSize = iconSize or QSize(64, 64)
+        self.__icon = icon
+
+        self.__iconItem = QGraphicsPixmapItem(self)
+        self.__iconLayoutItem = GraphicsItemLayoutItem(item=self.__iconItem)
+
+        self.__channelLayout = QGraphicsGridLayout()
+        self.__channelAnchors = []
+
+        if self.__direction == Qt.LeftToRight:
+            self.layout().addItem(self.__iconLayoutItem)
+            self.layout().addItem(self.__channelLayout)
+            channel_alignemnt = Qt.AlignRight
+
+        else:
+            self.layout().addItem(self.__channelLayout)
+            self.layout().addItem(self.__iconLayoutItem)
+            channel_alignemnt = Qt.AlignLeft
+
+        self.layout().setAlignment(self.__iconLayoutItem, Qt.AlignCenter)
+        self.layout().setAlignment(self.__channelLayout,
+                                   Qt.AlignVCenter | channel_alignemnt)
+