Пример #1
0
class EDict(dict):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.stack = QUndoStack()

    def _realSet(self, key, val):
        super().__setitem__(key, val)

    def __setitem__(self, key, val):
        if key in self.keys():
            self.stack.push(ModifyCommand(self, key, val))
        else:
            self.stack.push(AppendCommand(self, key, val))

    def undoText(self):
        return self.stack.undoText()

    def redoText(self):
        return self.stack.redoText()

    def undo(self):
        self.stack.undo()

    def redo(self):
        self.stack.redo()
Пример #2
0
class MainWindow(QMainWindow):
    _tr = QCoreApplication.translate

    def __init__(self, parent=None):
        super().__init__(parent)  # 调用父类构造函数,创建窗体
        self.__scene = None  # 创建QGraphicsScene
        self.__view = None  # 创建图形视图组件
        self.ui = Ui_MainWindow()  # 创建UI对象
        self.ui.setupUi(self)  # 构造UI界面
        self.operatorFile = OperatorFile(self)

        self.__translator = None
        title = self.tr("基于Python的图的绘制及相关概念的可视化展示")
        self.setWindowTitle(title)

        self.ui.nodeDetails.setEnabled(False)
        self.ui.edgeDetails.setEnabled(False)
        self.ui.actionSave.setEnabled(False)

        self.edgeModel = QStandardItemModel(5, 5, self)
        self.edgeSelectionModel = QItemSelectionModel(self.edgeModel)
        self.edgeModel.dataChanged.connect(self.do_updateEdgeWeight)

        self.nodeModel = QStandardItemModel(5, 4, self)
        self.nodeSelectionModel = QItemSelectionModel(self.nodeModel)
        self.nodeModel.dataChanged.connect(self.do_updateNodeWeight)

        self.spinWeight = WeightSpinDelegate(0, 200, 1, self)

        self.ui.tabWidget.setVisible(False)
        self.ui.tabWidget.clear()
        self.ui.tabWidget.setTabsClosable(True)
        self.ui.tabWidget.setDocumentMode(True)
        self.setCentralWidget(self.ui.tabWidget)
        self.setAutoFillBackground(True)

        self.__buildStatusBar()  # 构造状态栏
        self.__buildUndoCommand()  # 初始化撤销重做系统
        self.__initModeMenu()
        self.__lastColumnFlag = Qt.NoItemFlags

        self.iniGraphicsSystem()

        self.__ItemId = 0  # 绘图项自定义数据的key
        self.__ItemDesc = 1  # 绘图项自定义数据的key

        self.__nodeNum = 0  # 结点的序号
        self.__edgeNum = 0  # 边的序号
        self.__textNum = 0

        self.lastColumnFlags = (Qt.ItemIsSelectable | Qt.ItemIsUserCheckable
                                | Qt.ItemIsEnabled)

        self.__graph = Graph()

    ##  ==============自定义功能函数============

    def nodeNum(self):
        return self.__nodeNum

    def edgeNum(self):
        return self.__edgeNum

    def scene(self):
        self.viewAndScene()
        return self.__scene

    def view(self):
        self.viewAndScene()
        return self.__view

    def graph(self):
        return self.__graph

    def __buildStatusBar(self):  ##构造状态栏
        self.__labViewCord = QLabel(self._tr("MainWindow", "视图坐标:"))
        self.__labViewCord.setMinimumWidth(150)
        self.ui.statusbar.addWidget(self.__labViewCord)

        self.__labSceneCord = QLabel(self._tr("MainWindow", "场景坐标:"))
        self.__labSceneCord.setMinimumWidth(150)
        self.ui.statusbar.addWidget(self.__labSceneCord)

        self.__labItemCord = QLabel(self._tr("MainWindow", "图元坐标:"))
        self.__labItemCord.setMinimumWidth(150)
        self.ui.statusbar.addWidget(self.__labItemCord)

        self.__labItemInfo = QLabel(self._tr("MainWindow", "图元信息: "))
        self.ui.statusbar.addPermanentWidget(self.__labItemInfo)
        self.__labModeInfo = QLabel(self._tr("MainWindow", "有向图模式"))
        self.ui.statusbar.addPermanentWidget(self.__labModeInfo)

    def __buildUndoCommand(self):
        self.undoStack = QUndoStack()
        self.addAction(self.ui.actionUndo)
        self.addAction(self.ui.actionRedo)
        self.ui.undoView.setStack(self.undoStack)

    def __setItemProperties(self, item, desc):  ##item是具体类型的QGraphicsItem
        self.__nodeNum = len(self.singleItems(BezierNode))
        self.__edgeNum = len(self.singleItems(BezierEdge))
        self.__textNum = len(self.singleItems(BezierText))
        item.setFlag(QGraphicsItem.ItemIsFocusable)
        item.setFlag(QGraphicsItem.ItemIsMovable)
        item.setFlag(QGraphicsItem.ItemIsSelectable)
        item.setPos(-150 + randint(1, 200), -200 + randint(1, 200))

        if type(item) is BezierNode:
            newNum = self.checkSort(self.__scene.uniqueIdList(BezierNode))
            if newNum is not None:
                item.setData(self.__ItemId, newNum)
                item.textCp.setPlainText("V" + str(newNum))
            else:
                item.setData(self.__ItemId, self.__nodeNum)
                item.textCp.setPlainText("V" + str(self.__nodeNum))
            self.__nodeNum = 1 + self.__nodeNum
        elif type(item) is BezierEdge:
            newNum = self.checkSort(self.__scene.uniqueIdList(BezierEdge))
            if newNum is not None:
                item.setData(self.__ItemId, newNum)
                item.textCp.setPlainText("e" + str(newNum))
            else:
                item.setData(self.__ItemId, self.__edgeNum)
                item.textCp.setPlainText("e" + str(self.__edgeNum))
            self.__edgeNum = 1 + self.__edgeNum
        elif type(item) is BezierText:
            newNum = self.checkSort(self.__scene.uniqueIdList(BezierText))
            if newNum is not None:
                item.setData(self.__ItemId, newNum)
            else:
                item.setData(self.__ItemId, self.__textNum)
            self.__textNum = 1 + self.__textNum

        item.setData(self.__ItemDesc, desc)  # 图件描述

        self.__scene.addItem(item)
        self.__scene.clearSelection()

        item.setSelected(True)

    def __setBrushColor(self, item):  ##设置填充颜色
        color = item.brush().__color()
        color = QColorDialog.getColor(color, self,
                                      self._tr("MainWindow", "选择填充颜色"))
        if color.isValid():
            item.setBrush(QBrush(color))

    def __initFileMenu(self):
        self.ui.actionOpen.triggered.connect(self.do_open_file)
        self.ui.actionSave.triggered.connect(self.do_save_file)
        self.ui.actionQuit.triggered.connect(self.close)

    def __initModeMenu(self):
        modeMenuGroup = QActionGroup(self)
        modeMenuGroup.addAction(self.ui.actionDigraph_Mode)
        modeMenuGroup.addAction(self.ui.actionRedigraph_Mode)

    def __updateEdgeView(self):
        edges = self.singleItems(BezierEdge)
        if len(edges):
            self.ui.edgeDetails.setEnabled(True)
        else:
            return
        edgeColCount = 5

        self.edgeModel.clear()
        edgeHeaderList = [
            self._tr("MainWindow", 'ID'),
            self._tr("MainWindow", '始点'),
            self._tr("MainWindow", '终点'),
            self._tr("MainWindow", '坐标'),
            self._tr("MainWindow", '权重')
        ]
        self.edgeModel.setHorizontalHeaderLabels(edgeHeaderList)
        self.edgeSelectionModel.currentChanged.connect(self.do_curEdgeChanged)

        self.ui.edgeDetails.setModel(self.edgeModel)
        self.ui.edgeDetails.setSelectionModel(self.edgeSelectionModel)
        self.ui.edgeDetails.verticalHeader().setSectionResizeMode(
            QHeaderView.Fixed)
        self.ui.edgeDetails.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)
        self.ui.edgeDetails.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)
        self.ui.edgeDetails.setAlternatingRowColors(True)
        self.edgeModel.setRowCount(len(edges))
        self.ui.edgeDetails.setItemDelegateForColumn(4, self.spinWeight)
        edges.reverse()
        for i in range(len(edges)):
            edge: BezierEdge = edges[i]
            sourceNode = f'V{edge.sourceNode.data(self.__ItemId)}' if edge.sourceNode else None
            destNode = f'V{edge.destNode.data(self.__ItemId)}' if edge.destNode else None

            strList = [
                f"e{edge.data(self.__ItemId)}", sourceNode, destNode,
                f"x:{edge.pos().x()},y:{edge.pos().y()}", f"{edge.weight()}"
            ]

            for j in range(edgeColCount):
                item = QStandardItem(strList[j])
                if j != edgeColCount - 1:
                    item.setFlags(self.__lastColumnFlag)
                self.edgeModel.setItem(i, j, item)

    def __updateNodeView(self):
        nodes = self.singleItems(BezierNode)
        if len(nodes):
            self.ui.nodeDetails.setEnabled(True)
        else:
            return
        nodeColCount = 4
        self.nodeModel.clear()
        nodeHeaderList = [
            self._tr("MainWindow", 'ID'),
            self._tr("MainWindow", '边数'),
            self._tr("MainWindow", '坐标'),
            self._tr("MainWindow", '权重')
        ]
        if self.ui.actionDigraph_Mode.isChecked():
            nodeHeaderList.append(self._tr("MainWindow", '出度'))
            nodeHeaderList.append(self._tr("MainWindow", '入度'))
            nodeColCount += 2
        else:
            nodeHeaderList.append(self._tr("MainWindow", "度"))
            nodeColCount += 1
        self.nodeModel.setHorizontalHeaderLabels(nodeHeaderList)
        self.nodeModel.setRowCount(len(nodes))
        self.nodeSelectionModel.currentChanged.connect(self.do_curNodeChanged)
        self.ui.nodeDetails.setModel(self.nodeModel)
        self.ui.nodeDetails.setSelectionModel(self.nodeSelectionModel)
        self.ui.nodeDetails.verticalHeader().setSectionResizeMode(
            QHeaderView.Fixed)
        self.ui.nodeDetails.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)
        self.ui.nodeDetails.setAlternatingRowColors(True)
        self.ui.nodeDetails.setItemDelegateForColumn(3, self.spinWeight)
        nodes.reverse()
        for i in range(len(nodes)):
            node: BezierNode = nodes[i]
            strList = [
                f"V{node.data(self.__ItemId)}",
                str(len(node.bezierEdges)),
                f"x:{node.pos().x()},y:{node.pos().y()}",
                str(node.weight())
            ]
            if self.ui.actionDigraph_Mode.isChecked():
                strList.append(
                    f'{node.degrees(self.ui.actionDigraph_Mode.isChecked())[1]}'
                )
                strList.append(
                    f'{node.degrees(self.ui.actionDigraph_Mode.isChecked())[0]}'
                )
            else:
                strList.append(
                    f'{node.degrees(self.ui.actionDigraph_Mode.isChecked())}')
            for j in range(nodeColCount):
                item = QStandardItem(strList[j])
                if j != 3:
                    item.setFlags(self.__lastColumnFlag)
                self.nodeModel.setItem(i, j, item)

    def iniGraphicsSystem(self, name=None):  ##初始化 Graphics View系统
        scene = GraphicsScene()  # 创建QGraphicsScene
        view = GraphicsView(self, scene)  # 创建图形视图组件
        view.mouseMove.connect(self.do_mouseMove)  # 鼠标移动
        view.mouseClicked.connect(self.do_mouseClicked)  # 左键按下
        scene.itemMoveSignal.connect(self.do_shapeMoved)
        scene.itemLock.connect(self.do_nodeLock)
        scene.isHasItem.connect(self.do_checkIsHasItems)
        if name:
            title = name
        else:
            text = self.tr('未命名')
            title = f'{text}{self.ui.tabWidget.count()}'
        curIndex = self.ui.tabWidget.addTab(view, title)
        self.ui.tabWidget.setCurrentIndex(curIndex)
        self.ui.tabWidget.setVisible(True)

        ##  4个信号与槽函数的关联

        # self.view.mouseDoubleClick.connect(self.do_mouseDoubleClick)  # 鼠标双击
        # self.view.keyPress.connect(self.do_keyPress)  # 左键按下

    def singleItems(self, className) -> list:
        self.viewAndScene()
        return self.__scene.singleItems(className)

    def connectGraph(self):
        self.__graph.setMode(self.ui.actionDigraph_Mode.isChecked())
        items = self.__scene.uniqueItems()
        nodeList = []
        edgeList = []
        if not len(items):
            return
        for item in items:
            if type(item) is BezierNode:
                nodeList.append(item)
            elif type(item) is BezierEdge:
                edgeList.append(item)
        for node in nodeList:
            self.__graph.addVertex(node.data(self.__ItemId))

        badEdgeList = []
        for i in range(len(edgeList)):
            for edge in edgeList:
                edge: BezierEdge
                if edge.data(self.__ItemId) == i:
                    if edge.sourceNode and edge.destNode:
                        self.__graph.addEdge(
                            edge.sourceNode.data(self.__ItemId),
                            edge.destNode.data(self.__ItemId), edge.weight())
                    else:
                        badEdgeList.append(edge)

        if len(badEdgeList) != 0:
            self.disconnectGraph()
            string = ""
            for x in range(len(badEdgeList)):
                demo = "、"
                if x == len(badEdgeList) - 1:
                    demo = ""
                string = f'{string}e{badEdgeList[x].data(self.__ItemId)}{demo}'
            QMessageBox.warning(
                self, self._tr("MainWindow", "连接故障!"),
                self._tr("MainWindow", "警告,") + string +
                self._tr("MainWindow", "的连接不完整"))
            return False

        return True

    def disconnectGraph(self):
        self.__graph.clearAllData()

    def viewAndScene(self):
        if self.ui.tabWidget.count():
            self.__view: GraphicsView = self.ui.tabWidget.currentWidget()
            self.__scene = self.__view.scene()

    def standardGraphData(self):
        mode = int(self.ui.actionDigraph_Mode.isChecked())
        nodes = self.__scene.singleItems(BezierNode)
        edges = self.__scene.singleItems(BezierEdge)
        texts = self.__scene.singleItems(BezierText)
        nodeDataList = []
        edgeDataList = []
        textDataList = []
        for node in nodes:
            node: BezierNode
            data = [
                node.data(self.__ItemId),
                node.weight(),
                node.pos().x(),
                node.pos().y()
            ]
            nodeDataList.append(data)

        for edge in edges:
            edge: BezierEdge
            data = [edge.data(self.__ItemId)]
            if edge.sourceNode:
                data.append(edge.sourceNode.data(self.__ItemId))
            else:
                data.append(-1)
            if edge.destNode:
                data.append(edge.destNode.data(self.__ItemId))
            else:
                data.append(-1)

            data = data + [
                edge.weight(),
                edge.beginCp.point().x(),
                edge.beginCp.point().y(),
                edge.edge1Cp.point().x(),
                edge.edge1Cp.point().y(),
                edge.edge2Cp.point().x(),
                edge.edge2Cp.point().y(),
                edge.endCp.point().x(),
                edge.endCp.point().y(),
                edge.scenePos().x(),
                edge.scenePos().y()
            ]
            edgeDataList.append(data)

        for text in texts:
            text: BezierText
            data = [
                text.data(self.__ItemId),
                text.toPlainText(),
                text.scenePos().x(),
                text.scenePos().y()
            ]
            textDataList.append(data)

        nodeDataList.reverse()
        edgeDataList.reverse()
        textDataList.reverse()

        return [mode, nodeDataList, edgeDataList, textDataList]

    def reverseStandardData(self, excelData):
        graphName = excelData[0]
        mode = excelData[1]
        nodes = []
        edges = []
        texts = []
        self.ui.actionDigraph_Mode.setChecked(bool(mode))

        for nodeDetail in excelData[2]:
            node = BezierNode()
            node.textCp.setPlainText(f"V{nodeDetail[0]}")
            node.setData(self.__ItemId, nodeDetail[0])
            nodeText = self._tr("MainWindow", "顶点")
            node.setData(self.__ItemDesc, nodeText)
            if len(nodeDetail) < 3:
                for i in range(2):
                    intRandom = randint(-400, 400)
                    nodeDetail.append(intRandom)

            node.setPos(QPointF(nodeDetail[2], nodeDetail[3]))
            node.weightCp.setPlainText(str(nodeDetail[1]))

            nodes.append(node)

        for edgeDetail in excelData[3]:
            edge = BezierEdge()
            edge.setData(self.__ItemId, edgeDetail[0])
            edge.setData(self.__ItemDesc, "边")
            edge.textCp.setPlainText(f"e{edgeDetail[0]}")
            edge.weightCp.setPlainText(str(edgeDetail[3]))

            if len(edgeDetail) <= 4:
                for i in range(10):
                    intRandom = randint(-400, 400)
                    edgeDetail.append(intRandom)

            edge.setPos(QPointF(edgeDetail[12], edgeDetail[13]))

            if edgeDetail[1] >= 0:
                for node in nodes:
                    node: BezierNode
                    if node.data(self.__ItemId) == edgeDetail[1]:
                        edge.setSourceNode(node)
                        node.addBezierEdge(edge, ItemType.SourceType)
                        line = QLineF(edge.mapFromScene(node.pos()),
                                      edge.edge1Cp.point())
                        length = line.length()
                        edgeOffset = QPointF(line.dx() * 10 / length,
                                             line.dy() * 10 / length)
                        source = edge.mapFromScene(node.pos()) + edgeOffset
                        edge.setSpecialControlPoint(source,
                                                    ItemType.SourceType)
                        edge.beginCp.setVisible(False)
            else:
                edge.setSpecialControlPoint(
                    QPointF(edgeDetail[4], edgeDetail[5]), ItemType.SourceType)

            if edgeDetail[2] >= 0:
                for node in nodes:
                    node: BezierNode
                    if node.data(self.__ItemId) == edgeDetail[2]:
                        edge.setDestNode(node)
                        node.addBezierEdge(edge, ItemType.DestType)
                        line = QLineF(edge.mapFromScene(node.pos()),
                                      edge.edge2Cp.point())
                        length = line.length()
                        edgeOffset = QPointF(line.dx() * 10 / length,
                                             line.dy() * 10 / length)
                        if mode:
                            dest = edge.mapFromScene(
                                node.pos()) + edgeOffset * 2.3
                        else:
                            dest = edge.mapFromScene(node.pos()) + edgeOffset
                        edge.setSpecialControlPoint(dest, ItemType.DestType)
                        edge.endCp.setVisible(False)
            else:
                edge.setSpecialControlPoint(
                    QPointF(edgeDetail[10], edgeDetail[11]), ItemType.DestType)

            edge.setEdgeControlPoint(QPointF(edgeDetail[6], edgeDetail[7]),
                                     ItemType.SourceType)
            edge.setEdgeControlPoint(QPointF(edgeDetail[8], edgeDetail[9]),
                                     ItemType.DestType)
            edge.centerCp.setPoint(edge.updateCenterPos())

            edges.append(edge)

        if len(excelData) <= 4:
            return [graphName, nodes + edges]

        for textDetail in excelData[4]:
            text = BezierText(str(textDetail[1]))
            text.setData(self.__ItemId, textDetail[0])
            text.setData(self.__ItemDesc, "文本")
            text.setPos(textDetail[2], textDetail[3])
            texts.append(text)

        return [graphName, nodes + edges + texts]

    def setTranslator(self, translator, language):
        self.__translator = translator
        if language == 'EN':
            self.ui.actionSetEnglish.setChecked(True)
        else:
            self.ui.actionSetChinese.setChecked(True)

    @classmethod
    def checkSort(cls, index: list):
        index.sort()
        for i in range(len(index)):
            if i != index[i]:
                return i

    # ==============event处理函数==========================

    # def closeEvent(self, event):  # 退出函数
    #
    #     msgBox = QMessageBox()
    #     msgBox.setWindowTitle('关闭')
    #     msgBox.setText("是否保存")
    #     msgBox.setIcon(QMessageBox.Question)
    #     btn_Do_notSave = msgBox.addButton('不保存', QMessageBox.AcceptRole)
    #     btn_cancel = msgBox.addButton('取消', QMessageBox.RejectRole)
    #     btn_save = msgBox.addButton('保存', QMessageBox.AcceptRole)
    #     msgBox.setDefaultButton(btn_save)
    #     msgBox.exec_()
    #
    #     if msgBox.clickedButton() == btn_Do_notSave:
    #         event.accept()
    #     elif msgBox.clickedButton() == btn_cancel:
    #         event.ignore()
    #     elif msgBox.clickedButton() == btn_save:
    #         self.do_save_file()
    #         event.accept()

    # def contextMenuEvent(self, event):  # 右键菜单功能
    #     rightMouseMenu = QMenu(self)
    #
    #     rightMouseMenu.addAction(self.ui.actionNew)
    #     rightMouseMenu.addAction(self.ui.actionOpen)
    #
    #     self.action = rightMouseMenu.exec_(self.mapToGlobal(event.pos()))

    #  ==========由connectSlotsByName()自动连接的槽函数============
    @Slot()  # 新建画板
    def on_actionNew_triggered(self):
        self.iniGraphicsSystem()

    @Slot()  # 添加边
    def on_actionArc_triggered(self):  # 添加曲线
        item = BezierEdge()
        item.setGraphMode(self.ui.actionDigraph_Mode.isChecked())
        self.__setItemProperties(item, self._tr("MainWindow", "边"))
        self.do_addItem(item)
        self.__updateEdgeView()
        self.__updateNodeView()

    @Slot()  # 添加顶点
    def on_actionCircle_triggered(self):  # 添加原点
        self.viewAndScene()
        item = BezierNode()
        self.__setItemProperties(item, self._tr("MainWindow", "顶点"))
        self.do_addItem(item)
        self.__updateNodeView()
        self.__updateEdgeView()

    @Slot()  # 添加注释
    def on_actionAdd_Annotation_triggered(self):
        self.viewAndScene()
        strText, OK = QInputDialog.getText(self, self._tr("MainWindow", "输入"),
                                           self._tr("MainWindow", "请输入文字"))
        if not OK:
            return
        item = BezierText(strText)
        self.__setItemProperties(item, self._tr("MainWindow", "注释"))
        self.do_addItem(item)

    @Slot(bool)  # 显示和隐藏结点权重
    def on_actionShowNodesWeight_toggled(self, check: bool):
        nodes = self.__scene.singleItems(BezierNode)
        for node in nodes:
            node: BezierNode
            node.weightCp.setVisible(check)

        # if check:
        #     self.ui.actionShowNodesWeight.setText("隐藏顶点权重")
        # else:
        #     self.ui.actionShowNodesWeight.setText("显示顶点权重")

    @Slot(bool)  # 显示和隐藏边权重
    def on_actionShowEdgesWeight_toggled(self, check: bool):
        edges = self.__scene.singleItems(BezierEdge)
        for edge in edges:
            edge: BezierEdge
            edge.weightCp.setVisible(check)
        # if check:
        #     self.ui.actionShowEdgesWeight.setText("隐藏边权重")
        # else:
        #     self.ui.actionShowEdgesWeight.setText("显示边权重")

    @Slot(bool)  # 显示和隐藏边的控制点
    def on_actionHideControlPoint_toggled(self, check: bool):
        edges = self.__scene.singleItems(BezierEdge)
        for edge in edges:
            edge: BezierEdge
            for point in edge.pointList:
                point.setVisible(check)
            if edge.sourceNode:
                edge.beginCp.setVisible(False)
            if edge.destNode:
                edge.endCp.setVisible(False)

    @Slot()  # 简单通路
    def on_actionEasy_Pathway_triggered(self):
        self.viewAndScene()
        items = self.__scene.nodeList
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "对不起,你没有选择起始节点"))
            return
        elif len(items) != 2:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "选择的起始点数目不符合要求"))
            return

        if self.connectGraph():
            PathWay = ShowDataWidget(self,
                                     items,
                                     self.__graph,
                                     name=self._tr("MainWindow", "简单通路"))
            PathWay.pathSignal.connect(self.do_ShowSelectPath)
            if PathWay.easyPath():
                PathWay.updateToolWidget()
                PathWay.show()

        self.disconnectGraph()

    @Slot()  # 简单回路
    def on_actionEasy_Loop_triggered(self):
        self.viewAndScene()
        items = self.__scene.nodeList
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "对不起,你没有选择起点"))
            return

        if self.connectGraph():
            LoopWay = ShowDataWidget(self,
                                     items,
                                     self.__graph,
                                     name=self._tr("MainWindow", "简单回路"))
            LoopWay.pathSignal.connect(self.do_ShowSelectPath)
            if LoopWay.easyLoop():
                LoopWay.updateToolWidget(mode=1)
                LoopWay.show()
        self.disconnectGraph()

    @Slot()  # 初级通路
    def on_actionPrimary_Pathway_triggered(self):
        items = self.__scene.nodeList
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "对不起,你没有选择起始节点"))
            return
        elif len(items) != 2:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "选择的起始点数目不符合要求"))
            return

        if self.connectGraph():
            PathWay = ShowDataWidget(self, items, self.__graph, name="初级通路")
            PathWay.pathSignal.connect(self.do_ShowSelectPath)
            if PathWay.primaryPath():
                PathWay.updateToolWidget(path=1)
                PathWay.show()

        self.disconnectGraph()

    @Slot()  # 初级回路
    def on_actionPrimary_Loop_triggered(self):
        items = self.__scene.nodeList
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "对不起,你没有选择起点"))
            return

        if self.connectGraph():
            LoopWay = ShowDataWidget(self,
                                     items,
                                     self.__graph,
                                     name=self._tr("MainWindow", "初级回路"))
            LoopWay.pathSignal.connect(self.do_ShowSelectPath)
            if LoopWay.primaryLoop():
                LoopWay.updateToolWidget(mode=1, path=1)
                LoopWay.show()

        self.disconnectGraph()

    @Slot()  # 邻接矩阵 边数
    def on_action_EdgeNum_triggered(self):
        self.viewAndScene()
        items = self.__scene.singleItems(BezierNode)
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "图中没有结点"))
            return
        if self.connectGraph():
            MatrixTable = ShowMatrixWidget(self, self.__graph,
                                           self._tr("MainWindow", "邻接矩阵"), 0)
            MatrixTable.show()

    @Slot()  # 邻接矩阵 权重
    def on_actionWeight_triggered(self):
        self.viewAndScene()
        items = self.__scene.singleItems(BezierNode)
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "图中没有结点"))
            return
        if self.connectGraph():
            if not self.__graph.multipleOrSimple():
                MatrixTable = ShowMatrixWidget(self, self.__graph,
                                               self._tr("MainWindow", "邻接矩阵"),
                                               1)
                MatrixTable.show()
            else:
                QMessageBox.information(self, "Sorry", "这个图不是简单图")
                self.disconnectGraph()

    @Slot()  # 可达矩阵
    def on_actionReachable_Matrix_triggered(self):
        self.viewAndScene()
        items = self.__scene.singleItems(BezierNode)
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "图中没有结点"))
            return
        if self.connectGraph():
            MatrixTable = ShowMatrixWidget(self, self.__graph,
                                           self._tr("MainWindow", "可达矩阵"))
            MatrixTable.show()

    @Slot()  # 关联矩阵
    def on_actionIncidence_Matrix_Undigraph_triggered(self):
        self.viewAndScene()
        items = self.__scene.singleItems(BezierNode)
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "图中没有结点"))
            return
        if self.ui.actionDigraph_Mode.isChecked():
            items = self.singleItems(BezierEdge)
            badNodeList = []
            badNodes = ''
            for x in range(len(items)):
                if items[x].sourceNode is not None and items[
                        x].destNode is not None:
                    if items[x].sourceNode == items[x].destNode:
                        badNodeList.append(items[x])
                        demo = "、"
                        if x == len(items) - 1:
                            demo = ""
                        badNodes = f"{badNodes}V{items[x].sourceNode.data(self.__ItemId)}{demo}"
            if len(badNodeList):
                text = self._tr("MainWindow", '有向图的关联矩阵需要有向图无环,而')
                text1 = self._tr("MainWindow", '存在环!')
                QMessageBox.warning(self, self._tr("MainWindow", "致命错误"),
                                    f"{text}{badNodes}{text1}")
                return
        if self.connectGraph():
            MatrixTable = ShowMatrixWidget(self, self.__graph,
                                           self._tr("MainWindow", "关联矩阵"))
            MatrixTable.show()

    @Slot()  # 图的连通性
    def on_actionConnectivity_triggered(self):
        name = ''
        if self.connectGraph():
            num = self.__graph.connectivity()
            if num is False:
                name = self._tr("MainWindow", '此图为非连通图')
            elif num == 2:
                name = self._tr("MainWindow", "此图为单向连通图")
            elif num == 3:
                name = self._tr("MainWindow", "此图为强连通图")
            elif num == 1:
                name = self._tr("MainWindow", '此图为连通图')

            QMessageBox.information(self, self._tr("MainWindow", "图的连通性"),
                                    name)

            self.disconnectGraph()

    @Slot()  # 完全图判定
    def on_actionCompleteGraph_triggered(self):
        if self.connectGraph():
            edge = self.__graph.completeGraph()
            if edge:
                name = self._tr("MainWindow", "此图为完全图")
            else:
                name = self._tr("MainWindow", '此图不是完全图')

            QMessageBox.information(self, self._tr("MainWindow", "完全图判定"),
                                    name)

            self.disconnectGraph()

    @Slot()  # 简单图多重图判定
    def on_actionMultipleOrSimple_triggered(self):
        if self.connectGraph():
            edges = self.__graph.multipleOrSimple()
            if not edges:
                QMessageBox.information(self,
                                        self._tr("MainWindow", "简单图与多重图的判定"),
                                        self._tr("MainWindow", "此图为简单图"))

            else:
                parallelSides = ShowDataWidget(
                    self, edges, self.__graph,
                    self._tr("MainWindow", "简单图与多重图的判定"))
                parallelSides.multipleOrSimple()
                parallelSides.show()

    @Slot()  # 最短路径
    def on_actionShortestPath_triggered(self):

        items = self.__scene.nodeList
        if len(items) == 0:
            QMessageBox.warning(self, self._tr("MainWindow", "警告"),
                                self._tr("MainWindow", "对不起,你没有选择结点"))
            return
        if self.connectGraph():
            ShortestPath = ShowDataWidget(self,
                                          items,
                                          self.__graph,
                                          name=self._tr("MainWindow", "最短路径"))
            ShortestPath.pathSignal.connect(self.do_ShowSelectPath)
            if ShortestPath.shortestPath():
                ShortestPath.updateToolWidget(mode=1, path=2)
                ShortestPath.show()

    @Slot()  # 撤销
    def on_actionUndo_triggered(self):  # 撤销
        self.undoStack.undo()
        self.__updateEdgeView()
        self.__updateNodeView()

    @Slot()  # 重做
    def on_actionRedo_triggered(self):  # 重做
        self.viewAndScene()
        self.undoStack.redo()
        self.__updateEdgeView()
        self.__updateNodeView()

    @Slot()  # 帮助
    def on_actionHelp_Document_triggered(self):
        open("https://github.com/BBlance/Discrete_math.graph_theory")

    # @Slot()
    # def on_actionPen_Color_triggered(self):  # 画笔颜色
    #     iniColor = self.view.getPenColor()
    #     color = QColorDialog.getColor(iniColor, self, "选择颜色")
    #     if color.isValid():
    #         self.view.setPenColor(color)

    # @Slot()
    # def on_actionPen_Thickness_triggered(self):  # 画笔粗细
    #     self.viewAndScene()
    #     iniThickness = self.__view.getPenThickness()
    #     intPenStyle = self.__view.getPenStyle()
    #     thicknessDialog = ThicknessDialog(None, self._tr("MainWindow", "画笔粗细与样式"), iniThickness, intPenStyle)
    #     ret = thicknessDialog.exec_()
    #     thickness = thicknessDialog.getThickness()
    #     penStyle = thicknessDialog.getPenStyle()
    #     self.__view.setPenStyle(penStyle)
    #     self.__view.setPenThickness(thickness)

    @Slot()
    def on_actionBackground_Color_triggered(self):
        self.viewAndScene()
        # iniColor = self.__view.getBackgroundColor()
        # color = QColorDialog.getColor(iniColor, self, "选择颜色")
        # if color.isValid():
        #     self.__view.setBackgroundBrush(color)
        for item in self.standardGraphData():
            print(item)

    # @Slot(bool)
    # def on_actionProperty_And_History_triggered(self, checked):
    #     self.ui.dockWidget.setVisible(checked)

    @Slot()  # 保存文件
    def on_actionSave_triggered(self):
        self.viewAndScene()
        tableName = self.ui.tabWidget.tabText(self.ui.tabWidget.currentIndex())
        filename = self.operatorFile.saveGraphData(self.standardGraphData(),
                                                   tableName)
        if filename:
            index = self.ui.tabWidget.currentIndex()
            self.ui.tabWidget.setTabText(index, filename.baseName())

    @Slot()  # 读取文件
    def on_actionOpen_triggered(self):
        graph = self.operatorFile.openGraphData()
        if graph:
            graph = self.reverseStandardData(graph)
            self.iniGraphicsSystem(graph[0])
            for item in graph[1]:
                self.__scene.addItem(item)
            self.__updateNodeView()
            self.__updateEdgeView()
            self.__scene.update()

    @Slot()  # 另存为
    def on_actionSave_As_triggered(self):
        filename = self.operatorFile.saveExcelAs(self.standardGraphData())
        if filename:
            index = self.ui.tabWidget.currentIndex()
            self.ui.tabWidget.setTabText(index, filename.baseName())

    @Slot()  # 导出数据
    def on_actionOutputData_triggered(self):
        data = self.standardGraphData()
        data = data[:3]
        dataCpoy = [data[0], [], []]
        for node in data[1]:
            node = node[:2]
            dataCpoy[1].append(node)

        for edge in data[2]:
            edge = edge[:4]
            dataCpoy[2].append(edge)

        if self.operatorFile.outputData(dataCpoy):
            title = self.tr("恭喜")
            strInfo = self.tr("数据导出成功")
            QMessageBox.information(self, title, strInfo)

    @Slot()  # 导入数据
    def on_actionImportData_triggered(self):
        data = self.operatorFile.inputData()
        if data:
            graph = self.reverseStandardData(data)
            self.iniGraphicsSystem(graph[0])
            for item in graph[1]:
                self.__scene.addItem(item)
            self.__updateNodeView()
            self.__updateEdgeView()
            self.__scene.update()

    @Slot()
    def on_actionSave_Image_triggered(self):
        self.viewAndScene()
        savePath, fileType = QFileDialog.getSaveFileName(
            self, self._tr("MainWindow", '保存图片'), '.\\', '*bmp;;*.png')
        filename = os.path.basename(savePath)
        if filename != "":
            self.__view.saveImage(savePath, fileType)

    @Slot()
    def on_actionDelete_triggered(self):
        self.viewAndScene()
        self.do_deleteItem()

    @Slot(bool)
    def on_actionDigraph_Mode_toggled(self, checked: bool):
        self.__labModeInfo.setText(self._tr("MainWindow", "有向图模式"))
        self.__graph.setMode(checked)
        items = self.__scene.singleItems(BezierEdge)
        for item in items:
            item: BezierEdge
            item.setGraphMode(True)
            item.update()

    @Slot(bool)
    def on_actionRedigraph_Mode_toggled(self, checked: bool):
        self.__labModeInfo.setText(self._tr("MainWindow", "无向图模式"))
        self.__graph.setMode(checked)
        items = self.__scene.singleItems(BezierEdge)
        for item in items:
            item: BezierEdge
            item.setGraphMode(False)
            item.update()

    @Slot(int)
    def on_tabWidget_currentChanged(self, index):  # ui.tabWidget当前页面变化
        self.viewAndScene()
        if self.__view and self.__scene:
            self.__updateEdgeView()
            self.__updateNodeView()

        hasTabs = self.ui.tabWidget.count() > 0  # 再无页面时

        self.ui.tabWidget.setVisible(hasTabs)
        self.ui.dockWidget.setVisible(hasTabs)
        self.ui.actionProperty_And_History.setChecked(hasTabs)

    @Slot(int)
    def on_tabWidget_tabCloseRequested(self, index):  # 分页关闭时关闭窗体
        if index < 0:
            return
        view = self.ui.tabWidget.widget(index)
        view.close()
        # self.__view = None
        # self.__scene = None

    #  =============自定义槽函数===============================
    def do_nodeLock(self, item):
        self.__updateNodeView()
        self.__updateEdgeView()

    def do_mouseMove(self, point):  ##鼠标移动
        ##鼠标移动事件,point是 GraphicsView的坐标,物理坐标
        view = self._tr("MainWindow", '视图坐标:')
        scene = self._tr("MainWindow", '场景坐标:')
        self.__labViewCord.setText("%s%d,%d" % (view, point.x(), point.y()))
        pt = self.ui.tabWidget.currentWidget().mapToScene(point)  # 转换到Scene坐标
        self.__labSceneCord.setText("%s%.0f,%.0f" % (scene, pt.x(), pt.y()))

    def do_mouseClicked(self, point):  ##鼠标单击
        pt = self.__view.mapToScene(point)  # 转换到Scene坐标
        item = self.__scene.itemAt(pt, self.__view.transform())  # 获取光标下的图形项
        if item is None:
            return
        pm = item.mapFromScene(pt)  # 转换为绘图项的局部坐标
        itemInfo = self._tr("MainWindow", "Item 坐标:")
        self.__labItemCord.setText("%s%.0f,%.0f" % (itemInfo, pm.x(), pm.y()))
        data = f"{item.data(self.__ItemDesc)}, ItemId={item.data(self.__ItemId)}"
        if type(item) is BezierEdge:
            data = f"{data},EdgeId=e{item.data(self.__ItemId)}"
        elif type(item) is BezierNode:
            data = f"{data}, NodeId=V{item.data(self.__ItemId)}"
        self.__labItemInfo.setText(data)

    def do_mouseDoubleClick(self, point):  ##鼠标双击
        pt = self.__view.mapToScene(point)  # 转换到Scene坐标,QPointF
        item = self.__scene.itemAt(pt, self.__view.transform())  # 获取光标下的绘图项
        if item is None:
            return

        className = str(type(item))  # 将类名称转换为字符串

        if className.find("QGraphicsRectItem") >= 0:  # 矩形框
            self.__setBrushColor(item)
        elif className.find(
                "QGraphicsEllipseItem") >= 0:  # 椭圆和圆都是 QGraphicsEllipseItem
            self.__setBrushColor(item)
        elif className.find("QGraphicsPolygonItem") >= 0:  # 梯形和三角形
            self.__setBrushColor(item)
        elif className.find("QGraphicsLineItem") >= 0:  # 直线,设置线条颜色
            pen = item.pen()
            color = item.pen().__color()
            color = QColorDialog.getColor(color, self, "选择线条颜色")
            if color.isValid():
                pen.setColor(color)
                item.setPen(pen)
        elif className.find("QGraphicsTextItem") >= 0:  # 文字,设置字体
            font = item.font()
            font, OK = QFontDialog.getFont(font)
            if OK:
                item.setFont(font)

    def do_addItem(self, item):
        add = AddCommand(self, self.__scene, item)
        self.undoStack.push(add)

    def do_shapeMoved(self, item, pos):
        move = MoveCommand(item, pos)
        self.undoStack.push(move)

    def do_deleteItem(self):
        items = self.__scene.selectedItems()
        cnt = len(items)
        for i in range(cnt):
            item = items[i]
            if str(type(item)).find("BezierNode") >= 0:
                item: BezierNode
                for edge in item.bezierEdges:
                    for node, itemType in edge.items():
                        if itemType == ItemType.SourceType:
                            node.setSourceNode(None)
                        elif itemType == ItemType.DestType:
                            node.setDestNode(None)
                self.__nodeNum -= 1
            elif str(type(item)).find("BezierEdge") >= 0:
                item: BezierEdge
                sourceNode: BezierNode = item.sourceNode
                destNode: BezierNode = item.destNode
                if sourceNode:
                    sourceNodeList = sourceNode.bezierEdges
                    for sourceEdge in sourceNodeList:
                        for edge in sourceEdge.keys():
                            if item is edge:
                                sourceNodeList.remove(sourceEdge)
                if destNode:
                    destNodeList = destNode.bezierEdges
                    for destEdge in destNodeList:
                        for edge in destEdge.keys():
                            if item is edge:
                                destNodeList.remove(destEdge)
                self.__edgeNum -= 1

            self.__scene.removeItem(item)  # 删除绘图项

    def do_curEdgeChanged(self, current, previous):
        if current is not None:
            text = f"当前单元格{current.row()},{current.column()}"
            item = self.edgeModel.itemFromIndex(current)

    def do_curNodeChanged(self, current, previous):
        if current is not None:
            text = f"当前单元格{current.row()},{current.column()}"
            item = self.nodeModel.itemFromIndex(current)

    def do_updateEdgeWeight(self, topLeft, bottomRight):
        if topLeft.column() == 4:
            edges = self.__scene.singleItems(BezierEdge)
            for edge in edges:
                edge: BezierEdge
                if edge.textCp.toPlainText() == self.edgeModel.index(
                        topLeft.row(), 0, QModelIndex()).data():
                    edge.weightCp.setPlainText(str(topLeft.data()))
                    self.__scene.update()

    def do_updateNodeWeight(self, topLeft, bottomRight):
        if topLeft.column() == 3:
            nodes = self.__scene.singleItems(BezierNode)
            for node in nodes:
                node: BezierNode
                if node.textCp.toPlainText() == self.nodeModel.index(
                        topLeft.row(), 0, QModelIndex()).data():
                    node.weightCp.setPlainText(str(topLeft.data()))
                    self.__scene.update()

    def do_ShowSelectPath(self, pathList: list):
        self.__scene.clearSelection()
        items = self.__scene.uniqueItems()
        for item in items:
            if item.textCp.toPlainText() in pathList:
                item.setSelected(True)

    def do_checkIsHasItems(self, num):
        if num:
            self.ui.actionSave.setEnabled(True)
        else:
            self.ui.actionSave.setEnabled(False)
