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()
|
99
|
46 self.treeView.clicked.connect(self.itemActivated)
|
92
|
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()
|
99
|
126 if not s:
|
|
127 return
|
93
|
128 print(kind, 'name:', name)
|
|
129 kind = getattr(diagramitems, kind)
|
|
130 print(kind)
|
|
131 b = kind(s.uniqify(name))
|
91
|
132 b.setPos(pos)
|
|
133 s.addItem(b)
|
44
|
134
|
|
135 class LibraryModel(QStandardItemModel):
|
77
|
136 mimeTypes = lambda self: ['component/name']
|
44
|
137 def mimeData(self, idxs):
|
|
138 mimedata = QMimeData()
|
|
139 for idx in idxs:
|
|
140 if idx.isValid():
|
80
|
141 txt = self.data(idx, Qt.DisplayRole)
|
44
|
142 mimedata.setData('component/name', txt)
|
|
143 return mimedata
|
|
144
|
80
|
145 class ModelHierarchyModel(QAbstractItemModel):
|
78
|
146 def __init__(self):
|
|
147 super(ModelHierarchyModel, self).__init__()
|
79
|
148 self.rootDiagram = DiagramScene()
|
93
|
149 self.rootDiagram.structureChanged.connect(self.handlechange)
|
81
|
150 self.filename = None
|
93
|
151 def handlechange(self):
|
|
152 self.modelReset.emit()
|
80
|
153 def setDict(self, d):
|
|
154 self.rootDiagram.Dict = d
|
|
155 self.modelReset.emit()
|
91
|
156 def getDict(self):
|
|
157 return self.rootDiagram.Dict
|
|
158 Dict = property(getDict, setDict)
|
86
|
159 def gencode(self):
|
|
160 c = ['def topLevel():']
|
|
161 c += indent(self.rootDiagram.gencode())
|
|
162 c.append('print("Running model")')
|
|
163 c.append('topLevel()')
|
|
164 c.append('print("Done")')
|
|
165 return c
|
80
|
166 def index(self, row, column, parent=None):
|
|
167 if parent.isValid():
|
|
168 parent = parent.internalPointer().subModel
|
|
169 else:
|
|
170 parent = self.rootDiagram
|
|
171 blocks = sorted(parent.blocks, key=lambda b: b.name)
|
|
172 block = blocks[row]
|
|
173 # Store the index to retrieve it later in the parent function.
|
|
174 # TODO: solve this in a better way.
|
|
175 block.index = self.createIndex(row, column, block)
|
|
176 return block.index
|
|
177 def parent(self, index):
|
|
178 if index.isValid():
|
|
179 block = index.internalPointer()
|
|
180 if block.scene() == self.rootDiagram:
|
|
181 return QModelIndex()
|
|
182 else:
|
93
|
183 print(block)
|
80
|
184 outerBlock = block.scene().containingBlock
|
|
185 return outerBlock.index
|
|
186 print('parent: No valid index')
|
|
187 def data(self, index, role):
|
|
188 if index.isValid() and role == Qt.DisplayRole:
|
|
189 b = index.internalPointer()
|
92
|
190 if index.column() == 0:
|
|
191 return b.name
|
|
192 elif index.column() == 1:
|
|
193 return str(type(b))
|
78
|
194 def headerData(self, section, orientation, role):
|
|
195 if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
92
|
196 if section == 0:
|
|
197 return "Element"
|
|
198 elif section == 1:
|
|
199 return "Type"
|
|
200 else:
|
|
201 return "x"
|
78
|
202 def rowCount(self, parent):
|
91
|
203 if parent.column() > 0:
|
|
204 return 0
|
80
|
205 if parent.isValid():
|
91
|
206 block = parent.internalPointer()
|
|
207 if hasattr(block, 'subModel'):
|
|
208 return len(block.subModel.blocks)
|
|
209 else:
|
|
210 return 0
|
80
|
211 else:
|
91
|
212 return len(self.rootDiagram.blocks)
|
78
|
213 def columnCount(self, parent):
|
92
|
214 return 2
|
78
|
215
|
61
|
216 class LibraryWidget(QListView):
|
|
217 def __init__(self):
|
|
218 super(LibraryWidget, self).__init__(None)
|
54
|
219 self.libraryModel = LibraryModel(self)
|
|
220 self.libraryModel.setColumnCount(1)
|
|
221 # Create an icon with an icon:
|
|
222 pixmap = QPixmap(60, 60)
|
|
223 pixmap.fill()
|
|
224 painter = QPainter(pixmap)
|
|
225 painter.fillRect(10, 10, 40, 40, Qt.blue)
|
|
226 painter.setBrush(Qt.yellow)
|
|
227 painter.drawEllipse(20, 20, 20, 20)
|
|
228 painter.end()
|
|
229 # Fill library:
|
93
|
230 for name in ['CodeBlock:codeBlock', 'DiagramBlock:submod', 'Block:blk']:
|
78
|
231 self.libraryModel.appendRow(QStandardItem(QIcon(pixmap), name))
|
61
|
232 self.setModel(self.libraryModel)
|
|
233 self.setViewMode(self.IconMode)
|
|
234 self.setDragDropMode(self.DragOnly)
|
54
|
235
|
81
|
236 def warning(txt):
|
|
237 QMessageBox.warning(None, "Warning", txt)
|
|
238
|
78
|
239 def loadModel(filename):
|
80
|
240 try:
|
81
|
241 m = ModelHierarchyModel()
|
80
|
242 with open(filename, 'r') as f: data = f.read()
|
81
|
243 m.filename = filename
|
80
|
244 m.Dict = json.loads(data)
|
81
|
245 return m
|
|
246 except KeyError:
|
|
247 warning('Corrupt model: {0}'.format(filename))
|
|
248 except ValueError:
|
|
249 warning('Corrupt model: {0}'.format(filename))
|
|
250 except FileNotFoundError:
|
|
251 warning('File [{0}] not found'.format(filename))
|
78
|
252
|
61
|
253 class Main(QMainWindow):
|
|
254 def __init__(self):
|
|
255 super(Main, self).__init__(None)
|
72
|
256 self.editor = EditorGraphicsView()
|
61
|
257 self.setCentralWidget(self.editor)
|
|
258 self.setWindowTitle("Diagram editor")
|
81
|
259 def buildIcon(b64):
|
|
260 icon = base64.decodestring(b64)
|
|
261 pm = QPixmap()
|
|
262 pm.loadFromData(icon)
|
|
263 return QIcon(pm)
|
61
|
264 toolbar = self.addToolBar('Tools')
|
81
|
265 toolbar.setObjectName('Tools')
|
|
266 def act(name, shortcut, callback, icon=None):
|
|
267 a = QAction(icon, name, self) if icon else QAction(name, self)
|
72
|
268 a.setShortcuts(shortcut)
|
|
269 a.triggered.connect(callback)
|
|
270 toolbar.addAction(a)
|
81
|
271 act('New', QKeySequence.New, self.editor.newModel, buildIcon(newicon))
|
|
272 act('Save', QKeySequence.Save, self.editor.save, buildIcon(saveicon))
|
|
273 act('Load', QKeySequence.Open, self.editor.load, buildIcon(loadicon))
|
72
|
274 act('Full screen', QKeySequence("F11"), self.toggleFullScreen)
|
|
275 act('Fit in view', QKeySequence("F8"), self.editor.zoomAll)
|
77
|
276 act('Go up', QKeySequence(Qt.Key_Up), self.editor.goUp)
|
86
|
277 act('Model code', QKeySequence("F7"), self.editor.showCode)
|
78
|
278 def addDock(name, widget):
|
|
279 dock = QDockWidget(name, self)
|
81
|
280 dock.setObjectName(name)
|
78
|
281 dock.setWidget(widget)
|
|
282 self.addDockWidget(Qt.LeftDockWidgetArea, dock)
|
|
283 addDock('Library', LibraryWidget())
|
80
|
284 addDock('Model tree', self.editor.treeView)
|
81
|
285 self.settings = QSettings('windelsoft', 'diagrameditor')
|
|
286 self.loadSettings()
|
44
|
287 def toggleFullScreen(self):
|
|
288 self.setWindowState(self.windowState() ^ Qt.WindowFullScreen)
|
61
|
289 self.editor.zoomAll()
|
81
|
290 def loadSettings(self):
|
|
291 if self.settings.contains('mainwindowstate'):
|
|
292 self.restoreState(self.settings.value('mainwindowstate'))
|
|
293 if self.settings.contains('mainwindowgeometry'):
|
|
294 self.restoreGeometry(self.settings.value('mainwindowgeometry'))
|
|
295 if self.settings.contains('openedmodel'):
|
|
296 modelfile = self.settings.value('openedmodel')
|
|
297 self.editor.model = loadModel(modelfile)
|
|
298 def closeEvent(self, ev):
|
|
299 self.settings.setValue('mainwindowstate', self.saveState())
|
|
300 self.settings.setValue('mainwindowgeometry', self.saveGeometry())
|
|
301 if self.editor.model and self.editor.model.filename:
|
|
302 self.settings.setValue('openedmodel', self.editor.model.filename)
|
|
303 # TODO: ask for save of opened files
|
|
304 else:
|
|
305 self.settings.remove('openedmodel')
|
|
306 ev.accept()
|
44
|
307
|
|
308 if __name__ == '__main__':
|
45
|
309 if sys.version_info.major != 3:
|
|
310 print('Please use python 3.x')
|
|
311 sys.exit(1)
|
46
|
312 app = QApplication(sys.argv)
|
61
|
313 main = Main()
|
|
314 main.show()
|
44
|
315 app.exec_()
|
|
316
|