sdlSymbols.py 55.7 KB
Newer Older
Maxime Perrotin's avatar
Maxime Perrotin committed
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
    OpenGEODE - A tiny SDL Editor for TASTE

    This module contains the definition of the SDL symbols,
    including geometry and specific symbol behaviour when needed.

    All symbols inherit the generic Vertical- and Horizontal-
    Symbol classes defined in the "genericSymbols.py" module.

13
    Copyright (c) 2012-2020 European Space Agency
Maxime Perrotin's avatar
Maxime Perrotin committed
14
15
16
17
18
19
20
21

    Designed and implemented by Maxime Perrotin

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

__all__ = ['Input', 'Output', 'State', 'Task', 'ProcedureCall', 'Label',
           'Decision', 'DecisionAnswer', 'Join', 'Start', 'TextSymbol',
22
           'Procedure', 'ProcedureStart', 'ProcedureStop', 'ProcessType',
23
           'StateStart', 'Process', 'ContinuousSignal']
Maxime Perrotin's avatar
Maxime Perrotin committed
24

Maxime Perrotin's avatar
Maxime Perrotin committed
25
import traceback
26
import logging
Maxime Perrotin's avatar
Maxime Perrotin committed
27
from itertools import chain
Maxime Perrotin's avatar
Maxime Perrotin committed
28

Maxime Perrotin's avatar
Maxime Perrotin committed
29
30
31
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
Maxime Perrotin's avatar
Maxime Perrotin committed
32

Maxime Perrotin's avatar
Maxime Perrotin committed
33
34
from .genericSymbols import HorizontalSymbol, VerticalSymbol, Comment
from .Connectors import Connection, JoinConnection, Signalroute
35

Maxime Perrotin's avatar
Maxime Perrotin committed
36
from . import ogParser, ogAST
Maxime Perrotin's avatar
Maxime Perrotin committed
37
38
39
40


LOG = logging.getLogger('sdlSymbols')

41
AST = ogAST.AST()
42
CONTEXT = ogAST.Process()
Maxime Perrotin's avatar
Maxime Perrotin committed
43
44
45
46
47
48

# SDL-specific: reserved keywords, to be highlighted in textboxes
# Two kind of formatting are possible: black bold, and red bold
SDL_BLACKBOLD = ['\\b{word}\\b'.format(word=word) for word in (
                'DCL', 'CALL', 'ELSE', 'IF', 'THEN', 'MANTISSA', 'BASE',
                'EXPONENT', 'TRUE', 'FALSE', 'MOD', 'FI', 'WRITE', 'WRITELN',
49
                'LENGTH', 'PRESENT', 'FPAR', 'TODO', 'FIXME', 'XXX', 'ENDFOR',
Maxime Perrotin's avatar
Maxime Perrotin committed
50
                'CHECKME', 'PROCEDURE', 'EXTERNAL', 'IN', 'OUT', 'TIMER',
51
                'SET_TIMER', 'RESET_TIMER', 'VIA', 'ENTRY', 'EXIT', 'PRIORITY',
52
                'SYNTYPE', 'ENDSYNTYPE', 'CONSTANTS', 'ENDPROCEDURE', 'FOR',
53
                'COMMENT', 'SIGNAL', 'SIGNALLIST', 'USE', 'RETURNS', 'ANY',
Maxime Perrotin's avatar
Maxime Perrotin committed
54
                'NEWTYPE', 'ENDNEWTYPE', 'ARRAY', 'STRUCT', 'SYNONYM')]
Maxime Perrotin's avatar
Maxime Perrotin committed
55
56

SDL_REDBOLD = ['\\b{word}\\b'.format(word=word) for word in (
57
58
              'INPUT', 'OUTPUT', 'STATE', 'DECISION', 'NEXTSTATE', 'INTEGER',
              'CHARACTER', 'ASN1INT',
59
              'TASK', 'PROCESS', 'LABEL', 'JOIN', 'CONNECTION', 'CONNECT')]
Maxime Perrotin's avatar
Maxime Perrotin committed
60
61


62
63
64
65
66
def variables_autocompletion(symbol, type_filter=None):
    ''' Intelligent autocompletion for variables - including struct fields
        Optional: only variables of a type listed in type_filter are kept
    '''
    res = set()
Maxime Perrotin's avatar
Maxime Perrotin committed
67
68
69
70
71
72
    if not symbol.text:
        return res
    parts = symbol.text.context.split('!')
    if len(parts) == 0:
        return res
    elif len(parts) == 1:
73
74
75
76
77
        try:
            fpar = {fp['name']: (fp['type'], None) for fp in CONTEXT.fpar}
        except AttributeError:
            # not in the context of a procedure
            fpar = {}
78
79
        # Return the list of variables, possibly filterd by type
        if not type_filter:
Maxime Perrotin's avatar
Maxime Perrotin committed
80
81
82
83
            res = set( list(CONTEXT.variables.keys())
                      + list(CONTEXT.global_variables.keys())
                      + list(AST.asn1_constants.keys())
                      + list(fpar.keys()))
84
85
        else:
            constants = {name: (cty.type, None)
Maxime Perrotin's avatar
Maxime Perrotin committed
86
                         for name, cty in AST.asn1_constants.items()}
Maxime Perrotin's avatar
Maxime Perrotin committed
87
88
89
90
91
92
93
94
95
96
            try:
                type_filter_names = [ogParser.type_name(ty)
                                     for ty in type_filter]
            except AttributeError as err:
                # This would need to be investigated: it can happen when
                # using a parameter in an input just after the parameter was
                # added to the signal in the block view, and before any
                # variable has been declared....
                LOG.debug(str(err))
                return res
Maxime Perrotin's avatar
Maxime Perrotin committed
97
98
99
100
            for name, (asn1type, _) in chain(CONTEXT.variables.items(),
                                          CONTEXT.global_variables.items(),
                                          constants.items(),
                                          fpar.items()):
101
102
                if ogParser.type_name(asn1type) in type_filter_names:
                    res.add(name)
Maxime Perrotin's avatar
Maxime Perrotin committed
103
104
105
106
107
108
109
110
111
112
113
    else:
        var = parts[0].lower()
        try:
            var_t = ogParser.find_variable_type(var, CONTEXT)
            basic = ogParser.find_basic_type(var_t, AST.dataview)
            res = (field.replace('-', '_') for field in basic.Children.keys())
        except (AttributeError, TypeError):
            res = []
        else:
            for each in parts[1:-1]:
                try:
Maxime Perrotin's avatar
Maxime Perrotin committed
114
                    for child, childtype in basic.Children.items():
Maxime Perrotin's avatar
Maxime Perrotin committed
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
                        if child.lower() == each.lower().replace('_', '-'):
                            basic = ogParser.find_basic_type(childtype.type,
                                                             AST.dataview)
                            break
                    else:
                        res = ()
                        break
                except (AttributeError, TypeError):
                    res = ()
                    break
            else:
                try:
                    res = (field.replace('-', '_')
                           for field in basic.Children.keys())
                except AttributeError:
                    res = ()
    return res