Пример #3
0
class Flow(QGraphicsView):
    def __init__(self, main_window, parent_script, config=None):
        super(Flow, self).__init__()

        # SHORTCUTS
        place_new_node_shortcut = QShortcut(QKeySequence('Shift+P'), self)
        place_new_node_shortcut.activated.connect(
            self.place_new_node_by_shortcut)
        move_selected_nodes_left_shortcut = QShortcut(
            QKeySequence('Shift+Left'), self)
        move_selected_nodes_left_shortcut.activated.connect(
            self.move_selected_nodes_left)
        move_selected_nodes_up_shortcut = QShortcut(QKeySequence('Shift+Up'),
                                                    self)
        move_selected_nodes_up_shortcut.activated.connect(
            self.move_selected_nodes_up)
        move_selected_nodes_right_shortcut = QShortcut(
            QKeySequence('Shift+Right'), self)
        move_selected_nodes_right_shortcut.activated.connect(
            self.move_selected_nodes_right)
        move_selected_nodes_down_shortcut = QShortcut(
            QKeySequence('Shift+Down'), self)
        move_selected_nodes_down_shortcut.activated.connect(
            self.move_selected_nodes_down)
        select_all_shortcut = QShortcut(QKeySequence('Ctrl+A'), self)
        select_all_shortcut.activated.connect(self.select_all)
        copy_shortcut = QShortcut(QKeySequence.Copy, self)
        copy_shortcut.activated.connect(self.copy)
        cut_shortcut = QShortcut(QKeySequence.Cut, self)
        cut_shortcut.activated.connect(self.cut)
        paste_shortcut = QShortcut(QKeySequence.Paste, self)
        paste_shortcut.activated.connect(self.paste)

        # UNDO/REDO
        self.undo_stack = QUndoStack(self)
        self.undo_action = self.undo_stack.createUndoAction(self, 'undo')
        self.undo_action.setShortcuts(QKeySequence.Undo)
        self.redo_action = self.undo_stack.createRedoAction(self, 'redo')
        self.redo_action.setShortcuts(QKeySequence.Redo)

        undo_shortcut = QShortcut(QKeySequence.Undo, self)
        undo_shortcut.activated.connect(self.undo_activated)
        redo_shortcut = QShortcut(QKeySequence.Redo, self)
        redo_shortcut.activated.connect(self.redo_activated)

        # GENERAL ATTRIBUTES
        self.parent_script = parent_script
        self.all_node_instances: [NodeInstance] = []
        self.all_node_instance_classes = main_window.all_node_instance_classes  # ref
        self.all_nodes = main_window.all_nodes  # ref
        self.gate_selected: PortInstanceGate = None
        self.dragging_connection = False
        self.ignore_mouse_event = False  # for stylus - see tablet event
        self.last_mouse_move_pos: QPointF = None
        self.node_place_pos = QPointF()
        self.left_mouse_pressed_in_flow = False
        self.mouse_press_pos: QPointF = None
        self.tablet_press_pos: QPointF = None
        self.auto_connection_gate = None  # stores the gate that we may try to auto connect to a newly placed NI
        self.panning = False
        self.pan_last_x = None
        self.pan_last_y = None
        self.current_scale = 1
        self.total_scale_div = 1

        # SETTINGS
        self.algorithm_mode = Flow_AlgorithmMode()
        self.viewport_update_mode = Flow_ViewportUpdateMode()

        # CREATE UI
        scene = QGraphicsScene(self)
        scene.setItemIndexMethod(QGraphicsScene.NoIndex)
        scene.setSceneRect(0, 0, 10 * self.width(), 10 * self.height())

        self.setScene(scene)
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
        self.setRenderHint(QPainter.Antialiasing)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setDragMode(QGraphicsView.RubberBandDrag)
        scene.selectionChanged.connect(self.selection_changed)
        self.setAcceptDrops(True)

        self.centerOn(
            QPointF(self.viewport().width() / 2,
                    self.viewport().height() / 2))

        # NODE CHOICE WIDGET
        self.node_choice_proxy = FlowProxyWidget(self)
        self.node_choice_proxy.setZValue(1000)
        self.node_choice_widget = NodeChoiceWidget(
            self, main_window.all_nodes)  # , main_window.node_images)
        self.node_choice_proxy.setWidget(self.node_choice_widget)
        self.scene().addItem(self.node_choice_proxy)
        self.hide_node_choice_widget()

        # ZOOM WIDGET
        self.zoom_proxy = FlowProxyWidget(self)
        self.zoom_proxy.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
        self.zoom_proxy.setZValue(1001)
        self.zoom_widget = FlowZoomWidget(self)
        self.zoom_proxy.setWidget(self.zoom_widget)
        self.scene().addItem(self.zoom_proxy)
        self.set_zoom_proxy_pos()

        # STYLUS
        self.stylus_mode = ''
        self.current_drawing = None
        self.drawing = False
        self.drawings = []
        self.stylus_modes_proxy = FlowProxyWidget(self)
        self.stylus_modes_proxy.setFlag(
            QGraphicsItem.ItemIgnoresTransformations, True)
        self.stylus_modes_proxy.setZValue(1001)
        self.stylus_modes_widget = FlowStylusModesWidget(self)
        self.stylus_modes_proxy.setWidget(self.stylus_modes_widget)
        self.scene().addItem(self.stylus_modes_proxy)
        self.set_stylus_proxy_pos()
        self.setAttribute(Qt.WA_TabletTracking)

        # DESIGN THEME
        Design.flow_theme_changed.connect(self.theme_changed)

        if config:
            config: dict

            # algorithm mode
            if config.keys().__contains__('algorithm mode'):
                if config['algorithm mode'] == 'data flow':
                    self.parent_script.widget.ui.algorithm_data_flow_radioButton.setChecked(
                        True)
                    self.algorithm_mode.mode_data_flow = True
                else:  # 'exec flow'
                    self.parent_script.widget.ui.algorithm_exec_flow_radioButton.setChecked(
                        True)
                    self.algorithm_mode.mode_data_flow = False

            # viewport update mode
            if config.keys().__contains__('viewport update mode'):
                if config['viewport update mode'] == 'sync':
                    self.parent_script.widget.ui.viewport_update_mode_sync_radioButton.setChecked(
                        True)
                    self.viewport_update_mode.sync = True
                else:  # 'async'
                    self.parent_script.widget.ui.viewport_update_mode_async_radioButton.setChecked(
                        True)
                    self.viewport_update_mode.sync = False

            node_instances = self.place_nodes_from_config(config['nodes'])
            self.connect_nodes_from_config(node_instances,
                                           config['connections'])
            if list(config.keys()).__contains__(
                    'drawings'
            ):  # not all (old) project files have drawings arr
                self.place_drawings_from_config(config['drawings'])
            self.undo_stack.clear()

    def theme_changed(self, t):
        self.viewport().update()

    def algorithm_mode_data_flow_toggled(self, checked):
        self.algorithm_mode.mode_data_flow = checked

    def viewport_update_mode_sync_toggled(self, checked):
        self.viewport_update_mode.sync = checked

    def selection_changed(self):
        selected_items = self.scene().selectedItems()
        selected_node_instances = list(
            filter(find_NI_in_object, selected_items))
        if len(selected_node_instances) == 1:
            self.parent_script.show_NI_code(selected_node_instances[0])
        elif len(selected_node_instances) == 0:
            self.parent_script.show_NI_code(None)

    def contextMenuEvent(self, event):
        QGraphicsView.contextMenuEvent(self, event)
        # in the case of the menu already being shown by a widget under the mouse, the event is accepted here
        if event.isAccepted():
            return

        for i in self.items(event.pos()):
            if find_type_in_object(i, NodeInstance):
                ni: NodeInstance = i
                menu: QMenu = ni.get_context_menu()
                menu.exec_(event.globalPos())
                event.accept()

    def undo_activated(self):
        """Triggered by ctrl+z"""
        self.undo_stack.undo()
        self.viewport().update()

    def redo_activated(self):
        """Triggered by ctrl+y"""
        self.undo_stack.redo()
        self.viewport().update()

    def mousePressEvent(self, event):
        Debugger.debug('mouse press event received, point:', event.pos())

        # to catch tablet events (for some reason, it results in a mousePrEv too)
        if self.ignore_mouse_event:
            self.ignore_mouse_event = False
            return

        # there might be a proxy widget meant to receive the event instead of the flow
        QGraphicsView.mousePressEvent(self, event)

        # to catch any Proxy that received the event. Checking for event.isAccepted() or what is returned by
        # QGraphicsView.mousePressEvent(...) both didn't work so far, so I do it manually
        if self.ignore_mouse_event:
            self.ignore_mouse_event = False
            return

        if event.button() == Qt.LeftButton:
            if self.node_choice_proxy.isVisible():
                self.hide_node_choice_widget()
            else:
                if find_type_in_object(self.itemAt(event.pos()),
                                       PortInstanceGate):
                    self.gate_selected = self.itemAt(event.pos())
                    self.dragging_connection = True

            self.left_mouse_pressed_in_flow = True

        elif event.button() == Qt.RightButton:
            if len(self.items(event.pos())) == 0:
                self.node_choice_widget.reset_list()
                self.show_node_choice_widget(event.pos())

        elif event.button() == Qt.MidButton:
            self.panning = True
            self.pan_last_x = event.x()
            self.pan_last_y = event.y()
            event.accept()

        self.mouse_press_pos = self.mapToScene(event.pos())

    def mouseMoveEvent(self, event):

        QGraphicsView.mouseMoveEvent(self, event)

        if self.panning:  # middle mouse pressed
            self.pan(event.pos())
            event.accept()

        self.last_mouse_move_pos = self.mapToScene(event.pos())

        if self.dragging_connection:
            self.viewport().repaint()

    def mouseReleaseEvent(self, event):
        # there might be a proxy widget meant to receive the event instead of the flow
        QGraphicsView.mouseReleaseEvent(self, event)

        if self.ignore_mouse_event or \
                (event.button() == Qt.LeftButton and not self.left_mouse_pressed_in_flow):
            self.ignore_mouse_event = False
            return

        elif event.button() == Qt.MidButton:
            self.panning = False

        # connection dropped over specific gate
        if self.dragging_connection and self.itemAt(event.pos()) and \
                find_type_in_object(self.itemAt(event.pos()), PortInstanceGate):
            self.connect_gates__cmd(self.gate_selected,
                                    self.itemAt(event.pos()))

        # connection dropped over NodeInstance - auto connect
        elif self.dragging_connection and find_type_in_objects(
                self.items(event.pos()), NodeInstance):
            # find node instance
            ni_under_drop = None
            for item in self.items(event.pos()):
                if find_type_in_object(item, NodeInstance):
                    ni_under_drop = item
                    break
            # connect
            self.try_conn_gate_and_ni(self.gate_selected, ni_under_drop)

        # connection dropped somewhere else - show node choice widget
        elif self.dragging_connection:
            self.auto_connection_gate = self.gate_selected
            self.show_node_choice_widget(event.pos())

        self.left_mouse_pressed_in_flow = False
        self.dragging_connection = False
        self.gate_selected = None

        self.viewport().repaint()

    def keyPressEvent(self, event):
        QGraphicsView.keyPressEvent(self, event)

        if event.isAccepted():
            return

        if event.key() == Qt.Key_Escape:  # do I need that... ?
            self.clearFocus()
            self.setFocus()
            return True

        elif event.key() == Qt.Key_Delete:
            self.remove_selected_components()

    def wheelEvent(self, event):
        if event.modifiers() == Qt.CTRL and event.angleDelta().x() == 0:
            self.zoom(event.pos(), self.mapToScene(event.pos()),
                      event.angleDelta().y())
            event.accept()
            return True

        QGraphicsView.wheelEvent(self, event)

    def tabletEvent(self, event):
        """tabletEvent gets called by stylus operations.
        LeftButton: std, no button pressed
        RightButton: upper button pressed"""

        # if in edit mode and not panning or starting a pan, pass on to std mouseEvent handlers above
        if self.stylus_mode == 'edit' and not self.panning and not \
                (event.type() == QTabletEvent.TabletPress and event.button() == Qt.RightButton):
            return  # let the mousePress/Move/Release-Events handle it

        if event.type() == QTabletEvent.TabletPress:
            self.tablet_press_pos = event.pos()
            self.ignore_mouse_event = True

            if event.button() == Qt.LeftButton:
                if self.stylus_mode == 'comment':
                    new_drawing = self.create_and_place_drawing__cmd(
                        self.mapToScene(self.tablet_press_pos),
                        config=self.stylus_modes_widget.get_pen_settings())
                    self.current_drawing = new_drawing
                    self.drawing = True
            elif event.button() == Qt.RightButton:
                self.panning = True
                self.pan_last_x = event.x()
                self.pan_last_y = event.y()

        elif event.type() == QTabletEvent.TabletMove:
            self.ignore_mouse_event = True
            if self.panning:
                self.pan(event.pos())

            elif event.pointerType() == QTabletEvent.Eraser:
                if self.stylus_mode == 'comment':
                    for i in self.items(event.pos()):
                        if find_type_in_object(i, DrawingObject):
                            self.remove_drawing(i)
                            break
            elif self.stylus_mode == 'comment' and self.drawing:

                mapped = self.mapToScene(
                    QPoint(event.posF().x(),
                           event.posF().y()))
                # rest = QPointF(event.posF().x()%1, event.posF().y()%1)
                # exact = QPointF(mapped.x()+rest.x()%1, mapped.y()+rest.y()%1)
                # TODO: use exact position (event.posF() ). Problem: mapToScene() only uses QPoint, not QPointF. The
                #  calculation above didn't work

                if self.current_drawing.try_to_append_point(mapped):
                    self.current_drawing.stroke_weights.append(
                        event.pressure())
                self.current_drawing.update()
                self.viewport().update()

        elif event.type() == QTabletEvent.TabletRelease:
            if self.panning:
                self.panning = False
            if self.stylus_mode == 'comment' and self.drawing:
                Debugger.debug('drawing obj finished')
                self.current_drawing.finished()
                self.current_drawing = None
                self.drawing = False

    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('text/plain'):
            event.acceptProposedAction()

    def dragMoveEvent(self, event):
        if event.mimeData().hasFormat('text/plain'):
            event.acceptProposedAction()

    def dropEvent(self, event):
        text = event.mimeData().text()
        item: QListWidgetItem = event.mimeData()
        Debugger.debug('drop received in Flow:', text)

        j_obj = None
        type = ''
        try:
            j_obj = json.loads(text)
            type = j_obj['type']
        except Exception:
            return

        if type == 'variable':
            self.show_node_choice_widget(
                event.pos(),  # only show get_var and set_var nodes
                [
                    n for n in self.all_nodes
                    if find_type_in_object(n, GetVariable_Node)
                    or find_type_in_object(n, SetVariable_Node)
                ])

    def drawBackground(self, painter, rect):
        painter.fillRect(rect.intersected(self.sceneRect()), QColor('#333333'))
        painter.setPen(Qt.NoPen)
        painter.drawRect(self.sceneRect())

        self.set_stylus_proxy_pos(
        )  # has to be called here instead of in drawForeground to prevent lagging
        self.set_zoom_proxy_pos()

    def drawForeground(self, painter, rect):
        """Draws all connections and borders around selected items."""

        pen = QPen()
        if Design.flow_theme == 'dark std':
            # pen.setColor('#BCBBF2')
            pen.setWidth(5)
            pen.setCapStyle(Qt.RoundCap)
        elif Design.flow_theme == 'dark tron':
            # pen.setColor('#452666')
            pen.setWidth(4)
            pen.setCapStyle(Qt.RoundCap)
        elif Design.flow_theme == 'ghostly' or Design.flow_theme == 'blender':
            pen.setWidth(2)
            pen.setCapStyle(Qt.RoundCap)

        # DRAW CONNECTIONS
        for ni in self.all_node_instances:
            for o in ni.outputs:
                for cpi in o.connected_port_instances:
                    if o.type_ == 'data':
                        pen.setStyle(Qt.DashLine)
                    elif o.type_ == 'exec':
                        pen.setStyle(Qt.SolidLine)
                    path = self.connection_path(
                        o.gate.get_scene_center_pos(),
                        cpi.gate.get_scene_center_pos())
                    w = path.boundingRect().width()
                    h = path.boundingRect().height()
                    gradient = QRadialGradient(path.boundingRect().center(),
                                               pythagoras(w, h) / 2)
                    r = 0
                    g = 0
                    b = 0
                    if Design.flow_theme == 'dark std':
                        r = 188
                        g = 187
                        b = 242
                    elif Design.flow_theme == 'dark tron':
                        r = 0
                        g = 120
                        b = 180
                    elif Design.flow_theme == 'ghostly' or Design.flow_theme == 'blender':
                        r = 0
                        g = 17
                        b = 25

                    gradient.setColorAt(0.0, QColor(r, g, b, 255))
                    gradient.setColorAt(0.75, QColor(r, g, b, 200))
                    gradient.setColorAt(0.95, QColor(r, g, b, 0))
                    gradient.setColorAt(1.0, QColor(r, g, b, 0))
                    pen.setBrush(gradient)
                    painter.setPen(pen)
                    painter.drawPath(path)

        # DRAW CURRENTLY DRAGGED CONNECTION
        if self.dragging_connection:
            pen = QPen('#101520')
            pen.setWidth(3)
            pen.setStyle(Qt.DotLine)
            painter.setPen(pen)
            gate_pos = self.gate_selected.get_scene_center_pos()
            if self.gate_selected.parent_port_instance.direction == 'output':
                painter.drawPath(
                    self.connection_path(gate_pos, self.last_mouse_move_pos))
            else:
                painter.drawPath(
                    self.connection_path(self.last_mouse_move_pos, gate_pos))

        # DRAW SELECTED NIs BORDER
        for ni in self.selected_node_instances():
            pen = QPen(QColor('#245d75'))
            pen.setWidth(3)
            painter.setPen(pen)
            painter.setBrush(Qt.NoBrush)

            size_factor = 1.2
            x = ni.pos().x() - ni.boundingRect().width() / 2 * size_factor
            y = ni.pos().y() - ni.boundingRect().height() / 2 * size_factor
            w = ni.boundingRect().width() * size_factor
            h = ni.boundingRect().height() * size_factor
            painter.drawRoundedRect(x, y, w, h, 10, 10)

        # DRAW SELECTED DRAWINGS BORDER
        for p_o in self.selected_drawings():
            pen = QPen(QColor('#a3cc3b'))
            pen.setWidth(2)
            painter.setPen(pen)
            painter.setBrush(Qt.NoBrush)

            size_factor = 1.05
            x = p_o.pos().x() - p_o.width / 2 * size_factor
            y = p_o.pos().y() - p_o.height / 2 * size_factor
            w = p_o.width * size_factor
            h = p_o.height * size_factor
            painter.drawRoundedRect(x, y, w, h, 6, 6)
            painter.drawEllipse(p_o.pos().x(), p_o.pos().y(), 2, 2)

    def get_viewport_img(self):
        self.hide_proxies()
        img = QImage(self.viewport().rect().width(),
                     self.viewport().height(), QImage.Format_ARGB32)
        img.fill(Qt.transparent)

        painter = QPainter(img)
        painter.setRenderHint(QPainter.Antialiasing)
        self.render(painter, self.viewport().rect(), self.viewport().rect())
        self.show_proxies()
        return img

    def get_whole_scene_img(self):
        self.hide_proxies()
        img = QImage(self.sceneRect().width() / self.total_scale_div,
                     self.sceneRect().height() / self.total_scale_div,
                     QImage.Format_RGB32)
        img.fill(Qt.transparent)

        painter = QPainter(img)
        painter.setRenderHint(QPainter.Antialiasing)
        rect = QRectF()
        rect.setLeft(-self.viewport().pos().x())
        rect.setTop(-self.viewport().pos().y())
        rect.setWidth(img.rect().width())
        rect.setHeight(img.rect().height())
        # rect is right... but it only renders from the viewport's point down-and rightwards, not from topleft (0,0) ...
        self.render(painter, rect, rect.toRect())
        self.show_proxies()
        return img

    # PROXY POSITIONS
    def set_zoom_proxy_pos(self):
        self.zoom_proxy.setPos(
            self.mapToScene(self.viewport().width() - self.zoom_widget.width(),
                            0))

    def set_stylus_proxy_pos(self):
        self.stylus_modes_proxy.setPos(
            self.mapToScene(
                self.viewport().width() - self.stylus_modes_widget.width() -
                self.zoom_widget.width(), 0))

    def hide_proxies(self):
        self.stylus_modes_proxy.hide()
        self.zoom_proxy.hide()

    def show_proxies(self):
        self.stylus_modes_proxy.show()
        self.zoom_proxy.show()

    # NODE CHOICE WIDGET
    def show_node_choice_widget(self, pos, nodes=None):
        """Opens the node choice dialog in the scene."""

        # calculating position
        self.node_place_pos = self.mapToScene(pos)
        dialog_pos = QPoint(pos.x() + 1, pos.y() + 1)

        # ensure that the node_choice_widget stays in the viewport
        if dialog_pos.x() + self.node_choice_widget.width(
        ) / self.total_scale_div > self.viewport().width():
            dialog_pos.setX(dialog_pos.x() -
                            (dialog_pos.x() + self.node_choice_widget.width() /
                             self.total_scale_div - self.viewport().width()))
        if dialog_pos.y() + self.node_choice_widget.height(
        ) / self.total_scale_div > self.viewport().height():
            dialog_pos.setY(dialog_pos.y() -
                            (dialog_pos.y() +
                             self.node_choice_widget.height() /
                             self.total_scale_div - self.viewport().height()))
        dialog_pos = self.mapToScene(dialog_pos)

        # open nodes dialog
        # the dialog emits 'node_chosen' which is connected to self.place_node,
        # so this all continues at self.place_node below
        self.node_choice_widget.update_list(
            nodes if nodes is not None else self.all_nodes)
        self.node_choice_widget.update_view()
        self.node_choice_proxy.setPos(dialog_pos)
        self.node_choice_proxy.show()
        self.node_choice_widget.refocus()

    def hide_node_choice_widget(self):
        self.node_choice_proxy.hide()
        self.node_choice_widget.clearFocus()
        self.auto_connection_gate = None

    # PAN
    def pan(self, new_pos):
        self.horizontalScrollBar().setValue(
            self.horizontalScrollBar().value() -
            (new_pos.x() - self.pan_last_x))
        self.verticalScrollBar().setValue(self.verticalScrollBar().value() -
                                          (new_pos.y() - self.pan_last_y))
        self.pan_last_x = new_pos.x()
        self.pan_last_y = new_pos.y()

    # ZOOM
    def zoom_in(self, amount):
        local_viewport_center = QPoint(self.viewport().width() / 2,
                                       self.viewport().height() / 2)
        self.zoom(local_viewport_center,
                  self.mapToScene(local_viewport_center), amount)

    def zoom_out(self, amount):
        local_viewport_center = QPoint(self.viewport().width() / 2,
                                       self.viewport().height() / 2)
        self.zoom(local_viewport_center,
                  self.mapToScene(local_viewport_center), -amount)

    def zoom(self, p_abs, p_mapped, angle):
        by = 0
        velocity = 2 * (1 / self.current_scale) + 0.5
        if velocity > 3:
            velocity = 3

        direction = ''
        if angle > 0:
            by = 1 + (angle / 360 * 0.1 * velocity)
            direction = 'in'
        elif angle < 0:
            by = 1 - (-angle / 360 * 0.1 * velocity)
            direction = 'out'
        else:
            by = 1

        scene_rect_width = self.mapFromScene(
            self.sceneRect()).boundingRect().width()
        scene_rect_height = self.mapFromScene(
            self.sceneRect()).boundingRect().height()

        if direction == 'in':
            if self.current_scale * by < 3:
                self.scale(by, by)
                self.current_scale *= by
        elif direction == 'out':
            if scene_rect_width * by >= self.viewport().size().width(
            ) and scene_rect_height * by >= self.viewport().size().height():
                self.scale(by, by)
                self.current_scale *= by

        w = self.viewport().width()
        h = self.viewport().height()
        wf = self.mapToScene(QPoint(w - 1, 0)).x() - self.mapToScene(
            QPoint(0, 0)).x()
        hf = self.mapToScene(QPoint(0, h - 1)).y() - self.mapToScene(
            QPoint(0, 0)).y()
        lf = p_mapped.x() - p_abs.x() * wf / w
        tf = p_mapped.y() - p_abs.y() * hf / h

        self.ensureVisible(lf, tf, wf, hf, 0, 0)

        target_rect = QRectF(QPointF(lf, tf), QSizeF(wf, hf))
        self.total_scale_div = target_rect.width() / self.viewport().width()

        self.ensureVisible(target_rect, 0, 0)

    # NODE PLACING: -----
    def create_node_instance(self, node, config):
        return self.get_node_instance_class_from_node(node)(node, self, config)

    def add_node_instance(self, ni, pos=None):
        self.scene().addItem(ni)
        ni.enable_personal_logs()
        if pos:
            ni.setPos(pos)

        # select new NI
        self.scene().clearSelection()
        ni.setSelected(True)

        self.all_node_instances.append(ni)

    def add_node_instances(self, node_instances):
        for ni in node_instances:
            self.add_node_instance(ni)

    def remove_node_instance(self, ni):
        ni.about_to_remove_from_scene()  # to stop running threads

        self.scene().removeItem(ni)

        self.all_node_instances.remove(ni)

    def place_new_node_by_shortcut(self):  # Shift+P
        point_in_viewport = None
        selected_NIs = self.selected_node_instances()
        if len(selected_NIs) > 0:
            x = selected_NIs[-1].pos().x() + 150
            y = selected_NIs[-1].pos().y()
            self.node_place_pos = QPointF(x, y)
            point_in_viewport = self.mapFromScene(QPoint(x, y))
        else:  # place in center
            viewport_x = self.viewport().width() / 2
            viewport_y = self.viewport().height() / 2
            point_in_viewport = QPointF(viewport_x, viewport_y).toPoint()
            self.node_place_pos = self.mapToScene(point_in_viewport)

        self.node_choice_widget.reset_list()
        self.show_node_choice_widget(point_in_viewport)

    def place_nodes_from_config(self,
                                nodes_config,
                                offset_pos: QPoint = QPoint(0, 0)):
        new_node_instances = []

        for n_c in nodes_config:
            # find parent node by title, type, package name and description as identifiers
            parent_node_title = n_c['parent node title']
            parent_node_package_name = n_c['parent node package']
            parent_node = None
            for pn in self.all_nodes:
                pn: Node = pn
                if pn.title == parent_node_title and \
                        pn.package == parent_node_package_name:
                    parent_node = pn
                    break

            new_NI = self.create_node_instance(parent_node, n_c)
            self.add_node_instance(
                new_NI,
                QPoint(n_c['position x'], n_c['position y']) + offset_pos)
            new_node_instances.append(new_NI)

        return new_node_instances

    def place_node__cmd(self, node: Node, config=None):

        new_NI = self.create_node_instance(node, config)

        place_command = PlaceNodeInstanceInScene_Command(
            self, new_NI, self.node_place_pos)

        self.undo_stack.push(place_command)

        if self.auto_connection_gate:
            self.try_conn_gate_and_ni(self.auto_connection_gate,
                                      place_command.node_instance)

        return place_command.node_instance

    def remove_node_instance_triggered(
            self, node_instance):  # called from context menu of NodeInstance
        if node_instance in self.selected_node_instances():
            self.undo_stack.push(
                RemoveComponents_Command(self,
                                         self.scene().selectedItems()))
        else:
            self.undo_stack.push(
                RemoveComponents_Command(self, [node_instance]))

    def get_node_instance_class_from_node(self, node):
        return self.all_node_instance_classes[node]

    def get_custom_input_widget_classes(self):
        return self.parent_script.main_window.custom_node_input_widget_classes

    def connect_nodes_from_config(self, node_instances, connections_config):
        for c in connections_config:
            c_parent_node_instance_index = c['parent node instance index']
            c_output_port_index = c['output port index']
            c_connected_node_instance = c['connected node instance']
            c_connected_input_port_index = c['connected input port index']

            if c_connected_node_instance is not None:  # which can be the case when pasting
                parent_node_instance = node_instances[
                    c_parent_node_instance_index]
                connected_node_instance = node_instances[
                    c_connected_node_instance]

                self.connect_gates(
                    parent_node_instance.outputs[c_output_port_index].gate,
                    connected_node_instance.
                    inputs[c_connected_input_port_index].gate)

    # DRAWINGS
    def create_drawing(self, config=None):
        new_drawing = DrawingObject(self, config)
        return new_drawing

    def add_drawing(self, drawing_obj, pos=None):
        self.scene().addItem(drawing_obj)
        if pos:
            drawing_obj.setPos(pos)
        self.drawings.append(drawing_obj)

    def add_drawings(self, drawings):
        for d in drawings:
            self.add_drawing(d)

    def remove_drawing(self, drawing):
        self.scene().removeItem(drawing)
        self.drawings.remove(drawing)

    def place_drawings_from_config(self, drawings, offset_pos=QPoint(0, 0)):
        """
        :param offset_pos: position difference between the center of all selected items when they were copied/cut and
        the current mouse pos which is supposed to be the new center
        :param drawings: the drawing objects
        """
        new_drawings = []
        for d_config in drawings:
            x = d_config['pos x'] + offset_pos.x()
            y = d_config['pos y'] + offset_pos.y()
            new_drawing = self.create_drawing(config=d_config)
            self.add_drawing(new_drawing, QPointF(x, y))
            new_drawings.append(new_drawing)

        return new_drawings

    def create_and_place_drawing__cmd(self, pos, config=None):
        new_drawing_obj = self.create_drawing(config)
        place_command = PlaceDrawingObject_Command(self, pos, new_drawing_obj)
        self.undo_stack.push(place_command)
        return new_drawing_obj

    def move_selected_copmonents__cmd(self, x, y):
        new_rel_pos = QPointF(x, y)

        # if one node item would leave the scene (f.ex. pos.x < 0), stop
        left = False
        for i in self.scene().selectedItems():
            new_pos = i.pos() + new_rel_pos
            if new_pos.x() - i.width / 2 < 0 or \
                    new_pos.x() + i.width / 2 > self.scene().width() or \
                    new_pos.y() - i.height / 2 < 0 or \
                    new_pos.y() + i.height / 2 > self.scene().height():
                left = True
                break

        if not left:
            # moving the items
            items_group = self.scene().createItemGroup(
                self.scene().selectedItems())
            items_group.moveBy(new_rel_pos.x(), new_rel_pos.y())
            self.scene().destroyItemGroup(items_group)

            # saving the command
            self.undo_stack.push(
                MoveComponents_Command(self,
                                       self.scene().selectedItems(),
                                       p_from=-new_rel_pos,
                                       p_to=QPointF(0, 0)))

        self.viewport().repaint()

    def move_selected_nodes_left(self):
        self.move_selected_copmonents__cmd(-40, 0)

    def move_selected_nodes_up(self):
        self.move_selected_copmonents__cmd(0, -40)

    def move_selected_nodes_right(self):
        self.move_selected_copmonents__cmd(+40, 0)

    def move_selected_nodes_down(self):
        self.move_selected_copmonents__cmd(0, +40)

    def selected_components_moved(self, pos_diff):
        items_list = self.scene().selectedItems()

        self.undo_stack.push(
            MoveComponents_Command(self,
                                   items_list,
                                   p_from=-pos_diff,
                                   p_to=QPointF(0, 0)))

    def selected_node_instances(self):
        selected_NIs = []
        for i in self.scene().selectedItems():
            if find_type_in_object(i, NodeInstance):
                selected_NIs.append(i)
        return selected_NIs

    def selected_drawings(self):
        selected_drawings = []
        for i in self.scene().selectedItems():
            if find_type_in_object(i, DrawingObject):
                selected_drawings.append(i)
        return selected_drawings

    def select_all(self):
        for i in self.scene().items():
            if i.ItemIsSelectable:
                i.setSelected(True)
        self.viewport().repaint()

    def select_components(self, comps):
        self.scene().clearSelection()
        for c in comps:
            c.setSelected(True)

    def copy(self):  # ctrl+c
        data = {
            'nodes':
            self.get_node_instances_json_data(self.selected_node_instances()),
            'connections':
            self.get_connections_json_data(self.selected_node_instances()),
            'drawings':
            self.get_drawings_json_data(self.selected_drawings())
        }
        QGuiApplication.clipboard().setText(json.dumps(data))

    def cut(self):  # called from shortcut ctrl+x
        data = {
            'nodes':
            self.get_node_instances_json_data(self.selected_node_instances()),
            'connections':
            self.get_connections_json_data(self.selected_node_instances()),
            'drawings':
            self.get_drawings_json_data(self.selected_drawings())
        }
        QGuiApplication.clipboard().setText(json.dumps(data))
        self.remove_selected_components()

    def paste(self):
        data = {}
        try:
            data = json.loads(QGuiApplication.clipboard().text())
        except Exception as e:
            return

        self.clear_selection()

        # calculate offset
        positions = []
        for d in data['drawings']:
            positions.append({'x': d['pos x'], 'y': d['pos y']})
        for n in data['nodes']:
            positions.append({'x': n['position x'], 'y': n['position y']})

        offset_for_middle_pos = QPointF(0, 0)
        if len(positions) > 0:
            rect = QRectF(positions[0]['x'], positions[0]['y'], 0, 0)
            for p in positions:
                x = p['x']
                y = p['y']
                if x < rect.left():
                    rect.setLeft(x)
                if x > rect.right():
                    rect.setRight(x)
                if y < rect.top():
                    rect.setTop(y)
                if y > rect.bottom():
                    rect.setBottom(y)

            offset_for_middle_pos = self.last_mouse_move_pos - rect.center()

        self.undo_stack.push(Paste_Command(self, data, offset_for_middle_pos))

    def add_component(self, e):
        if find_type_in_object(e, NodeInstance):
            self.add_node_instance(e)
        elif find_type_in_object(e, DrawingObject):
            self.add_drawing(e)

    def remove_component(self, e):
        if find_type_in_object(e, NodeInstance):
            self.remove_node_instance(e)
        elif find_type_in_object(e, DrawingObject):
            self.remove_drawing(e)

    def remove_selected_components(self):
        self.undo_stack.push(
            RemoveComponents_Command(self,
                                     self.scene().selectedItems()))

        self.viewport().update()

    # NODE SELECTION: ----
    def clear_selection(self):
        self.scene().clearSelection()

    # CONNECTIONS: ----
    def connect_gates__cmd(self, parent_gate: PortInstanceGate,
                           child_gate: PortInstanceGate):
        self.undo_stack.push(
            ConnectGates_Command(self,
                                 parent_port=parent_gate.parent_port_instance,
                                 child_port=child_gate.parent_port_instance))

    def connect_gates(self, parent_gate: PortInstanceGate,
                      child_gate: PortInstanceGate):
        parent_port_instance: PortInstance = parent_gate.parent_port_instance
        child_port_instance: PortInstance = child_gate.parent_port_instance

        # if they, their directions and their parent node instances are not equal and if their types are equal
        if parent_port_instance.direction != child_port_instance.direction and \
                parent_port_instance.parent_node_instance != child_port_instance.parent_node_instance and \
                parent_port_instance.type_ == child_port_instance.type_:
            try:  # remove connection if port instances are already connected
                index = parent_port_instance.connected_port_instances.index(
                    child_port_instance)
                parent_port_instance.connected_port_instances.remove(
                    child_port_instance)
                parent_port_instance.disconnected()
                child_port_instance.connected_port_instances.remove(
                    parent_port_instance)
                child_port_instance.disconnected()

            except ValueError:  # connect port instances
                # remove all connections from parent port instance if it's a data input
                if parent_port_instance.direction == 'input' and parent_port_instance.type_ == 'data':
                    for cpi in parent_port_instance.connected_port_instances:
                        self.connect_gates__cmd(
                            parent_gate,
                            cpi.gate)  # actually disconnects the gates

                # remove all connections from child port instance it it's a data input
                if child_port_instance.direction == 'input' and child_port_instance.type_ == 'data':
                    for cpi in child_port_instance.connected_port_instances:
                        self.connect_gates__cmd(
                            child_gate,
                            cpi.gate)  # actually disconnects the gates

                parent_port_instance.connected_port_instances.append(
                    child_port_instance)
                child_port_instance.connected_port_instances.append(
                    parent_port_instance)
                parent_port_instance.connected()
                child_port_instance.connected()

        self.viewport().repaint()

    def try_conn_gate_and_ni(self, parent_gate: PortInstanceGate,
                             child_ni: NodeInstance):
        parent_port_instance: PortInstance = parent_gate.parent_port_instance

        if parent_port_instance.direction == 'output':
            for inp in child_ni.inputs:
                if parent_port_instance.type_ == inp.type_:
                    self.connect_gates__cmd(parent_gate, inp.gate)
                    return
        elif parent_port_instance.direction == 'input':
            for out in child_ni.outputs:
                if parent_port_instance.type_ == out.type_:
                    self.connect_gates__cmd(parent_gate, out.gate)
                    return

    @staticmethod
    def connection_path(p1: QPointF, p2: QPointF):
        """Returns the nice looking QPainterPath of a connection for two given points."""

        path = QPainterPath()

        path.moveTo(p1)

        distance_x = abs(p1.x()) - abs(p2.x())
        distance_y = abs(p1.y()) - abs(p2.y())

        if ((p1.x() < p2.x() - 30)
                or math.sqrt((distance_x**2) +
                             (distance_y**2)) < 100) and (p1.x() < p2.x()):
            path.cubicTo(p1.x() + ((p2.x() - p1.x()) / 2), p1.y(),
                         p1.x() + ((p2.x() - p1.x()) / 2), p2.y(), p2.x(),
                         p2.y())
        elif p2.x() < p1.x() - 100 and abs(distance_x) / 2 > abs(distance_y):
            path.cubicTo(p1.x() + 100 + (p1.x() - p2.x()) / 10, p1.y(),
                         p1.x() + 100 + (p1.x() - p2.x()) / 10,
                         p1.y() - (distance_y / 2),
                         p1.x() - (distance_x / 2),
                         p1.y() - (distance_y / 2))
            path.cubicTo(p2.x() - 100 - (p1.x() - p2.x()) / 10,
                         p2.y() + (distance_y / 2),
                         p2.x() - 100 - (p1.x() - p2.x()) / 10, p2.y(), p2.x(),
                         p2.y())
        else:
            path.cubicTo(p1.x() + 100 + (p1.x() - p2.x()) / 3, p1.y(),
                         p2.x() - 100 - (p1.x() - p2.x()) / 3, p2.y(), p2.x(),
                         p2.y())
        return path

    # GET JSON DATA
    def get_json_data(self):
        flow_dict = {
            'algorithm mode':
            'data flow' if self.algorithm_mode.mode_data_flow else 'exec flow',
            'viewport update mode':
            'sync' if self.viewport_update_mode.sync else 'async',
            'nodes':
            self.get_node_instances_json_data(self.all_node_instances),
            'connections':
            self.get_connections_json_data(self.all_node_instances),
            'drawings':
            self.get_drawings_json_data(self.drawings)
        }
        return flow_dict

    def get_node_instances_json_data(self, node_instances):
        script_node_instances_list = []
        for ni in node_instances:
            node_instance_dict = ni.get_json_data()
            script_node_instances_list.append(node_instance_dict)

        return script_node_instances_list

    def get_connections_json_data(self,
                                  node_instances,
                                  only_with_connections_to=None):
        script_ni_connections_list = []
        for ni in node_instances:
            for out in ni.outputs:
                if len(out.connected_port_instances) > 0:
                    for connected_port in out.connected_port_instances:

                        # this only applies when saving config data through deleting node instances:
                        if only_with_connections_to is not None and \
                                connected_port.parent_node_instance not in only_with_connections_to and \
                                ni not in only_with_connections_to:
                            continue
                        # because I am not allowed to save connections between nodes connected to each other and both
                        # connected to the deleted node, only the connections to the deleted node shall be saved

                        connection_dict = {
                            'parent node instance index':
                            node_instances.index(ni),
                            'output port index': ni.outputs.index(out)
                        }

                        # yes, very important: when copying components, there might be connections going outside the
                        # selected lists, these should be ignored. When saving a project, all components are considered,
                        # so then the index values will never be none
                        connected_ni_index = node_instances.index(connected_port.parent_node_instance) if \
                            node_instances.__contains__(connected_port.parent_node_instance) else \
                            None
                        connection_dict[
                            'connected node instance'] = connected_ni_index

                        connected_ip_index = connected_port.parent_node_instance.inputs.index(connected_port) if \
                            connected_ni_index is not None else None
                        connection_dict[
                            'connected input port index'] = connected_ip_index

                        script_ni_connections_list.append(connection_dict)

        return script_ni_connections_list

    def get_drawings_json_data(self, drawings):
        drawings_list = []
        for drawing in drawings:
            drawing_dict = drawing.get_json_data()

            drawings_list.append(drawing_dict)

        return drawings_list
