sdlSymbols.py 56.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
                'EXPORTED', 'REFERENCED', 'MONITOR',
Maxime Perrotin's avatar
Maxime Perrotin committed
55
                'NEWTYPE', 'ENDNEWTYPE', 'ARRAY', 'STRUCT', 'SYNONYM')]
Maxime Perrotin's avatar
Maxime Perrotin committed
56
57

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


63
64
65
66
67
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
68
69
70
71
72
73
    if not symbol.text:
        return res
    parts = symbol.text.context.split('!')
    if len(parts) == 0:
        return res
    elif len(parts) == 1:
74
75
76
77
78
        try:
            fpar = {fp['name']: (fp['type'], None) for fp in CONTEXT.fpar}
        except AttributeError:
            # not in the context of a procedure
            fpar = {}
79
80
        # Return the list of variables, possibly filterd by type
        if not type_filter:
Maxime Perrotin's avatar
Maxime Perrotin committed
81
82
83
84
            res = set( list(CONTEXT.variables.keys())
                      + list(CONTEXT.global_variables.keys())
                      + list(AST.asn1_constants.keys())
                      + list(fpar.keys()))
85
86
        else:
            constants = {name: (cty.type, None)
Maxime Perrotin's avatar
Maxime Perrotin committed
87
                         for name, cty in AST.asn1_constants.items()}
Maxime Perrotin's avatar
Maxime Perrotin committed
88
89
90
91
92
93
94
95
96
97
            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
98
99
100
101
            for name, (asn1type, _) in chain(CONTEXT.variables.items(),
                                          CONTEXT.global_variables.items(),
                                          constants.items(),
                                          fpar.items()):
102
103
                if ogParser.type_name(asn1type) in type_filter_names:
                    res.add(name)
Maxime Perrotin's avatar
Maxime Perrotin committed
104
105
106
107
108
109
110
111
112
113
114
    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
115
                    for child, childtype in basic.Children.items():
Maxime Perrotin's avatar
Maxime Perrotin committed
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
                        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
