view python/diagrameditor.py @ 129:9e350a7dde98

Changed architecture and updated the util
author Windel Bouwman
date Fri, 18 Jan 2013 12:52:11 +0100
parents 6efbeb903777
children
line wrap: on
line source

#!/usr/bin/python

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys, json, base64

from diagramitems import Connection, ResizeSelectionHandle, Block, DiagramScene, CodeBlock
from icons import newicon, saveicon, loadicon
import diagramitems

"""
 Author: Windel Bouwman
 Year: 2012
 Description: This script implements a diagram editor.
 run with python 3.x as:
  $ python [thisfile.py]
"""
def indent(lines):
   return ['   ' + line for line in lines]

class ParameterDialog(QDialog):
   def __init__(self, block, parent = None):
      super(ParameterDialog, self).__init__(parent)
      self.block = block
      self.button = QPushButton('Ok', self)
      self.nameEdit = QLineEdit(self.block.name)
      self.codeEdit = QTextEdit(self)
      self.codeEdit.setPlainText(self.block.code)
      l = QFormLayout(self)
      l.addRow('Name:', self.nameEdit)
      l.addRow('Code:', self.codeEdit)
      l.addWidget(self.button)
      self.button.clicked.connect(self.OK)
   def OK(self):
      self.block.name = self.nameEdit.text()
      self.block.code = self.codeEdit.toPlainText()
      self.close()

class EditorGraphicsView(QGraphicsView):
   def __init__(self, parent=None):
      QGraphicsView.__init__(self, parent)
      self.setDragMode(QGraphicsView.RubberBandDrag)
      self.delShort = QShortcut(QKeySequence.Delete, self)
      self._model = None
      self.treeView = QTreeView()
      self.treeView.clicked.connect(self.itemActivated)
   def itemActivated(self, idx):
      b = idx.internalPointer()
      s = b.scene()
      s.clearSelection()
      b.setSelected(True)
   def setDiagram(self, d):
      self.setScene(d)
      self.delShort.activated.connect(d.deleteItems)
   def getModel(self): 
      return self._model
   def setModel(self, m):
      self._model = m
      if m:
         self.treeView.setModel(m)
         self.diagram = m.rootDiagram
         self.model.modelReset.connect(self.treeView.expandAll)
   model = property(getModel, setModel)
   diagram = property(lambda s: s.scene(), setDiagram)
   def save(self):
      if self.model:
         if not self.model.filename:
            self.model.filename = QFileDialog.getSaveFileName(self)
         if self.model.filename:
            with open(self.model.filename, 'w') as f:
               f.write(json.dumps(self.model.Dict, indent=2))
   def load(self):
      filename = QFileDialog.getOpenFileName(self)
      if filename:
         self.model = loadModel(filename)
   def newModel(self):
      self.model = ModelHierarchyModel()
   def goUp(self):
      if hasattr(self.diagram, 'containingBlock'):
         self.diagram = self.diagram.containingBlock.scene()
         self.zoomAll()
   def showCode(self):
      if self.model:
         c = self.model.gencode()
         c = '\n'.join(c)
         d = QDialog()
         l = QFormLayout(d)
         codeview = QTextEdit()
         codeview.setPlainText(c)
         l.addRow('code', codeview)
         runButton = QPushButton('Run')
         outputview = QTextEdit()
         l.addRow('Output', outputview)
         l.addWidget(runButton)
         def print2(txt):
            txt2 = outputview.toPlainText()
            outputview.setPlainText(txt2 + '\n' + txt)
         def runIt():
            outputview.clear()
            globs = {'print': print2}
            exec(codeview.toPlainText(), globs)
         runButton.clicked.connect(runIt)
         d.exec_()
   def zoomAll(self):
      """ zoom to fit all items """
      rect = self.diagram.itemsBoundingRect()
      self.fitInView(rect, Qt.KeepAspectRatio)
   def wheelEvent(self, event):
      pos = event.pos()
      posbefore = self.mapToScene(pos)
      degrees = event.delta() / 8.0
      sx = (100.0 + degrees) / 100.0
      self.scale(sx, sx)
      event.accept()
   def dragEnterEvent(self, event):
      if event.mimeData().hasFormat('component/name'):
         event.accept()
   def dragMoveEvent(self, event):
      if event.mimeData().hasFormat('component/name'): event.accept()
   def dropEvent(self, event):
      if event.mimeData().hasFormat('component/name'):
         name = bytes(event.mimeData().data('component/name')).decode()
         kind, name = name.split(':')
         pos = self.mapToScene(event.pos())
         s = self.scene()
         if not s:
            return
         print(kind, 'name:', name)
         kind = getattr(diagramitems, kind)
         print(kind)
         b = kind(s.uniqify(name))
         b.setPos(pos)
         s.addItem(b)

class LibraryModel(QStandardItemModel):
   mimeTypes = lambda self: ['component/name']
   def mimeData(self, idxs):
      mimedata = QMimeData()
      for idx in idxs:
         if idx.isValid():
            txt = self.data(idx, Qt.DisplayRole)
            mimedata.setData('component/name', txt)
      return mimedata