Пример #4
0

if __name__ == '__main__':

    undo_stack = QUndoStack()

    dictionary = {"a": "AAA", "b": "BBB"}
    print('* initial dict', dictionary)

    undo_stack.push(AppendCommand(dictionary, "c", "CCC"))
    print(undo_stack.undoText(), dictionary)

    undo_stack.push(AppendCommand(dictionary, "d", "DDD"))
    print(undo_stack.undoText(), dictionary)

    undo_stack.undo()
    print(undo_stack.redoText(), dictionary)

    undo_stack.undo()
    print(undo_stack.redoText(), dictionary)

    undo_stack.redo()
    print(undo_stack.undoText(), dictionary)

    undo_stack.push(ModifyCommand(dictionary, "a", "---"))
    print(undo_stack.undoText(), dictionary)

    undo_stack.undo()
    print(undo_stack.redoText(), dictionary)

    undo_stack.redo()
Пример #5
0
class URList(UserList):
    class __SingleStackCommand__(QUndoCommand):
        def __init__(self, myList: 'URList', key: Union[int, slice],
                     value: Any):
            QUndoCommand.__init__(self)
            self._list = myList
            self._key = key
            try:
                self._old_value = self._list[key]
            except IndexError:
                self._old_value = None
            self._new_value = value

        def undo(self) -> NoReturn:
            if self._old_value is None:
                del self._list[self._key]
            else:
                self._list.__realsetitem__(self._key, self._old_value)

        def redo(self) -> NoReturn:
            self._list.__realsetitem__(self._key, self._new_value)

    class __MultiStackCommand__(QUndoCommand):
        def __init__(self, myList: 'URList', key: Union[int, slice]):
            QUndoCommand.__init__(self)
            self._key = key
            self._list = myList

        def undo(self) -> NoReturn:
            self._list[self._key].undo()

        def redo(self) -> NoReturn:
            self._list[self._key].redo()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__stack__ = QUndoStack()
        self._macroRunning = False

    def __setitem__(self, key: Union[int, slice], value: Any) -> NoReturn:
        self.__stack__.push(self.__SingleStackCommand__(self, key, value))

    def __getitem__(self, key):
        if isinstance(key, slice):
            myList = []
            if key.step is None:
                myRange = range(key.start, key.stop + 1)
            else:
                myRange = range(key.start, key.stop + 1, key.step)
            for cKey in myRange:
                myList.append(super().__getitem__(cKey))
            return myList
        return super().__getitem__(key)

    def __realsetitem__(self, key: Union[int, slice], value: Any) -> NoReturn:
        def keyUpdate(base, key, value):
            if key > (len(base.data) - 1):
                super().append(value)
            super().__setitem__(key, value)

        if isinstance(key, slice):
            if key.step is None:
                myRange = range(key.start, key.stop + 1)
            else:
                myRange = range(key.start, key.stop + 1, key.step)
            for cKey in myRange:
                keyUpdate(self, cKey, value[cKey])
                super().__setitem__(cKey, value[cKey])
            return
        keyUpdate(self, key, value)

    def undo(self) -> NoReturn:
        self.__stack__.undo()

    def redo(self) -> NoReturn:
        self.__stack__.redo()

    def startBulkUpdate(self) -> NoReturn:
        self.__stack__.beginMacro('Bulk update')
        self._macroRunning = True

    def endBulkUpdate(self) -> NoReturn:
        self.__stack__.endMacro()
        self._macroRunning = False

    def append(self, item) -> None:
        self.__setitem__(len(self.data), item)