137
# pylint: disable=R0904
138
class Input(HorizontalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
139
140
141
    ''' SDL INPUT Symbol '''
    _unique_followers = ['Comment']
    _insertable_followers = ['Task', 'ProcedureCall', 'Output', 'Decision',
142
                             'Input', 'Label', 'Connect', 'ContinuousSignal']
Maxime Perrotin's avatar
Maxime Perrotin committed
143
144
145
146
147
148
149
150
151
152
    _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()
153
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
154
        self.branch_entrypoint = None
Maxime Perrotin's avatar
Maxime Perrotin committed
155
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
156
157
158
        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
159
        super().__init__(parent,
Maxime Perrotin's avatar
Maxime Perrotin committed
160
161
162
163
                                    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
164
165
166
167
168
169
170
171
172
173
174
175
        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 '''
176
177
178
        # 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
179
180
                       else parent.parentItem())
        self.branch_entrypoint = item_parent.branch_entrypoint
181
        super().insert_symbol(item_parent, x, y)
Maxime Perrotin's avatar
Maxime Perrotin committed
182

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


Maxime Perrotin's avatar
Maxime Perrotin committed
187
188
    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
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)
197
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
198

Maxime Perrotin's avatar
Maxime Perrotin committed
199
200
201
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
202
        if '(' in str(self):
203
            # Input parameter: return the list of variables of this type
Maxime Perrotin's avatar
Maxime Perrotin committed
204
            input_name = str(self).split('(')[0].strip().lower()
Maxime Perrotin's avatar
Maxime Perrotin committed
205
            asn1_filter = [sig.get('type') for sig in CONTEXT.input_signals if
206
207
                           sig['name'] == input_name]
            return variables_autocompletion(self, asn1_filter)
Maxime Perrotin's avatar
Maxime Perrotin committed
208
        else:
209
210
211
            # 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
212

Maxime Perrotin's avatar
Maxime Perrotin committed
213

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

Maxime Perrotin's avatar
Maxime Perrotin committed
222
223
    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
224
225
226
227
        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
228
229
            path.moveTo(width / 2, 0)
            path.lineTo(width / 2, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
230
231
232
233
            #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
234
235
236
237
238

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

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

249

Maxime Perrotin's avatar
Maxime Perrotin committed
250
# pylint: disable=R0904
251
class Output(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
252
253
254
255
256
257
258
259
260
261
262
263
    ''' 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()
264
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
265
        self.width, self.height = 0, 0
266
        super().__init__(parent=parent,
267
                text=ast.inputString, x=ast.pos_x or 0, y=ast.pos_y or 0,
Maxime Perrotin's avatar
Maxime Perrotin committed
268
269
270
271
272
273
274
275
276
277
278
                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
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)
287
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
288

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

Maxime Perrotin's avatar
Maxime Perrotin committed
302
303

# pylint: disable=R0904
304
class Decision(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
305
306
    ''' SDL DECISION Symbol '''
    _unique_followers = ['Comment']
Maxime Perrotin's avatar
Maxime Perrotin committed
307
308
    _insertable_followers = ['DecisionAnswer', 'Task', 'ProcedureCall',
                             'Output', 'Decision', 'Label']
Maxime Perrotin's avatar
Maxime Perrotin committed
309
310
311
312
313
314
315
316
317
    _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()
318
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
319
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
320
321
        # Define the point where all branches of the decision can join again
        self.connectionPoint = QPoint(ast.width / 2, ast.height + 30)
322
        super().__init__(parent, text=ast.inputString,
323
                x=ast.pos_x or 0, y=ast.pos_y or 0, hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
        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
341
        return True
Maxime Perrotin's avatar
Maxime Perrotin committed
342

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

Maxime Perrotin's avatar
Maxime Perrotin committed
348
349
350
351
352
353
354
    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
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)
363
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
364
365
366
367

    def resize_item(self, rect):
        ''' On resize event, make sure connection points are updated '''
        delta_y = self.boundingRect().height() - rect.height()
368
        super().resize_item(rect)
Maxime Perrotin's avatar
Maxime Perrotin committed
369
370
371
372
373
374
        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 '''
375
        super().update_connections()
Maxime Perrotin's avatar
Maxime Perrotin committed
376
377
378
        for branch in self.branches():
            for cnx in branch.last_branch_item.connections():
                cnx.reshape()
379
        self.updateConnectionPointPosition()
Maxime Perrotin's avatar
Maxime Perrotin committed
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395

    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)))
396
                # Don't set parent item to None to avoid Qt segfault
397
398
399
400
401
402
403
404
405
                # 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)
406
                last_cnx.setParentItem(self)
Maxime Perrotin's avatar
Maxime Perrotin committed
407
408
409
410
411
412
            except ValueError:
                pass
            branch_len = branch.y() + (
                    branch.boundingRect() |
                    branch.childrenBoundingRect()).height()
            try:
413
414
                if last.scene() != last_cnx.scene():
                    last.scene().addItem(last_cnx) # workaround Qt's bug 18616
Maxime Perrotin's avatar
Maxime Perrotin committed
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
                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:
436
                child.pos_y += delta
Maxime Perrotin's avatar
Maxime Perrotin committed
437
438
            except AttributeError:
                pass
439
        #self.update_connections()
Maxime Perrotin's avatar
Maxime Perrotin committed
440
441
442


# pylint: disable=R0904
443
class DecisionAnswer(HorizontalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
444
445
446
447
448
449
450
451
452
453
454
    ''' 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()
455
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
456
        self.width, self.height = 0, 0 #ast.width, ast.height
Maxime Perrotin's avatar
Maxime Perrotin committed
457
458
459
460
        self.terminal_symbol = False
        # last_branch_item is used to compute branch length
        # for the connection point positionning
        self.last_branch_item = self
461
462
463
464
465
        super().__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
466
467
468
469
470
471
472
473
        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
474
        # Make sure that parent is not a sibling answer
Maxime Perrotin's avatar
Maxime Perrotin committed
475
476
        item_parent = (parent if not isinstance(parent, DecisionAnswer)
                       else parent.parentItem())
477
        super().insert_symbol(item_parent, x, y)
Maxime Perrotin's avatar
Maxime Perrotin committed
478
479
        self.last_branch_item.connectionBelow = \
                JoinConnection(self.last_branch_item, item_parent)
Maxime Perrotin's avatar
Maxime Perrotin committed
480
        self.text.try_resize()
Maxime Perrotin's avatar
Maxime Perrotin committed
481
482
483
484
485
486

    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
487
488
489
490
491
492
493
494
495
496
497
        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)
498
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
499

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

Maxime Perrotin's avatar
Maxime Perrotin committed
505
506

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

    def __init__(self, parent=None, ast=None):
517
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
518
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
519
        if not ast:
520
            ast = ogAST.Terminator(defName='')
Maxime Perrotin's avatar
Maxime Perrotin committed
521
522
523
            ast.pos_y = 0
            ast.width = 35
            ast.height = 35
524
        super().__init__(parent,
525
526
527
528
                                   text=ast.inputString,
                                   x=ast.pos_x,
                                   y=ast.pos_y,
                                   hyperlink=ast.hyperlink)
Maxime Perrotin's avatar
Maxime Perrotin committed
529
530
531
532
533
534
535
536
537
538
        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)
539
        super().resize_item(rect)
Maxime Perrotin's avatar
Maxime Perrotin committed
540
541
542

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

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

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

Maxime Perrotin's avatar
Maxime Perrotin committed
569

570
class ProcedureStop(Join):
Maxime Perrotin's avatar
Maxime Perrotin committed
571
572
573
574
    ''' 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
575

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

    def set_shape(self, width, height):
        ''' Define the symbol shape '''
Maxime Perrotin's avatar
Maxime Perrotin committed
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
        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
603

Maxime Perrotin's avatar
Maxime Perrotin committed
604
605
606
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
607
608
609
610
611
612
613
614
615
616
        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)
617
618
619
        if not ast:
            # in case of syntax error in the symbol text
            return
620
        try:
621
            CONTEXT.state_exitpoints.add(str(self))
622
623
624
        except AttributeError:
            # No state exit points in a procedure
            pass
Maxime Perrotin's avatar
Maxime Perrotin committed
625

Maxime Perrotin's avatar
Maxime Perrotin committed
626
627

# pylint: disable=R0904
628
class Label(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
629
630
631
632
633
634
635
636
637
638
639
640
641
    ''' 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()
642
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
643
        self.width, self.height = 0, 0
644
        super().__init__(parent,
645
646
647
648
                                    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
649
650
651
652
653
654
655
656
657
658
659
660
        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
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)
676
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
677

678
679
680
681
682
683
684
685
686
687
    @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)
