Mercurial > lcfOS
changeset 390:b77f3290ac79
Added diagram editor tests
author | Windel Bouwman |
---|---|
date | Fri, 16 May 2014 10:12:16 +0200 |
parents | e07c2a9abac1 |
children | a139da1f44f6 |
files | python/other/diagrameditor.py python/other/diagramitems.py test/testdiagrameditor.py |
diffstat | 3 files changed, 235 insertions(+), 106 deletions(-) [+] |
line wrap: on
line diff
--- a/python/other/diagrameditor.py Fri May 02 14:51:46 2014 +0200 +++ b/python/other/diagrameditor.py Fri May 16 10:12:16 2014 +0200 @@ -1,11 +1,16 @@ #!/usr/bin/python -from PyQt4.QtGui import * -from PyQt4.QtCore import * -import sys, json, base64 +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 -from icons import newicon, saveicon, loadicon import diagramitems """ @@ -15,10 +20,13 @@ run with python 3.x as: $ python [thisfile.py] """ + + def indent(lines): return [' ' + line for line in lines] -class ParameterDialog(QDialog): + +class ParameterDialog(QtWidgets.QDialog): def __init__(self, block, parent = None): super(ParameterDialog, self).__init__(parent) self.block = block @@ -36,24 +44,30 @@ self.block.code = self.codeEdit.toPlainText() self.close() -class EditorGraphicsView(QGraphicsView): + +class EditorGraphicsView(QtWidgets.QGraphicsView): def __init__(self, parent=None): - QGraphicsView.__init__(self, parent) - self.setDragMode(QGraphicsView.RubberBandDrag) - self.delShort = QShortcut(QKeySequence.Delete, self) + super().__init__(parent) + self.setObjectName('Editor') + self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag) + self.delShort = QtWidgets.QShortcut(QtGui.QKeySequence.Delete, self) self._model = None - self.treeView = QTreeView() + 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: @@ -70,15 +84,19 @@ 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) + filename = QtWidgets.QFileDialog.getOpenFileName(self) + if filename: + self.model = loadModel(filename) + def newModel(self): - self.model = ModelHierarchyModel() + 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() @@ -101,10 +119,12 @@ 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) @@ -112,11 +132,15 @@ sx = (100.0 + degrees) / 100.0 self.scale(sx, sx) event.accept() + def dragEnterEvent(self, event): - if event.mimeData().hasFormat('component/name'): - event.accept() + if event.mimeData().hasFormat('component/name'): + event.accept() + def dragMoveEvent(self, event): - if event.mimeData().hasFormat('component/name'): event.accept() + 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() @@ -132,29 +156,39 @@ 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 LibraryModel(QtGui.QStandardItemModel): + def __init__(self, parent): + super().__init__(parent) + self.setObjectName('Library') -class ModelHierarchyModel(QAbstractItemModel): + 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():'] @@ -163,6 +197,7 @@ c.append('topLevel()') c.append('print("Done")') return c + def index(self, row, column, parent=None): if parent.isValid(): parent = parent.internalPointer().subModel @@ -174,6 +209,7 @@ # 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() @@ -184,6 +220,7 @@ 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() @@ -191,6 +228,7 @@ 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: @@ -199,6 +237,7 @@ return "Type" else: return "x" + def rowCount(self, parent): if parent.column() > 0: return 0 @@ -210,32 +249,37 @@ 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) + +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 = QPixmap(60, 60) + pixmap = QtGui.QPixmap(60, 60) pixmap.fill() - painter = QPainter(pixmap) + 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(QStandardItem(QIcon(pixmap), name)) + 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() @@ -250,8 +294,9 @@ except FileNotFoundError: warning('File [{0}] not found'.format(filename)) -class Main(QMainWindow): - def __init__(self): + +class Main(QtWidgets.QMainWindow): + def __init__(self): super(Main, self).__init__(None) self.editor = EditorGraphicsView() self.setCentralWidget(self.editor) @@ -264,30 +309,32 @@ 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 = QtWidgets.QAction(icon, name, self) if icon else QtWidgets.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) + 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 = QDockWidget(name, self) + 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 = QSettings('windelsoft', 'diagrameditor') + self.settings = QtCore.QSettings('windelsoft', 'diagrameditor') self.loadSettings() - def toggleFullScreen(self): - self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) - self.editor.zoomAll() - def loadSettings(self): + + 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'): @@ -295,7 +342,8 @@ if self.settings.contains('openedmodel'): modelfile = self.settings.value('openedmodel') self.editor.model = loadModel(modelfile) - def closeEvent(self, ev): + + 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: @@ -309,7 +357,7 @@ if sys.version_info.major != 3: print('Please use python 3.x') sys.exit(1) - app = QApplication(sys.argv) + app = QtWidgets.QApplication(sys.argv) main = Main() main.show() app.exec_()
--- a/python/other/diagramitems.py Fri May 02 14:51:46 2014 +0200 +++ b/python/other/diagramitems.py Fri May 16 10:12:16 2014 +0200 @@ -2,43 +2,55 @@ Contains all blocks that can be used to build models. """ -from PyQt4.QtGui import * -from PyQt4.QtCore import * +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 + def uniqify(name, names): - newname, i = name, 1 - while newname in names: newname, i = name + str(i), i + 1 - return newname + newname, i = name, 1 + while newname in names: newname, i = name + str(i), i + 1 + return newname + def enum(**enums): - return type('Enum', (), enums) + return type('Enum', (), enums) + Position = enum(TOP=0, TOP_RIGHT=1, RIGHT=2, BOTTOM_RIGHT=3, BOTTOM=4, BOTTOM_LEFT=5, LEFT=6, TOP_LEFT=7) + def buildPath(pts): - path = QPainterPath(pts[0]) - for pt in pts[1:]: path.lineTo(pt) - return path + path = QtGui.QPainterPath(pts[0]) + for pt in pts[1:]: path.lineTo(pt) + return path def equalSpace(n, l, offset=15): - if n == 1: - return [l / 2] - elif n > 1: - return [offset + (l - offset*2)/(n - 1)*i for i in range(n)] - return [] + if n == 1: + return [l / 2] + elif n > 1: + return [offset + (l - offset*2)/(n - 1)*i for i in range(n)] + return [] -class Connection(QGraphicsPathItem): + +class Connection(QtWidgets.QGraphicsPathItem): """ A connection between blocks """ def __init__(self, fromPort=None, toPort=None): super(Connection, self).__init__() self.pos2 = self.fromPort = self.toPort = None self.setFlags(self.ItemIsSelectable | self.ItemClipsToShape) - pen = QPen(Qt.blue, 2, cap=Qt.RoundCap) + pen = QtGui.QPen(Qt.blue, 2, cap=Qt.RoundCap) self.setPen(pen) - self.arrowhead = QGraphicsPathItem(self) + self.arrowhead = QtGui.QGraphicsPathItem(self) self.arrowhead.setPath(buildPath([QPointF(0.0, 0.0), QPointF(-6.0, 10.0), QPointF(6.0, 10.0), QPointF(0.0, 0.0)])) self.arrowhead.setPen(pen) - self.arrowhead.setBrush(QBrush(pen.color())) + self.arrowhead.setBrush(QtGui.QBrush(pen.color())) self.vias = [] self.setFromPort(fromPort) self.setToPort(toPort) @@ -99,7 +111,7 @@ scene = self.scene() vias = [pos1 + QPointF(20, 0)] + self.vias + [pos2 + QPointF(-20, 0)] if scene: - litem = QGraphicsLineItem() + litem = QtGui.QGraphicsLineItem() litem.setFlags(self.ItemIsSelectable) scene.addItem(litem) for p1, p2 in zip(vias[:-1], vias[1:]): @@ -115,19 +127,19 @@ self.setPath(p) """ Create a shape outline using the path stroker """ s = super(Connection, self).shape() - pps = QPainterPathStroker() + pps = QtGui.QPainterPathStroker() pps.setWidth(10) self.myshape = pps.createStroke(s).simplified() -class PortItem(QGraphicsPathItem): +class PortItem(QtWidgets.QGraphicsPathItem): """ Represents a port to a subsystem """ def __init__(self, name, block): super(PortItem, self).__init__(block) - self.textItem = QGraphicsTextItem(self) + self.textItem = QtGui.QGraphicsTextItem(self) self.connection = None self.block = block - self.setCursor(QCursor(Qt.CrossCursor)) - self.setPen(QPen(Qt.blue, 2, cap=Qt.RoundCap)) + self.setCursor(QtGui.QCursor(Qt.CrossCursor)) + self.setPen(QtGui.QPen(Qt.blue, 2, cap=Qt.RoundCap)) self.name = name self.posCallbacks = [] self.setFlag(self.ItemSendsScenePositionChanges, True) @@ -160,15 +172,15 @@ super(InputPort, self).__init__(name, block) self.setPath(buildPath([QPointF(-d, -d), QPointF(0, 0), QPointF(-d, d)])) -class Handle(QGraphicsEllipseItem): +class Handle(QtWidgets.QGraphicsEllipseItem): """ A handle that can be moved by the mouse """ def __init__(self, dx=10.0, parent=None): - super(Handle, self).__init__(QRectF(-0.5*dx,-0.5*dx,dx,dx), parent) - self.setBrush(QBrush(Qt.white)) + super(Handle, self).__init__(QtCore.QRectF(-0.5*dx,-0.5*dx,dx,dx), parent) + self.setBrush(QtGui.QBrush(Qt.white)) self.setFlags(self.ItemIsMovable) self.setZValue(1) self.setVisible(False) - self.setCursor(QCursor(Qt.SizeFDiagCursor)) + self.setCursor(QtGui.QCursor(Qt.SizeFDiagCursor)) def mouseMoveEvent(self, event): """ Move function without moving the other selected elements """ p = self.mapToParent(event.pos()) @@ -180,38 +192,38 @@ self.position = position self.block = block if position in [Position.TOP_LEFT, Position.BOTTOM_RIGHT]: - self.setCursor(QCursor(Qt.SizeFDiagCursor)) + self.setCursor(QtGui.QCursor(Qt.SizeFDiagCursor)) elif position in [Position.TOP_RIGHT, Position.BOTTOM_LEFT]: self.setCursor(QCursor(Qt.SizeBDiagCursor)) elif position in [Position.TOP, Position.BOTTOM]: self.setCursor(QCursor(Qt.SizeVerCursor)) elif position in [Position.LEFT, Position.RIGHT]: - self.setCursor(QCursor(Qt.SizeHorCursor)) + self.setCursor(QtGui.QCursor(Qt.SizeHorCursor)) def mouseMoveEvent(self, event): self.block.sizerMoveEvent(self, event.scenePos()) -class Block(QGraphicsRectItem): +class Block(QtWidgets.QGraphicsRectItem): """ Represents a block in the diagram. """ def __init__(self, name='Untitled', parent=None): super(Block, self).__init__(parent) self.selectionHandles = [ResizeSelectionHandle(i, self) for i in range(8)] # Properties of the rectangle: - self.setPen(QPen(Qt.blue, 2)) - self.setBrush(QBrush(Qt.lightGray)) + self.setPen(QtGui.QPen(Qt.blue, 2)) + self.setBrush(QtGui.QBrush(Qt.lightGray)) self.setFlags(self.ItemIsSelectable | self.ItemIsMovable | self.ItemSendsScenePositionChanges) - self.setCursor(QCursor(Qt.PointingHandCursor)) + self.setCursor(QtGui.QCursor(Qt.PointingHandCursor)) self.setAcceptHoverEvents(True) - self.label = QGraphicsTextItem(name, self) + self.label = QtWidgets.QGraphicsTextItem(name, self) self.name = name # Create corner for resize: - button = QPushButton('+in') + button = QtWidgets.QPushButton('+in') button.clicked.connect(self.newInputPort) - self.buttonItemAddInput = QGraphicsProxyWidget(self) + self.buttonItemAddInput = QtWidgets.QGraphicsProxyWidget(self) self.buttonItemAddInput.setWidget(button) self.buttonItemAddInput.setVisible(False) - button = QPushButton('+out') + button = QtWidgets.QPushButton('+out') button.clicked.connect(self.newOutputPort) - self.buttonItemAddOutput = QGraphicsProxyWidget(self) + self.buttonItemAddOutput = QtWidgets.QGraphicsProxyWidget(self) self.buttonItemAddOutput.setWidget(button) self.buttonItemAddOutput.setVisible(False) # Inputs and outputs of the block: @@ -329,18 +341,22 @@ self.label.setPos((w - rect.width()) / 2, (h - rect.height()) / 2) self.updateSize() + class CodeBlock(Block): - def __init__(self, name='Untitled', parent=None): - super(CodeBlock, self).__init__(name, parent) - self.code = '' - def setDict(self, d): - super(CodeBlock, self).setDict(d) - self.code = d['code'] - def getDict(self): - d = super(CodeBlock, self).getDict() - d['code'] = self.code - return d - def gencode(self): + def __init__(self, name='Untitled', parent=None): + super(CodeBlock, self).__init__(name, parent) + self.code = '' + + def setDict(self, d): + super(CodeBlock, self).setDict(d) + self.code = d['code'] + + def getDict(self): + d = super(CodeBlock, self).getDict() + d['code'] = self.code + return d + + def gencode(self): c = ['def {0}():'.format(self.name)] if self.code: c += indent(self.code.split('\n')) @@ -348,14 +364,17 @@ c += indent(['pass']) return c + class DiagramBlock(Block): - def __init__(self, name='Untitled', parent=None): + def __init__(self, name='Untitled', parent=None): super(DiagramBlock, self).__init__(name, parent) self.subModel = DiagramScene() self.subModel.containingBlock = self - def setDict(self, d): - self.subModel.Dict = d['submodel'] - def mouseDoubleClickEvent(self, event): + + def setDict(self, d): + self.subModel.Dict = d['submodel'] + + def mouseDoubleClickEvent(self, event): # descent into child diagram #self.editParameters() print('descent') @@ -365,7 +384,8 @@ view.diagram = self.subModel view.zoomAll() -class DiagramScene(QGraphicsScene): + +class DiagramScene(QtWidgets.QGraphicsScene): """ A diagram scene consisting of blocks and connections """ structureChanged = pyqtSignal() def __init__(self):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/testdiagrameditor.py Fri May 16 10:12:16 2014 +0200 @@ -0,0 +1,61 @@ + +import unittest +import os +import sys +import time + +try: + otherpath = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'python','other')) + sys.path.insert(0, otherpath) + import diagrameditor + + from PyQt5.QtWidgets import QApplication + from PyQt5.QtTest import QTest + from PyQt5.QtCore import Qt, QTimer + skip_it = False + + # When creating an app per testcase, this fails horribly.. + app = QApplication(sys.argv) +except ImportError as e: + skip_it = True + + + +class DiagramEditorTestCase(unittest.TestCase): + def setUp(self): + if skip_it: + self.skipTest('No qt5 or X server') + return + #print('Instance:', QApplication.instance()) + self.main = diagrameditor.Main() + self.main.show() + QTest.qWaitForWindowActive(self.main) + + def tearDown(self): + QTimer.singleShot(100, app.quit) + app.exec_() + + def cmdNewModel(self): + # Press ctrl+N: + QTest.keyClick(self.main, Qt.Key_N, Qt.ControlModifier) + + def dragItemIntoScene(self): + library = self.main.findChild(diagrameditor.LibraryWidget, 'LibraryWidget') + editor = self.main.findChild(diagrameditor.EditorGraphicsView, 'Editor') + #ilibrary. + QTest.mousePress(library, Qt.LeftButton) + print(editor, type(editor)) + QTest.mouseMove(editor) + QTest.mouseRelease(editor, Qt.LeftButton) + + def testScenario1(self): + self.cmdNewModel() + self.dragItemIntoScene() + + def testB(self): + print('b') + + +if __name__ == '__main__': + unittest.main() +