Пример #6
0
class UndoableDict(PathDict):
    """
    The UndoableDict class implements a PathDict-base_dict class with undo/redo
    functionality base_dict on QUndoStack.
    """
    def __init__(self, *args, **kwargs):
        self.__stack = QUndoStack()
        self._macroRunning = False
        super().__init__(*args, **kwargs)

    # Public methods: dictionary-related

    def __setitem__(self, key: str, val: Any) -> NoReturn:
        """
        Calls the undoable command to override PathDict assignment to self[key]
        implementation and pushes this command on the stack.
        """
        if key in self:
            self.__stack.push(_SetItemCommand(self, key, val))
        else:
            self.__stack.push(_AddItemCommand(self, key, val))

    def setItemByPath(self, keys: list, value: Any) -> NoReturn:
        """
        Calls the undoable command to set a value in a nested object
        by key sequence and pushes this command on the stack.
        """
        self.__stack.push(_SetItemCommand(self, keys, value))

    # Public methods: undo/redo-related

    def clearUndoStack(self) -> NoReturn:
        """
        Clears the command stack by deleting all commands on it, and
        returns the stack to the clean state.
        """
        self.__stack.clear()

    def canUndo(self) -> bool:
        """
        :return true if there is a command available for undo;
        otherwise returns false.
        """
        return self.__stack.canUndo()

    def canRedo(self) -> bool:
        """
        :return true if there is a command available for redo;
        otherwise returns false.
        """
        return self.__stack.canRedo()

    def undo(self) -> NoReturn:
        """
        Undoes the current command on stack.
        """
        self.__stack.undo()

    def redo(self) -> NoReturn:
        """
        Redoes the current command on stack.
        """
        self.__stack.redo()

    def undoText(self) -> str:
        """
        :return the current command on stack.
        """
        return self.__stack.undoText()

    def redoText(self) -> str:
        """
        :return the current command on stack.
        """
        return self.__stack.redoText()

    def startBulkUpdate(self, text='Bulk update') -> NoReturn:
        """
        Begins composition of a macro command with the given text description.
        """
        if self._macroRunning:
            print('Macro already running')
            return
        self.__stack.beginMacro(text)
        self._macroRunning = True

    def endBulkUpdate(self) -> NoReturn:
        """
        Ends composition of a macro command.
        """
        if not self._macroRunning:
            print('Macro not running')
            return
        self.__stack.endMacro()
        self._macroRunning = False

    def bulkUpdate(self,
                   key_list: list,
                   item_list: list,
                   text='Bulk update') -> NoReturn:
        """
        Performs a bulk update base_dict on a list of keys and a list of values
        :param key_list: list of keys or path keys to be updated
        :param item_list: the value to be updated
        :return: None
        """
        self.startBulkUpdate(text)
        for key, value in zip(key_list, item_list):
            self.setItemByPath(key, value)
        self.endBulkUpdate()
