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