Mercurial > lcfOS
comparison python/diagrameditor.py @ 95:4a37d6992bd3
movage
author | windel |
---|---|
date | Mon, 24 Dec 2012 13:24:59 +0100 |
parents | python/apps/diagrameditor.py@f7ec7517cabb |
children | 6efbeb903777 |
comparison
equal
deleted
inserted
replaced
94:1be00bcfaabb | 95:4a37d6992bd3 |
---|---|
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.activated.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 print(kind, 'name:', name) | |
127 kind = getattr(diagramitems, kind) | |
128 print(kind) | |
129 b = kind(s.uniqify(name)) | |
130 b.setPos(pos) | |
131 s.addItem(b) | |
132 | |
133 class LibraryModel(QStandardItemModel): | |
134 mimeTypes = lambda self: ['component/name'] | |
135 def mimeData(self, idxs): | |
136 mimedata = QMimeData() | |
137 for idx in idxs: | |
138 if idx.isValid(): | |
139 txt = self.data(idx, Qt.DisplayRole) | |
140 mimedata.setData('component/name', txt) | |
141 return mimedata | |
142 | |
143 class ModelHierarchyModel(QAbstractItemModel): | |
144 def __init__(self): | |
145 super(ModelHierarchyModel, self).__init__() | |
146 self.rootDiagram = DiagramScene() | |
147 self.rootDiagram.structureChanged.connect(self.handlechange) | |
148 self.filename = None | |
149 def handlechange(self): | |
150 self.modelReset.emit() | |
151 def setDict(self, d): | |
152 self.rootDiagram.Dict = d | |
153 self.modelReset.emit() | |
154 def getDict(self): | |
155 return self.rootDiagram.Dict | |
156 Dict = property(getDict, setDict) | |
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 | |
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: | |
181 print(block) | |
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() | |
188 if index.column() == 0: | |
189 return b.name | |
190 elif index.column() == 1: | |
191 return str(type(b)) | |
192 def headerData(self, section, orientation, role): | |
193 if orientation == Qt.Horizontal and role == Qt.DisplayRole: | |
194 if section == 0: | |
195 return "Element" | |
196 elif section == 1: | |
197 return "Type" | |
198 else: | |
199 return "x" | |
200 def rowCount(self, parent): | |
201 if parent.column() > 0: | |
202 return 0 | |
203 if parent.isValid(): | |
204 block = parent.internalPointer() | |
205 if hasattr(block, 'subModel'): | |
206 return len(block.subModel.blocks) | |
207 else: | |
208 return 0 | |
209 else: | |
210 return len(self.rootDiagram.blocks) | |
211 def columnCount(self, parent): | |
212 return 2 | |
213 | |
214 class LibraryWidget(QListView): | |
215 def __init__(self): | |
216 super(LibraryWidget, self).__init__(None) | |
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: | |
228 for name in ['CodeBlock:codeBlock', 'DiagramBlock:submod', 'Block:blk']: | |
229 self.libraryModel.appendRow(QStandardItem(QIcon(pixmap), name)) | |
230 self.setModel(self.libraryModel) | |
231 self.setViewMode(self.IconMode) | |
232 self.setDragDropMode(self.DragOnly) | |
233 | |
234 def warning(txt): | |
235 QMessageBox.warning(None, "Warning", txt) | |
236 | |
237 def loadModel(filename): | |
238 try: | |
239 m = ModelHierarchyModel() | |
240 with open(filename, 'r') as f: data = f.read() | |
241 m.filename = filename | |
242 m.Dict = json.loads(data) | |
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)) | |
250 | |
251 class Main(QMainWindow): | |
252 def __init__(self): | |
253 super(Main, self).__init__(None) | |
254 self.editor = EditorGraphicsView() | |
255 self.setCentralWidget(self.editor) | |
256 self.setWindowTitle("Diagram editor") | |
257 def buildIcon(b64): | |
258 icon = base64.decodestring(b64) | |
259 pm = QPixmap() | |
260 pm.loadFromData(icon) | |
261 return QIcon(pm) | |
262 toolbar = self.addToolBar('Tools') | |
263 toolbar.setObjectName('Tools') | |
264 def act(name, shortcut, callback, icon=None): | |
265 a = QAction(icon, name, self) if icon else QAction(name, self) | |
266 a.setShortcuts(shortcut) | |
267 a.triggered.connect(callback) | |
268 toolbar.addAction(a) | |
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)) | |
272 act('Full screen', QKeySequence("F11"), self.toggleFullScreen) | |
273 act('Fit in view', QKeySequence("F8"), self.editor.zoomAll) | |
274 act('Go up', QKeySequence(Qt.Key_Up), self.editor.goUp) | |
275 act('Model code', QKeySequence("F7"), self.editor.showCode) | |
276 def addDock(name, widget): | |
277 dock = QDockWidget(name, self) | |
278 dock.setObjectName(name) | |
279 dock.setWidget(widget) | |
280 self.addDockWidget(Qt.LeftDockWidgetArea, dock) | |
281 addDock('Library', LibraryWidget()) | |
282 addDock('Model tree', self.editor.treeView) | |
283 self.settings = QSettings('windelsoft', 'diagrameditor') | |
284 self.loadSettings() | |
285 def toggleFullScreen(self): | |
286 self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) | |
287 self.editor.zoomAll() | |
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() | |
305 | |
306 if __name__ == '__main__': | |
307 if sys.version_info.major != 3: | |
308 print('Please use python 3.x') | |
309 sys.exit(1) | |
310 app = QApplication(sys.argv) | |
311 main = Main() | |
312 main.show() | |
313 app.exec_() | |
314 |