Пример #7
0
class URDict(UserDict):
    """
    The URDict class implements a dictionary-based class with undo/redo
    functionality based on QUndoStack.
    """

    def __init__(self, *args, **kwargs):
        self._stack = QUndoStack()
        super().__init__(*args, **kwargs)
        self._macroRunning = False

    # Private URDict dictionary-based methods to be called via the QUndoCommand-based classes.

    def _realSetItem(self, key: Union[str, List], value: Any) -> NoReturn:
        """Actually changes the value for the existing key in dictionary."""
        if isinstance(key, list):
            self.getItemByPath(key[:-1])[key[-1]] = value
        else:
            super().__setitem__(key, value)

    def _realAddItem(self, key: str, value: Any) -> NoReturn:
        """Actually adds a key-value pair to dictionary."""
        super().__setitem__(key, value)

    def _realDelItem(self, key: str) -> NoReturn:
        """Actually deletes a key-value pair from dictionary."""
        del self[key]

    def _realSetItemByPath(self, keys: list, value: Any) -> NoReturn:
        """Actually sets the value in a nested object by the key sequence."""
        self.getItemByPath(keys[:-1])[keys[-1]] = value

    # Public URDict dictionary-based methods

    def __setitem__(self, key: str, val: Any) -> NoReturn:
        """Overrides default dictionary assignment to self[key] implementation.
        Calls the undoable command and pushes this command on the stack."""
        if key in self:
            self._stack.push(_SetItemCommand(self, key, val))
        else:
            self._stack.push(_AddItemCommand(self, key, val))

    def setItemByPath(self, keys: list, value: Any) -> NoReturn:
        """Calls the undoable command to set a value in a nested object
        by key sequence and pushes this command on the stack."""
        self._stack.push(_SetItemCommand(self, keys, value))

    def getItemByPath(self, keys: list, default=None) -> Any:
        """Returns a value in a nested object by key sequence."""
        item = self
        for key in keys:
            if key in item.keys():
                item = item[key]
            else:
                return default
        return item

    def getItem(self, key: Union[str, list], default=None):
        """Returns a value in a nested object. Key can be either a sequence
        or a simple string."""
        if isinstance(key, list):
            return self.getItemByPath(key, default)
        else:
            return self.get(key, default)

    # Public URDict undostack-based methods

    def undoText(self) -> NoReturn:
        """Returns the text of the command which will be undone in the next
        call to undo()."""
        return self._stack.undoText()

    def redoText(self) -> NoReturn:
        """Returns the text of the command which will be redone in the next
        call to redo()."""
        return self._stack.redoText()

    def undo(self) -> NoReturn:
        """Undoes the current command on stack."""
        self._stack.undo()

    def redo(self) -> NoReturn:
        """Redoes the current command on stack."""
        self._stack.redo()

    def startBulkUpdate(self, text='Bulk update') -> NoReturn:
        """Begins composition of a macro command with the given text description."""
        if self._macroRunning:
            print('Macro already running')
            return
        self._stack.beginMacro(text)
        self._macroRunning = True

    def endBulkUpdate(self) -> NoReturn:
        """Ends composition of a macro command."""
        if not self._macroRunning:
            print('Macro not running')
            return
        self._stack.endMacro()
        self._macroRunning = False
