Commit 5242bc0e authored by Maxime Perrotin's avatar Maxime Perrotin
Browse files

Merge https://github.com/esa/opengeode into python3-pyside2

parents 834f2b94 26a3262c
......@@ -124,6 +124,12 @@ The background pattern was downloaded from www.subtlepatterns.com
Changelog
=========
**3.3.6 (12/2020)**
- Minor bugfix with predure parsing
**3.3.5 (11/2020)**
- Improve link between error messages and graphical symbols
**3.3.4 (10/2020)**
- Fix bug when drawing connections in decision branches
......
......@@ -50,8 +50,10 @@ class Connection(QGraphicsPathItem):
self.childRect = child.sceneBoundingRect()
# Activate cache mode to boost rendering by calling paint less often
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
# When the child moves, the connection may need to adjust the end point
# When the parent or child move, the connection may need
# to adjust the end point: done upon signal reception
self.child.moved.connect(self.child_moved)
self.parent.moved.connect(self.parent_moved)
# Syntax error indicator
self.syntax_error: Boolean = False
......@@ -60,6 +62,11 @@ class Connection(QGraphicsPathItem):
''' When the connection child moves - redefine in subclasses '''
pass
@Slot(float, float)
def parent_moved(self, delta_x, delta_y):
''' When the connection parent moves - redefine in subclasses '''
self.parent.update_connections()
@property
def start_point(self):
''' Compute connection origin - redefine in subclasses '''
......@@ -427,8 +434,36 @@ class Channel(Signalroute):
@Slot(float, float)
def child_moved(self, delta_x, delta_y):
''' When the connection child moves - redefined function '''
# compute the distance between the start and end points
dist_x = abs(self.end_point.x() - self.start_point.x())
dist_y = abs(self.end_point.y() - self.start_point.y())
new_dist_x = dist_x - delta_x
new_dist_y = dist_y - delta_y
self._end_point.setX(self._end_point.x() - delta_x)
self._end_point.setY(self._end_point.y() - delta_y)
x_shift, y_shift = [], []
middle_points = list(self.middle_points)
self._middle_points = []
for ratio, point in zip(self._ratios, middle_points):
fact_x, fact_y = ratio
sp = self.start_point
new_x = (sp.x() + new_dist_x * fact_x) if 0 <= fact_x <= 1 \
else point.x() - delta_x
new_y = (sp.y() + new_dist_y * fact_y) if 0 <= fact_y <= 1 \
else point.y() - delta_y
self._middle_points.append(
self.parent.mapToScene(QPointF(new_x, new_y)))
self.reshape()
self.update() # force a repaint
@Slot(float, float)
def parent_moved(self, delta_x, delta_y):
''' When the connection parent moves - redefined function '''
self.reshape()
self.update() # force a repaint
@property
def start_point(self):
......@@ -458,6 +493,25 @@ class Channel(Signalroute):
@middle_points.setter
def middle_points(self, points_scene_coord):
''' Redefined function: also store the relative position (percentage)
to the line length, in order to ensure proper dimensionning of
the connection when blocks are moved '''
# compute the distance between the start and end points
dist_x = abs(self.end_point.x() - self.start_point.x())
dist_y = abs(self.end_point.y() - self.start_point.y())
# Compute the distance ratio
self._ratios = []
for point in points_scene_coord:
pCoord = self.parent.mapFromScene(point)
len_x = abs(pCoord.x() - self.start_point.x())
len_y = abs(pCoord.y() - self.start_point.y())
fact_x = 1 if dist_x == 0 else len_x / dist_x
fact_y = 1 if dist_y == 0 else len_y / dist_y
if pCoord.y() < self.start_point.y():
fact_y = -fact_y
if pCoord.x() < self.start_point.x():
fact_x = -fact_x
self._ratios.append((fact_x, fact_y))
self._middle_points = points_scene_coord
def add_point(self, scene_coord):
......
......@@ -634,8 +634,10 @@ class Symbol(QObject, QGraphicsPathItem):
self.cam(self.coord, self.position)
# Emit signal to indicate that the symbol moved
# typically caught by connectors
self.moved.emit(self.coord.x() - self.pos_x,
self.coord.y() - self.pos_y)
# Moved to sdlSymbols.Process class, this event is actually
# sent while moving, so that connection is updated on the fly
#self.moved.emit(self.coord.x() - self.pos_x,
# self.coord.y() - self.pos_y)
self.mode = ''
def updateConnectionPoints(self):
......@@ -661,11 +663,18 @@ class Symbol(QObject, QGraphicsPathItem):
top_level = top_level.parentItem() or top_level.parent
return top_level
def cam_group(self):
''' Set the graphical boundaries of the item to apply the CAM on
This can be redifined in subclasses, for example to exclude
connections '''
return (self.sceneBoundingRect() |
self.mapRectToScene(self.childrenBoundingRect()))
# pylint: disable=R0914
def cam(self, old_pos, new_pos, ignore=None):
''' Collision Avoidance Manoeuvre for top level symbols '''
# Since the cam function is recursive it may be time consuming
# Call the Qt event prcessing to avoid blocking the application
# Call the Qt event processing to avoid blocking the application
# Removed (had bad visual side effects)
# QApplication.processEvents()
#print 'CAM', str(self)[slice(0, 20)]
......@@ -686,8 +695,7 @@ class Symbol(QObject, QGraphicsPathItem):
delta = new_pos - old_pos
# Rectangle of current group of item in scene coordinates
rect = (self.sceneBoundingRect() |
self.mapRectToScene(self.childrenBoundingRect()))
rect = self.cam_group()
# Move the rectangle to the new position, and move the current item
animation = False
......@@ -860,7 +868,7 @@ class Comment(Symbol):
QPoint(w, h), QPoint(x, h)])
def mouse_move(self, event):
''' Handle item move '''
''' Comment symbol: Handle item move '''
super().mouse_move(event)
if self.mode == 'Move':
self.pos_y += event.pos().y() - event.lastPos().y()
......@@ -1098,7 +1106,7 @@ class HorizontalSymbol(Symbol):
return None
def mouse_move(self, event):
''' Will prevent move from being above the parent '''
''' Horizontal symbols: prevent move from being above the parent '''
if self.mode == 'Move':
event_pos = event.pos()
new_y = self.pos_y + (event_pos.y() - event.lastPos().y())
......
......@@ -2941,8 +2941,13 @@ def procedure_pre(root, parent=None, context=None):
proc.external = True
elif child.type == lexer.FPAR:
params, err, warn = fpar(child)
errors.extend(err)
warnings.extend(warn)
# convert error strings to the proper list
for each in err:
errors.append([f'In procedure signature: {each}',
[0, 0], []])
for each in warn:
warnings.append([f'In procedure {proc.inputString}: {each}',
[textarea.pos_x or 0, textarea.pos_y or 0], []])
proc.fpar = params
elif child.type == lexer.RETURNS:
# Declaration not in a text area...
......
......@@ -141,7 +141,7 @@ except ImportError:
__all__ = ['opengeode', 'SDL_Scene', 'SDL_View', 'parse']
__version__ = '3.3.4'
__version__ = '3.3.6'
if hasattr(sys, 'frozen'):
# Detect if we are running on Windows (py2exe-generated)
......@@ -165,8 +165,16 @@ else:
# they sometimes get destroyed and disappear from the scene.
# As if a GC was deleting these object *even if they belong to the scene*
# (but have no parentItem). Most likely a Qt/Pyside bug.
# NOTE: This was not re-evaluated with PySide2
G_SYMBOLS = set()
# There is a bug in Pyside2 with the setData(..) function. It should
# normally allow to store any type (as QVariant does in C++), however
# it does not manage to store QGraphicsItems (i.e. symbols). Because
# of that we must keep a table to find symbols that contain an error
# The table is cleared each time the Check Model is called
G_ERRORS = list()
# Other Qt bug:
# QGraphicsTextItem don't stand that their parent item (usually an
......@@ -199,6 +207,7 @@ ACTIONS = {
def log_errors(window, errors, warnings, clearfirst=True):
''' Report Error and Warnings on the console and in the log window '''
G_ERRORS.clear()
if window and clearfirst:
window.clear()
for error in errors:
......@@ -207,18 +216,15 @@ def log_errors(window, errors, warnings, clearfirst=True):
# problem is in decision answers branches
error[0] = 'Internal error - ' + str(error[0])
LOG.error(error[0])
item = QListWidgetItem(u'[ERROR] ' + error[0])
item = QListWidgetItem('[ERROR] ' + error[0])
if len(error) == 3:
item.setData(Qt.UserRole, error[1])
#found = self.scene().symbol_near(QPoint(*error[1]), 1)
# Pyside bug: setData cannot store 'found' directly
#item.setData(Qt.UserRole + 1, id(found))
item.setData(Qt.UserRole + 1, error[2])
if window:
window.addItem(item)
for warning in warnings:
LOG.warning(warning[0])
item = QListWidgetItem(u'[WARNING] ' + str(warning[0]))
item = QListWidgetItem('[WARNING] ' + str(warning[0]))
if len(warning) == 3:
item.setData(Qt.UserRole, warning[1])
item.setData(Qt.UserRole + 1, warning[2])
......@@ -228,7 +234,6 @@ def log_errors(window, errors, warnings, clearfirst=True):
window.addItem('No errors, no warnings!')
class Vi_bar(QLineEdit, object):
''' Line editor for the Vi-like command mode '''
def __init__(self):
......@@ -443,7 +448,6 @@ class SDL_Scene(QGraphicsScene):
# only applies to 1st level hierarchy (process) allowing to have
# unmodifiable list of DCL and STATES at the 1st level of hierarchy
ACTIONS[self.context] = []
#self.allowed_symbols = [] if readonly else ACTIONS[self.context]
def is_aggregation(self):
''' Determine if the current scene is a state aggregation, i.e. if
......@@ -655,16 +659,14 @@ class SDL_Scene(QGraphicsScene):
# Render nested scenes, recursively:
for each in (item for item in dest_scene.visible_symb
if item.nested_scene):
LOG.debug(u'Recursive scene: ' + str(each))
LOG.debug('Recursive scene: ' + str(each))
if isinstance(each.nested_scene, ogAST.CompositeState) \
and (not each.nested_scene.statename
or each.nested_scene in already_created):
# Ignore nested state scenes that already exist
LOG.debug('Subscene "{}" ignored'.format(str(each)))
continue
subscene = \
self.create_subscene(each.context_name,
dest_scene)
subscene = self.create_subscene(each.context_name, dest_scene)
already_created.append(each.nested_scene)
subscene.name = str(each)
......@@ -848,11 +850,13 @@ class SDL_Scene(QGraphicsScene):
# line 1 is the CIF comment which is not visible
line_nb, col = line_col
line_nb = int(line_nb) - 1
split[1] = '{}:{}'.format(line_nb, col)
split[1] = f'{line_nb}:{col}'
pos = each.scenePos()
split.append (u'in "{}"'.format(str(each)))
split.append (f'in "{str(each)}"')
fmt = [[' '.join(split), [pos.x(), pos.y()], self.path]]
log_errors(self.messages_window, fmt, [], clearfirst=False)
for view in self.views():
view.find_symbols_and_update_errors()
for each in self.all_nested_scenes:
if each not in ignore:
......@@ -1484,18 +1488,27 @@ class SDL_Scene(QGraphicsScene):
valid = (symb and symb.__class__.__name__
in self.connection_start._conn_sources and
self.connection_start.__class__.__name__
in symb._conn_targets and
len(self.edge_points) > 2)
in symb._conn_targets)# and
#len(self.edge_points) > 2)
# (The above was commented because it prevented
# direct lines between two blocks)
# "valid" could also check if it's allowed to connect
# a symbol to itself.
if symb and valid:
nb_segments = len(self.edge_points) - 1
for each in self.temp_lines[-nb_segments:]:
# check lines that collide with the source or dest TODO
pass
# Clicked on a symbol: create the actual connector
# Use a Channel type by default, but this could be something
# else in a different context
connector = Channel(parent=self.connection_start, child=symb)
# Set start and end points first, so that the distance can
# be computed when storing the middle points's relative
# positions
connector.start_point = self.edge_points[0]
connector.middle_points = self.edge_points[1:-1]
connector.end_point = self.border_point(symb, point)
connector.middle_points = self.edge_points[1:-1]
self.cancel()
super().mouseReleaseEvent(event)
......@@ -2240,6 +2253,7 @@ clean:
self.scene().render_everything(block)
except AttributeError as err:
LOG.debug("[Rendering] " + str(err))
self.find_symbols_and_update_errors()
self.toolbar.update_menu(self.scene())
self.scene().name = 'block {}[*]'.format(process.processName)
self.wrapping_window.setWindowTitle(self.scene().name)
......@@ -2252,7 +2266,6 @@ clean:
sdlSymbols.AST = ast
sdlSymbols.CONTEXT = block
self.update_datadict.emit()
#os.chdir(cwd)
def open_diagram(self):
''' Load one or several .pr file and display the state machine '''
......@@ -2332,6 +2345,7 @@ clean:
scene.semantic_errors = True if errors else False
log_errors(self.messages_window, errors, warnings,
clearfirst=False)
self.find_symbols_and_update_errors()
self.update_asn1_dock.emit(ast)
return "Done"
except Exception as err:
......@@ -2340,18 +2354,39 @@ clean:
LOG.debug(str(traceback.format_exc()))
return "Syntax Errors"
def show_item(self, item):
'''
Select an item and make sure it is visible - change scene if needed
Used when user clicks on a warning or error to locate the symbol
def find_symbols_and_update_errors(self):
''' Update the list of errors with the actual symbol location
error list entries contain Qt Data including path and graphical
coordinates. Based on this, retrieve the actual symbol. Once
found, it can be read by show_item (i.e. when user clicks on the
line in the list) to highlight the correct symbol even if it has
moved.
This function is called after each call of log_errors()
'''
coord = item.data(Qt.UserRole)
path = item.data(Qt.UserRole + 1)
if not coord:
LOG.debug('Corresponding symbol not found (no coordinates)')
return
messages : QListWidget = self.messages_window
current_scene = self.scene().path
for idx in range(messages.count()):
line : QListWidgetItem = messages.item(idx)
coord = line.data(Qt.UserRole)
path = line.data(Qt.UserRole + 1)
if not coord:
# All lines do not contain errors - discard them
pass
else:
# Find the scene containing the symbol
scene = self.scene()
if not self.go_to_scene_path(path):
continue
pos = QPoint(*coord)
symbol = self.scene().symbol_near(pos=pos, dist=1)
G_ERRORS.append(symbol)
line.setData(Qt.UserRole + 2, len(G_ERRORS) - 1)
_ = self.go_to_scene_path(current_scene)
# Find the scene containing the symbol
def go_to_scene_path(self, path) -> bool:
''' Reach a specific path (scene) by going up/down. This makes sure
that the Up button is properly set when the scene is reached '''
while self.up_button.isEnabled():
self.go_up()
......@@ -2359,45 +2394,59 @@ clean:
try:
kind, name = each.split()
except ValueError as err:
LOG.error('Cannot locate item: ' + str(each))
continue
LOG.debug(f'In go_to_scene_path: {str(each)}')
return False
name = str(name).lower()
if kind.lower() == 'process':
for process in self.scene().processes:
if str(process).lower() == name:
self.go_down(process.nested_scene,
name=u'process {}'.format(name))
name='process {}'.format(name))
break
else:
LOG.error('Process {} not found'.format(name))
LOG.error(f'Process {name} not found')
return False
elif kind.lower() == 'state':
for state in self.scene().states:
if str(state).lower() == name:
self.go_down(state.nested_scene,
name=u'state {}'.format(name))
name=f'state {name}')
break
else:
LOG.error('Composite state {} not found'.format(name))
LOG.error(f'Composite state {name} not found')
return False
elif kind.lower() == 'procedure':
for proc in self.scene().procedures:
if str(proc).lower() == name:
self.go_down(proc.nested_scene,
name=u'procedure {}'.format(name))
name=f'procedure {name}')
break
else:
LOG.error('Procedure {} not found'.format(name))
LOG.error(f'Procedure {name} not found')
return False
return True
pos = QPoint(*coord)
symbol = self.scene().symbol_near(pos=pos, dist=1)
if symbol:
def show_item(self, item):
'''
Select an item and make sure it is visible - change scene if needed
Used when user clicks on a warning or error to locate the symbol
'''
coord = item.data(Qt.UserRole)
path = item.data(Qt.UserRole + 1)
symb_idx = item.data(Qt.UserRole + 2)
if symb_idx is not None:
symbol = G_ERRORS[symb_idx]
self.scene().clearSelection()
self.scene().clear_highlight()
self.scene().clear_focus()
if not self.go_to_scene_path(path):
return
symbol.select()
self.scene().highlight(symbol)
self.ensureVisible(symbol)
else:
LOG.info('No symbol at given coordinates in the current scene')
LOG.debug('No coordinates or symbol found')
return
def generate_ada(self):
''' Generate Ada code '''
......@@ -2415,6 +2464,7 @@ clean:
except ValueError:
process = None
log_errors(self.messages_window, errors, warnings)
self.find_symbols_and_update_errors()
if len(errors) > 0:
self.messages_window.addItem(
'Aborting: too many errors to generate code')
......@@ -2445,6 +2495,7 @@ clean:
except ValueError:
process = None
log_errors(self.messages_window, errors, warnings)
self.find_symbols_and_update_errors()
if len(errors) > 0:
self.messages_window.addItem(
'Aborting: too many errors to generate code')
......@@ -2479,6 +2530,7 @@ clean:
except ValueError:
process = None
log_errors(self.messages_window, errors, warnings)
self.find_symbols_and_update_errors()
if len(errors) > 0:
self.messages_window.addItem(
'Aborting: too many errors to generate code')
......@@ -2977,8 +3029,9 @@ class OG_MainWindow(QMainWindow):
settings = QSettings(ini_filename, QSettings.IniFormat)
self.restoreGeometry(settings.value('geometry'))
self.restoreState(settings.value('windowState'))
else:
self.setWindowState(Qt.WindowMaximized)
#else:
# Commented: maximizing the window is annoying
# self.setWindowState(Qt.WindowMaximized)
class FilterEvent(QObject):
......
......@@ -1074,7 +1074,7 @@ class Process(HorizontalSymbol):
blackbold = SDL_BLACKBOLD
redbold = SDL_REDBOLD
completion_list = set()
is_singleton = True
is_singleton = True #(False to allow multiple processes)
arrow_head = 'angle'
arrow_tail = 'angle'
# Process can be connected to other processes by the user
......@@ -1159,6 +1159,26 @@ class Process(HorizontalSymbol):
self.setPath(path)
super().set_shape(width, height)
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())
def update_completion_list(self, pr_text):
''' When text was entered, update completion list at block level '''
for each in CONTEXT.processes:
......
= OpenGEODE Design documentation =
== Introduction ==
This document describes some parts of the design of the tool, as well as
practical guidelines to implement new features.
The first chapter explains the steps to be followed in order to implement SDL
language features that are not yet supported by the tool and is based on a
concrete use case: adding support for ''Continuous signals''.
The source of this document is in the ''Markdown'' format and other formats are
generated using the ''pandoc'' tool. Please take it in consideration when
updating the text.
== Guideline for extending OpenGEODE with Continuous Signals ==
Continuous signals are part of the SDL language and their semantics and syntax
is formally specified in the Z100 standard.
This chapter explains how concretely to add support for this construct to the
tool. It covers:
* Extending the ANTLR grammar
* Updating the parser and the AST (metamodel), including syntax/semantic checks
* Creating a new graphical symbol and updating the renderer and the menus
* Updating the backend to parse the graphical model and save PR files
* Updating other backends such as the Statechart renderer and code generators
* Updating the clipboard functionality
This is explained step by step in an order which allows to understand the logic
easily.
=== Add the new grammar to sdl92.g ===
The file <code>sdl92.g</code> contains the ANTLR3 grammar of the SDL language.
Please refer to the ANTLR documentation if needed.
We add the following production:
<pre>continuous_signal
: cif?
hyperlink?
PROVIDED expression e=end
(PRIORITY p=INT end)?
transition?
-&gt; ^(PROVIDED expression cif? hyperlink? $p? $e? transition?)
;</pre>
When a new token is needed (here: ''PROVIDED'') it must be added at several
places in the ANTLR grammar:
* in the &quot;symbolname&quot; production (for the CIF part)
* as a new keyword (<code>PROVIDED : P R O V I D E D</code>)
* in the list of tokens (tokens { ... })
'''Notes:'''
* ''cif'' and ''hyperlink'' are optional
* the ''transition'' is also optional, to allow partial model saving
* the expression is always the first child, since it does not have a dedicated
* token, and is the only mandatory field
* ''end'' corresponds to the ''COMMENT'' part in SDL
Then the new production is added as a child option to the (existing) ''state'':
<pre>state_part
: input_part
| save_part
| spontaneous_transition
| continuous_signal // &lt;==== HERE
| connect_part
;</pre>
=== Prepare the parser in ogParser.py ===
Find the parent rule (''state'' here) and add a branch to parse the new child.
Usually the rule is a function named after the production name. So look for
<code>def state (...)</code>
<pre>def state(root, parent, context):
'''
Parse a STATE.
&quot;parent&quot; is used to compute absolute coordinates
&quot;context&quot; is the AST used to store global data
(process/procedure)
'''</pre>
Each rule parses all its children based on the token name from ANTLR. It is
therefore straightforward to add the parsing of a new child:
<pre>for child in root.getChildren():
if child.type == lexer.CIF:
....