Mercurial > lcfOS
comparison python/other/diagrameditor.py @ 290:7b38782ed496
File moves
author | Windel Bouwman |
---|---|
date | Sun, 24 Nov 2013 11:24:15 +0100 |
parents | python/diagrameditor.py@6efbeb903777 |
children | b77f3290ac79 |
comparison
equal
deleted
inserted
replaced
289:bd2593de3ff8 | 290:7b38782ed496 |
---|---|
1 #!/usr/bin/python | |
2 | |
3 from PyQt4.QtGui import * | |
4 from PyQt4.QtCore import * | |
5 import sys, json, base64 | |
6 | |
7 from diagramitems import Connection, ResizeSelectionHandle, Block, DiagramScene, CodeBlock | |
8 from icons import newicon, saveicon, loadicon | |
9 import diagramitems | |
10 | |
11 """ | |
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] | |
17 """ | |
18 def indent(lines): | |
19 return [' ' + line for line in lines] | |
20 | |
21 class ParameterDialog(QDialog): | |
22 def __init__(self, block, parent = None): | |
23 super(ParameterDialog, self).__init__(parent) | |
24 self.block = block | |
25 self.button = QPushButton('Ok', self) | |
26 self.nameEdit = QLineEdit(self.block.name) | |
27 self.codeEdit = QTextEdit(self) | |
28 self.codeEdit.setPlainText(self.block.code) | |
29 l = QFormLayout(self) | |
30 l.addRow('Name:', self.nameEdit) | |
31 l.addRow('Code:', self.codeEdit) | |
32 l.addWidget(self.button) | |
33 self.button.clicked.connect(self.OK) | |
34 def OK(self): | |
35 self.block.name = self.nameEdit.text() | |
36 self.block.code = self.codeEdit.toPlainText() | |
37 self.close() | |
38 | |
39 class EditorGraphicsView(QGraphicsView): | |
40 def __init__(self, parent=None): | |
41 QGraphicsView.__init__(self, parent) | |
42 self.setDragMode(QGraphicsView.RubberBandDrag) | |
43 self.delShort = QShortcut(QKeySequence.Delete, self) | |
44 self._model = None | |
45 self.treeView = QTreeView() | |
46 self.treeView.clicked.connect(self.itemActivated) | |
47 def itemActivated(self, idx): | |
48 b = idx.internalPointer() | |
49 s = b.scene() | |
50 s.clearSelection() | |
51 b.setSelected(True) | |
52 def setDiagram(self, d): | |
53 self.setScene(d) | |
54 self.delShort.activated.connect(d.deleteItems) | |
55 def getModel(self): | |
56 return self._model | |
57 def setModel(self, m): | |
58 self._model = m | |
59 if m: | |
60 self.treeView.setModel(m) | |
61 self.diagram = m.rootDiagram | |
62 self.model.modelReset.connect(self.treeView.expandAll) | |
63 model = property(getModel, setModel) | |
64 diagram = property(lambda s: s.scene(), setDiagram) | |
65 def save(self): | |
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: | |
71 f.write(json.dumps(self.model.Dict, indent=2)) | |
72 def load(self): | |
73 filename = QFileDialog.getOpenFileName(self) | |
74 if filename: | |
75 self.model = loadModel(filename) | |
76 def newModel(self): | |
77 self.model = ModelHierarchyModel() | |
78 def goUp(self): | |
79 if hasattr(self.diagram, 'containingBlock'): | |
80 self.diagram = self.diagram.containingBlock.scene() | |
81 self.zoomAll() | |
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_() | |
104 def zoomAll(self): | |
105 """ zoom to fit all items """ | |
106 rect = self.diagram.itemsBoundingRect() | |
107 self.fitInView(rect, Qt.KeepAspectRatio) | |
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() | |
115 def dragEnterEvent(self, event): | |
116 if event.mimeData().hasFormat('component/name'): | |
117 event.accept() | |
118 def dragMoveEvent(self, event): | |
119 if event.mimeData().hasFormat('component/name'): event.accept() | |
120 def dropEvent(self, event): | |
121 if event.mimeData().hasFormat('component/name'): | |
122 name = bytes(event.mimeData().data('component/name')).decode() | |
123 kind, name = name.split(':') | |
124 pos = self.mapToScene(event.pos()) | |
125 s = self.scene() | |
126 if not s: | |
127 return | |
128 print(kind, 'name:', name) | |
129 kind = getattr(diagramitems, kind) | |
130 print(kind) | |
131 b = kind(s.uniqify(name)) | |
132 b.setPos(pos) | |
133 s.addItem(b) | |
134 | |
135 class LibraryModel(QStandardItemModel): | |
136 mimeTypes = lambda self: ['component/name'] | |
137 def mimeData(self, idxs): | |
138 mimedata = QMimeData() | |
139 for idx in idxs: | |
140 if idx.isValid(): | |
141 txt = self.data(idx, Qt.DisplayRole) | |
142 mimedata.setData('component/name', txt) | |
143 return mimedata | |
144 | |
145 class ModelHierarchyModel(QAbstractItemModel): | |
146 def __init__(self): | |
147 super(ModelHierarchyModel, self).__init__() | |
148 self.rootDiagram = DiagramScene() | |
149 self.rootDiagram.structureChanged.connect(self.handlechange) | |
150 self.filename = None | |
151 def handlechange(self): | |
152 self.modelReset.emit() | |
153 def setDict(self, d): | |
154 self.rootDiagram.Dict = d | |
155 self.modelReset.emit() | |
156 def getDict(self): | |
157 return self.rootDiagram.Dict | |
158 Dict = property(getDict, setDict) | |
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 | |
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: | |
183 print(block) | |
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() | |
190 if index.column() == 0: | |
191 return b.name | |
192 elif index.column() == 1: | |
193 return str(type(b)) | |
194 def headerData(self, section, orientation, role): | |
195 if orientation == Qt.Horizontal and role == Qt.DisplayRole: | |
196 if section == 0: | |
197 return "Element" | |
198 elif section == 1: | |
199 return "Type" | |
200 else: | |
201 return "x" | |
202 def rowCount(self, parent): | |
203 if parent.column() > 0: | |
204 return 0 | |
205 if parent.isValid(): | |
206 block = parent.internalPointer() | |
207 if hasattr(block, 'subModel'): | |
208 return len(block.subModel.blocks) | |
209 else: | |
210 return 0 | |
211 else: | |
212 return len(self.rootDiagram.blocks) | |
213 def columnCount(self, parent): | |
214 return 2 | |
215 | |
216 class LibraryWidget(QListView): | |
217 def __init__(self): | |
218 super(LibraryWidget, self).__init__(None) | |
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: | |
230 for name in ['CodeBlock:codeBlock', 'DiagramBlock:submod', 'Block:blk']: | |
231 self.libraryModel.appendRow(QStandardItem(QIcon(pixmap), name)) | |
232 self.setModel(self.libraryModel) | |
233 self.setViewMode(self.IconMode) | |
234 self.setDragDropMode(self.DragOnly) | |
235 | |
236 def warning(txt): | |
237 QMessageBox.warning(None, "Warning", txt) | |
238 | |
239 def loadModel(filename): | |
240 try: | |
241 m = ModelHierarchyModel() | |
242 with open(filename, 'r') as f: data = f.read() | |
243 m.filename = filename | |
244 m.Dict = json.loads(data) | |
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)) | |
252 | |
253 class Main(QMainWindow): | |
254 def __init__(self): | |
255 super(Main, self).__init__(None) | |
256 self.editor = EditorGraphicsView() | |
257 self.setCentralWidget(self.editor) | |
258 self.setWindowTitle("Diagram editor") | |
259 def buildIcon(b64): | |
260 icon = base64.decodestring(b64) | |
261 pm = QPixmap() | |
262 pm.loadFromData(icon) | |
263 return QIcon(pm) | |
264 toolbar = self.addToolBar('Tools') | |
265 toolbar.setObjectName('Tools') | |
266 def act(name, shortcut, callback, icon=None): | |
267 a = QAction(icon, name, self) if icon else QAction(name, self) | |
268 a.setShortcuts(shortcut) | |
269 a.triggered.connect(callback) | |
270 toolbar.addAction(a) | |
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)) | |
274 act('Full screen', QKeySequence("F11"), self.toggleFullScreen) | |
275 act('Fit in view', QKeySequence("F8"), self.editor.zoomAll) | |
276 act('Go up', QKeySequence(Qt.Key_Up), self.editor.goUp) | |
277 act('Model code', QKeySequence("F7"), self.editor.showCode) | |
278 def addDock(name, widget): | |
279 dock = QDockWidget(name, self) | |
280 dock.setObjectName(name) | |
281 dock.setWidget(widget) | |
282 self.addDockWidget(Qt.LeftDockWidgetArea, dock) | |
283 addDock('Library', LibraryWidget()) | |
284 addDock('Model tree', self.editor.treeView) | |
285 self.settings = QSettings('windelsoft', 'diagrameditor') | |
286 self.loadSettings() | |
287 def toggleFullScreen(self): | |
288 self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) | |
289 self.editor.zoomAll() | |
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() | |
307 | |
308 if __name__ == '__main__': | |
309 if sys.version_info.major != 3: | |
310 print('Please use python 3.x') | |
311 sys.exit(1) | |
312 app = QApplication(sys.argv) | |
313 main = Main() | |
314 main.show() | |
315 app.exec_() | |
316 |