688
689
690
        if not ast:
            # in case of syntax error in the symbol text
            return
691
        for each in CONTEXT.labels:
Maxime Perrotin's avatar
Maxime Perrotin committed
692
            if each.inputString == str(self):
693
694
695
696
697
                # Ignore if already defined
                break
        else:
            CONTEXT.labels.append(ast)

Maxime Perrotin's avatar
Maxime Perrotin committed
698
699

# pylint: disable=R0904
700
class Task(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
701
702
703
704
705
706
707
708
709
710
711
712
713
    ''' 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()
714
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
715
        self.width, self.height = 0, 0
716
        super().__init__(parent,
717
718
719
720
                                   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
721
722
723
724
725
726
727
728
729
        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
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)
737
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
738

739
740
    @property
    def completion_list(self):
Maxime Perrotin's avatar
Maxime Perrotin committed
741
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
742
        elems = str(self).lower().strip().split()
743
744
745
746
747
748
749
750
751
752
        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
753
754
755
756
757
                         for name, cty in AST.asn1_constants.items()}
            for name, (asn1ty, _) in chain (CONTEXT.variables.items(),
                                          CONTEXT.global_variables.items(),
                                          constants.items(),
                                          fpar.items()):
758
759
760
761
                if name == varname:
                    asn1_filter = [asn1ty]
                    break
        return chain(variables_autocompletion(self, asn1_filter),
Maxime Perrotin's avatar
Maxime Perrotin committed
762
                     ogParser.SPECIAL_OPERATORS.keys())
Maxime Perrotin's avatar
Maxime Perrotin committed
763
764

# pylint: disable=R0904
765
class ProcedureCall(VerticalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
766
767
768
769
770
771
772
773
774
775
776
777
    ''' 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):
778
        ast = ast or ogAST.Output(defName='')
779
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
780
        self.width, self.height = 0, 0
781
        super().__init__(parent,
782
783
784
785
                                            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
786
787
788
789
790
791
792
793
794
        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
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)
803
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
804

Maxime Perrotin's avatar
Maxime Perrotin committed
805
806
807
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
808
        if '(' in str(self):
809
            # Get the variables of the type of the current parameter
Maxime Perrotin's avatar
Maxime Perrotin committed
810
811
            count = str(self).count(',')
            procname = str(self).split('(')[0].strip().lower()
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
            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
836

Maxime Perrotin's avatar
Maxime Perrotin committed
837
838

# pylint: disable=R0904
839
class TextSymbol(HorizontalSymbol):
Maxime Perrotin's avatar
Maxime Perrotin committed
840
841
    ''' Text symbol - used to declare variables, etc. '''
    common_name = 'text_area'
Maxime Perrotin's avatar
Maxime Perrotin committed
842
    default_size = 'any'
Maxime Perrotin's avatar
Maxime Perrotin committed
843
844
845
846
847
848
849
850
    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()
851
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
852
        self.width, self.height = 0, 0
