view python/other/diagrameditor.py @ 392:bb4289c84907

Added some sort of drop event test
author Windel Bouwman
date Fri, 16 May 2014 13:05:10 +0200
parents b77f3290ac79
children
line wrap: on
line source

#!/usr/bin/python

import sys
import json
import base64
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'ide'))

from qtwrapper import QtGui, QtCore, QtWidgets, pyqtSignal, get_icon
from qtwrapper import abspath, Qt

from diagramitems import Connection, ResizeSelectionHandle, Block, DiagramScene, CodeBlock
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(QtWidgets.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(QtWidgets.QGraphicsView):
   def __init__(self, parent=None):
      super().__init__(parent)
      self.setObjectName('Editor')
      self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
      self.delShort = QtWidgets.QShortcut(QtGui.QKeySequence.Delete, self)
      self._model = None
      self.treeView = QtWidgets.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 = QtWidgets.QFileDialog.getOpenFileName(self)
        if filename:
            self.model = loadModel(filename)

   def newModel(self):
        print('NEW')
        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.angleDelta().y() / 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(QtGui.QStandardItemModel):
    def __init__(self, parent):
        super().__init__(parent)
        self.setObjectName('Library')

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


class ModelHierarchyModel(QtCore.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 QtCore.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(QtWidgets.QListView):
    def __init__(self):
      super().__init__()
      self.setObjectName('LibraryWidget')
      self.libraryModel = LibraryModel(self)
      self.libraryModel.setColumnCount(1)
      # Create an icon with an icon:
      pixmap = QtGui.QPixmap(60, 60)
      pixmap.fill()
      painter = QtGui.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(QtGui.QStandardItem(QtGui.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(QtWidgets.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 = QtWidgets.QAction(icon, name, self) if icon else QtWidgets.QAction(name, self)
         a.setShortcuts(shortcut)
         a.triggered.connect(callback)
         toolbar.addAction(a)
      act('New', QtGui.QKeySequence.New, self.editor.newModel)
      act('Save', QtGui.QKeySequence.Save, self.editor.save)
      act('Load', QtGui.QKeySequence.Open, self.editor.load)
      act('Full screen', QtGui.QKeySequence("F11"), self.toggleFullScreen)
      act('Fit in view', QtGui.QKeySequence("F8"), self.editor.zoomAll)
      act('Go up', QtGui.QKeySequence(Qt.Key_Up), self.editor.goUp)
      act('Model code', QtGui.QKeySequence("F7"), self.editor.showCode)
      def addDock(name, widget):
         dock = QtWidgets.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 = QtCore.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 = QtWidgets.QApplication(sys.argv)
    main = Main()
    main.show()
    app.exec_()