Пример #8
0
class URDict(dict):
    class _StackCommand(QUndoCommand):
        def __init__(self, dictionary, key, value):
            QUndoCommand.__init__(self)
            self._dictionary = dictionary
            self._key = key
            self._old_value = None

            thisKey = key
            if isinstance(thisKey, list):
                thisKey = thisKey[0]
            if thisKey in dictionary:
                self._old_value = dictionary[key]

            self._new_value = value

        def undo(self):
            # self.setText("     undo command {} - {}:{} = ".format(self._dictionary, self._key, self._value))
            if self._old_value is None:
                self.setText("     undo command {} - {}:{} = ".format(
                    self._dictionary, self._key, self._new_value))
                del self._dictionary[self._key]
            else:
                self._dictionary.__realsetitem__(self._key, self._old_value)

        def redo(self):
            # self.setText("  do/redo command {} + {}:{} = ".format(self._dictionary, self._key, self._value))
            self._dictionary.__realsetitem__(self._key, self._new_value)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__stack__ = QUndoStack()

    def __setitem__(self, key, val):
        self.__stack__.push(self._StackCommand(self, key, val))

    def __getitem__(self, key):
        if isinstance(key, list):
            return self.getByPath(key)
        return super().__getitem__(key)

    def __realsetitem__(self, key, val):
        if isinstance(key, list):
            self.setByPath(key, val)
        else:
            super().__setitem__(key, val)

    def undoText(self):
        return self.__stack__.undoText()

    def redoText(self):
        return self.__stack__.redoText()

    def undo(self):
        self.__stack__.undo()

    def redo(self):
        self.__stack__.redo()

    def getByPath(self, keys):
        """Access a nested object in root by key sequence.
        We can't use reduce and operator"""
        item = self
        for key in keys:
            if key in item.keys():
                item = item[key]
            else:
                raise KeyError
        return item

    def setByPath(self, keys, value):
        """Get a value in a nested object in root by key sequence."""
        self.getByPath(keys[:-1])[keys[-1]] = value