853
        super().__init__(parent=None,
854
855
856
857
                                         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
858
859
860
        self.set_shape(ast.width, ast.height)
        self.setBrush(QBrush(QColor(249, 249, 249)))
        self.terminal_symbol = False
861
        self.position = QPointF(ast.pos_x or 0, ast.pos_y or 0)
Maxime Perrotin's avatar
Maxime Perrotin committed
862
863
864
865
866
867
        # 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
868
869
870
871
872
873
    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);
874

Maxime Perrotin's avatar
Maxime Perrotin committed
875

Maxime Perrotin's avatar
Maxime Perrotin committed
876
    def update_completion_list(self, pr_text):
877
        ''' When text was entered, update list of variables/FPAR/Timers '''
878
879
880
881
        # 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
882
        # Get AST for the symbol
Maxime Perrotin's avatar
Maxime Perrotin committed
883
        ast, _, _, _, _ = self.parser.parseSingleElement('text_area', pr_text)
884
885
886
        if not ast:
            # in case of syntax error in the symbol text
            return
887
888
889
890
        try:
            CONTEXT.variables.update(ast.variables)
            CONTEXT.timers = list(set(CONTEXT.timers + ast.timers))
        except AttributeError:
891
            # context may not have variables/timers (eg if context = block)
892
            pass
893
        try:
894
895
896
897
            existing = {proc.inputString.lower()
                       for proc in CONTEXT.procedures}
            CONTEXT.procedures += [proc for proc in ast.procedures
                                   if proc.inputString.lower() not in existing]
898
899
900
            CONTEXT.fpar.extend(ast.fpar)
        except AttributeError:
            pass
901
        # Update completion list of Signalroutes
902
903
904
        try:
            Signalroute.completion_list |= set(sig['name']
                                               for sig in ast.signals)
905
906
            # Here: update input signals of the process AST since the
            # signature of the signals may have changed...TODO
907
            CONTEXT.signals += ast.signals
908
        except AttributeError:
909
            # no AST, e.g. in case of syntax errors in the text area
910
            pass
Maxime Perrotin's avatar
Maxime Perrotin committed
911

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

Maxime Perrotin's avatar
Maxime Perrotin committed
920
921
    def set_shape(self, width, height):
        ''' Define the polygon of the text symbol '''
Maxime Perrotin's avatar
Maxime Perrotin committed
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)
933
            super().set_shape(width, height)
Maxime Perrotin's avatar
Maxime Perrotin committed
934
935
936
937
938
939
940
941
942
943

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

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

983
984
985
    @property
    def allow_nesting(self):
        ''' Redefinition - must be checked according to context '''
Maxime Perrotin's avatar
Maxime Perrotin committed
986
        # nesting permitted only if single plain state
Maxime Perrotin's avatar
Maxime Perrotin committed
987
        result = not any(elem in str(self).lower().strip()
Maxime Perrotin's avatar
Maxime Perrotin committed
988
                for elem in ('-', ',', '*', ':', 'via'))
989
990
991
992
993
994
995
        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
996
997
    def double_click(self):
        ''' Catch a double click - Set nested scene '''
Maxime Perrotin's avatar
Maxime Perrotin committed
998
999
        for each, value in self.scene().composite_states.items():
            if str(self).split()[0].lower() == str(each):
Maxime Perrotin's avatar
Maxime Perrotin committed
1000
1001
1002
1003
1004
                self.nested_scene = value
                break
        else:
            self.nested_scene = None

1005
1006
1007
1008
1009
    @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
1010
    def update_completion_list(self, pr_text):
Maxime Perrotin's avatar
Maxime Perrotin committed
1011
        ''' When text was entered, update state completion list '''
1012
        # Get AST for the symbol and update the context dictionnary
Maxime Perrotin's avatar
Maxime Perrotin committed
1013
        ast, _, _, _, _ = self.parser.parseSingleElement('state', pr_text)
1014
1015
1016
1017
        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
1018

Maxime Perrotin's avatar
Maxime Perrotin committed
1019
1020
1021
    @property
    def completion_list(self):
        ''' Set auto-completion list '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1022
        elems = str(self).lower().strip().split()
1023
1024
        if len(elems) == 2 and elems[1] == 'via':
            # Get list of entry point of the nested state
1025
1026
1027
1028
1029
1030
            # check if it is an instance
            name = elems[0].split(':')
            if len(name) == 2:
                statename = name[1]
            else:
                statename = name[0]
1031
1032
1033
            for each in CONTEXT.composite_states:
                if each.statename == statename:
                    return each.state_entrypoints
1034
1035
            # the entry points may not be defined yet
            return set()
1036
1037
        else:
            return set(state for state in CONTEXT.mapping if state != 'START')
Maxime Perrotin's avatar
Maxime Perrotin committed
1038
1039
1040

    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1041
1042
1043
        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
1044
        else:
Maxime Perrotin's avatar
Maxime Perrotin committed
1045
            self.setPen(QPen(Qt.SolidLine))
Maxime Perrotin's avatar
Maxime Perrotin committed
1046
1047
1048
1049
1050

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

Maxime Perrotin's avatar
Maxime Perrotin committed
1053
    def get_ast(self, pr_text):
Maxime Perrotin's avatar
Maxime Perrotin committed
1054
        ''' Redefinition of the get_ast function for the state '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1055
