Connectors.py 22.3 KB
Newer Older
Maxime Perrotin's avatar
Maxime Perrotin committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
    Diagram elements for connectors. Different types of connectors are
    available by default, such as lines, arrows, and bezier curves.

    The "Connection" class is the mother class, all other connectors must
    inherit from it and possibly redefine some functions or shape.

    Copyright (c) 2012-2014 European Space Agency

    Designed and implemented by Maxime Perrotin

    Contact: maxime.perrotin@esa.int
"""

import math
import logging

21
from PySide.QtCore import Qt, QPointF, QLineF, Slot
Maxime Perrotin's avatar
Maxime Perrotin committed
22

23
24
25
from PySide.QtGui import(QGraphicsPathItem, QPainterPath, QGraphicsItem, QPen,
                         QPainter, QFont, QGraphicsTextItem, QColor,
                         QFontMetrics, QTextBlockFormat, QTextCursor)
Maxime Perrotin's avatar
Maxime Perrotin committed
26

27
28
29
import ogParser
from TextInteraction import EditableText

Maxime Perrotin's avatar
Maxime Perrotin committed
30
31
32
LOG = logging.getLogger(__name__)


Maxime Perrotin's avatar
Maxime Perrotin committed
33
34
35
36
37
38
39
40
41
42
43
44
# pylint: disable=R0904
class Connection(QGraphicsPathItem, object):
    ''' Connection between two symbols (top-level class) '''
    # Default connections are not selectable
    default_cursor = Qt.ArrowCursor
    hasParent = False

    def __init__(self, parent, child):
        ''' Create a new connection between a parent and a child item '''
        super(Connection, self).__init__(parent)
        self.parent = parent
        self.child = child
Maxime Perrotin's avatar
Maxime Perrotin committed
45
        self._start_point = None
46
47
        self._end_point = None
        self._middle_points = []
Maxime Perrotin's avatar
Maxime Perrotin committed
48
49
50
51
52
53
54
55
56
        pen = QPen()
        pen.setColor(Qt.blue)
        pen.setCosmetic(False)
        self.setPen(pen)
        self.parent_rect = parent.sceneBoundingRect()
        self.childRect = child.sceneBoundingRect()
        # Activate cache mode to boost rendering by calling paint less often
        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    @property
    def start_point(self):
        ''' Compute connection origin - redefine in subclasses '''
        return self._start_point or QPointF(0, 0)

    @property
    def end_point(self):
        ''' Compute connection end point - redefine in subclasses '''
        return self._end_point or QPointF(0, 0)

    @property
    def middle_points(self):
        ''' Compute connection intermediate points - redefine in subclasses '''
        return self._middle_points

72
73
74
75
76
    def top_level(self):
        ''' In case the connection has a text, implement top_level()
            as needed by TextInteraction '''
        return self.parent

Maxime Perrotin's avatar
Maxime Perrotin committed
77
78
79
80
81
82
    def simple_arrow(self, origin='head', path=None):
        ''' Compute the two points of an vertical arrow head '''
        if origin == 'head':
            endp = self.end_point
        else:
            endp = self.start_point
83
84
85
        return (QPointF(endp.x() - 5, endp.y() - 5),
                QPointF(endp.x() + 5, endp.y() - 5))

Maxime Perrotin's avatar
Maxime Perrotin committed
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
    def angle_arrow(self, path, origin='head'):
        ''' Compute the two points of the arrow head with the right angle '''
        if origin == 'tail':
            path = path.toReversed()
        length = path.length()
        percent = path.percentAtLength(length - 10.0)
        src = path.pointAtPercent(percent)
        #path.moveTo(path.pointAtPercent(1))
        end_point = path.pointAtPercent(1)
        #end_point = path.currentPosition()
        line = QLineF(src, end_point)
        angle = math.acos(line.dx() / (line.length() or 1))
        if line.dy() >= 0:
            angle = math.pi * 2 - angle
        arrow_size = 10.0
        arrow_p1 = end_point + QPointF(
Maxime Perrotin's avatar
Maxime Perrotin committed
102
103
                math.sin(angle - math.pi / 3) * arrow_size,
                math.cos(angle - math.pi / 3) * arrow_size)
Maxime Perrotin's avatar
Maxime Perrotin committed
104
        arrow_p2 = end_point + QPointF(
Maxime Perrotin's avatar
Maxime Perrotin committed
105
106
                math.sin(angle - math.pi + math.pi / 3) * arrow_size,
                math.cos(angle - math.pi + math.pi / 3) * arrow_size)
Maxime Perrotin's avatar
Maxime Perrotin committed
107
108
109
110
111
112
113
114
        return (arrow_p1, arrow_p2)

    def draw_arrow_head(self, shape, origin='head', kind='simple'):
        ''' Generic function to draw a simple arrow '''
        if kind == 'simple':
            arrowhead = self.simple_arrow(path=shape)
        else:
            arrowhead = self.angle_arrow(shape, origin)
115
        shape.lineTo(arrowhead[0])
Maxime Perrotin's avatar
Maxime Perrotin committed
116
        shape.moveTo(self.end_point if origin == 'head' else self.start_point)
117
118
        shape.lineTo(arrowhead[1])

Maxime Perrotin's avatar
Maxime Perrotin committed
119
120
121
122
123
124
125
    def __str__(self):
        ''' Print connection information for debug purpose'''
        return 'Connection: parent = {p}, child = {c}'.format(
                p=str(self.parentItem()), c=str(self.child))

    def reshape(self):
        ''' Update the connection or arrow shape '''
126
127
128
129
130
131
132
        shape = QPainterPath()
        shape.moveTo(self.start_point)
        for point in self.middle_points:
            shape.lineTo(point)
        shape.lineTo(self.end_point)
        # If required draw an arrow head (e.g. in SDL NEXTSTATE and JOIN)
        if self.child.arrow_head:
Maxime Perrotin's avatar
Maxime Perrotin committed
133
134
135
136
137
138
            self.draw_arrow_head(shape, origin='head',
                                 kind=self.child.arrow_head)
        if self.child.arrow_tail:
            shape.moveTo(shape.pointAtPercent(0))
            self.draw_arrow_head(shape, origin='tail',
                                 kind=self.child.arrow_head)
139
        self.setPath(shape)
Maxime Perrotin's avatar
Maxime Perrotin committed
140
141
142
143


class RakeConnection(Connection):
    ''' Fork-like connection, e.g. between a state and an input symbol '''
144
145
146
    @property
    def start_point(self):
        ''' Compute connection origin - redefined function '''
Maxime Perrotin's avatar
Maxime Perrotin committed
147
        parent_rect = self.parentItem().boundingRect()
148
149
150
151
152
153
154
155
156
157
158
159
160
161
        return QPointF(parent_rect.width() / 2, parent_rect.height())

    @property
    def end_point(self):
        ''' Compute connection end point - redefined function '''
        coord = self.child.pos()
        coord.setX(coord.x() + self.child.boundingRect().width() / 2)
        return coord

    @property
    def middle_points(self):
        ''' Compute connection intermediate points - redefined function '''
        yield QPointF(self.start_point.x(), self.start_point.y() + 10)
        yield QPointF(self.end_point.x(), self.start_point.y() + 10)
Maxime Perrotin's avatar
Maxime Perrotin committed
162
163
164
165
166


class JoinConnection(Connection):
    ''' Inverted fork-like connection, to join to a common point '''
    def reshape(self):
167
168
        ''' Update the connection shape - redefined: if the last element
        of a branch is e.g. a nextstate, don't join the connection point '''
Maxime Perrotin's avatar
Maxime Perrotin committed
169
        if self.parentItem().terminal_symbol:
170
            self.setPath(QPainterPath())
Maxime Perrotin's avatar
Maxime Perrotin committed
171
        else:
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
            super(JoinConnection, self).reshape()

    @property
    def start_point(self):
        ''' Compute connection origin - redefined function '''
        parent_rect = self.parentItem().boundingRect()
        return getattr(self.parentItem(), 'connectionPoint',
                       QPointF(parent_rect.width() / 2, parent_rect.height()))

    @property
    def end_point(self):
        ''' Compute connection end point - redefined function '''
        point_scene = self.child.mapToScene(self.child.connectionPoint)
        return self.mapFromScene(point_scene)

    @property
    def middle_points(self):
        ''' Compute connection intermediate points - redefined function '''
        yield QPointF(self.start_point.x(), self.end_point.y() - 10)
        yield QPointF(self.end_point.x(), self.end_point.y() - 10)
Maxime Perrotin's avatar
Maxime Perrotin committed
192
193
194
195


class VerticalConnection(Connection):
    ''' Vertical line with or without arrow '''
196
197
198
    @property
    def start_point(self):
        ''' Compute connection origin - redefined function '''
Maxime Perrotin's avatar
Maxime Perrotin committed
199
        parent_rect = self.parentItem().boundingRect()
200
201
202
203
204
205
206
207
208
        return getattr(self.parentItem(), 'connectionPoint',
                       QPointF(parent_rect.width() / 2, parent_rect.height()))

    @property
    def end_point(self):
        ''' Compute connection end point - redefined function '''
        point = self.child.pos()
        point.setX(self.start_point.x())
        return point
Maxime Perrotin's avatar
Maxime Perrotin committed
209
210
211
212
213


class CommentConnection(Connection):
    ''' Class handling connection to comment symbols, fixed at the right
        of a symbol '''
214
215
216
    @property
    def start_point(self):
        ''' Compute connection origin - redefined function '''
Maxime Perrotin's avatar
Maxime Perrotin committed
217
        parent_rect = self.parentItem().boundingRect()
218
219
220
221
222
        return QPointF(parent_rect.width(), parent_rect.height() / 2)

    @property
    def end_point(self):
        ''' Compute connection end point - redefined function '''
Maxime Perrotin's avatar
Maxime Perrotin committed
223
        if self.child.on_the_right:
224
225
            return QPointF(self.child.x(),
                   self.child.y() + self.child.boundingRect().height() / 2)
Maxime Perrotin's avatar
Maxime Perrotin committed
226
        else:
227
228
229
230
231
232
            return QPointF(self.child.x() + self.child.boundingRect().width(),
                   self.child.y() + self.child.boundingRect().height() / 2)

    @property
    def middle_points(self):
        ''' Compute connection intermediate points - redefined function '''
Maxime Perrotin's avatar
Maxime Perrotin committed
233
234
235
236
237
238
239
240
        # Make sure the connection does not overlap the comment item
        if (self.child.on_the_right or
               (not self.child.on_the_right and
               self.child.x() + self.child.boundingRect().width()
               < self.parentItem().boundingRect().width())):
            go_to_point = self.start_point.x() + 5
        else:
            go_to_point = self.end_point.x() + 5
241
242
        yield QPointF(go_to_point, self.start_point.y())
        yield QPointF(go_to_point, self.end_point.y())
Maxime Perrotin's avatar
Maxime Perrotin committed
243
244


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
class SignalList(EditableText):
    ''' Simplified text editor for signal lists '''
    def __init__(self, parent, text='', hyperlink = None):
        ''' Smaller font than normal text '''
        super(SignalList, self).__init__(parent, text)
        self.setFont(QFont('Ubuntu', pointSize=8))

    def set_text_alignment(self):
        ''' Text justification - ignore '''
        pass

    def set_textbox_position(self):
        ''' Alignment vs parent - ignore, done by the parent '''
        pass

    def try_resize(self):
261
        ''' Resizing of the parent when size expands - ignore '''
262
263
        pass

264
265
266
267
268
    # pylint: disable=C0103
    def focusOutEvent(self, event):
        ''' Redefined function - update in_sig and out_sig in parent '''
        super(SignalList, self).focusOutEvent(event)

269
270

class Signalroute(Connection):
Maxime Perrotin's avatar
Maxime Perrotin committed
271
    ''' Subclass of Connection used to draw channels between processes '''
272
    in_sig = out_sig = None
273
274
    completion_list = set()

Maxime Perrotin's avatar
Maxime Perrotin committed
275
    def __init__(self, process):
Maxime Perrotin's avatar
Maxime Perrotin committed
276
        ''' Set generic parameters from Connection class '''
277
278
279
280
281
282
283
        super(Signalroute, self).__init__(process, process)
        self.parser = ogParser
        self.blackbold = ()
        self.redbold = ()
        self.label_in = SignalList(parent=self)
        self.label_out = SignalList(parent=self)
        if not Signalroute.in_sig:
284
285
286
287
288
289
            # keep at class level as long as only one process is supported
            # when copy-pasting a process the challel in/out signal lists
            # are not parsed. Workaround is to keep the list "global"
            # to allow a copy of both process and channel
            # Needed for the image exporter, that copies the scene to a
            # temporary one
290
            Signalroute.in_sig = '{}'.format(',\n'.join(sig['name']
291
                                   for sig in process.input_signals))
292
            Signalroute.out_sig = '{}'.format(',\n'.join(sig['name']
293
                                    for sig in process.output_signals))
294
295
        self.label_in.setPlainText('[{}]'.format(self.in_sig))
        self.label_out.setPlainText('[{}]'.format(self.out_sig))
296
297
        self.label_in.document().contentsChanged.connect(self.change_siglist)
        self.label_out.document().contentsChanged.connect(self.change_siglist)
298
299
        for each in (self.label_in, self.label_out):
            each.show()
Maxime Perrotin's avatar
Maxime Perrotin committed
300
        self.process = process
301
        self.reshape()
Maxime Perrotin's avatar
Maxime Perrotin committed
302

303
304
305
306
307
308
309
310
311
312
313
314
315
    @Slot()
    def change_siglist(self):
        ''' Called when user modified the text of label_in or label_out '''
        for each in self.label_in, self.label_out:
            if not each.hasFocus():
                if not unicode(each).startswith('['):
                    each.setPlainText('[{}'.format(unicode(each)))
                if not unicode(each).endswith(']'):
                    each.setPlainText('{}]'.format(unicode(each)))
        Signalroute.in_sig = unicode(self.label_in)[1:-1]
        Signalroute.out_sig = unicode(self.label_out)[1:-1]


Maxime Perrotin's avatar
Maxime Perrotin committed
316
317
318
319
320
    @property
    def start_point(self):
        ''' Compute connection origin - redefined function '''
        parent_rect = self.process.boundingRect()
        return QPointF(parent_rect.x(), parent_rect.height() / 2)
Maxime Perrotin's avatar
Maxime Perrotin committed
321

Maxime Perrotin's avatar
Maxime Perrotin committed
322
323
324
325
    @property
    def end_point(self):
        ''' Compute connection end point - redefined function '''
        # Arrow always bumps at the screen edge
326
327
328
        try:
            view = self.scene().views()[0]
            view_pos = view.mapToScene(
Maxime Perrotin's avatar
Maxime Perrotin committed
329
                           view.viewport().geometry()).boundingRect().topLeft()
330
331
            scene_pos_x = self.mapFromScene(view_pos).x()
            return QPointF(scene_pos_x, self.start_point.y())
332
        except (IndexError, AttributeError):
333
            # In case there is no view (e.g. Export PNG from cmd line)
334
335
336
337
            return QPointF(self.start_point.x() - 250, self.start_point.y())

    def reshape(self):
        ''' Redefine shape function to add the text areas '''
338
        super(Signalroute, self).reshape()
339
340
341
342
343
344
345

        width_in = self.label_in.boundingRect().width()

        self.label_in.setX(self.start_point.x() - width_in)
        self.label_in.setY(self.start_point.y() + 5)
        self.label_out.setX(self.end_point.x() + 10)
        self.label_out.setY(self.end_point.y() + 5)
Maxime Perrotin's avatar
Maxime Perrotin committed
346

347
348
349
350
351
352
353
354
355
356
357
358
    def check_syntax(self, text):
        ''' Check the syntax of the IN and OUT signal lists '''
        try:
            _, syntax_errors, _, _, _ = self.parser.parseSingleElement(
                                           'signalroute', text)
        except (AssertionError, AttributeError) as err:
            LOG.error('Checker failed:' + str(err))
        else:
            return syntax_errors

    def update_completion_list(self, pr_text):
        ''' Called after text has been edited '''
359
        # TODO - call parseSingleElement, check sdlSymbols for examples
360
        pass
361
362
363
364
365

    def resize_item(self, new_rect):
        ''' Called after signallist text has been edited '''
        pass

Maxime Perrotin's avatar
Maxime Perrotin committed
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446

class Controlpoint(QGraphicsPathItem, object):
    ''' Class handling one edge control point (to change bezier curves) '''
    def __init__(self, pos, edge):
        ''' Set the original control point - with color, shape '''
        path = QPainterPath()
        path.addEllipse(pos.x() - 5, pos.y() - 5, 10, 10)
        super(Controlpoint, self).__init__(path, parent=edge)
        self.setPen(QColor(50, 100, 120, 200))
        self.setBrush(QColor(200, 200, 210, 120))
        self.setFlags(QGraphicsItem.ItemIsSelectable |
                      QGraphicsItem.ItemIsMovable)
        self.edge = edge
        self.hide()

    @property
    def center(self):
        ''' Return the position of the center of the shape '''
        return self.pos() + self.boundingRect().center()

    def mouseMoveEvent(self, event):
        ''' When user moves a control point, update the connection shape '''
        super(Controlpoint, self).mouseMoveEvent(event)
        self.edge.reshape()


class Connectionpoint(Controlpoint):
    '''
        Class handling an end connection point - similar to Controlpoint,
        except the shape and the behaviour when moved. Connection point
        must check with its owning symbol if the move is valid.
    '''
    def __init__(self, pos, edge, symbol):
        ''' Create the point - as a small, lightblue box '''
        super(Connectionpoint, self).__init__(pos, edge=edge)
        path = QPainterPath()
        path.addRect(0, 0, 10, 10)
        self.setPath(path)
        self.setPos(pos.x() - 5, pos.y() - 5)
        # Symbol actually owning the connection point
        self.symbol = symbol

    def update_position(self):
        '''
            Update the position of the end point so that it is always
            placed correctly on the symbol that contains it (but that may
            not be the parent of the point if is the end point)
            After the position update, reshape the edge so that the lines
            are properly drawn to the new position
        '''
        # Transform position to owning symbol coordinates:
        center_pos = self.scenePos() + self.boundingRect().center()
        pos_symb = self.symbol.mapFromScene(center_pos)

        # Ask to symbol the nearest valid position on its shape
        # to place the connection point, and adjust position
        nearest_x, nearest_y = self.symbol.closest_connection_point(pos_symb)

        self.moveBy(-nearest_x, -nearest_y)

        self.edge.reshape()

    def mouseMoveEvent(self, event):
        '''
            When user moves the point, check with the connected symbol
            where the actual connection point should be placed
        '''
        # calling super function moves to the new point
        super(Connectionpoint, self).mouseMoveEvent(event)
        # Then compute a valid position, based on the owning symbol
        self.update_position()


class Edge(Connection):
    ''' B-spline/Bezier connection shape '''
    def __init__(self, edge, graph):
        ''' Set generic parameters from Connection class '''
        self.text_label = None
        super(Edge, self).__init__(edge['source'], edge['target'])
        self.edge = edge
        self.graph = graph
447
448
        # Set connection points as not visible, by default
        self.bezier_visible = False
Maxime Perrotin's avatar
Maxime Perrotin committed
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467

        # Initialize control point coordinates
        self.bezier = [self.mapFromScene(*self.edge['spline'][0])]
        # Bezier control points (groups of three points):
        assert(len(self.edge['spline']) % 3 == 1)
        for i in xrange(1, len(self.edge['spline']), 3):
            self.bezier.append([Controlpoint(
                          self.mapFromScene(*self.edge['spline'][i + j]), self)
                          for j in range(3)])

        # Create connection points at start and end of the edge
        self.source_connection = Connectionpoint(
                self.start_point or self.bezier[0], self, self.parent)
        self.parent.movable_points.append(self.source_connection)
        self.end_connection = Connectionpoint(
                self.end_point or self.bezier[-1], self, self.child)
        self.child.movable_points.append(self.end_connection)
        self.reshape()

468
469
470
471
472
473
474
475
476
477
478
479
480
    @property
    def start_point(self):
        ''' Compute connection origin - redefine in subclasses '''
        # Start point is optional - graphviz decision
        return self.mapFromScene(*self.edge['start']) \
               if self.edge.get('start') else None

    @property
    def end_point(self):
        ''' Compute connection end point - redefine in subclasses '''
        return self.mapFromScene(*self.edge['end']) \
               if self.edge.get('end') else None

Maxime Perrotin's avatar
Maxime Perrotin committed
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
    def bezier_set_visible(self, visible=True):
        ''' Display or hide the edge control points '''
        self.bezier_visible = visible
        for group in self.bezier[1:]:
            for ctrl_point in group:
                if visible:
                    ctrl_point.show()
                else:
                    ctrl_point.hide()
        if visible:
            self.end_connection.show()
            self.source_connection.show()
        else:
            self.end_connection.hide()
            self.source_connection.hide()
        self.update()

    def mousePressEvent(self, event):
        ''' On a mouse click, display the control points '''
        self.bezier_set_visible(True)

502
    # pylint: disable=R0914
Maxime Perrotin's avatar
Maxime Perrotin committed
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
    def reshape(self):
        ''' Update the shape of the edge (redefined function) '''
        path = QPainterPath()
        # If there is a starting point, draw a line to the first curve point
        if self.start_point:
            path.moveTo(self.source_connection.center)
            path.lineTo(self.bezier[0])
        else:
            path.moveTo(self.source_connection.center)
        # Loop over the curve points:
        for group in self.bezier[1:]:
            path.cubicTo(*[point.center for point in group])

        # If there is an ending point, draw a line to it
        if self.end_point:
            path.lineTo(self.end_connection.center)

        end_point = path.currentPosition()
Maxime Perrotin's avatar
Maxime Perrotin committed
521
        arrowhead = self.angle_arrow(path)
522
523
524
        path.lineTo(arrowhead[0])
        path.moveTo(end_point)
        path.lineTo(arrowhead[1])
Maxime Perrotin's avatar
Maxime Perrotin committed
525
526
527
528
        path.moveTo(end_point)
        try:
            # Add the transition label, if any (none for the START edge)
            font = QFont('arial', pointSize=8)
529
            metrics = QFontMetrics(font)
530
            label = self.edge.get('label', '') or ''
531
532
533
534
            lines = label.split('\n')
            width = metrics.width(max(lines)) # longest line
            height = metrics.height() * len(lines)
            # lp is the position of the center of the text
Maxime Perrotin's avatar
Maxime Perrotin committed
535
536
537
538
            pos = self.mapFromScene(*self.edge['lp'])
            if not self.text_label:
                self.text_label = QGraphicsTextItem(
                                 self.edge.get('label', ''), parent=self)
539
540
541
542
543
544
545
546
547
548
549
550
551
552
                self.text_label.setX(pos.x() - width / 2)
                self.text_label.setY(pos.y() - height / 2)
                self.text_label.setFont(font)
                # Make horizontal center alignment, as dot does
                self.text_label.setTextWidth(
                        self.text_label.boundingRect().width())
                fmt = QTextBlockFormat()
                fmt.setAlignment(Qt.AlignHCenter)
                cursor = self.text_label.textCursor()
                cursor.select(QTextCursor.Document)
                cursor.mergeBlockFormat(fmt)
                cursor.clearSelection()
                self.text_label.setTextCursor(cursor)
                self.text_label.show()
Maxime Perrotin's avatar
Maxime Perrotin committed
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
        except KeyError:
            # no label
            pass
        self.setPath(path)

    def __str__(self):
        ''' user-friendly information about the edge coordinates '''
        return('Edge between ' + self.edge['source'].name + ' and ' +
                self.edge['target'].name + str(self.edge['spline'][0]))

    def paint(self, painter, option, widget):
        ''' Apply anti-aliasing to Edge Connections '''
        painter.setRenderHint(QPainter.Antialiasing, True)
        super(Edge, self).paint(painter, option, widget)
        # Draw lines between connection points, if visible
        if self.bezier_visible:
            painter.setPen(
                    QPen(Qt.lightGray, 0, Qt.SolidLine))
            painter.setBrush(Qt.NoBrush)
            points_flat = [point.center
                           for sub1 in self.bezier[1:] for point in sub1]
            painter.drawPolyline([self.source_connection.center]
                                  + points_flat + [self.end_connection.center])