class ModelHierarchyModel(QAbstractItemModel):
   def __init__(self):
      super(ModelHierarchyModel, self).__init__()
      self.rootDiagram = DiagramScene()
      self.rootDiagram.structureChanged.connect(self.handlechange)
      self.filename = None
   def handlechange(self):
      self.modelReset.emit()
   def setDict(self, d):
      self.rootDiagram.Dict = d
      self.modelReset.emit()
   def getDict(self):
      return self.rootDiagram.Dict
   Dict = property(getDict, setDict)
   def gencode(self):
      c = ['def topLevel():']
      c += indent(self.rootDiagram.gencode())
      c.append('print("Running model")')
      c.append('topLevel()')
      c.append('print("Done")')
      return c
   def index(self, row, column, parent=None):
      if parent.isValid():
         parent = parent.internalPointer().subModel
      else:
         parent = self.rootDiagram
      blocks = sorted(parent.blocks, key=lambda b:  b.name)
      block = blocks[row]
      # Store the index to retrieve it later in the parent function.
      # TODO: solve this in a better way.
      block.index = self.createIndex(row, column, block)
      return block.index
   def parent(self, index):
      if index.isValid():
         block = index.internalPointer()
         if block.scene() == self.rootDiagram:
            return QModelIndex()
         else:
            print(block)
            outerBlock = block.scene().containingBlock
            return outerBlock.index
      print('parent: No valid index')
   def data(self, index, role):
      if index.isValid() and role == Qt.DisplayRole:
         b = index.internalPointer()
         if index.column() == 0:
            return b.name
         elif index.column() == 1:
            return str(type(b))
   def headerData(self, section, orientation, role):
      if orientation == Qt.Horizontal and role == Qt.DisplayRole:
         if section == 0:
            return "Element"
         elif section == 1:
            return "Type"
         else:
            return "x"
   def rowCount(self, parent):
      if parent.column() > 0: 
         return 0
      if parent.isValid():
         block = parent.internalPointer()
         if hasattr(block, 'subModel'):
            return len(block.subModel.blocks)
         else:
            return 0
      else:
         return len(self.rootDiagram.blocks)
   def columnCount(self, parent):
      return 2

class LibraryWidget(QListView):
   def __init__(self):
      super(LibraryWidget, self).__init__(None)
      self.libraryModel = LibraryModel(self)
      self.libraryModel.setColumnCount(1)
      # Create an icon with an icon:
      pixmap = QPixmap(60, 60)
      pixmap.fill()
      painter = QPainter(pixmap)
      painter.fillRect(10, 10, 40, 40, Qt.blue)
      painter.setBrush(Qt.yellow)
      painter.drawEllipse(20, 20, 20, 20)
      painter.end()
      # Fill library:
      for name in ['CodeBlock:codeBlock', 'DiagramBlock:submod', 'Block:blk']:
         self.libraryModel.appendRow(QStandardItem(QIcon(pixmap), name))
      self.setModel(self.libraryModel)
      self.setViewMode(self.IconMode)
      self.setDragDropMode(self.DragOnly)

def warning(txt):
   QMessageBox.warning(None, "Warning", txt)

def loadModel(filename):
   try:
      m = ModelHierarchyModel()
      with open(filename, 'r') as f: data = f.read()
      m.filename = filename
      m.Dict = json.loads(data)
      return m
   except KeyError:
      warning('Corrupt model: {0}'.format(filename))
   except ValueError:
      warning('Corrupt model: {0}'.format(filename))
   except FileNotFoundError:
      warning('File [{0}] not found'.format(filename))

class Main(QMainWindow):
   def __init__(self):
      super(Main, self).__init__(None)
      self.editor = EditorGraphicsView()
      self.setCentralWidget(self.editor)
      self.setWindowTitle("Diagram editor")
      def buildIcon(b64):
         icon = base64.decodestring(b64)
         pm = QPixmap()
         pm.loadFromData(icon)
         return QIcon(pm)
      toolbar = self.addToolBar('Tools')
      toolbar.setObjectName('Tools')
      def act(name, shortcut, callback, icon=None):
         a = QAction(icon, name, self) if icon else QAction(name, self)
         a.setShortcuts(shortcut)
         a.triggered.connect(callback)
         toolbar.addAction(a)
      act('New', QKeySequence.New, self.editor.newModel, buildIcon(newicon))
      act('Save', QKeySequence.Save, self.editor.save, buildIcon(saveicon))
      act('Load', QKeySequence.Open, self.editor.load, buildIcon(loadicon))
      act('Full screen', QKeySequence("F11"), self.toggleFullScreen)
      act('Fit in view', QKeySequence("F8"), self.editor.zoomAll)
      act('Go up', QKeySequence(Qt.Key_Up), self.editor.goUp)
      act('Model code', QKeySequence("F7"), self.editor.showCode)
      def addDock(name, widget):
         dock = QDockWidget(name, self)
         dock.setObjectName(name)
         dock.setWidget(widget)
         self.addDockWidget(Qt.LeftDockWidgetArea, dock)
      addDock('Library', LibraryWidget())
      addDock('Model tree', self.editor.treeView)
      self.settings = QSettings('windelsoft', 'diagrameditor')
      self.loadSettings()
   def toggleFullScreen(self):
      self.setWindowState(self.windowState() ^ Qt.WindowFullScreen)
      self.editor.zoomAll()
   def loadSettings(self):
      if self.settings.contains('mainwindowstate'):
         self.restoreState(self.settings.value('mainwindowstate'))
      if self.settings.contains('mainwindowgeometry'):
         self.restoreGeometry(self.settings.value('mainwindowgeometry'))
      if self.settings.contains('openedmodel'):
         modelfile = self.settings.value('openedmodel')
         self.editor.model = loadModel(modelfile)
   def closeEvent(self, ev):
      self.settings.setValue('mainwindowstate', self.saveState())
      self.settings.setValue('mainwindowgeometry', self.saveGeometry())
      if self.editor.model and self.editor.model.filename:
         self.settings.setValue('openedmodel', self.editor.model.filename)
         # TODO: ask for save of opened files
      else:
         self.settings.remove('openedmodel')
      ev.accept()

if __name__ == '__main__':
   if sys.version_info.major != 3:
      print('Please use python 3.x')
      sys.exit(1)
   app = QApplication(sys.argv)
   main = Main()
   main.show()
   app.exec_()