1056
1057
        ast, _, _, _, terminators = self.parser.parseSingleElement('state',
                                                                   pr_text)
        return ast, terminators
1058

1059
1060
1061
1062
1063
1064
    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
1065
1066


1067
1068
1069
1070
class Process(HorizontalSymbol):
    ''' Process symbol '''
    _unique_followers = ['Comment']
    _allow_nesting = True
Maxime Perrotin's avatar
Maxime Perrotin committed
1071
    default_size = 'any'
1072
1073
1074
1075
1076
1077
    common_name = 'process_definition'
    needs_parent = False
    # Define reserved keywords for the syntax highlighter
    blackbold = SDL_BLACKBOLD
    redbold = SDL_REDBOLD
    completion_list = set()
Maxime Perrotin's avatar
Maxime Perrotin committed
1078
    is_singleton = True #(False to allow multiple processes)
Maxime Perrotin's avatar
Maxime Perrotin committed
1079
1080
    arrow_head = 'angle'
    arrow_tail = 'angle'
1081
1082
    # Process can be connected to other processes by the user
    user_can_connect = True
1083
1084
    _conn_sources = ['Process']
    _conn_targets = ['Process']
1085
    context_name = "process"
1086

1087
1088
1089
    def __init__(self,
                 ast=None,
                 subscene=None):
1090
        ast = ast or ogAST.Process()
1091
        self.ast = ast
Maxime Perrotin's avatar
Maxime Perrotin committed
1092
        self.width, self.height = 0, 0
Maxime Perrotin's avatar
Maxime Perrotin committed
1093
1094
1095
        label = (ast.processName or "") + (': {}'
                                            .format(ast.instance_of_name)
                                            if ast.instance_of_name else '')
Maxime Perrotin's avatar
Maxime Perrotin committed
1096
1097
1098
1099
1100
        # 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.
1101
1102
1103
1104
1105
        super().__init__(parent=None,
                         text=label,
                         x=ast.pos_x,
                         y=ast.pos_y,
                         hyperlink=ast.hyperlink)
1106
1107
1108
1109
1110
1111
        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
1112
1113
1114
        self.input_signals = ast.input_signals
        self.output_signals = ast.output_signals
        self.insert_symbol(None, self.x(), self.y())
1115

1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
    @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
1135
1136
    def insert_symbol(self, parent, x, y):
        ''' Redefinition - adds connection line to env '''
1137
        super().insert_symbol(parent, x, y)
1138
1139
        if not self.connection:
            self.connection = self.connect_to_parent()
Maxime Perrotin's avatar
Maxime Perrotin committed
1140
1141

    def connect_to_parent(self):
1142
1143
        ''' Redefinition: creates connection to env with a signalroute '''
        return Signalroute(self)
Maxime Perrotin's avatar
Maxime Perrotin committed
1144

1145
1146
    def set_shape(self, width, height):
        ''' Compute the polygon to fit in width, height '''
Maxime Perrotin's avatar
Maxime Perrotin committed
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
        #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)
1161
            super().set_shape(width, height)
1162

Maxime Perrotin's avatar
Maxime Perrotin committed
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
    def cam_group(self):
        ''' Redefine the graphical boundaries of the item to apply the CAM
            If process has child connections (connections to other processes)
            the CAM group should only include the process block itself,
            not all the lines around it '''
        return self.sceneBoundingRect()

    def mouse_move(self, event):
        ''' In addition to default behaviour: update channel connections '''
        #super().mouse_move(event)
        if self.mode == 'Move':
            event_pos = event.pos()
            new_y = self.pos_y + (event_pos.y() - event.lastPos().y())
            new_x = self.pos_x + (event_pos.x() - event.lastPos().x())
            self.position = QPointF(new_x, new_y)
            # Signal the move to the connections
            self.moved.emit(event.lastPos().x() - event.pos().x(),
                            event.lastPos().y() - event.pos().y())


1183
1184
1185
    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
1186
            if str(self.text).lower() == each.processName: