44
|
1 #!/usr/bin/python
|
|
2
|
|
3 from PyQt4.QtGui import *
|
|
4 from PyQt4.QtCore import *
|
81
|
5 import sys, json, base64
|
44
|
6
|
92
|
7 from diagramitems import Connection, ResizeSelectionHandle, Block, DiagramScene, CodeBlock
|
91
|
8 from icons import newicon, saveicon, loadicon
|
93
|
9 import diagramitems
|
88
|
10
|
56
|
11 """
|
53
|
12 Author: Windel Bouwman
|
|
13 Year: 2012
|
|
14 Description: This script implements a diagram editor.
|
|
15 run with python 3.x as:
|
|
16 $ python [thisfile.py]
|
44
|
17 """
|
86
|
18 def indent(lines):
|
|
19 return [' ' + line for line in lines]
|
|
20
|
44
|
21 class ParameterDialog(QDialog):
|
49
|
22 def __init__(self, block, parent = None):
|
44
|
23 super(ParameterDialog, self).__init__(parent)
|
49
|
24 self.block = block
|
44
|
25 self.button = QPushButton('Ok', self)
|
51
|
26 self.nameEdit = QLineEdit(self.block.name)
|
60
|
27 self.codeEdit = QTextEdit(self)
|
|
28 self.codeEdit.setPlainText(self.block.code)
|
75
|
29 l = QFormLayout(self)
|
|
30 l.addRow('Name:', self.nameEdit)
|
|
31 l.addRow('Code:', self.codeEdit)
|
|
32 l.addWidget(self.button)
|
44
|
33 self.button.clicked.connect(self.OK)
|
|
34 def OK(self):
|
75
|
35 self.block.name = self.nameEdit.text()
|
60
|
36 self.block.code = self.codeEdit.toPlainText()
|
50
|
37 self.close()
|
56
|
38
|
44
|
39 class EditorGraphicsView(QGraphicsView):
|
72
|
40 def __init__(self, parent=None):
|
|
41 QGraphicsView.__init__(self, parent)
|
54
|
42 self.setDragMode(QGraphicsView.RubberBandDrag)
|
78
|
43 self.delShort = QShortcut(QKeySequence.Delete, self)
|
80
|
44 self._model = None
|
|
45 self.treeView = QTreeView()
|
92
|
46 self.treeView.activated.connect(self.itemActivated)
|
|
47 def itemActivated(self, idx):
|
|
48 b = idx.internalPointer()
|
|
49 s = b.scene()
|
|
50 s.clearSelection()
|
|
51 b.setSelected(True)
|
80
|
52 def setDiagram(self, d):
|
|
53 self.setScene(d)
|
|
54 self.delShort.activated.connect(d.deleteItems)
|
91
|
55 def getModel(self):
|
|
56 return self._model
|
78
|
57 def setModel(self, m):
|
80
|
58 self._model = m
|
81
|
59 if m:
|
|
60 self.treeView.setModel(m)
|
|
61 self.diagram = m.rootDiagram
|
83
|
62 self.model.modelReset.connect(self.treeView.expandAll)
|
80
|
63 model = property(getModel, setModel)
|
|
64 diagram = property(lambda s: s.scene(), setDiagram)
|
72
|
65 def save(self):
|
81
|
66 if self.model:
|
|
67 if not self.model.filename:
|
|
68 self.model.filename = QFileDialog.getSaveFileName(self)
|
|
69 if self.model.filename:
|
|
70 with open(self.model.filename, 'w') as f:
|
92
|
71 f.write(json.dumps(self.model.Dict, indent=2))
|
72
|
72 def load(self):
|
|
73 filename = QFileDialog.getOpenFileName(self)
|
81
|
74 if filename:
|
|
75 self.model = loadModel(filename)
|
|
76 def newModel(self):
|
|
77 self.model = ModelHierarchyModel()
|
77
|
78 def goUp(self):
|
80
|
79 if hasattr(self.diagram, 'containingBlock'):
|
|
80 self.diagram = self.diagram.containingBlock.scene()
|
77
|
81 self.zoomAll()
|
86
|
82 def showCode(self):
|
|
83 if self.model:
|
|
84 c = self.model.gencode()
|
|
85 c = '\n'.join(c)
|
|
86 d = QDialog()
|
|
87 l = QFormLayout(d)
|
|
88 codeview = QTextEdit()
|
|
89 codeview.setPlainText(c)
|
|
90 l.addRow('code', codeview)
|
|
91 runButton = QPushButton('Run')
|
|
92 outputview = QTextEdit()
|
|
93 l.addRow('Output', outputview)
|
|
94 l.addWidget(runButton)
|
|
95 def print2(txt):
|
|
96 txt2 = outputview.toPlainText()
|
|
97 outputview.setPlainText(txt2 + '\n' + txt)
|
|
98 def runIt():
|
|
99 outputview.clear()
|
|
100 globs = {'print': print2}
|
|
101 exec(codeview.toPlainText(), globs)
|
|
102 runButton.clicked.connect(runIt)
|
|
103 d.exec_()
|
72
|
104 def zoomAll(self):
|
|
105 """ zoom to fit all items """
|
80
|
106 rect = self.diagram.itemsBoundingRect()
|
72
|
107 self.fitInView(rect, Qt.KeepAspectRatio)
|
55
|
108 def wheelEvent(self, event):
|
|
109 pos = event.pos()
|
|
110 posbefore = self.mapToScene(pos)
|
|
111 degrees = event.delta() / 8.0
|
|
112 sx = (100.0 + degrees) / 100.0
|
|
113 self.scale(sx, sx)
|
|
114 event.accept()
|
44
|
115 def dragEnterEvent(self, event):
|
93
|
116 if event.mimeData().hasFormat('component/name'):
|
|
117 event.accept()
|
44
|
118 def dragMoveEvent(self, event):
|
77
|
119 if event.mimeData().hasFormat('component/name'): event.accept()
|
44
|
120 def dropEvent(self, event):
|
|
121 if event.mimeData().hasFormat('component/name'):
|
48
|
122 name = bytes(event.mimeData().data('component/name')).decode()
|
93
|
123 kind, name = name.split(':')
|
58
|
124 pos = self.mapToScene(event.pos())
|
91
|
125 s = self.scene()
|
93
|
126 print(kind, 'name:', name)
|
|
127 kind = getattr(diagramitems, kind)
|
|
128 print(kind)
|
|
129 b = kind(s.uniqify(name))
|
91
|
130 b.setPos(pos)
|
|
131 s.addItem(b)
|
44
|
132
|
|
133 class LibraryModel(QStandardItemModel):
|
77
|
134 mimeTypes = lambda self: ['component/name']
|
44
|
135 def mimeData(self, idxs):
|
|
136 mimedata = QMimeData()
|
|
137 for idx in idxs:
|
|
138 if idx.isValid():
|
80
|
139 txt = self.data(idx, Qt.DisplayRole)
|
44
|
140 mimedata.setData('component/name', txt)
|
|
141 return mimedata
|
|
142
|
80
|
143 class ModelHierarchyModel(QAbstractItemModel):
|
78
|
144 def __init__(self):
|
|
145 super(ModelHierarchyModel, self).__init__()
|
79
|
146 self.rootDiagram = DiagramScene()
|
93
|
147 self.rootDiagram.structureChanged.connect(self.handlechange)
|
81
|
148 self.filename = None
|
93
|
149 def handlechange(self):
|
|
150 self.modelReset.emit()
|
80
|
151 def setDict(self, d):
|
|
152 self.rootDiagram.Dict = d
|
|
153 self.modelReset.emit()
|
91
|
154 def getDict(self):
|
|
155 return self.rootDiagram.Dict
|
|
156 Dict = property(getDict, setDict)
|
86
|
157 def gencode(self):
|
|
158 c = ['def topLevel():']
|
|
159 c += indent(self.rootDiagram.gencode())
|
|
160 c.append('print("Running model")')
|
|
161 c.append('topLevel()')
|
|
162 c.append('print("Done")')
|
|
163 return c
|
80
|
164 def index(self, row, column, parent=None):
|
|
165 if parent.isValid():
|
|
166 parent = parent.internalPointer().subModel
|
|
167 else:
|
|
168 parent = self.rootDiagram
|
|
169 blocks = sorted(parent.blocks, key=lambda b: b.name)
|
|
170 block = blocks[row]
|
|
171 # Store the index to retrieve it later in the parent function.
|
|
172 # TODO: solve this in a better way.
|
|
173 block.index = self.createIndex(row, column, block)
|
|
174 return block.index
|
|
175 def parent(self, index):
|
|
176 if index.isValid():
|
|
177 block = index.internalPointer()
|
|
178 if block.scene() == self.rootDiagram:
|
|
179 return QModelIndex()
|
|
180 else:
|
93
|
181 print(block)
|
80
|
182 outerBlock = block.scene().containingBlock
|
|
183 return outerBlock.index
|
|
184 print('parent: No valid index')
|
|
185 def data(self, index, role):
|
|
186 if index.isValid() and role == Qt.DisplayRole:
|
|
187 b = index.internalPointer()
|
92
|
188 if index.column() == 0:
|
|
189 return b.name
|
|
190 elif index.column() == 1:
|
|
191 return str(type(b))
|
78
|
192 def headerData(self, section, orientation, role):
|
|
193 if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
92
|
194 if section == 0:
|
|
195 return "Element"
|
|
196 elif section == 1:
|
|
197 return "Type"
|
|
198 else:
|
|
199 return "x"
|
78
|
200 def rowCount(self, parent):
|
91
|
201 if parent.column() > 0:
|
|
202 return 0
|
80
|
203 if parent.isValid():
|
91
|
204 block = parent.internalPointer()
|
|
205 if hasattr(block, 'subModel'):
|
|
206 return len(block.subModel.blocks)
|
|
207 else:
|
|
208 return 0
|
80
|
209 else:
|
91
|
210 return len(self.rootDiagram.blocks)
|
78
|
211 def columnCount(self, parent):
|
92
|
212 return 2
|
78
|
213
|
61
|
214 class LibraryWidget(QListView):
|
|
215 def __init__(self):
|
|
216 super(LibraryWidget, self).__init__(None)
|
54
|
217 self.libraryModel = LibraryModel(self)
|
|
218 self.libraryModel.setColumnCount(1)
|
|
219 # Create an icon with an icon:
|
|
220 pixmap = QPixmap(60, 60)
|
|
221 pixmap.fill()
|
|
222 painter = QPainter(pixmap)
|
|
223 painter.fillRect(10, 10, 40, 40, Qt.blue)
|
|
224 painter.setBrush(Qt.yellow)
|
|
225 painter.drawEllipse(20, 20, 20, 20)
|
|
226 painter.end()
|
|
227 # Fill library:
|
93
|
228 for name in ['CodeBlock:codeBlock', 'DiagramBlock:submod', 'Block:blk']:
|
78
|
229 self.libraryModel.appendRow(QStandardItem(QIcon(pixmap), name))
|
61
|
230 self.setModel(self.libraryModel)
|
|
231 self.setViewMode(self.IconMode)
|
|
232 self.setDragDropMode(self.DragOnly)
|
54
|
233
|
81
|
234 def warning(txt):
|
|
235 QMessageBox.warning(None, "Warning", txt)
|
|
236
|
78
|
237 def loadModel(filename):
|
80
|
238 try:
|
81
|
239 m = ModelHierarchyModel()
|
80
|
240 with open(filename, 'r') as f: data = f.read()
|
81
|
241 m.filename = filename
|
80
|
242 m.Dict = json.loads(data)
|
81
|
243 return m
|
|
244 except KeyError:
|
|
245 warning('Corrupt model: {0}'.format(filename))
|
|
246 except ValueError:
|
|
247 warning('Corrupt model: {0}'.format(filename))
|
|
248 except FileNotFoundError:
|
|
249 warning('File [{0}] not found'.format(filename))
|
78
|
250
|
61
|
251 class Main(QMainWindow):
|
|
252 def __init__(self):
|
|
253 super(Main, self).__init__(None)
|
72
|
254 self.editor = EditorGraphicsView()
|
61
|
255 self.setCentralWidget(self.editor)
|
|
256 self.setWindowTitle("Diagram editor")
|
81
|
257 def buildIcon(b64):
|
|
258 icon = base64.decodestring(b64)
|
|
259 pm = QPixmap()
|
|
260 pm.loadFromData(icon)
|
|
261 return QIcon(pm)
|
61
|
262 toolbar = self.addToolBar('Tools')
|
81
|
263 toolbar.setObjectName('Tools')
|
|
264 def act(name, shortcut, callback, icon=None):
|
|
265 a = QAction(icon, name, self) if icon else QAction(name, self)
|
72
|
266 a.setShortcuts(shortcut)
|
|
267 a.triggered.connect(callback)
|
|
268 toolbar.addAction(a)
|
81
|
269 act('New', QKeySequence.New, self.editor.newModel, buildIcon(newicon))
|
|
270 act('Save', QKeySequence.Save, self.editor.save, buildIcon(saveicon))
|
|
271 act('Load', QKeySequence.Open, self.editor.load, buildIcon(loadicon))
|
72
|
272 act('Full screen', QKeySequence("F11"), self.toggleFullScreen)
|
|
273 act('Fit in view', QKeySequence("F8"), self.editor.zoomAll)
|
77
|
274 act('Go up', QKeySequence(Qt.Key_Up), self.editor.goUp)
|
86
|
275 act('Model code', QKeySequence("F7"), self.editor.showCode)
|
78
|
276 def addDock(name, widget):
|
|
277 dock = QDockWidget(name, self)
|
81
|
278 dock.setObjectName(name)
|
78
|
279 dock.setWidget(widget)
|
|
280 self.addDockWidget(Qt.LeftDockWidgetArea, dock)
|
|
281 addDock('Library', LibraryWidget())
|
80
|
282 addDock('Model tree', self.editor.treeView)
|
81
|
283 self.settings = QSettings('windelsoft', 'diagrameditor')
|
|
284 self.loadSettings()
|
44
|
285 def toggleFullScreen(self):
|
|
286 self.setWindowState(self.windowState() ^ Qt.WindowFullScreen)
|
61
|
287 self.editor.zoomAll()
|
81
|
288 def loadSettings(self):
|
|
289 if self.settings.contains('mainwindowstate'):
|
|
290 self.restoreState(self.settings.value('mainwindowstate'))
|
|
291 if self.settings.contains('mainwindowgeometry'):
|
|
292 self.restoreGeometry(self.settings.value('mainwindowgeometry'))
|
|
293 if self.settings.contains('openedmodel'):
|
|
294 modelfile = self.settings.value('openedmodel')
|
|
295 self.editor.model = loadModel(modelfile)
|
|
296 def closeEvent(self, ev):
|
|
297 self.settings.setValue('mainwindowstate', self.saveState())
|
|
298 self.settings.setValue('mainwindowgeometry', self.saveGeometry())
|
|
299 if self.editor.model and self.editor.model.filename:
|
|
300 self.settings.setValue('openedmodel', self.editor.model.filename)
|
|
301 # TODO: ask for save of opened files
|
|
302 else:
|
|
303 self.settings.remove('openedmodel')
|
|
304 ev.accept()
|
44
|
305
|
|
306 if __name__ == '__main__':
|
45
|
307 if sys.version_info.major != 3:
|
|
308 print('Please use python 3.x')
|
|
309 sys.exit(1)
|
46
|
310 app = QApplication(sys.argv)
|
61
|
311 main = Main()
|
|
312 main.show()
|
44
|
313 app.exec_()
|
|
314
|