sdlSymbols.py 55.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
21
#!/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.

    Copyright (c) 2012-2013 European Space Agency

    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
227
228
229
230
231
232
        if width != self.width or height != self.height:
            self.setPen(QPen(Qt.blue))
            self.textbox_alignment = Qt.AlignLeft | Qt.AlignTop
            path = QPainterPath()
            path.moveTo(0, 0)
            path.lineTo(0, height)
            #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
    def update_completion_list(self, pr_text):
868
        ''' When text was entered, update list of variables/FPAR/Timers '''
869
870
871
872
        # 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
873
        # Get AST for the symbol
Maxime Perrotin's avatar
Maxime Perrotin committed
874
        ast, _, _, _, _ = self.parser.parseSingleElement('text_area', pr_text)
875
876
877
        if not ast:
            # in case of syntax error in the symbol text
            return
878
879
880
881
        try:
            CONTEXT.variables.update(ast.variables)
            CONTEXT.timers = list(set(CONTEXT.timers + ast.timers))
        except AttributeError:
882
            # context may not have variables/timers (eg if context = block)
883
            pass
884
        try:
885
886
887
888
            existing = {proc.inputString.lower()
                       for proc in CONTEXT.procedures}
            CONTEXT.procedures += [proc for proc in ast.procedures
                                   if proc.inputString.lower() not in existing]
889
890
891
            CONTEXT.fpar.extend(ast.fpar)
        except AttributeError:
            pass
892
        # Update completion list of Signalroutes
893
894
895
        try:
            Signalroute.completion_list |= set(sig['name']
                                               for sig in ast.signals)
896
897
            # Here: update input signals of the process AST since the
            # signature of the signals may have changed...TODO
898
            CONTEXT.signals += ast.signals
899
        except AttributeError:
900
            # no AST, e.g. in case of syntax errors in the text area
901
            pass
Maxime Perrotin's avatar
Maxime Perrotin committed
902

Maxime Perrotin's avatar
Maxime Perrotin committed
903
904
905
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
906
        try:
907
            return set(AST.dataview.keys())
908
        except AttributeError:
909
            return [] # No Dataview
Maxime Perrotin's avatar
Maxime Perrotin committed
910

Maxime Perrotin's avatar
Maxime Perrotin committed
911
912
    def set_shape(self, width, height):
        ''' Define the polygon of the text symbol '''
Maxime Perrotin's avatar
Maxime Perrotin committed
913
914
915
916
917
918
919
920
921
922
923
924
        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
925
926
927
928
929
930
931
932
933
934

    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
