Commit 1c5933ff authored by Maxime Perrotin's avatar Maxime Perrotin
Browse files

Fixed segfaults due to pyside bugs

parent 736a6fb7
......@@ -19,6 +19,7 @@ import os
import sys
import math
import logging
from collections import namedtuple
from PySide.QtCore import Qt, QPointF, QLineF
......@@ -39,8 +40,9 @@ class Connection(QGraphicsPathItem, object):
super(Connection, self).__init__(parent)
self.parent = parent
self.child = child
self.start_point = QPointF(0, 0)
self.end_point = QPointF(0, 0)
self._start_point = None
self._end_point = None
self._middle_points = []
pen = QPen()
pen.setColor(Qt.blue)
pen.setCosmetic(False)
......@@ -50,6 +52,34 @@ class Connection(QGraphicsPathItem, object):
# Activate cache mode to boost rendering by calling paint less often
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
@property
def start_point(self):
''' Compute connection origin - redefine in subclasses '''
return self._start_point or QPointF(0, 0)
@property
def end_point(self):
''' Compute connection end point - redefine in subclasses '''
return self._end_point or QPointF(0, 0)
@property
def middle_points(self):
''' Compute connection intermediate points - redefine in subclasses '''
return self._middle_points
def arrow(self, path=None):
''' Compute the two points of an arrow head - vertical by default '''
endp = self.end_point
return (QPointF(endp.x() - 5, endp.y() - 5),
QPointF(endp.x() + 5, endp.y() - 5))
def draw_arrow_head(self, shape):
''' Generic function to draw any arrow head - don't redefine '''
arrowhead = self.arrow(shape)
shape.lineTo(arrowhead[0])
shape.moveTo(self.end_point)
shape.lineTo(arrowhead[1])
def __str__(self):
''' Print connection information for debug purpose'''
return 'Connection: parent = {p}, child = {c}'.format(
......@@ -57,112 +87,108 @@ class Connection(QGraphicsPathItem, object):
def reshape(self):
''' Update the connection or arrow shape '''
new_shape = QPainterPath()
self.setPath(new_shape)
return
shape = QPainterPath()
shape.moveTo(self.start_point)
for point in self.middle_points:
shape.lineTo(point)
shape.lineTo(self.end_point)
# If required draw an arrow head (e.g. in SDL NEXTSTATE and JOIN)
if self.child.arrow_head:
self.draw_arrow_head(shape)
self.setPath(shape)
class RakeConnection(Connection):
''' Fork-like connection, e.g. between a state and an input symbol '''
def reshape(self):
''' Update the connection or arrow shape '''
new_shape = QPainterPath()
@property
def start_point(self):
''' Compute connection origin - redefined function '''
parent_rect = self.parentItem().boundingRect()
# Define connection start point
self.start_point = \
QPointF(parent_rect.width() / 2, parent_rect.height())
# Define connection end point
self.end_point = self.child.pos()
# Move to start point and draw the connection
new_shape.moveTo(self.start_point)
self.end_point.setX(self.child.pos().x() +
self.child.boundingRect().width() / 2)
new_shape.lineTo(self.start_point.x(), self.start_point.y() + 10)
new_shape.lineTo(self.end_point.x(), self.start_point.y() + 10)
new_shape.lineTo(self.end_point)
self.setPath(new_shape)
return QPointF(parent_rect.width() / 2, parent_rect.height())
@property
def end_point(self):
''' Compute connection end point - redefined function '''
coord = self.child.pos()
coord.setX(coord.x() + self.child.boundingRect().width() / 2)
return coord
@property
def middle_points(self):
''' Compute connection intermediate points - redefined function '''
yield QPointF(self.start_point.x(), self.start_point.y() + 10)
yield QPointF(self.end_point.x(), self.start_point.y() + 10)
class JoinConnection(Connection):
''' Inverted fork-like connection, to join to a common point '''
def reshape(self):
''' Update the connection or arrow shape '''
new_shape = QPainterPath()
''' Update the connection shape - redefined: if the last element
of a branch is e.g. a nextstate, don't join the connection point '''
if self.parentItem().terminal_symbol:
self.setPath(new_shape)
return
parent_rect = self.parentItem().boundingRect()
# Define connection start point
if hasattr(self.parentItem(), 'connectionPoint'):
self.start_point = self.parentItem().connectionPoint
self.setPath(QPainterPath())
else:
self.start_point = \
QPointF(parent_rect.width() / 2, parent_rect.height())
# Define connection end point
connection_point_scene = \
self.child.mapToScene(self.child.connectionPoint)
connection_point_local = \
self.mapFromScene(connection_point_scene)
self.end_point = connection_point_local
# Move to start point and draw the connection
new_shape.moveTo(self.start_point)
new_shape.lineTo(self.start_point.x(), self.end_point.y() - 10)
new_shape.lineTo(self.end_point.x(), self.end_point.y() - 10)
new_shape.lineTo(self.end_point)
self.setPath(new_shape)
super(JoinConnection, self).reshape()
@property
def start_point(self):
''' Compute connection origin - redefined function '''
parent_rect = self.parentItem().boundingRect()
return getattr(self.parentItem(), 'connectionPoint',
QPointF(parent_rect.width() / 2, parent_rect.height()))
@property
def end_point(self):
''' Compute connection end point - redefined function '''
point_scene = self.child.mapToScene(self.child.connectionPoint)
return self.mapFromScene(point_scene)
@property
def middle_points(self):
''' Compute connection intermediate points - redefined function '''
yield QPointF(self.start_point.x(), self.end_point.y() - 10)
yield QPointF(self.end_point.x(), self.end_point.y() - 10)
class VerticalConnection(Connection):
''' Vertical line with or without arrow '''
def reshape(self):
''' Connection shape: vertical line '''
new_shape = QPainterPath()
if self.parentItem().terminal_symbol:
self.setPath(new_shape)
return
@property
def start_point(self):
''' Compute connection origin - redefined function '''
parent_rect = self.parentItem().boundingRect()
# Define connection start point
if hasattr(self.parentItem(), 'connectionPoint'):
self.start_point = self.parentItem().connectionPoint
else:
self.start_point = QPointF(parent_rect.width() / 2,
parent_rect.height())
# Define connection end point
self.end_point = self.child.pos()
self.end_point.setX(self.start_point.x())
# Move to start point and draw the connection
new_shape.moveTo(self.start_point)
new_shape.lineTo(self.end_point)
# If required draw an arrow head (e.g. in SDL NEXTSTATE and JOIN)
if self.child.arrow_head:
new_shape.lineTo(self.end_point.x() - 5, self.end_point.y() - 5)
new_shape.moveTo(self.end_point)
new_shape.lineTo(self.end_point.x() + 5, self.end_point.y() - 5)
self.setPath(new_shape)
return getattr(self.parentItem(), 'connectionPoint',
QPointF(parent_rect.width() / 2, parent_rect.height()))
@property
def end_point(self):
''' Compute connection end point - redefined function '''
point = self.child.pos()
point.setX(self.start_point.x())
return point
class CommentConnection(Connection):
''' Class handling connection to comment symbols, fixed at the right
of a symbol '''
def reshape(self):
''' Set the connection shape '''
new_shape = QPainterPath()
@property
def start_point(self):
''' Compute connection origin - redefined function '''
parent_rect = self.parentItem().boundingRect()
# Define connection start point
self.start_point = \
QPointF(parent_rect.width(), parent_rect.height() / 2)
# Define connection end point
return QPointF(parent_rect.width(), parent_rect.height() / 2)
@property
def end_point(self):
''' Compute connection end point - redefined function '''
if self.child.on_the_right:
self.end_point = QPointF(self.child.x(),
self.child.y() +
self.child.boundingRect().height() / 2)
return QPointF(self.child.x(),
self.child.y() + self.child.boundingRect().height() / 2)
else:
self.end_point = QPointF(self.child.x() +
self.child.boundingRect().width(),
self.child.y() +
self.child.boundingRect().height() / 2)
# Move to start point and draw the connection
new_shape.moveTo(self.start_point)
return QPointF(self.child.x() + self.child.boundingRect().width(),
self.child.y() + self.child.boundingRect().height() / 2)
@property
def middle_points(self):
''' Compute connection intermediate points - redefined function '''
# Make sure the connection does not overlap the comment item
if (self.child.on_the_right or
(not self.child.on_the_right and
......@@ -171,11 +197,8 @@ class CommentConnection(Connection):
go_to_point = self.start_point.x() + 5
else:
go_to_point = self.end_point.x() + 5
new_shape.lineTo(go_to_point, self.start_point.y())
new_shape.lineTo(go_to_point, self.end_point.y())
new_shape.lineTo(self.end_point.x(), self.end_point.y())
new_shape.lineTo(self.end_point)
self.setPath(new_shape)
yield QPointF(go_to_point, self.start_point.y())
yield QPointF(go_to_point, self.end_point.y())
class Channel(Connection):
......@@ -278,13 +301,10 @@ class Edge(Connection):
super(Edge, self).__init__(edge['source'], edge['target'])
self.edge = edge
self.graph = graph
# Set connection points as not visible, by default
self.bezier_visible = False
# Initialize control point coordinates
# Start and End points are optional - graphviz decision
self.start_point = (self.mapFromScene(*self.edge['start']) if
self.edge.get('start') else None)
self.end_point = (self.mapFromScene(*self.edge['end']) if
self.edge.get('end') else None)
self.bezier = [self.mapFromScene(*self.edge['spline'][0])]
# Bezier control points (groups of three points):
assert(len(self.edge['spline']) % 3 == 1)
......@@ -292,8 +312,6 @@ class Edge(Connection):
self.bezier.append([Controlpoint(
self.mapFromScene(*self.edge['spline'][i + j]), self)
for j in range(3)])
# Set connection points as not visible, by default
self.bezier_visible = False
# Create connection points at start and end of the edge
self.source_connection = Connectionpoint(
......@@ -304,6 +322,19 @@ class Edge(Connection):
self.child.movable_points.append(self.end_connection)
self.reshape()
@property
def start_point(self):
''' Compute connection origin - redefine in subclasses '''
# Start point is optional - graphviz decision
return self.mapFromScene(*self.edge['start']) \
if self.edge.get('start') else None
@property
def end_point(self):
''' Compute connection end point - redefine in subclasses '''
return self.mapFromScene(*self.edge['end']) \
if self.edge.get('end') else None
def bezier_set_visible(self, visible=True):
''' Display or hide the edge control points '''
self.bezier_visible = visible
......@@ -325,6 +356,25 @@ class Edge(Connection):
''' On a mouse click, display the control points '''
self.bezier_set_visible(True)
def arrow(self, path):
''' Compute the two points of the arrow head with the right angle '''
length = path.length()
percent = path.percentAtLength(length - 10.0)
src = path.pointAtPercent(percent)
end_point = path.currentPosition()
line = QLineF(src, end_point)
angle = math.acos(line.dx() / line.length())
if line.dy() >= 0:
angle = math.pi * 2 - angle
arrow_size = 10.0
arrow_p1 = end_point + QPointF(
math.sin(angle - math.pi/3) * arrow_size,
math.cos(angle - math.pi/3) * arrow_size)
arrow_p2 = end_point + QPointF(
math.sin(angle - math.pi + math.pi/3) * arrow_size,
math.cos(angle - math.pi + math.pi/3) * arrow_size)
return (arrow_p1, arrow_p2)
# pylint: disable=R0914
def reshape(self):
''' Update the shape of the edge (redefined function) '''
......@@ -343,27 +393,11 @@ class Edge(Connection):
if self.end_point:
path.lineTo(self.end_connection.center)
# Draw the arrow head
length = path.length()
percent = path.percentAtLength(length - 10.0)
src = path.pointAtPercent(percent)
#angle = path.angleAtPercent(percent)
#print angle
end_point = path.currentPosition()
line = QLineF(src, end_point)
angle = math.acos(line.dx() / line.length())
if line.dy() >= 0:
angle = math.pi * 2 - angle
arrow_size = 10.0
arrow_p1 = end_point + QPointF(
math.sin(angle - math.pi/3) * arrow_size,
math.cos(angle - math.pi/3) * arrow_size)
arrow_p2 = end_point + QPointF(
math.sin(angle - math.pi + math.pi/3) * arrow_size,
math.cos(angle - math.pi + math.pi/3) * arrow_size)
path.lineTo(arrow_p1)
path.lineTo(end_point)
path.lineTo(arrow_p2)
arrowhead = self.arrow(path)
path.lineTo(arrowhead[0])
path.moveTo(end_point)
path.lineTo(arrowhead[1])
path.moveTo(end_point)
try:
# Add the transition label, if any (none for the START edge)
......@@ -403,6 +437,3 @@ class Edge(Connection):
for sub1 in self.bezier[1:] for point in sub1]
painter.drawPolyline([self.source_connection.center]
+ points_flat + [self.end_connection.center])
......@@ -7,6 +7,6 @@
defined language, and generate Ada code from the models.
"""
__version__ = "0.99"
__version__ = "0.991"
from opengeode import opengeode
......@@ -857,7 +857,6 @@ class Symbol(QObject, QGraphicsPathItem, object):
'''
# Save current position to be able to revert move
self.coord = self.pos()
#event_pos = event.pos()
rect = self.boundingRect()
self.height = rect.height()
if self.grabber.resize_mode != '':
......@@ -873,7 +872,7 @@ class Symbol(QObject, QGraphicsPathItem, object):
def mouse_move(self, event):
''' Handle resizing of items - moving is handled in subclass '''
self.updateConnectionPoints()
# If any, update movable end points of connections
# If any, update movable end points of connections
for point in self.movable_points:
point.edge.end_connection.update_position()
if self.mode == 'Resize':
......
......@@ -2,7 +2,7 @@
 
# Resource object code
#
# Created: Mon Apr 28 21:41:51 2014
# Created: Mon May 5 22:01:46 2014
# by: The Resource Compiler for PySide (Qt v4.8.6)
#
# WARNING! All changes made in this file will be lost!
......@@ -86,7 +86,7 @@ except ImportError:
pass
__all__ = ['opengeode']
__version__ = '0.99'
__version__ = '0.991'
if hasattr(sys, 'frozen'):
# Detect if we are running on Windows (py2exe-generated)
......@@ -961,7 +961,9 @@ class SDL_Scene(QtGui.QGraphicsScene, object):
item.select()
except AttributeError:
pass
self.removeItem(self.select_rect)
#self.removeItem(self.select_rect)
# XXX stop with removeItem, it provokes segfault
self.select_rect.hide()
self.mode = 'idle'
super(SDL_Scene, self).mouseReleaseEvent(event)
......@@ -1095,6 +1097,8 @@ class SDL_Scene(QtGui.QGraphicsScene, object):
class SDL_View(QtGui.QGraphicsView, object):
''' Main graphic view used to display the SDL scene and handle zoom '''
# signal to ask the main application that a new scene is needed
need_new_scene = QtCore.Signal()
def __init__(self, scene):
''' Create the SDL view holding the scene '''
super(SDL_View, self).__init__(scene)
......@@ -1194,7 +1198,9 @@ class SDL_View(QtGui.QGraphicsView, object):
scene_rect.setWidth(max(scene_rect.width(), view_size.width()))
scene_rect.setHeight(max(scene_rect.height(), view_size.height()))
if self.phantom_rect and self.phantom_rect in self.scene().items():
self.scene().removeItem(self.phantom_rect)
#self.scene().removeItem(self.phantom_rect)
# XXX stop with removeItem, it provokes segfault
self.phantom_rect.hide()
self.phantom_rect = self.scene().addRect(scene_rect,
pen=QtGui.QPen(QtGui.QColor(0, 0, 0, 0)))
# Hide the rectangle so that it does not collide with the symbols
......@@ -1318,7 +1324,10 @@ class SDL_View(QtGui.QGraphicsView, object):
# Adjust scrollbars if diagram got bigger due to a move
if self.scene().context != 'statechart':
# Make sure scene size remains OK when adding/moving symbols
self.refresh()
# Avoid doing it when editing texts - it would prevent text
# selection or cursor move
if not isinstance(self.scene().focusItem(), EditableText):
self.refresh()
super(SDL_View, self).mouseReleaseEvent(evt)
def save_as(self):
......@@ -1419,46 +1428,54 @@ class SDL_View(QtGui.QGraphicsView, object):
def open_diagram(self):
''' Load one or several .pr file and display the state machine '''
if self.new_diagram():
filenames, _ = QtGui.QFileDialog.getOpenFileNames(self,
"Open model(s)", ".", "SDL model (*.pr)")
if not filenames:
return
else:
self.load_file(filenames)
if not self.new_diagram():
return
filenames, _ = QtGui.QFileDialog.getOpenFileNames(self,
"Open model(s)", ".", "SDL model (*.pr)")
if not filenames:
return
else:
self.load_file(filenames)
def isModelClean(self):
''' Check recursively if anything has changed in any scene '''
if self.parent_scene:
scene = self.parent_scene[0][0]
else:
scene = self.scene()
for each in chain([scene], scene.all_nested_scenes):
if not each.undo_stack.isClean():
return False
return True
def propose_to_save(self):
''' Display a dialog to let the user save his diagram '''
msg_box = QtGui.QMessageBox(self)
msg_box.setWindowTitle('OpenGEODE')
msg_box.setText("The model has been modified.")
msg_box.setInformativeText("Do you want to save your changes?")
msg_box.setStandardButtons(QtGui.QMessageBox.Save |
QtGui.QMessageBox.Discard |
QtGui.QMessageBox.Cancel)
msg_box.setDefaultButton(QtGui.QMessageBox.Save)
ret = msg_box.exec_()
if ret == QtGui.QMessageBox.Save:
if not self.save_diagram():
return False
elif ret == QtGui.QMessageBox.Cancel:
return False
return True
def new_diagram(self):
''' If model state is clean, reset current diagram '''
# FIXME: incomplete check of the cleanliness of the model
# - must check all subscenes
if self.scene().undo_stack.isClean():
is_clean = True
while self.parent_scene:
self.go_up()
if not self.scene().undo_stack.isClean():
is_clean = False
else:
is_clean = False
if not is_clean:
if not self.isModelClean():
# If changes occured since last save, pop up a window
msg_box = QtGui.QMessageBox(self)
msg_box.setWindowTitle('OpenGEODE')
msg_box.setText("The model has been modified.")
msg_box.setInformativeText("Do you want to save your changes?")
msg_box.setStandardButtons(QtGui.QMessageBox.Save |
QtGui.QMessageBox.Discard |
QtGui.QMessageBox.Cancel)
msg_box.setDefaultButton(QtGui.QMessageBox.Save)
ret = msg_box.exec_()
if ret == QtGui.QMessageBox.Save:
if not self.save_diagram():
return False
elif ret == QtGui.QMessageBox.Discard:
pass
elif ret == QtGui.QMessageBox.Cancel:
if not self.propose_to_save():
return False
self.scene().undo_stack.clear()
self.scene().clear()
self.need_new_scene.emit()
self.parent_scene = []
#self.scene().undo_stack.clear()
#self.scene().clear()
G_SYMBOLS.clear()
self.scene().process_name = ''
self.filename = None
......@@ -1549,10 +1566,21 @@ class OG_MainWindow(QtGui.QMainWindow, object):
self.datatypes_scene = None
self.asn1_area = None
def new_scene(self):
''' Create a new, clean SDL scene. This function is necessary because
it is not possible to use QGraphicsScene.clear(), because of Pyside
bugs with deletion of items on application exit '''
self.scene = SDL_Scene(context='block')
if self.view:
self.scene.messages_window = self.view.messages_window
self.view.setScene(self.scene)
self.view.refresh()
def start(self, file_name):
''' Initializes all objects to start the application '''
# Create a graphic scene: the main canvas
self.scene = SDL_Scene(context='block')
self.new_scene()
# Find SDL_View widget
self.view = self.findChild(SDL_View, 'graphicsView')
self.view.setScene(self.scene)
......@@ -1579,6 +1607,9 @@ class OG_MainWindow(QtGui.QMainWindow, object):
ada_action.activated.connect(self.view.generate_ada)
png_action.activated.connect(self.view.save_png)
# Connect signal to let the view request a new scene
self.view.need_new_scene.connect(self.new_scene)
# Add a toolbar widget (not in .ui file due to pyside bugs)
toolbar = Sdl_toolbar(self)
......@@ -1688,11 +1719,11 @@ class OG_MainWindow(QtGui.QMainWindow, object):
_, pattern, new, _ = search.match(command).groups()
LOG.debug('Replacing {this} with {that}'
.format(this=pattern, that=new))
self.scene.search(pattern, replace_with=new)
self.view.scene().search(pattern, replace_with=new)
except AttributeError:
if command.startswith('/') and len(command) > 1:
LOG.debug('Searching for ' + command[1:])
self.scene.search(command[1:])
self.view.scene().search(command[1:])
else:
saveclose = re.compile(r':(w)?(q)?(!)?')
try:
......@@ -1702,7 +1733,7 @@ class OG_MainWindow(QtGui.QMainWindow, object):
if not saved and not force and close_app:
return
if force and close_app:
self.scene.undo_stack.clear()
self.view.scene().undo_stack.clear()
if close_app:
self.close()
except AttributeError:
......@@ -1712,7 +1743,11 @@ class OG_MainWindow(QtGui.QMainWindow, object):
def keyPressEvent(self, key_event):
''' Handle keyboard: Statechart rendering '''
if key_event.key() == Qt.Key_F4 and graphviz:
graph = self.scene.sdl_to_statechart()
if self.view.parent_scene:
scene = self.view.parent_scene[0][0]
else:
scene = self.view.scene()
graph = scene.sdl_to_statechart()
try:
Statechart.render_statechart(self.statechart_scene, graph)
except (IOError, TypeError) as err:
......@@ -1730,16 +1765,17 @@ class OG_MainWindow(QtGui.QMainWindow, object):
# pylint: disable=C0103
def closeEvent(self, event):
''' Close main application '''
if self.view.new_diagram():
# Clear the list of top-level symbols to avoid possible exit-crash
# due to pyside badly handling items that are not part of any scene
G_SYMBOLS.