Maxime Perrotin's avatar
Maxime Perrotin committed
136
# pylint: disable=R0904
137
class Input(HorizontalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
138
139
140
    ''' SDL INPUT Symbol '''
    _unique_followers = ['Comment']
    _insertable_followers = ['Task', 'ProcedureCall', 'Output', 'Decision',
141
                             'Input', 'Label', 'Connect', 'ContinuousSignal']
Maxime Perrotin's avatar
Maxime Perrotin committed
142
143
144
145
146
147
148
149
150
151
    _terminal_followers = ['Join', 'State', 'ProcedureStop']

    common_name = 'input_part'
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD

    def __init__(self, parent=None, ast=None):
        ''' Create the INPUT symbol '''
        ast = ast or ogAST.Input()
152
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
153
        self.branch_entrypoint = None
Maxime Perrotin's avatar
Maxime Perrotin committed
154
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
155
156
157
        if not ast.pos_y and parent:
            # Make sure the item is placed below its parent
            ast.pos_y = parent.y() + parent.boundingRect().height() + 10
Maxime Perrotin's avatar
Maxime Perrotin committed
158
159
160
161
162
        super(Input, self).__init__(parent,
                                    text=ast.inputString,
                                    x=ast.pos_x or 0,
                                    y=ast.pos_y or 0,
                                    hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
163
164
165
166
167
168
169
170
171
172
173
174
        self.set_shape(ast.width, ast.height)
        gradient = QRadialGradient(50, 50, 50, 50, 50)
        gradient.setColorAt(0, QColor(255, 240, 170))
        gradient.setColorAt(1, Qt.white)
        self.setBrush(QBrush(gradient))
        self.terminal_symbol = False
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)

    def insert_symbol(self, parent, x, y):
        ''' Insert Input symbol - propagate branch Entry point '''
175
176
177
        # Make sure that parent is a state, not a sibling symbol
        item_parent = (parent if not isinstance(parent, (Input,
                                                         ContinuousSignal))
Maxime Perrotin's avatar
Maxime Perrotin committed
178
179
180
181
                       else parent.parentItem())
        self.branch_entrypoint = item_parent.branch_entrypoint
        super(Input, self).insert_symbol(item_parent, x, y)

Maxime Perrotin's avatar
Maxime Perrotin committed
182
183
184
185
    def boundingRect(self):
        return QRectF(0, 0, self.width, self.height)


Maxime Perrotin's avatar
Maxime Perrotin committed
186
187
    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
188
189
190
191
192
193
194
195
196
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.lineTo(width, 0)
            path.lineTo(width - 11, height / 2)
            path.lineTo(width, height)
            path.lineTo(0, height)
            path.lineTo(0, 0)
            self.setPath(path)
            super(Input, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
197

Maxime Perrotin's avatar
Maxime Perrotin committed
198
199
200
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
201
        if '(' in str(self):
202
            # Input parameter: return the list of variables of this type
Maxime Perrotin's avatar
Maxime Perrotin committed
203
            input_name = str(self).split('(')[0].strip().lower()
Maxime Perrotin's avatar
Maxime Perrotin committed
204
            asn1_filter = [sig.get('type') for sig in CONTEXT.input_signals if
205
206
                           sig['name'] == input_name]
            return variables_autocompletion(self, asn1_filter)
Maxime Perrotin's avatar
Maxime Perrotin committed
207
        else:
208
209
210
            # Return the list of input signals and timers
            return (set(sig['name'] for sig in CONTEXT.input_signals).union(
                    CONTEXT.global_timers + CONTEXT.timers))
Maxime Perrotin's avatar
Maxime Perrotin committed
211

Maxime Perrotin's avatar
Maxime Perrotin committed
212

213
214
215
class Connect(Input):
    ''' Connect point below a nested state '''
    common_name = 'connect_part'
216
    auto_expand = True
Maxime Perrotin's avatar
Maxime Perrotin committed
217
218
219
    resizeable = False
    # Symbol must not use antialiasing, otherwise the middle line is too thick
    _antialiasing = False
Maxime Perrotin's avatar
Maxime Perrotin committed
220

Maxime Perrotin's avatar
Maxime Perrotin committed
221
222
    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
223
224
225
226
        if width != self.width or height != self.height:
            self.setPen(QPen(Qt.blue))
            self.textbox_alignment = Qt.AlignLeft | Qt.AlignTop
            path = QPainterPath()
Maxime Perrotin's avatar
Maxime Perrotin committed
227
228
            path.moveTo(width / 2, 0)
            path.lineTo(width / 2, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
229
230
231
232
            #path.moveTo(0, height / 2)
            #path.lineTo(width, height / 2)
            self.setPath(path)
            super(Input, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
233
234
235
236
237

    def resize_item(self, rect):
        ''' Symbol cannot be resized '''
        return

238
239
240
    @property
    def completion_list(self):
        ''' Set auto-completion list: list of exit points of nested state '''
Maxime Perrotin's avatar
Maxime Perrotin committed
241
        parent_state = str(self.parentItem()).lower()
242
243
244
245
246
247
        for each in CONTEXT.composite_states:
            if each.statename == parent_state:
                return each.state_exitpoints
        else:
            return set()

248

Maxime Perrotin's avatar
Maxime Perrotin committed
249
# pylint: disable=R0904
250
class Output(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
251
252
253
254
255
256
257
258
259
260
261
262
    ''' SDL OUTPUT Symbol '''
    _unique_followers = ['Comment']
    _insertable_followers = [
            'Task', 'ProcedureCall', 'Output', 'Decision', 'Label']
    _terminal_followers = ['Join', 'State', 'ProcedureStop']
    common_name = 'output'
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD

    def __init__(self, parent=None, ast=None):
        ast = ast or ogAST.Output()
263
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
264
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
265
        super(Output, self).__init__(parent=parent,
266
                text=ast.inputString, x=ast.pos_x or 0, y=ast.pos_y or 0,
Maxime Perrotin's avatar
Maxime Perrotin committed
267
268
269
270
271
272
273
274
275
276
277
                hyperlink=ast.hyperlink)
        self.set_shape(ast.width, ast.height)

        self.setBrush(QBrush(QColor(255, 255, 202)))
        self.terminal_symbol = False
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)

    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
278
279
280
281
282
283
284
285
286
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.lineTo(width - 11, 0)
            path.lineTo(width, height / 2)
            path.lineTo(width - 11, height)
            path.lineTo(0, height)
            path.lineTo(0, 0)
            self.setPath(path)
            super(Output, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
287

Maxime Perrotin's avatar
Maxime Perrotin committed
288
289
290
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
291
        if '(' in str(self):
292
            # Output parameter: return the list of variables of this type
Maxime Perrotin's avatar
Maxime Perrotin committed
293
            output_name = str(self).split('(')[0].strip().lower()
294
            asn1_filter = [sig['type'] for sig in CONTEXT.output_signals if
295
                           hasattr(sig, 'type') and sig['name'] == output_name]
296
297
298
299
            return variables_autocompletion(self, asn1_filter)
        else:
            # Return the list of output signals
            return (set(sig['name'] for sig in CONTEXT.output_signals))
Maxime Perrotin's avatar
Maxime Perrotin committed
300

Maxime Perrotin's avatar
Maxime Perrotin committed
301
302

# pylint: disable=R0904
303
class Decision(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
304
305
    ''' SDL DECISION Symbol '''
    _unique_followers = ['Comment']
Maxime Perrotin's avatar
Maxime Perrotin committed
306
307
    _insertable_followers = ['DecisionAnswer', 'Task', 'ProcedureCall',
                             'Output', 'Decision', 'Label']
Maxime Perrotin's avatar
Maxime Perrotin committed
308
309
310
311
312
313
314
315
316
    _terminal_followers = ['Join', 'State', 'ProcedureStop']
    common_name = 'decision'
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD + ['\\b{}\\b'.format(word)
                                   for word in ('AND', 'OR')]
    redbold = SDL_REDBOLD

    def __init__(self, parent=None, ast=None):
        ast = ast or ogAST.Decision()
317
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
318
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
319
320
321
        # Define the point where all branches of the decision can join again
        self.connectionPoint = QPoint(ast.width / 2, ast.height + 30)
        super(Decision, self).__init__(parent, text=ast.inputString,
322
                x=ast.pos_x or 0, y=ast.pos_y or 0, hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
        self.set_shape(ast.width, ast.height)
        self.setBrush(QColor(255, 255, 202))
        self.minDistanceToSymbolAbove = 0
        self.parser = ogParser
        self.text_alignment = Qt.AlignHCenter
        if ast.comment:
            Comment(parent=self, ast=ast.comment)

    @property
    def terminal_symbol(self):
        '''
            Compute dynamically if the item is terminal by checking
            if all its branches end with a terminator
        '''
        for branch in self.branches():
            if not branch.last_branch_item.terminal_symbol:
                return False
340
        return True
Maxime Perrotin's avatar
Maxime Perrotin committed
341

Maxime Perrotin's avatar
Maxime Perrotin committed
342
343
344
345
346
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
        return chain(variables_autocompletion(self), ('length', 'present'))

Maxime Perrotin's avatar
Maxime Perrotin committed
347
348
349
350
351
352
353
    def branches(self):
        ''' Return the list of decision answers (as a generator) '''
        return (branch for branch in self.childSymbols()
                if isinstance(branch, DecisionAnswer))

    def set_shape(self, width, height):
        ''' Define polygon points to draw the symbol '''
Maxime Perrotin's avatar
Maxime Perrotin committed
354
355
356
357
358
359
360
361
362
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.moveTo(width / 2, 0)
            path.lineTo(width, height / 2)
            path.lineTo(width / 2, height)
            path.lineTo(0, height / 2)
            path.lineTo(width / 2, 0)
            self.setPath(path)
            super(Decision, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
363
364
365
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

    def resize_item(self, rect):
        ''' On resize event, make sure connection points are updated '''
        delta_y = self.boundingRect().height() - rect.height()
        super(Decision, self).resize_item(rect)
        self.connectionPoint.setX(self.boundingRect().center().x())
        self.connectionPoint.setY(self.connectionPoint.y() - delta_y)
        self.update_connections()

    def update_connections(self):
        ''' Redefined - update arrows shape below connection point '''
        super(Decision, self).update_connections()
        for branch in self.branches():
            for cnx in branch.last_branch_item.connections():
                cnx.reshape()

    def updateConnectionPointPosition(self):
        ''' Compute the joining point of decision branches '''
        new_y = 0
        new_x = self.boundingRect().width() / 2.0
        answers = False
        for branch in self.branches():
            answers = True
            last_cnx = None
            last = branch.last_branch_item
            try:
                # To compute the branch length, we must keep only the symbols,
                # so we must remove the last connection (if any)
                last_cnx, = (c for c in last.childItems() if
                    isinstance(c, Connection) and not
                    isinstance(c.child, (Comment, HorizontalSymbol)))
394
                # Don't set parent item to None to avoid Qt segfault
395
396
397
398
399
400
401
402
403
                # The bug with setParentItem is a Qt bug documented here:
                # https://bugreports.qt.io/browse/QTBUG-18616
                # the crash may happen if the scene of the new parent
                # is different from the scene of the object. the doc says
                # it is allowed but an assert in the code makes it crash
                # workaround: first put the item manually in the right scene
                # then call setParentItem
                if self.scene() != last_cnx.scene():
                    self.scene().addItem(last_cnx)
404
                last_cnx.setParentItem(self)
Maxime Perrotin's avatar
Maxime Perrotin committed
405
406
407
408
409
410
            except ValueError:
                pass
            branch_len = branch.y() + (
                    branch.boundingRect() |
                    branch.childrenBoundingRect()).height()
            try:
411
412
                if last.scene() != last_cnx.scene():
                    last.scene().addItem(last_cnx) # workaround Qt's bug 18616
Maxime Perrotin's avatar
Maxime Perrotin committed
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
                last_cnx.setParentItem(last)
            except AttributeError:
                pass
            # If last item was a decision, use its connection point
            # position to get the length of the branch:
            try:
                branch_len = (last.connectionPoint.y() +
                        self.mapFromScene(0, last.scenePos().y()).y())
            except AttributeError:
                pass
            # Rounded with int() -> mandatory when view scale has changed
            new_y = int(max(new_y, branch_len))
        if not answers:
            new_y = int(self.boundingRect().height())
        new_y += 15
        delta = new_y - self.connectionPoint.y()
        self.connectionPoint.setY(new_y)
        self.connectionPoint.setX(new_x)
        if delta != 0:
            child = self.next_aligned_symbol()
            try:
434
                child.pos_y += delta
Maxime Perrotin's avatar
Maxime Perrotin committed
435
436
437
438
439
440
            except AttributeError:
                pass
        self.update_connections()


# pylint: disable=R0904
441
class DecisionAnswer(HorizontalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
442
443
444
445
446
447
448
449
450
451
452
    ''' If Decision is a "switch", DecisionAnswer is a "case" '''
    _insertable_followers = ['DecisionAnswer', 'Task', 'ProcedureCall',
                        'Output', 'Decision', 'Label']
    _terminal_followers = ['Join', 'State', 'ProcedureStop']
    common_name = 'alternative_part'
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD

    def __init__(self, parent=None, ast=None):
        ast = ast or ogAST.Answer()
453
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
454
        self.width, self.height = 0, 0 #ast.width, ast.height
Maxime Perrotin's avatar
Maxime Perrotin committed
455
456
457
458
459
        self.terminal_symbol = False
        # last_branch_item is used to compute branch length
        # for the connection point positionning
        self.last_branch_item = self
        super(DecisionAnswer, self).__init__(parent,
460
461
462
463
                                             text=ast.inputString,
                                             x=ast.pos_x or 0,
                                             y=ast.pos_y or 0,
                                             hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
464
465
466
467
468
469
470
471
        self.set_shape(ast.width, ast.height)
        self.branch_entrypoint = self
        self.parser = ogParser

    def insert_symbol(self, parent, x, y):
        ''' ANSWER-specific insersion behaviour: link to connection point '''
        if not parent:
            return
Maxime Perrotin's avatar
Maxime Perrotin committed
472
        # Make sure that parent is not a sibling answer
Maxime Perrotin's avatar
Maxime Perrotin committed
473
474
475
        item_parent = (parent if not isinstance(parent, DecisionAnswer)
                       else parent.parentItem())
        super(DecisionAnswer, self).insert_symbol(item_parent, x, y)
Maxime Perrotin's avatar
Maxime Perrotin committed
476
477
        self.last_branch_item.connectionBelow = \
                JoinConnection(self.last_branch_item, item_parent)
Maxime Perrotin's avatar
Maxime Perrotin committed
478
        self.text.try_resize()
Maxime Perrotin's avatar
Maxime Perrotin committed
479
480
481
482
483
484

    def boundingRect(self):
        return QRectF(0, 0, self.width, self.height)

    def set_shape(self, width, height):
        ''' ANSWER has round, disjoint sides - does not fit in a polygon '''
Maxime Perrotin's avatar
Maxime Perrotin committed
485
486
487
488
489
490
491
492
493
494
495
496
        if width != self.width or height != self.height:
            point = 20
            path = QPainterPath()
            left = QRect(0, 0, point, height)
            right = QRect(width - point, 0, point, height)
            path.arcMoveTo(left, 125)
            path.arcTo(left, 125, 110)
            path.arcMoveTo(right, -55)
            path.arcTo(right, -55, 110)
            path.moveTo(width, height)
            self.setPath(path)
            super(DecisionAnswer, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
497

498
499
500
501
502
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
        return ['ELSE']

Maxime Perrotin's avatar
Maxime Perrotin committed
503
504

# pylint: disable=R0904
505
class Join(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
506
    ''' JOIN symbol (GOTO) '''
507
    auto_expand = True
Maxime Perrotin's avatar
Maxime Perrotin committed
508
    arrow_head = 'simple'
Maxime Perrotin's avatar
Maxime Perrotin committed
509
510
511
512
513
514
    common_name = 'terminator_statement'
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD

    def __init__(self, parent=None, ast=None):
515
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
516
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
517
        if not ast:
518
            ast = ogAST.Terminator(defName='')
Maxime Perrotin's avatar
Maxime Perrotin committed
519
520
521
            ast.pos_y = 0
            ast.width = 35
            ast.height = 35
522
523
524
525
526
        super(Join, self).__init__(parent,
                                   text=ast.inputString,
                                   x=ast.pos_x,
                                   y=ast.pos_y,
                                   hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
527
528
529
530
531
532
533
534
535
536
        self.set_shape(ast.width, ast.height)
        self.setPen(QPen(Qt.blue))
        self.terminal_symbol = True
        self.parser = ogParser

    def resize_item(self, rect):
        ''' Redefinition of the resize item (block is a square) '''
        size = min(rect.width(), rect.height())
        rect.setWidth(size)
        rect.setHeight(size)
Maxime Perrotin's avatar
Maxime Perrotin committed
537
        super(Join, self).resize_item(rect)
Maxime Perrotin's avatar
Maxime Perrotin committed
538
539
540

    def set_shape(self, width, height):
        ''' Define the bouding rectangle of the JOIN symbol '''
Maxime Perrotin's avatar
Maxime Perrotin committed
541
542
543
544
545
546
        if width != self.width or height != self.height:
            circ = min(width, height)
            path = QPainterPath()
            path.addEllipse(0, 0, circ, circ)
            self.setPath(path)
            super(Join, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
547

548
549
550
551
552
    @property
    def completion_list(self):
        ''' Set auto-completion list - list of labels '''
        return (label.inputString for label in CONTEXT.labels)

553
    def update_completion_list(self, pr_text: str) -> None:
554
555
556
        ''' When text was entered, update list of join terminators '''
        ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name,
                                                         pr_text)
557
558
559
        if not ast:
            # in case of syntax error in the symbol text
            return
560
        for each in (t for t in CONTEXT.terminators if t.kind == 'join'):
Maxime Perrotin's avatar
Maxime Perrotin committed
561
            if each.inputString == str(self):
562
563
564
565
566
                # Ignore if already defined
                break
        else:
            CONTEXT.terminators.append(ast)

Maxime Perrotin's avatar
Maxime Perrotin committed
567

568
class ProcedureStop(Join):
Maxime Perrotin's avatar
Maxime Perrotin committed
569
570
571
572
    ''' Procedure STOP symbol - very similar to JOIN '''
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
Maxime Perrotin's avatar
Maxime Perrotin committed
573

Maxime Perrotin's avatar
Maxime Perrotin committed
574
    def __init__(self, parent=None, ast=None):
575
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
576
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
577
        if not ast:
578
            ast = ogAST.Terminator(defName='')
Maxime Perrotin's avatar
Maxime Perrotin committed
579
580
581
582
583
584
585
            ast.pos_y = 0
            ast.width = 35
            ast.height = 35
        super(ProcedureStop, self).__init__(parent, ast)

    def set_shape(self, width, height):
        ''' Define the symbol shape '''
Maxime Perrotin's avatar
Maxime Perrotin committed
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
        if width != self.width or height != self.height:
            circ = min(width, height)
            path = QPainterPath()
            path.addEllipse(0, 0, circ, circ)
            point1 = path.pointAtPercent(0.625)
            point2 = path.pointAtPercent(0.125)
            point3 = path.pointAtPercent(0.875)
            point4 = path.pointAtPercent(0.375)
            path.moveTo(point1)
            path.lineTo(point2)
            path.moveTo(point3)
            path.lineTo(point4)
            self.setPath(path)
            # call Join superclass, otherwise symbol will take Join shape
            super(Join, self).set_shape(circ, circ)
Maxime Perrotin's avatar
Maxime Perrotin committed
601

Maxime Perrotin's avatar
Maxime Perrotin committed
602
603
604
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
605
606
607
608
609
610
611
612
613
614
        try:
            return CONTEXT.state_exitpoints
        except AttributeError:
            # Not in a state but in a procedure
            return set()

    def update_completion_list(self, pr_text):
        ''' When text was entered, if in a nested state update exit points '''
        ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name,
                                                         pr_text)
615
616
617
        if not ast:
            # in case of syntax error in the symbol text
            return
618
        try:
619
            CONTEXT.state_exitpoints = \
Maxime Perrotin's avatar
Maxime Perrotin committed
620
                    set(CONTEXT.state_exitpoints) | set(str(self))
621
622
623
        except AttributeError:
            # No state exit points in a procedure
            pass
Maxime Perrotin's avatar
Maxime Perrotin committed
624

Maxime Perrotin's avatar
Maxime Perrotin committed
625
626

# pylint: disable=R0904
627
class Label(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
628
629
630
631
632
633
634
635
636
637
638
639
640
    ''' LABEL symbol '''
    _insertable_followers = [
            'Task', 'ProcedureCall', 'Output', 'Decision', 'Label']
    _terminal_followers = ['Join', 'State', 'ProcedureStop']
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
    # Symbol must not use antialiasing, otherwise the middle line is too thick
    _antialiasing = False

    def __init__(self, parent=None, ast=None):
        ast = ast or ogAST.Label()
641
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
642
        self.width, self.height = 0, 0
643
644
645
646
647
        super(Label, self).__init__(parent,
                                    text=ast.inputString,
                                    x=ast.pos_x or 0,
                                    y=ast.pos_y or 0,
                                    hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
648
649
650
651
652
653
654
655
656
657
658
659
        self.set_shape(ast.width, ast.height)
        self.setPen(QPen(Qt.blue))
        self.terminal_symbol = False
        self.textbox_alignment = Qt.AlignLeft | Qt.AlignTop
        self.parser = ogParser

    @property
    def common_name(self):
        return 'label' if self.hasParent else 'floating_label'

    def set_shape(self, width, height):
        ''' Define the shape of the LABEL symbol '''
Maxime Perrotin's avatar
Maxime Perrotin committed
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.addEllipse(0, height / 2, width / 4, height / 2)
            path.moveTo(width / 4, height * 3 / 4)
            path.lineTo(width / 2, height * 3 / 4)
            # Add arrow head
            path.moveTo(width / 2 - 5, height * 3 / 4 - 5)
            path.lineTo(width / 2, height * 3 / 4)
            path.lineTo(width / 2 - 5, height * 3 / 4 + 5)
            # Add vertical line in the middle of the symbol
            path.moveTo(width / 2, 0)
            path.lineTo(width / 2, height)
            # Make sure the bounding rect is withing specifications
            path.moveTo(width, height)
            self.setPath(path)
            super(Label, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
676

677
678
679
680
681
682
683
684
685
686
    @property
    def completion_list(self):
        ''' Set auto-completion list - list of JOIN '''
        return (term.inputString
                for term in CONTEXT.terminators if term.kind == 'join')

    def update_completion_list(self, pr_text):
        ''' When text was entered, update list of labels in current context '''
        ast, _, _, _, _ = self.parser.parseSingleElement(self.common_name,
                                                         pr_text)
687
688
689
        if not ast:
            # in case of syntax error in the symbol text
            return
690
        for each in CONTEXT.labels:
Maxime Perrotin's avatar
Maxime Perrotin committed
691
            if each.inputString == str(self):
692
693
694
695
696
                # Ignore if already defined
                break
        else:
            CONTEXT.labels.append(ast)

Maxime Perrotin's avatar
Maxime Perrotin committed
697
698

# pylint: disable=R0904
699
class Task(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
700
701
702
703
704
705
706
707
708
709
710
711
712
    ''' TASK symbol '''
    _unique_followers = ['Comment']
    _insertable_followers = [
            'Task', 'ProcedureCall', 'Output', 'Decision', 'Label']
    _terminal_followers = ['Join', 'State', 'ProcedureStop']
    common_name = 'task'
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD

    def __init__(self, parent=None, ast=None):
        ''' Initializes the TASK symbol '''
        ast = ast or ogAST.Task()
713
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
714
        self.width, self.height = 0, 0
715
716
717
718
719
        super(Task, self).__init__(parent,
                                   text=ast.inputString,
                                   x=ast.pos_x or 0,
                                   y=ast.pos_y or 0,
                                   hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
720
721
722
723
724
725
726
727
728
        self.set_shape(ast.width, ast.height)
        self.setBrush(QBrush(QColor(255, 255, 202)))
        self.terminal_symbol = False
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)

    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
729
730
731
732
733
734
735
736
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.lineTo(width, 0)
            path.lineTo(width, height)
            path.lineTo(0, height)
            path.lineTo(0, 0)
            self.setPath(path)
            super(Task, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
737

738
739
    @property
    def completion_list(self):
Maxime Perrotin's avatar
Maxime Perrotin committed
740
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
741
        elems = str(self).lower().strip().split()
742
743
744
745
746
747
748
749
750
751
        asn1_filter = []
        if len(elems) == 2 and elems[1] == ':=':
            # Find type of variable on the left and filter accordingly
            varname = elems[0]
            try:
                fpar = {fp['name']: (fp['type'], None) for fp in CONTEXT.fpar}
            except AttributeError:
                # not in the context of a procedure
                fpar = {}
            constants = {name: (cty.type, None)
Maxime Perrotin's avatar
Maxime Perrotin committed
752
753
754
755
756
                         for name, cty in AST.asn1_constants.items()}
            for name, (asn1ty, _) in chain (CONTEXT.variables.items(),
                                          CONTEXT.global_variables.items(),
                                          constants.items(),
                                          fpar.items()):
757
758
759
760
                if name == varname:
                    asn1_filter = [asn1ty]
                    break
        return chain(variables_autocompletion(self, asn1_filter),
Maxime Perrotin's avatar
Maxime Perrotin committed
761
                     ogParser.SPECIAL_OPERATORS.keys())
Maxime Perrotin's avatar
Maxime Perrotin committed
762
763

# pylint: disable=R0904
764
class ProcedureCall(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
765
766
767
768
769
770
771
772
773
774
775
776
    ''' PROCEDURE CALL symbol '''
    _unique_followers = ['Comment']
    _insertable_followers = [
            'Task', 'ProcedureCall', 'Output', 'Decision', 'Label']
    _terminal_followers = ['Join', 'State', 'ProcedureStop']
    common_name = 'procedure_call'
    # Define reserved keywords for the syntax highlighter
    blackbold = ['\\bWRITELN\\b', '\\bWRITE\\b',
                 '\\bSET_TIMER\\b', '\\bRESET_TIMER\\b']
    redbold = SDL_REDBOLD

    def __init__(self, parent=None, ast=None):
777
        ast = ast or ogAST.Output(defName='')
778
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
779
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
780
        super(ProcedureCall, self).__init__(parent,
781
782
783
784
                                            text=ast.inputString,
                                            x=ast.pos_x or 0,
                                            y=ast.pos_y or 0,
                                            hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
785
786
787
788
789
790
791
792
793
        self.set_shape(ast.width, ast.height)
        self.setBrush(QBrush(QColor(255, 255, 202)))
        self.terminal_symbol = False
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)

    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
794
795
796
797
798
799
800
801
802
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.addRect(0, 0, width, height)
            path.moveTo(7, 0)
            path.lineTo(7, height)
            path.moveTo(width - 7, 0)
            path.lineTo(width - 7, height)
            self.setPath(path)
            super(ProcedureCall, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
803

Maxime Perrotin's avatar
Maxime Perrotin committed
804
805
806
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
807
        if '(' in str(self):
808
            # Get the variables of the type of the current parameter
Maxime Perrotin's avatar
Maxime Perrotin committed
809
810
            count = str(self).count(',')
            procname = str(self).split('(')[0].strip().lower()
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
            for each in (proc for proc in CONTEXT.procedures
                         if proc.inputString.lower() == procname):
                param_types = [p['type'] for p in each.fpar]
                break
            else:
                # Procedure not defined, check special operators
                if (procname == 'set_timer' and count == 1) or (
                        procname == 'reset_timer' and count == 0):
                    return chain(CONTEXT.timers, CONTEXT.global_timers)
                elif procname in ('write', 'writeln'):
                    # Could filter for OCTET STRINGS/Strings/Integer/Booleans
                    return variables_autocompletion(self)
                else:
                    return ()
            if count + 1 > len(param_types):
                # User tries to set more parameters than defined
                return ()
            else:
                # Return variables of the type of the parameter
                asn1_filter = param_types[slice(count, count + 1)]
                return variables_autocompletion(self, asn1_filter)
        else:
            return chain((proc.inputString for proc in CONTEXT.procedures),
                         ('set_timer', 'reset_timer', 'write', 'writeln'))
Maxime Perrotin's avatar
Maxime Perrotin committed
835

Maxime Perrotin's avatar
Maxime Perrotin committed
836
837

# pylint: disable=R0904
838
class TextSymbol(HorizontalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
839
840
    ''' Text symbol - used to declare variables, etc. '''
    common_name = 'text_area'
Maxime Perrotin's avatar
Maxime Perrotin committed
841
    default_size = 'any'
Maxime Perrotin's avatar
Maxime Perrotin committed
842
843
844
845
846
847
848
849
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD

    def __init__(self, ast=None):
        ''' Create a Text Symbol '''
        ast = ast or ogAST.TextArea()
850
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
851
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
852
        super(TextSymbol, self).__init__(parent=None,
853
854
855
856
                                         text=ast.inputString,
                                         x=ast.pos_x or 0,
                                         y=ast.pos_y or 0,
                                         hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
857
858
859
        self.set_shape(ast.width, ast.height)
        self.setBrush(QBrush(QColor(249, 249, 249)))
        self.terminal_symbol = False
860
        self.position = QPointF(ast.pos_x or 0, ast.pos_y or 0)
Maxime Perrotin's avatar
Maxime Perrotin committed
861
862
863
864
865
866
        # Disable hyperlinks for Text symbols
        self._no_hyperlink = True
        # Text is not centered in the box - change default alignment:
        self.textbox_alignment = Qt.AlignLeft | Qt.AlignTop
        self.parser = ogParser

Maxime Perrotin's avatar
Maxime Perrotin committed
867
868
869
870
871
872
873
874
    def check_syntax(self, pr_text):
        ''' Redefinition of the check syntax function for the text symbol '''
        # Standard behaviour except that we permit the last character to be
        # a semi-colon, since that is always the case with declarations
        # and the text box cannot be followed by a COMMENT symbol
        return super().check_syntax(pr_text, check_last_semi=False);
        

Maxime Perrotin's avatar
Maxime Perrotin committed
875
    def update_completion_list(self, pr_text):
876
        ''' When text was entered, update list of variables/FPAR/Timers '''
877
878
879
880
        # note, on standalone systems, if the textbox contains a
        # USE Dataview comment 'file.asn'. this file is parsed when leaving
        # the textbox. This gives the impression that this function is slow,
        # it it is not! - no need to investigate performance issues here
Maxime Perrotin's avatar
Maxime Perrotin committed
881
        # Get AST for the symbol
Maxime Perrotin's avatar
Maxime Perrotin committed
882
        ast, _, _, _, _ = self.parser.parseSingleElement('text_area', pr_text)
883
884
885
        if not ast:
            # in case of syntax error in the symbol text
            return
886
887
888
889
        try:
            CONTEXT.variables.update(ast.variables)
            CONTEXT.timers = list(set(CONTEXT.timers + ast.timers))
        except AttributeError:
890
            # context may not have variables/timers (eg if context = block)
891
            pass
892
        try:
893
894
895
896
            existing = {proc.inputString.lower()
                       for proc in CONTEXT.procedures}
            CONTEXT.procedures += [proc for proc in ast.procedures
                                   if proc.inputString.lower() not in existing]
897
898
899
            CONTEXT.fpar.extend(ast.fpar)
        except AttributeError:
            pass
900
        # Update completion list of Signalroutes
901
902
903
        try:
            Signalroute.completion_list |= set(sig['name']
                                               for sig in ast.signals)
904
905
            # Here: update input signals of the process AST since the
            # signature of the signals may have changed...TODO
906
            CONTEXT.signals += ast.signals
907
        except AttributeError:
908
            # no AST, e.g. in case of syntax errors in the text area
909
            pass
Maxime Perrotin's avatar
Maxime Perrotin committed
910

Maxime Perrotin's avatar
Maxime Perrotin committed
911
912
913
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
914
        try:
915
            return set(AST.dataview.keys())
916
        except AttributeError:
917
            return [] # No Dataview
Maxime Perrotin's avatar
Maxime Perrotin committed
918

Maxime Perrotin's avatar
Maxime Perrotin committed
919
920
    def set_shape(self, width, height):
        ''' Define the polygon of the text symbol '''
Maxime Perrotin's avatar
Maxime Perrotin committed
921
922
923
924
925
926
927
928
929
930
931
932
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.moveTo(width - 10, 0)
            path.lineTo(0, 0)
            path.lineTo(0, height)
            path.lineTo(width, height)
            path.lineTo(width, 10)
            path.lineTo(width - 10, 10)
            path.lineTo(width - 10, 0)
            path.lineTo(width, 10)
            self.setPath(path)
            super(TextSymbol, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
933
934
935
936
937
938
939
940
941
942

    def resize_item(self, rect):
        ''' Text Symbol only resizes down or right '''
        if self.grabber.resize_mode.endswith('left'):
            return
        self.prepareGeometryChange()
        self.set_shape(rect.width(), rect.height())


# pylint: disable=R0904
943
class State(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
944
945
    ''' SDL STATE Symbol '''
    _unique_followers = ['Comment']
946
    _insertable_followers = ['Input', 'Connect', 'ContinuousSignal']
Maxime Perrotin's avatar
Maxime Perrotin committed
947
    arrow_head = 'simple'
Maxime Perrotin's avatar
Maxime Perrotin committed
948
949
950
951
952
    common_name = 'terminator_statement'
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
953
    context_name = "state"
Maxime Perrotin's avatar
Maxime Perrotin committed
954
955
956

    def __init__(self, parent=None, ast=None):
        ast = ast or ogAST.State()
957
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
958
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
959
        ast.inputString = getattr(ast, 'via', None) or ast.inputString
Maxime Perrotin's avatar
Maxime Perrotin committed
960
        super(State, self).__init__(parent=parent,
961
962
963
964
                                    text=ast.inputString,
                                    x=ast.pos_x or 0,
                                    y=ast.pos_y or 0,
                                    hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
965
966
967
968
969
970
        self.set_shape(ast.width, ast.height)
        self.setBrush(QBrush(QColor(255, 228, 213)))
        self.terminal_symbol = True
        if parent:
            try:
                # Map AST scene coordinates to get actual position
971
972
                self.position += self.mapFromScene(ast.pos_x or 0,
                                                   ast.pos_y or 0)
Maxime Perrotin's avatar
Maxime Perrotin committed
973
974
975
976
            except TypeError:
                self.update_position()
        else:
            # Use scene coordinates to position
977
            self.position = QPointF(ast.pos_x or 0, ast.pos_y or 0)
Maxime Perrotin's avatar
Maxime Perrotin committed
978
979
980
981
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)

982
983
984
    @property
    def allow_nesting(self):
        ''' Redefinition - must be checked according to context '''
Maxime Perrotin's avatar
Maxime Perrotin committed
985
        result = not any(elem in str(self).lower().strip()
Maxime Perrotin's avatar
Maxime Perrotin committed
986
                       for elem in ('-', ',', '*'))
987
988
989
990
991
992
993
        return result

    @property
    def nested_scene(self):
        ''' Redefined - nested scene per state must be unique '''
        return self._nested_scene

Maxime Perrotin's avatar
Maxime Perrotin committed
994
995
    def double_click(self):
        ''' Catch a double click - Set nested scene '''
Maxime Perrotin's avatar
Maxime Perrotin committed
996
997
        for each, value in self.scene().composite_states.items():
            if str(self).split()[0].lower() == str(each):
Maxime Perrotin's avatar
Maxime Perrotin committed
998
999
1000
                self.nested_scene = value
                break
        else:
For faster browsing, not all history is shown. View entire blame