935
class State(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
936
937
    ''' SDL STATE Symbol '''
    _unique_followers = ['Comment']
938
    _insertable_followers = ['Input', 'Connect', 'ContinuousSignal']
Maxime Perrotin's avatar
Maxime Perrotin committed
939
    arrow_head = 'simple'
Maxime Perrotin's avatar
Maxime Perrotin committed
940
941
942
943
944
    common_name = 'terminator_statement'
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
945
    context_name = "state"
Maxime Perrotin's avatar
Maxime Perrotin committed
946
947
948

    def __init__(self, parent=None, ast=None):
        ast = ast or ogAST.State()
949
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
950
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
951
        ast.inputString = getattr(ast, 'via', None) or ast.inputString
Maxime Perrotin's avatar
Maxime Perrotin committed
952
        super(State, self).__init__(parent=parent,
953
954
955
956
                                    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
957
958
959
960
961
962
        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
963
964
                self.position += self.mapFromScene(ast.pos_x or 0,
                                                   ast.pos_y or 0)
Maxime Perrotin's avatar
Maxime Perrotin committed
965
966
967
968
            except TypeError:
                self.update_position()
        else:
            # Use scene coordinates to position
969
            self.position = QPointF(ast.pos_x or 0, ast.pos_y or 0)
Maxime Perrotin's avatar
Maxime Perrotin committed
970
971
972
973
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)

974
975
976
    @property
    def allow_nesting(self):
        ''' Redefinition - must be checked according to context '''
Maxime Perrotin's avatar
Maxime Perrotin committed
977
        result = not any(elem in str(self).lower().strip()
Maxime Perrotin's avatar
Maxime Perrotin committed
978
                       for elem in ('-', ',', '*'))
979
980
981
982
983
984
985
        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
986
987
    def double_click(self):
        ''' Catch a double click - Set nested scene '''
Maxime Perrotin's avatar
Maxime Perrotin committed
988
989
        for each, value in self.scene().composite_states.items():
            if str(self).split()[0].lower() == str(each):
Maxime Perrotin's avatar
Maxime Perrotin committed
990
991
992
993
994
                self.nested_scene = value
                break
        else:
            self.nested_scene = None

995
996
997
998
999
    @nested_scene.setter
    def nested_scene(self, value):
        ''' Set the value of the nested scene '''
        self._nested_scene = value

Maxime Perrotin's avatar
Maxime Perrotin committed
1000
    def update_completion_list(self, pr_text):
Maxime Perrotin's avatar
Maxime Perrotin committed
1001
        ''' When text was entered, update state completion list '''
1002
        # Get AST for the symbol and update the context dictionnary
Maxime Perrotin's avatar
Maxime Perrotin committed
1003
        ast, _, _, _, _ = self.parser.parseSingleElement('state', pr_text)
1004
1005
1006
1007
        if ast:
            # None if there were syntax errors in the symbol
            for each in ast.statelist:
                CONTEXT.mapping[each.lower()] = None
Maxime Perrotin's avatar
Maxime Perrotin committed
1008

Maxime Perrotin's avatar
Maxime Perrotin committed
1009
1010
1011
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1012
        elems = str(self).lower().strip().split()
1013
1014
1015
1016
1017
1018
1019
1020
        if len(elems) == 2 and elems[1] == 'via':
            # Get list of entry point of the nested state
            statename = elems[0]
            for each in CONTEXT.composite_states:
                if each.statename == statename:
                    return each.state_entrypoints
        else:
            return set(state for state in CONTEXT.mapping if state != 'START')
Maxime Perrotin's avatar
Maxime Perrotin committed
1021
1022
1023

    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1024
1025
1026
        if self.nested_scene and self.is_composite():
            # Distinguish composite states with dash line
            self.setPen(QPen(Qt.DashLine))
Maxime Perrotin's avatar
Maxime Perrotin committed
1027
        else:
Maxime Perrotin's avatar
Maxime Perrotin committed
1028
            self.setPen(QPen(Qt.SolidLine))
Maxime Perrotin's avatar
Maxime Perrotin committed
1029
1030
1031
1032
1033
1034

        if width != self.width or height != self.height:
            path = QPainterPath()
            path.addRoundedRect(0, 0, width, height, height / 4, height)
            self.setPath(path)
            super(State, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
1035

Maxime Perrotin's avatar
Maxime Perrotin committed
1036
    def get_ast(self, pr_text):
Maxime Perrotin's avatar
Maxime Perrotin committed
1037
        ''' Redefinition of the get_ast function for the state '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1038
1039
1040
        ast, _, _, _, terminators = self.parser.parseSingleElement('state',
                                                                   pr_text)
        return ast, terminators
1041

1042
1043
1044
1045
1046
1047
    def check_syntax(self, pr_text):
        ''' Redefinition of the check syntax function for the state '''
        name = self.common_name if self.hasParent else 'state'
        _, err, _, _, _ = \
                self.parser.parseSingleElement(name, pr_text)
        return err
Maxime Perrotin's avatar
Maxime Perrotin committed
1048
1049


1050
1051
1052
1053
class Process(HorizontalSymbol):
    ''' Process symbol '''
    _unique_followers = ['Comment']
    _allow_nesting = True
Maxime Perrotin's avatar
Maxime Perrotin committed
1054
    default_size = 'any'
1055
1056
1057
1058
1059
1060
    common_name = 'process_definition'
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
    completion_list = set()
1061
    is_singleton = True
Maxime Perrotin's avatar
Maxime Perrotin committed
1062
1063
    arrow_head = 'angle'
    arrow_tail = 'angle'
1064
1065
    # Process can be connected to other processes by the user
    user_can_connect = True
1066
1067
    _conn_sources = ['Process']
    _conn_targets = ['Process']
1068
    context_name = "process"
1069
1070
1071

    def __init__(self, ast=None, subscene=None):
        ast = ast or ogAST.Process()
1072
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
1073
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
1074
1075
1076
        label = (ast.processName or "") + (': {}'
                                            .format(ast.instance_of_name)
                                            if ast.instance_of_name else '')
Maxime Perrotin's avatar
Maxime Perrotin committed
1077
1078
1079
1080
1081
        # At creation, call the init of Horizontal symbol, which creates
        # the TextInteraction instance, which calls try_resize, which
        # makes a call to resize_item, which calls set_shape using a size
        # defined by the label. set shape will then be called again using
        # the ast-defined size.
1082
        super(Process, self).__init__(parent=None,
1083
                                      text=label,
1084
1085
1086
1087
1088
1089
1090
1091
1092
                                      x=ast.pos_x,
                                      y=ast.pos_y,
                                      hyperlink=ast.hyperlink)
        self.set_shape(ast.width, ast.height)
        self.setBrush(QBrush(QColor(255, 255, 202)))
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)
        self.nested_scene = subscene
1093
1094
1095
        self.input_signals = ast.input_signals
        self.output_signals = ast.output_signals
        self.insert_symbol(None, self.x(), self.y())
1096

1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
    @property
    def conn_start_zones(self):
        ''' Redefined - define the zones in the symbol from which user can
        start a connection with another symbol '''
        rect = self.boundingRect()
        yield QRect(15, 5, rect.width() - 30, 10)
        yield QRect(5, 5, 10, rect.height() - 10)
        yield QRect(rect.width() - 15, 5, 10, rect.height() - 10)
        yield QRect(15, rect.height() - 15, rect.width() - 30, 10)

    @property
    def conn_end_zones(self):
        ''' Redefined - define the zones that can receive a connection '''
        rect = self.boundingRect()
        yield QRect(15, 5, rect.width() - 30, 10)
        yield QRect(5, 5, 10, rect.height() - 10)
        yield QRect(rect.width() - 15, 5, 10, rect.height() - 10)
        yield QRect(15, rect.heigth() - 15, rect.width() - 30, 10)

Maxime Perrotin's avatar
Maxime Perrotin committed
1116
1117
1118
    def insert_symbol(self, parent, x, y):
        ''' Redefinition - adds connection line to env '''
        super(Process, self).insert_symbol(parent, x, y)
1119
1120
        if not self.connection:
            self.connection = self.connect_to_parent()
Maxime Perrotin's avatar
Maxime Perrotin committed
1121
1122

    def connect_to_parent(self):
1123
1124
        ''' Redefinition: creates connection to env with a signalroute '''
        return Signalroute(self)
Maxime Perrotin's avatar
Maxime Perrotin committed
1125

1126
1127
    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
        #LOG.debug(traceback.print_stack())
        if width != self.width or height != self.height:
            # Don't compute a new path if size has not changed
            path = QPainterPath()
            path.moveTo(7, 0)
            path.lineTo(0, 7)
            path.lineTo(0, height - 7)
            path.lineTo(7, height)
            path.lineTo(width - 7, height)
            path.lineTo(width, height - 7)
            path.lineTo(width, 7)
            path.lineTo(width - 7, 0)
            path.lineTo(7, 0)
            self.setPath(path)
            super(Process, self).set_shape(width, height)
1143

1144
1145
1146
    def update_completion_list(self, pr_text):
        ''' When text was entered, update completion list at block level '''
        for each in CONTEXT.processes:
Maxime Perrotin's avatar
Maxime Perrotin committed
1147
            if str(self.text).lower() == each.processName:
1148
1149
1150
                break
        else:
            new_proc = ogAST.Process()
Maxime Perrotin's avatar
Maxime Perrotin committed
1151
            new_proc.processName = str(self.text).lower()
1152
1153
            CONTEXT.processes.append(new_proc)

1154
1155
1156

class Procedure(Process):
    ''' Procedure declaration symbol - Very similar to Process '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1157
    _unique_followers = ['Comment']
1158
    _allow_nesting = True
Maxime Perrotin's avatar
Maxime Perrotin committed
1159
1160
1161
1162
1163
1164
    common_name = 'procedure'
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
    completion_list = set()
1165
    is_singleton = False
1166
    user_can_connect = False
1167
    context_name = "procedure"
Maxime Perrotin's avatar
Maxime Perrotin committed
1168
1169
1170

    def __init__(self, ast=None, subscene=None):
        ast = ast or ogAST.Procedure()
1171
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
1172
        self.width, self.height = 0, 0
1173
        super(Process, self).__init__(parent=None,
1174
1175
1176
1177
                                      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
1178
1179
1180
1181
1182
1183
        self.set_shape(ast.width, ast.height)
        self.setBrush(QBrush(QColor(255, 255, 202)))
        self.parser = ogParser
        if ast.comment:
            Comment(parent=self, ast=ast.comment)
        self.nested_scene = subscene
Maxime Perrotin's avatar
Maxime Perrotin committed
1184

Maxime Perrotin's avatar
Maxime Perrotin committed
1185
1186
1187
    def insert_symbol(self, parent, x, y):
        ''' Redefinition - no connection line to env '''
        super(Process, self).insert_symbol(parent, x, y)
Maxime Perrotin's avatar
Maxime Perrotin committed
1188
1189
1190

    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
        if width != self.width or height != self.height:
            path = QPainterPath()
            path.addRect(7, 0, width - 14, height)
            path.moveTo(7, 0)
            path.lineTo(0, 7)
            path.lineTo(0, height - 7)
            path.lineTo(7, height)
            path.moveTo(width - 7, 0)
            path.lineTo(width, 7)
            path.lineTo(width, height - 7)
            path.lineTo(width - 7, height)
            self.setPath(path)
            super(Process, self).set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
1204

Maxime Perrotin's avatar
Maxime Perrotin committed
1205
    def update_completion_list(self, pr_text):
Maxime Perrotin's avatar
Maxime Perrotin committed
1206
        ''' When text was entered, update completion list of ProcedureCall '''
1207
        for each in CONTEXT.procedures:
Maxime Perrotin's avatar
Maxime Perrotin committed
1208
            if str(self.text).lower() == each.inputString:
1209
1210
1211
                break
        else:
            new_proc = ogAST.Procedure()
Maxime Perrotin's avatar
Maxime Perrotin committed
1212
            new_proc.inputString = str(self.text).lower()
1213
1214
            CONTEXT.procedures.append(new_proc)

1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
class ProcessType(Procedure):
    ''' PROCESS TYPE (floating symbol with no connections '''
    _unique_followers = ['Comment']
    _allow_nesting = True
    common_name = 'process_definition'
    context_name = 'process'
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
    completion_list = set()
    is_singleton = False
    user_can_connect = False