Beispiel #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()
class stackable:
    def __init__(self):
        self.stack = QUndoStack()

    def _myWrapper(self, func):
        def inner(thisDict, key, val):
            if key in thisDict.keys():
                print('Overwriting Key: {}'.format(key))
                self.stack.push(ModifyCommand(thisDict, key, val))
            else:
                self.stack.push(AppendCommand(thisDict, key, val))

        return inner

    def _delWrapper(self, func):
        def inner(dict, key):
            dict._realDelItem(key)

        return inner
Beispiel #3
0
class UndoableDict(PathDict):
    """
    The UndoableDict class implements a PathDict-based class with undo/redo
    functionality based on QUndoStack.
    """
    def __init__(self, *args, **kwargs):
        self.stack = QUndoStack()
        super().__init__(*args, **kwargs)

    # Public methods

    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))
Beispiel #4
0
class NodeGraph(QtCore.QObject):
    def __init__(self, parent=None):
        super(NodeGraph, self).__init__(parent)
        self._model = NodeGraphModel()
        self._viewer = NodeViewer()
        self._undo_stack = QUndoStack(self)
        self._init_actions()
        self._wire_signals()

    def _wire_signals(self):
        self._viewer.moved_nodes.connect(self._on_nodes_moved)
        self._viewer.search_triggered.connect(self._on_search_triggered)
        self._viewer.connection_changed.connect(self._on_connection_changed)

    def _init_actions(self):
        # setup tab search shortcut.
        tab = QAction('Search Nodes', self)
        tab.setShortcut('tab')
        tab.triggered.connect(self._toggle_tab_search)
        self._viewer.addAction(tab)
        setup_actions(self)

    def _toggle_tab_search(self):
        """
        toggle the tab search widget.
        """
        self._viewer.tab_search_set_nodes(NodeVendor.names)
        self._viewer.tab_search_toggle()

    def _on_nodes_moved(self, node_data):
        """
        called when selected nodes in the viewer has changed position.

        Args:
            node_data (dict): {<node_view>: <previous_pos>}
        """
        self._undo_stack.beginMacro('moved nodes')
        for node_view, prev_pos in node_data.items():
            node = self._model.nodes[node_view.id]
            self._undo_stack.push(NodeMovedCmd(node, node.pos(), prev_pos))
        self._undo_stack.endMacro()

    def _on_search_triggered(self, node_type, pos):
        """
        called when the tab search widget is triggered in the viewer.

        Args:
            node_type (str): node identifier.
            pos (tuple): x,y position for the node.
        """
        self.create_node(node_type, pos=pos)

    def _on_connection_changed(self, disconnected, connected):
        """
        called when a pipe connection has been changed in the viewer.

        Args:
            disconnected (list[list[widgets.port.PortItem]):
                pair list of port view items.
            connected (list[list[widgets.port.PortItem]]):
                pair list of port view items.
        """
        if not (disconnected or connected):
            return

        label = 'connected node(s)' if connected else 'disconnected node(s)'
        ptypes = {'in': 'inputs', 'out': 'outputs'}

        self._undo_stack.beginMacro(label)
        for p1_view, p2_view in disconnected:
            node1 = self._model.nodes[p1_view.node.id]
            node2 = self._model.nodes[p2_view.node.id]
            port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name]
            port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name]
            port1.disconnect_from(port2)
        for p1_view, p2_view in connected:
            node1 = self._model.nodes[p1_view.node.id]
            node2 = self._model.nodes[p2_view.node.id]
            port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name]
            port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name]
            port1.connect_to(port2)
        self._undo_stack.endMacro()

    @property
    def model(self):
        """
        Return the node graph model.

        Returns:
            NodeGraphModel: model object.
        """
        return self._model

    def show(self):
        """
        Show node graph viewer widget.
        """
        self._viewer.show()

    def hide(self):
        """
        Hide node graph viewer widget.
        """
        self._viewer.hide()

    def close(self):
        """
        Close node graph viewer widget.
        """
        self._viewer.close()

    def viewer(self):
        """
        Return the node graph viewer widget object.

        Returns:
            NodeGraphQt.widgets.viewer.NodeViewer: viewer widget.
        """
        return self._viewer

    def scene(self):
        """
        Return the scene object.

        Returns:
            NodeGraphQt.widgets.scene.NodeScene: node scene.
        """
        return self._viewer.scene()

    def undo_stack(self):
        """
        Returns the undo stack used in the node graph

        Returns:
            QUndoStack: undo stack.
        """
        return self._undo_stack

    def begin_undo(self, name='undo'):
        """
        Start of an undo block followed by a end_undo().

        Args:
            name (str): name for the undo block.
        """
        self._undo_stack.beginMacro(name)

    def end_undo(self):
        """
        End of an undo block started by begin_undo().
        """
        self._undo_stack.endMacro()

    def context_menu(self):
        """
        Returns a node graph context menu object.

        Returns:
            ContextMenu: node graph context menu object instance.
        """
        return self._viewer.context_menu()

    def acyclic(self):
        """
        Returns true if the current node graph is acyclic.

        Returns:
            bool: true if acyclic.
        """
        return self._model.acyclic

    def set_acyclic(self, mode=True):
        """
        Set the node graph to be acyclic or not. (default=True)

        Args:
            mode (bool): false to disable acyclic.
        """
        self._model.acyclic = mode
        self._viewer.acyclic = mode

    def set_pipe_layout(self, layout='curved'):
        """
        Set node graph pipes to be drawn straight or curved by default
        all pipes are set curved. (default='curved')

        Args:
            layout (str): 'straight' or 'curved'
        """
        self._viewer.set_pipe_layout(layout)

    def fit_to_selection(self):
        """
        Sets the zoom level to fit selected nodes.
        If no nodes are selected then all nodes in the graph will be framed.
        """
        nodes = self.selected_nodes() or self.all_nodes()
        if not nodes:
            return
        self._viewer.zoom_to_nodes([n.view for n in nodes])

    def reset_zoom(self):
        """
        Reset the zoom level
        """
        self._viewer.reset_zoom()

    def set_zoom(self, zoom=0):
        """
        Set the zoom factor of the Node Graph the default is 0.0

        Args:
            zoom (float): zoom factor max zoom out -0.9 max zoom in 2.0
        """
        self._viewer.set_zoom(zoom)

    def get_zoom(self):
        """
        Get the current zoom level of the node graph.

        Returns:
            float: the current zoom level.
        """
        return self._viewer.get_zoom()

    def center_on(self, nodes=None):
        """
        Center the node graph on the given nodes or all nodes by default.

        Args:
            nodes (list[NodeGraphQt.Node]): a list of nodes.
        """
        self._viewer.center_selection(nodes)

    def center_selection(self):
        """
        Center the node graph on the current selected nodes.
        """
        nodes = self._viewer.selected_nodes()
        self._viewer.center_selection(nodes)

    def registered_nodes(self):
        """
        Return a list of all node types that have been registered.
        To register a node see "NodeGraphWidget.register_node()"

        Returns:
            list[str]: node types.
        """
        return sorted(NodeVendor.nodes.keys())

    def register_node(self, node, alias=None):
        """
        Register a node.

        Args:
            node (NodeGraphQt.Node): node object.
            alias (str): custom alias name for the node type.
        """
        NodeVendor.register_node(node, alias)

    def create_node(self,
                    node_type,
                    name=None,
                    selected=True,
                    color=None,
                    pos=None):
        """
        Create a new node in the node graph.
        To list all node types see "NodeGraphWidget.registered_nodes()"

        Args:
            node_type (str): node instance type.
            name (str): set name of the node.
            selected (bool): set created node to be selected.
            color (tuple or str): node color (255, 255, 255) or '#FFFFFF'.
            pos (tuple): set position of the node (x, y).

        Returns:
            NodeGraphQt.Node: created instance of a node.
        """
        NodeInstance = NodeVendor.create_node_instance(node_type)
        if NodeInstance:
            node = NodeInstance()
            node._graph = self
            node.update()

            self._undo_stack.beginMacro('created node')
            self._undo_stack.push(NodeAddedCmd(self, node, pos))
            if name:
                node.set_name(name)
            else:
                node.set_name(node.NODE_NAME)
            if color:
                if isinstance(color, str):
                    color = color[1:] if color[0] is '#' else color
                    color = tuple(int(color[i:i + 2], 16) for i in (0, 2, 4))
                node.set_color(*color)
            node.set_selected(selected)
            self._undo_stack.endMacro()
            return node
        raise Exception('\n\n>> Cannot find node:\t"{}"\n'.format(node_type))

    def add_node(self, node):
        """
        Add a node into the node graph.

        Args:
            node (NodeGraphQt.Node): node object.
        """
        assert isinstance(node, NodeObject), 'node must be a Node instance.'
        node._graph = self
        node.NODE_NAME = self.get_unique_name(node.NODE_NAME)
        node.model.name = node.NODE_NAME
        node.update()
        self._undo_stack.push(NodeAddedCmd(self, node))

    def delete_node(self, node):
        """
        Remove the node from the node graph.

        Args:
            node (NodeGraphQt.Node): node object.
        """
        assert isinstance(node, NodeObject), \
            'node must be a instance of a NodeObject.'
        self._undo_stack.push(NodeRemovedCmd(self, node))

    def delete_nodes(self, nodes):
        """
        Remove a list of nodes from the node graph.

        Args:
            nodes (list[NodeGraphQt.Node]): list of node instances.
        """
        self._undo_stack.beginMacro('deleted nodes')
        [self.delete_node(n) for n in nodes]
        self._undo_stack.endMacro()

    def all_nodes(self):
        """
        Return all nodes in the node graph.

        Returns:
            list[NodeGraphQt.Node]: list of nodes.
        """
        return list(self._model.nodes.values())

    def selected_nodes(self):
        """
        Return all selected nodes that are in the node graph.

        Returns:
            list[NodeGraphQt.Node]: list of nodes.
        """
        nodes = []
        for item in self._viewer.selected_nodes():
            node = self._model.nodes[item.id]
            nodes.append(node)
        return nodes

    def select_all(self):
        """
        Select all nodes in the current node graph.
        """
        self._undo_stack.beginMacro('select all')
        for node in self.all_nodes():
            node.set_selected(True)
        self._undo_stack.endMacro()

    def clear_selection(self):
        """
        Clears the selection in the node graph.
        """
        self._undo_stack.beginMacro('deselected nodes')
        for node in self.all_nodes():
            node.set_selected(False)
        self._undo_stack.endMacro()

    def get_node_by_id(self, node_id=None):
        """
        Get the node object by it's id.

        Args:
            node_id (str): node id

        Returns:
            NodeGraphQt.NodeObject: node object.
        """
        return self._model.nodes.get(node_id)

    def get_node_by_name(self, name):
        """
        Returns node object that matches the name.

        Args:
            name (str): name of the node.
        Returns:
            NodeGraphQt.Node: node object.
        """
        for node_id, node in self._model.nodes.items():
            if node.name() == name:
                return node

    def get_unique_name(self, name):
        """
        return a unique node name for the node.

        Args:
            name (str): node name.

        Returns:
            str: unique node name.
        """
        name = ' '.join(name.split())
        node_names = [n.name() for n in self.all_nodes()]
        if name not in node_names:
            return name

        regex = re.compile('[\w ]+(?: )*(\d+)')
        search = regex.search(name)
        if not search:
            for x in range(1, len(node_names) + 1):
                new_name = '{} {}'.format(name, x)
                if new_name not in node_names:
                    return new_name

        version = search.group(1)
        name = name[:len(version) * -1].strip()
        for x in range(1, len(node_names) + 1):
            new_name = '{} {}'.format(name, x)
            if new_name not in node_names:
                return new_name

    def current_session(self):
        """
        returns the file path to the currently loaded session.

        Returns:
            str: path to the currently loaded session
        """
        return self._model.session

    def clear_session(self):
        """
        clear the loaded node layout session.
        """
        for n in self.all_nodes():
            self.delete_node(n)
        self._undo_stack.clear()
        self._model.session = None

    def _serialize(self, nodes):
        """
        serialize nodes to a dict.

        Args:
            nodes (list[NodeGraphQt.Nodes]): list of node instances.

        Returns:
            dict: serialized data.
        """
        serial_data = {'nodes': {}, 'connections': []}
        nodes_data = {}
        for n in nodes:

            # update the node model.
            n.update_model()

            nodes_data.update(n.model.to_dict)

        for n_id, n_data in nodes_data.items():
            serial_data['nodes'][n_id] = n_data

            inputs = n_data.pop('inputs') if n_data.get('inputs') else {}
            outputs = n_data.pop('outputs') if n_data.get('outputs') else {}

            for pname, conn_data in inputs.items():
                for conn_id, prt_names in conn_data.items():
                    for conn_prt in prt_names:
                        pipe = {
                            'in': [n_id, pname],
                            'out': [conn_id, conn_prt]
                        }
                        if pipe not in serial_data['connections']:
                            serial_data['connections'].append(pipe)

            for pname, conn_data in outputs.items():
                for conn_id, prt_names in conn_data.items():
                    for conn_prt in prt_names:
                        pipe = {
                            'out': [n_id, pname],
                            'in': [conn_id, conn_prt]
                        }
                        if pipe not in serial_data['connections']:
                            serial_data['connections'].append(pipe)

        if not serial_data['connections']:
            serial_data.pop('connections')

        return serial_data

    def _deserialize(self, data, relative_pos=False, pos=None):
        """
        deserialize node data.

        Args:
            data (dict): node data.
            relative_pos (bool): position node relative to the cursor.

        Returns:
            list[NodeGraphQt.Nodes]: list of node instances.
        """
        nodes = {}

        # build the nodes.
        for n_id, n_data in data.get('nodes', {}).items():
            identifier = n_data['type']
            NodeInstance = NodeVendor.create_node_instance(identifier)
            if NodeInstance:
                node = NodeInstance()
                node._graph = self

                name = self.get_unique_name(n_data.get('name', node.NODE_NAME))
                n_data['name'] = name

                # set properties.
                for prop, val in node.model.properties.items():
                    if prop in n_data.keys():
                        setattr(node.model, prop, n_data[prop])

                # set custom properties.
                for prop, val in n_data.get('custom', {}).items():
                    if prop in node.model.custom_properties.keys():
                        node.model.custom_properties[prop] = val

                node.update()

                self._undo_stack.push(
                    NodeAddedCmd(self, node, n_data.get('pos')))
                nodes[n_id] = node

        # build the connections.
        for connection in data.get('connections', []):
            nid, pname = connection.get('in', ('', ''))
            in_node = nodes.get(nid)
            if not in_node:
                continue
            in_port = in_node.inputs().get(pname) if in_node else None

            nid, pname = connection.get('out', ('', ''))
            out_node = nodes.get(nid)
            if not out_node:
                continue
            out_port = out_node.outputs().get(pname) if out_node else None

            if in_port and out_port:
                self._undo_stack.push(PortConnectedCmd(in_port, out_port))

        node_objs = list(nodes.values())
        if relative_pos:
            self._viewer.move_nodes([n.view for n in node_objs])
            [setattr(n.model, 'pos', n.view.pos) for n in node_objs]
        elif pos:
            self._viewer.move_nodes([n.view for n in node_objs], pos=pos)

        return node_objs

    def save_session(self, file_path):
        """
        Saves the current node graph session layout to a JSON formatted file.

        Args:
            file_path (str): path to the saved node layout.
        """
        serliazed_data = self._serialize(self.selected_nodes())
        file_path = file_path.strip()
        with open(file_path, 'w') as file_out:
            json.dump(serliazed_data,
                      file_out,
                      indent=2,
                      separators=(',', ':'))

    def load_session(self, file_path):
        """
        Load node graph session layout file.

        Args:
            file_path (str): path to the serialized layout file.
        """
        file_path = file_path.strip()
        if not os.path.isfile(file_path):
            raise IOError('file does not exist.')

        self.clear_session()

        try:
            with open(file_path) as data_file:
                layout_data = json.load(data_file)
        except Exception as e:
            layout_data = None
            print('Cannot read data from file.\n{}'.format(e))

        if not layout_data:
            return

        self._deserialize(layout_data)
        self._undo_stack.clear()
        self._model.session = file_path

    def copy_nodes(self, nodes=None):
        """
        copy nodes to the clipboard by default this method copies
        the selected nodes from the node graph.

        Args:
            nodes (list[NodeGraphQt.Node]): list of node instances.
        """
        nodes = nodes or self.selected_nodes()
        if not nodes:
            return False
        clipboard = QClipboard()
        serial_data = self._serialize(nodes)
        serial_str = json.dumps(serial_data)
        if serial_str:
            clipboard.setText(serial_str)
            return True
        return False

    def paste_nodes(self):
        """
        Pastes nodes from the clipboard.
        """
        clipboard = QClipboard()
        cb_string = clipboard.text()
        if not cb_string:
            return

        self._undo_stack.beginMacro('pasted nodes')
        serial_data = json.loads(cb_string)
        self.clear_selection()
        nodes = self._deserialize(serial_data, True)
        [n.set_selected(True) for n in nodes]
        self._undo_stack.endMacro()

    def duplicate_nodes(self, nodes):
        """
        Create duplicates nodes.

        Args:
            nodes (list[NodeGraphQt.Node]): list of node objects.
        Returns:
            list[NodeGraphQt.Node]: list of duplicated node instances.
        """
        if not nodes:
            return

        self._undo_stack.beginMacro('duplicated nodes')

        self.clear_selection()
        serial = self._serialize(nodes)
        new_nodes = self._deserialize(serial)
        offset = 50
        for n in new_nodes:
            x, y = n.pos()
            n.set_pos(x + offset, y + offset)
            n.set_property('selected', True)

        self._undo_stack.endMacro()
        return new_nodes

    def disable_nodes(self, nodes, mode=None):
        """
        Disable/Enable specified nodes.

        Args:
            nodes (list[NodeGraphQt.Node]): list of node instances.
            mode (bool): (optional) disable state of the nodes.
        """
        if not nodes:
            return
        if mode is None:
            mode = not nodes[0].disabled()
        if len(nodes) > 1:
            text = {False: 'enabled', True: 'disabled'}[mode]
            text = '{} ({}) nodes'.format(text, len(nodes))
            self._undo_stack.beginMacro(text)
            [n.set_disabled(mode) for n in nodes]
            self._undo_stack.endMacro()
            return
        nodes[0].set_disabled(mode)
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)
Beispiel #6
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
Beispiel #7
0
        #self.setText("     undo command {} - {}:{} = ".format(self._dictionary, self._key, self._value))
        dictionary[self._key] = self._old_value

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


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", "---"))
Beispiel #8
0
class NodeGraph(QtCore.QObject):
    """
    base node graph controller.

    Args:
        tab_search_key(str): hotkey for the tab search widget (default: "tab").
    """

    #: signal for when a node has been created in the node graph.
    node_created = QtCore.Signal(NodeObject)
    #: signal for when a node is selected.
    node_selected = QtCore.Signal(NodeObject)
    #: signal for when a node has been connected.
    port_connected = QtCore.Signal(Port, Port)
    #: signal for when drop data has been added to the graph.
    data_dropped = QtCore.Signal(QtCore.QMimeData, QtCore.QPoint)

    def __init__(self, parent=None, tab_search_key='tab'):
        super(NodeGraph, self).__init__(parent)
        self.setObjectName('NodeGraphQt')
        self._model = NodeGraphModel()
        self._viewer = NodeViewer()
        self._vendor = NodeVendor()
        self._undo_stack = QUndoStack(self)

        tab = QAction('Search Nodes', self)
        tab.setShortcut(tab_search_key)
        tab.triggered.connect(self._toggle_tab_search)
        self._viewer.addAction(tab)

        self._wire_signals()

    def _wire_signals(self):
        # internal signals.
        self._viewer.search_triggered.connect(self._on_search_triggered)
        self._viewer.connection_changed.connect(self._on_connection_changed)
        self._viewer.moved_nodes.connect(self._on_nodes_moved)

        # pass through signals.
        self._viewer.node_selected.connect(self._on_node_selected)
        self._viewer.data_dropped.connect(self._on_node_data_dropped)

    def _toggle_tab_search(self):
        """
        toggle the tab search widget.
        """
        self._viewer.tab_search_set_nodes(self._vendor.names)
        self._viewer.tab_search_toggle()

    def _on_node_selected(self, node_id):
        """
        called when a node in the viewer is selected on left click.
        (emits the node object when the node is clicked)

        Args:
            node_id (str): node id emitted by the viewer.
        """
        node = self.get_node_by_id(node_id)
        self.node_selected.emit(node)

    def _on_node_data_dropped(self, data, pos):
        """
        called when data has been dropped on the viewer.

        Args:
            data (QtCore.QMimeData): mime data.
            pos (QtCore.QPoint): scene position relative to the drop.
        """
        self.data_dropped.emit(data, pos)

    def _on_nodes_moved(self, node_data):
        """
        called when selected nodes in the viewer has changed position.

        Args:
            node_data (dict): {<node_view>: <previous_pos>}
        """
        self._undo_stack.beginMacro('moved nodes')
        for node_view, prev_pos in node_data.items():
            node = self._model.nodes[node_view.id]
            self._undo_stack.push(NodeMovedCmd(node, node.pos(), prev_pos))
        self._undo_stack.endMacro()

    def _on_search_triggered(self, node_type, pos):
        """
        called when the tab search widget is triggered in the viewer.

        Args:
            node_type (str): node identifier.
            pos (tuple): x,y position for the node.
        """
        self.create_node(node_type, pos=pos)

    def _on_connection_changed(self, disconnected, connected):
        """
        called when a pipe connection has been changed in the viewer.

        Args:
            disconnected (list[list[widgets.port.PortItem]):
                pair list of port view items.
            connected (list[list[widgets.port.PortItem]]):
                pair list of port view items.
        """
        if not (disconnected or connected):
            return

        label = 'connected node(s)' if connected else 'disconnected node(s)'
        ptypes = {'in': 'inputs', 'out': 'outputs'}

        self._undo_stack.beginMacro(label)
        for p1_view, p2_view in disconnected:
            node1 = self._model.nodes[p1_view.node.id]
            node2 = self._model.nodes[p2_view.node.id]
            port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name]
            port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name]
            port1.disconnect_from(port2)
        for p1_view, p2_view in connected:
            node1 = self._model.nodes[p1_view.node.id]
            node2 = self._model.nodes[p2_view.node.id]
            port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name]
            port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name]
            port1.connect_to(port2)
        self._undo_stack.endMacro()

    @property
    def model(self):
        """
        Returns the model used to store the node graph data.

        Returns:
            NodeGraphQt.base.model.NodeGraphModel: node graph model.
        """
        return self._model

    def show(self):
        """
        Show node graph viewer widget this is just a convenience
        function to :meth:`NodeGraph.viewer().show()`.
        """
        self._viewer.show()

    def close(self):
        """
        Close node graph NodeViewer widget this is just a convenience
        function to :meth:`NodeGraph.viewer().close()`.
        """
        self._viewer.close()

    def viewer(self):
        """
        Return the node graph viewer widget.

        Returns:
            NodeGraphQt.widgets.viewer.NodeViewer: viewer widget.
        """
        return self._viewer

    def scene(self):
        """
        Return the scene object.

        Returns:
            NodeGraphQt.widgets.scene.NodeScene: node scene.
        """
        return self._viewer.scene()

    def undo_stack(self):
        """
        Returns the undo stack used in the node graph

        Returns:
            QtWidgets.QUndoStack: undo stack.
        """
        return self._undo_stack

    def clear_undo_stack(self):
        """
        Clears the undo stack.
        (convenience function to :meth:`NodeGraph.undo_stack().clear`)
        """
        self._undo_stack.clear()

    def begin_undo(self, name='undo'):
        """
        Start of an undo block followed by a
        :meth:`NodeGraph.end_undo()`.

        Args:
            name (str): name for the undo block.
        """
        self._undo_stack.beginMacro(name)

    def end_undo(self):
        """
        End of an undo block started by
        :meth:`NodeGraph.begin_undo()`.
        """
        self._undo_stack.endMacro()

    def context_menu(self):
        """
        Returns the node graph root context menu object.

        Returns:
            Menu: context menu object.
        """
        return Menu(self._viewer, self._viewer.context_menu())

    def acyclic(self):
        """
        Returns true if the current node graph is acyclic.

        Returns:
            bool: true if acyclic (default: True).
        """
        return self._model.acyclic

    def set_acyclic(self, mode=True):
        """
        Set the node graph to be acyclic or not. (default=True)

        Args:
            mode (bool): false to disable acyclic.
        """
        self._model.acyclic = mode
        self._viewer.acyclic = mode

    def set_pipe_layout(self, layout='curved'):
        """
        Set node graph pipes to be drawn straight or curved by default
        all pipes are set curved. (default='curved')

        Args:
            layout (str): 'straight' or 'curved'
        """
        self._viewer.set_pipe_layout(layout)

    def fit_to_selection(self):
        """
        Sets the zoom level to fit selected nodes.
        If no nodes are selected then all nodes in the graph will be framed.
        """
        nodes = self.selected_nodes() or self.all_nodes()
        if not nodes:
            return
        self._viewer.zoom_to_nodes([n.view for n in nodes])

    def reset_zoom(self):
        """
        Reset the zoom level
        """
        self._viewer.reset_zoom()

    def set_zoom(self, zoom=0):
        """
        Set the zoom factor of the Node Graph the default is 0.0

        Args:
            zoom (float): zoom factor (max zoom out -0.9 / max zoom in 2.0)
        """
        self._viewer.set_zoom(zoom)

    def get_zoom(self):
        """
        Get the current zoom level of the node graph.

        Returns:
            float: the current zoom level.
        """
        return self._viewer.get_zoom()

    def center_on(self, nodes=None):
        """
        Center the node graph on the given nodes or all nodes by default.

        Args:
            nodes (list[NodeGraphQt.Node]): a list of nodes.
        """
        self._viewer.center_selection(nodes)

    def center_selection(self):
        """
        Centers on the current selected nodes.
        """
        nodes = self._viewer.selected_nodes()
        self._viewer.center_selection(nodes)

    def registered_nodes(self):
        """
        Return a list of all node types that have been registered.

        To register a node see :meth:`NodeGraph.register_node`

        Returns:
            list[str]: list of node type identifiers.
        """
        return sorted(self._vendor.nodes.keys())

    def register_node(self, node, alias=None):
        """
        Register the node to the node graph vendor.

        Args:
            node (NodeGraphQt.NodeObject): node.
            alias (str): custom alias name for the node type.
        """
        self._vendor.register_node(node, alias)

    def create_node(self,
                    node_type,
                    name=None,
                    selected=True,
                    color=None,
                    pos=None):
        """
        Create a new node in the node graph.

        (To list all node types see :meth:`NodeGraph.registered_nodes`)

        Args:
            node_type (str): node instance type.
            name (str): set name of the node.
            selected (bool): set created node to be selected.
            color (tuple or str): node color (255, 255, 255) or '#FFFFFF'.
            pos (list[int, int]): initial x, y position for the node (default: (0, 0)).

        Returns:
            NodeGraphQt.Node: the created instance of the node.
        """
        NodeCls = self._vendor.create_node_instance(node_type)
        if NodeCls:
            node = NodeCls()

            node._graph = self
            node.model._graph_model = self.model

            wid_types = node.model.__dict__.pop('_TEMP_property_widget_types')
            prop_attrs = node.model.__dict__.pop('_TEMP_property_attrs')

            graph_attrs = self.model.node_property_attrs
            if node.type not in graph_attrs.keys():
                graph_attrs[node.type] = {
                    n: {
                        'widget_type': wt
                    }
                    for n, wt in wid_types.items()
                }
                for pname, pattrs in prop_attrs.items():
                    graph_attrs[node.type][pname].update(pattrs)

            node.NODE_NAME = self.get_unique_name(name or node.NODE_NAME)
            node.model.name = node.NODE_NAME
            node.model.selected = selected
            if color:
                if isinstance(color, str):
                    color = color[1:] if color[0] is '#' else color
                    color = tuple(int(color[i:i + 2], 16) for i in (0, 2, 4))
                node.model.color = color
            if pos:
                node.model.pos = [float(pos[0]), float(pos[1])]

            node.update()

            undo_cmd = NodeAddedCmd(self, node, node.model.pos)
            undo_cmd.setText('created node')
            self._undo_stack.push(undo_cmd)
            self.node_created.emit(node)
            return node
        raise Exception('\n\n>> Cannot find node:\t"{}"\n'.format(node_type))

    def add_node(self, node):
        """
        Add a node into the node graph.

        Args:
            node (NodeGraphQt.Node): node object.
        """
        assert isinstance(node, NodeObject), 'node must be a Node instance.'

        wid_types = node.model.__dict__.pop('_TEMP_property_widget_types')
        prop_attrs = node.model.__dict__.pop('_TEMP_property_attrs')

        graph_attrs = self.model.node_property_attrs
        if node.type not in graph_attrs.keys():
            graph_attrs[node.type] = {
                n: {
                    'widget_type': wt
                }
                for n, wt in wid_types.items()
            }
            for pname, pattrs in prop_attrs.items():
                graph_attrs[node.type][pname].update(pattrs)

        node._graph = self
        node.NODE_NAME = self.get_unique_name(node.NODE_NAME)
        node.model._graph_model = self.model
        node.model.name = node.NODE_NAME
        node.update()
        self._undo_stack.push(NodeAddedCmd(self, node))

    def delete_node(self, node):
        """
        Remove the node from the node graph.

        Args:
            node (NodeGraphQt.Node): node object.
        """
        assert isinstance(node, NodeObject), \
            'node must be a instance of a NodeObject.'
        self._undo_stack.push(NodeRemovedCmd(self, node))

    def delete_nodes(self, nodes):
        """
        Remove a list of specified nodes from the node graph.

        Args:
            nodes (list[NodeGraphQt.Node]): list of node instances.
        """
        self._undo_stack.beginMacro('deleted nodes')
        [self.delete_node(n) for n in nodes]
        self._undo_stack.endMacro()

    def all_nodes(self):
        """
        Return all nodes in the node graph.

        Returns:
            list[NodeGraphQt.Node]: list of nodes.
        """
        return list(self._model.nodes.values())

    def selected_nodes(self):
        """
        Return all selected nodes that are in the node graph.

        Returns:
            list[NodeGraphQt.Node]: list of nodes.
        """
        nodes = []
        for item in self._viewer.selected_nodes():
            node = self._model.nodes[item.id]
            nodes.append(node)
        return nodes

    def select_all(self):
        """
        Select all nodes in the node graph.
        """
        self._undo_stack.beginMacro('select all')
        for node in self.all_nodes():
            node.set_selected(True)
        self._undo_stack.endMacro()

    def clear_selection(self):
        """
        Clears the selection in the node graph.
        """
        self._undo_stack.beginMacro('deselected nodes')
        for node in self.all_nodes():
            node.set_selected(False)
        self._undo_stack.endMacro()

    def get_node_by_id(self, node_id=None):
        """
        Returns the node from the node id string.

        Args:
            node_id (str): node id (:meth:`NodeObject.id`)

        Returns:
            NodeGraphQt.NodeObject: node object.
        """
        return self._model.nodes.get(node_id)

    def get_node_by_name(self, name):
        """
        Returns node that matches the name.

        Args:
            name (str): name of the node.
        Returns:
            NodeGraphQt.NodeObject: node object.
        """
        for node_id, node in self._model.nodes.items():
            if node.name() == name:
                return node

    def get_unique_name(self, name):
        """
        Creates a unique node name to avoid having nodes with the same name.

        Args:
            name (str): node name.

        Returns:
            str: unique node name.
        """
        name = ' '.join(name.split())
        node_names = [n.name() for n in self.all_nodes()]
        if name not in node_names:
            return name

        regex = re.compile('[\w ]+(?: )*(\d+)')
        search = regex.search(name)
        if not search:
            for x in range(1, len(node_names) + 1):
                new_name = '{} {}'.format(name, x)
                if new_name not in node_names:
                    return new_name

        version = search.group(1)
        name = name[:len(version) * -1].strip()
        for x in range(1, len(node_names) + 1):
            new_name = '{} {}'.format(name, x)
            if new_name not in node_names:
                return new_name

    def current_session(self):
        """
        Returns the file path to the currently loaded session.

        Returns:
            str: path to the currently loaded session
        """
        return self._model.session

    def clear_session(self):
        """
        Clears the current node graph session.
        """
        for n in self.all_nodes():
            self.delete_node(n)
        self._undo_stack.clear()
        self._model.session = None

    def _serialize(self, nodes):
        """
        serialize nodes to a dict.
        (used internally by the node graph)

        Args:
            nodes (list[NodeGraphQt.Nodes]): list of node instances.

        Returns:
            dict: serialized data.
        """
        serial_data = {'nodes': {}, 'connections': []}
        nodes_data = {}
        for n in nodes:

            # update the node model.
            n.update_model()

            nodes_data.update(n.model.to_dict)

        for n_id, n_data in nodes_data.items():
            serial_data['nodes'][n_id] = n_data

            inputs = n_data.pop('inputs') if n_data.get('inputs') else {}
            outputs = n_data.pop('outputs') if n_data.get('outputs') else {}

            for pname, conn_data in inputs.items():
                for conn_id, prt_names in conn_data.items():
                    for conn_prt in prt_names:
                        pipe = {
                            'in': [n_id, pname],
                            'out': [conn_id, conn_prt]
                        }
                        if pipe not in serial_data['connections']:
                            serial_data['connections'].append(pipe)

            for pname, conn_data in outputs.items():
                for conn_id, prt_names in conn_data.items():
                    for conn_prt in prt_names:
                        pipe = {
                            'out': [n_id, pname],
                            'in': [conn_id, conn_prt]
                        }
                        if pipe not in serial_data['connections']:
                            serial_data['connections'].append(pipe)

        if not serial_data['connections']:
            serial_data.pop('connections')

        return serial_data

    def _deserialize(self, data, relative_pos=False, pos=None):
        """
        deserialize node data.
        (used internally by the node graph)

        Args:
            data (dict): node data.
            relative_pos (bool): position node relative to the cursor.

        Returns:
            list[NodeGraphQt.Nodes]: list of node instances.
        """
        nodes = {}

        # build the nodes.
        for n_id, n_data in data.get('nodes', {}).items():
            identifier = n_data['type']
            NodeCls = self._vendor.create_node_instance(identifier)
            if NodeCls:
                node = NodeCls()
                node._graph = self

                name = self.get_unique_name(n_data.get('name', node.NODE_NAME))
                n_data['name'] = name

                # set properties.
                for prop, val in node.model.properties.items():
                    if prop in n_data.keys():
                        setattr(node.model, prop, n_data[prop])

                # set custom properties.
                for prop, val in n_data.get('custom', {}).items():
                    if prop in node.model.custom_properties.keys():
                        node.model.custom_properties[prop] = val

                node.update()

                self._undo_stack.push(
                    NodeAddedCmd(self, node, n_data.get('pos')))
                nodes[n_id] = node

        # build the connections.
        for connection in data.get('connections', []):
            nid, pname = connection.get('in', ('', ''))
            in_node = nodes.get(nid)
            if not in_node:
                continue
            in_port = in_node.inputs().get(pname) if in_node else None

            nid, pname = connection.get('out', ('', ''))
            out_node = nodes.get(nid)
            if not out_node:
                continue
            out_port = out_node.outputs().get(pname) if out_node else None

            if in_port and out_port:
                self._undo_stack.push(PortConnectedCmd(in_port, out_port))

        node_objs = list(nodes.values())
        if relative_pos:
            self._viewer.move_nodes([n.view for n in node_objs])
            [setattr(n.model, 'pos', n.view.pos) for n in node_objs]
        elif pos:
            self._viewer.move_nodes([n.view for n in node_objs], pos=pos)

        return node_objs

    def serialize_session(self):
        """
        Serializes the current node graph layout to a dictionary.

        Returns:
            dict: serialized session of the current node layout.
        """
        return self._serialize(self.all_nodes())

    def save_session(self, file_path):
        """
        Saves the current node graph session layout to a `JSON` formatted file.

        Args:
            file_path (str): path to the saved node layout.
        """
        serliazed_data = self._serialize(self.all_nodes())
        file_path = file_path.strip()
        with open(file_path, 'w') as file_out:
            json.dump(serliazed_data,
                      file_out,
                      indent=2,
                      separators=(',', ':'))

    def load_session(self, file_path):
        """
        Load node graph session layout file.

        Args:
            file_path (str): path to the serialized layout file.
        """
        file_path = file_path.strip()
        if not os.path.isfile(file_path):
            raise IOError('file does not exist.')

        self.clear_session()

        try:
            with open(file_path) as data_file:
                layout_data = json.load(data_file)
        except Exception as e:
            layout_data = None
            print('Cannot read data from file.\n{}'.format(e))

        if not layout_data:
            return

        self._deserialize(layout_data)
        self._undo_stack.clear()
        self._model.session = file_path

    def copy_nodes(self, nodes=None):
        """
        Copy nodes to the clipboard.

        Args:
            nodes (list[NodeGraphQt.Node]): list of nodes (default: selected nodes).
        """
        nodes = nodes or self.selected_nodes()
        if not nodes:
            return False
        clipboard = QApplication.clipboard()
        serial_data = self._serialize(nodes)
        serial_str = json.dumps(serial_data)
        if serial_str:
            clipboard.setText(serial_str)
            return True
        return False

    def paste_nodes(self):
        """
        Pastes nodes copied from the clipboard.
        """
        clipboard = QApplication.clipboard()
        cb_string = clipboard.text()
        if not cb_string:
            return

        self._undo_stack.beginMacro('pasted nodes')
        serial_data = json.loads(cb_string)
        self.clear_selection()
        nodes = self._deserialize(serial_data, True)
        [n.set_selected(True) for n in nodes]
        self._undo_stack.endMacro()

    def duplicate_nodes(self, nodes):
        """
        Create duplicate copy from the list of nodes.

        Args:
            nodes (list[NodeGraphQt.Node]): list of nodes.
        Returns:
            list[NodeGraphQt.Node]: list of duplicated node instances.
        """
        if not nodes:
            return

        self._undo_stack.beginMacro('duplicated nodes')

        self.clear_selection()
        serial = self._serialize(nodes)
        new_nodes = self._deserialize(serial)
        offset = 50
        for n in new_nodes:
            x, y = n.pos()
            n.set_pos(x + offset, y + offset)
            n.set_property('selected', True)

        self._undo_stack.endMacro()
        return new_nodes

    def disable_nodes(self, nodes, mode=None):
        """
        Set weather to Disable or Enable specified nodes.

        see: :meth:`NodeObject.set_disabled`

        Args:
            nodes (list[NodeGraphQt.Node]): list of node instances.
            mode (bool): (optional) disable state of the nodes.
        """
        if not nodes:
            return
        if mode is None:
            mode = not nodes[0].disabled()
        if len(nodes) > 1:
            text = {False: 'enabled', True: 'disabled'}[mode]
            text = '{} ({}) nodes'.format(text, len(nodes))
            self._undo_stack.beginMacro(text)
            [n.set_disabled(mode) for n in nodes]
            self._undo_stack.endMacro()
            return
        nodes[0].set_disabled(mode)

    def question_dialog(self, text, title='Node Graph'):
        """
        Prompts a question open dialog with "Yes" and "No" buttons in
        the node graph.

        (convenience function to :meth:`NodeGraph.viewer().question_dialog`)

        Args:
            text (str): question text.
            title (str): dialog window title.

        Returns:
            bool: true if user clicked yes.
        """
        self._viewer.question_dialog(text, title)

    def message_dialog(self, text, title='Node Graph'):
        """
        Prompts a file open dialog in the node graph.

        (convenience function to :meth:`NodeGraph.viewer().message_dialog`)

        Args:
            text (str): message text.
            title (str): dialog window title.
        """
        self._viewer.message_dialog(text, title)

    def load_dialog(self, current_dir=None, ext=None):
        """
        Prompts a file open dialog in the node graph.

        (convenience function to :meth:`NodeGraph.viewer().load_dialog`)

        Args:
            current_dir (str): path to a directory.
            ext (str): custom file type extension (default: json)

        Returns:
            str: selected file path.
        """
        return self._viewer.load_dialog(current_dir, ext)

    def save_dialog(self, current_dir=None, ext=None):
        """
        Prompts a file save dialog in the node graph.

        (convenience function to :meth:`NodeGraph.viewer().save_dialog`)

        Args:
            current_dir (str): path to a directory.
            ext (str): custom file type extension (default: json)

        Returns:
            str: selected file path.
        """
        return self._viewer.save_dialog(current_dir, ext)
Beispiel #9
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)
Beispiel #10
0
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.undoStack = QUndoStack(self)
        self.undoStack.cleanChanged.connect(self.cleanChanged)

        self.undoAction = self.undoStack.createUndoAction(self, "&Undo")
        self.undoAction.setShortcut(QKeySequence.Undo)

        self.redoAction = self.undoStack.createRedoAction(self, "&Redo")
        self.redoAction.setShortcut(QKeySequence.Redo)

        self.messageLabel = QLabel()
        self.coordLabel = QLabel()
        self.stopwatchLabel = QLabel()

        self.time = QTime(0, 0)
        self.stopwatch = QTimer()

        self.undoView = QUndoView(self.undoStack)

        self.graphicsScene = GraphicsScene(self)

        self.copyList = []

    def setupUi(self):
        if QIcon.themeName() == "":
            QIcon.setThemeName('breeze')
        self.openIcon = QIcon().fromTheme("document-open")
        self.actionOpen_Datasets.setIcon(self.openIcon)
        self.saveIcon = QIcon().fromTheme("document-save")
        self.actionSave.setIcon(self.saveIcon)
        self.closeIcon = QIcon().fromTheme("document-close")
        self.actionClose_Dataset.setIcon(self.closeIcon)

        self.statusbar.addWidget(self.messageLabel)
        self.statusbar.addWidget(self.coordLabel)
        self.statusbar.addWidget(self.stopwatchLabel)

        self.stopwatch.setInterval(1000)
        self.stopwatch.timeout.connect(self.updateStopWatchLabel)

        self.stopwatchStartIcon = QIcon().fromTheme("chronometer-start")
        self.actionTimer_Start.setIcon(self.stopwatchStartIcon)
        self.actionTimer_Start.triggered.connect(self.startStopWatch)

        self.stopwatchStopIcon = QIcon().fromTheme("chronometer-pause")
        self.actionTimer_Stop.setIcon(self.stopwatchStopIcon)
        self.actionTimer_Stop.triggered.connect(self.stopStopWatch)

        self.stopwatchResetIcon = QIcon().fromTheme("chronometer-reset")
        self.actionTimer_Reset.setIcon(self.stopwatchResetIcon)
        self.actionTimer_Reset.triggered.connect(self.resetStopWatch)

        self.leftIcon = QIcon().fromTheme("go-previous")
        self.actionSend_To_Left.setIcon(self.leftIcon)
        self.rightIcon = QIcon().fromTheme("go-next")
        self.actionSend_To_Right.setIcon(self.rightIcon)
        self.upIcon = QIcon().fromTheme("go-up")
        self.actionPrevious_Item.setIcon(self.upIcon)
        self.downIcon = QIcon().fromTheme("go-down")
        self.actionNext_Item.setIcon(self.downIcon)

        self.undoView.setWindowTitle("Command List")
        self.undoView.show()
        self.undoView.setAttribute(Qt.WA_QuitOnClose, False)

        self.menuEdit.addAction(self.undoAction)
        self.menuEdit.addAction(self.redoAction)

        self.graphicsView.setScene(self.graphicsScene)
        self.graphicsView.mouseMoved.connect(self.coordLabel.setText)
        self.graphicsScene.tabWidget = self.tabWidget
        self.graphicsScene.comboBox = self.comboBox
        self.graphicsScene.signalHandler.boxPressed.connect(self.selectBox)
        self.graphicsScene.signalHandler.boxChanged.connect(self.changeBox)
        self.graphicsScene.signalHandler.boxCreated.connect(self.createItem)

    @Slot()
    def sendToLeft(self):
        originIndex = self.tabWidget.currentIndex()
        numTabs = self.tabWidget.count()
        targetIndex = (numTabs + originIndex - 1) % numTabs
        modelIndex = self.tabWidget.getCurrentTableView().currentIndex()
        if modelIndex.isValid():
            self.undoStack.beginMacro(f"Send item to {targetIndex}")
            sendToCommand = SendToCommand(originIndex, targetIndex,
                                          modelIndex.row(), self.tabWidget,
                                          self.graphicsScene)
            self.undoStack.push(sendToCommand)
            modelIndex = modelIndex.model().index(modelIndex.row(),
                                                  modelIndex.column())
            if modelIndex.isValid():
                self.cellClicked(originIndex, modelIndex, originIndex,
                                 modelIndex)
            self.undoStack.endMacro()

    @Slot()
    def sendToRight(self):
        originIndex = self.tabWidget.currentIndex()
        numTabs = self.tabWidget.count()
        targetIndex = (originIndex + 1) % numTabs
        modelIndex = self.tabWidget.getCurrentTableView().currentIndex()
        if modelIndex.isValid():
            self.undoStack.beginMacro(f"Send item to {targetIndex}")
            sendToCommand = SendToCommand(originIndex, targetIndex,
                                          modelIndex.row(), self.tabWidget,
                                          self.graphicsScene)
            self.undoStack.push(sendToCommand)
            modelIndex = modelIndex.model().index(modelIndex.row(),
                                                  modelIndex.column())
            if modelIndex.isValid():
                self.cellClicked(originIndex, modelIndex, originIndex,
                                 modelIndex)
            self.undoStack.endMacro()

    @Slot()
    @Slot(int)
    def closeDataset(self, i=-1):
        # put a dialog if there is a pending modification
        # say that modifications are not lost and retrievable with CTRL-Z
        if self.tabWidget.count() > 0:
            if i == -1:
                i = self.tabWidget.currentIndex()
            deleteDatasetCommand = DeleteDatasetCommand([i], self.tabWidget,
                                                        self.comboBox,
                                                        self.graphicsView,
                                                        self.graphicsScene)
            self.undoStack.push(deleteDatasetCommand)

    @Slot()
    def openDatasets(self):
        """Open dataset directory"""
        prevTabIndex = self.tabWidget.currentIndex()
        prevModelIndex = self.tabWidget.getCurrentSelectedCell()
        numTabs = self.tabWidget.count()
        (filenames, _ext) = QFileDialog.getOpenFileNames(
            self,
            QApplication.translate("MainWindow", "Open datasets", None, -1),
            "/home/kwon-young/Documents/PartageVirtualBox/data/omr_dataset/choi_dataset",
            "*.csv")
        if filenames:
            self.undoStack.beginMacro(f"open Datasets {filenames}")
            filenames.sort()
            openDatasetCommand = OpenDatasetCommand(filenames, self.tabWidget,
                                                    self.comboBox,
                                                    self.graphicsScene,
                                                    self.messageLabel)
            self.undoStack.push(openDatasetCommand)
            tabIndex = numTabs
            if self.tabWidget.count() > 0:
                modelIndex = self.tabWidget.getTableModel(tabIndex).index(0, 0)
                if modelIndex.isValid():
                    self.cellClicked(tabIndex, modelIndex, prevTabIndex,
                                     prevModelIndex)
                self.undoStack.endMacro()

    @Slot(int)
    def currentTabChanged(self, index):
        self.tabWidget.setCurrentIndex(index)
        for tabIndex in range(self.tabWidget.count()):
            self.graphicsScene.changeTabColor(
                tabIndex, self.tabWidget.color_map(tabIndex))

    @Slot(int, QModelIndex, int, QModelIndex)
    def cellClicked(self, tabIndex, cellIndex, prevTabIndex, prevCellIndex):
        cellClickedCommand = CellClickedCommand(
            tabIndex, cellIndex, prevTabIndex, prevCellIndex, self.tabWidget,
            self.graphicsScene, self.graphicsView, self.comboBox,
            self.messageLabel)
        self.undoStack.push(cellClickedCommand)

    @Slot()
    def SelectNextItem(self):
        if self.tabWidget.count() > 0:
            tabIndex = self.tabWidget.currentIndex()
            prevCellIndex = self.tabWidget.getCurrentSelectedCell()
            model = self.tabWidget.getCurrentTableModel()
            rowCount = model.rowCount(QModelIndex())
            nextRow = (prevCellIndex.row() + 1) % rowCount
            cellIndex = model.index(nextRow, prevCellIndex.column())
            cellClickedCommand = CellClickedCommand(
                tabIndex, cellIndex, tabIndex, prevCellIndex, self.tabWidget,
                self.graphicsScene, self.graphicsView, self.comboBox,
                self.messageLabel)
            self.undoStack.push(cellClickedCommand)

    @Slot()
    def SelectPreviousItem(self):
        if self.tabWidget.count() > 0:
            tabIndex = self.tabWidget.currentIndex()
            prevCellIndex = self.tabWidget.getCurrentSelectedCell()
            model = self.tabWidget.getCurrentTableModel()
            rowCount = model.rowCount(QModelIndex())
            nextRow = (rowCount + prevCellIndex.row() - 1) % rowCount
            cellIndex = model.index(nextRow, prevCellIndex.column())
            cellClickedCommand = CellClickedCommand(
                tabIndex, cellIndex, tabIndex, prevCellIndex, self.tabWidget,
                self.graphicsScene, self.graphicsView, self.comboBox,
                self.messageLabel)
            self.undoStack.push(cellClickedCommand)

    @Slot()
    def SelectNextPage(self):
        if self.tabWidget.count() > 0:
            tabIndex = self.tabWidget.currentIndex()
            prevCellIndex = self.tabWidget.getCurrentSelectedCell()
            model = self.tabWidget.getCurrentTableModel()
            prevPage = model.pageAtIndex(prevCellIndex)
            rowCount = model.rowCount(QModelIndex())
            for i in range(0, rowCount):
                row = (prevCellIndex.row() + i) % rowCount
                cellIndex = model.index(row, prevCellIndex.column())
                page = model.pageAtIndex(cellIndex)
                if prevPage.split("-")[0] != page.split("-")[0]:
                    break
            cellClickedCommand = CellClickedCommand(
                tabIndex, cellIndex, tabIndex, prevCellIndex, self.tabWidget,
                self.graphicsScene, self.graphicsView, self.comboBox,
                self.messageLabel)
            self.undoStack.push(cellClickedCommand)

    @Slot()
    def SelectPreviousPage(self):
        if self.tabWidget.count() > 0:
            tabIndex = self.tabWidget.currentIndex()
            prevCellIndex = self.tabWidget.getCurrentSelectedCell()
            model = self.tabWidget.getCurrentTableModel()
            prevPage = model.pageAtIndex(prevCellIndex)
            rowCount = model.rowCount(QModelIndex())
            for i in range(0, rowCount):
                row = (prevCellIndex.row() - i) % rowCount
                cellIndex = model.index(row, prevCellIndex.column())
                page = model.pageAtIndex(cellIndex)
                if prevPage.split("-")[0] != page.split("-")[0]:
                    break
            cellClickedCommand = CellClickedCommand(
                tabIndex, cellIndex, tabIndex, prevCellIndex, self.tabWidget,
                self.graphicsScene, self.graphicsView, self.comboBox,
                self.messageLabel)
            self.undoStack.push(cellClickedCommand)

    @Slot()
    def selectNextLabel(self):
        if self.comboBox.count() > 0:
            index = self.comboBox.currentIndex()
            newIndex = (index + 1) % self.comboBox.count()
            label = self.comboBox.itemText(newIndex)
            tabIndex = self.tabWidget.currentIndex()
            cellIndex = self.tabWidget.getCurrentSelectedCell()
            labelChangedCommand = LabelChangedCommand(label, tabIndex,
                                                      cellIndex,
                                                      self.tabWidget,
                                                      self.graphicsScene,
                                                      self.comboBox)
            self.undoStack.push(labelChangedCommand)

    @Slot()
    def selectPreviousLabel(self):
        if self.comboBox.count() > 0:
            index = self.comboBox.currentIndex()
            newIndex = ((self.comboBox.count() + index - 1) %
                        self.comboBox.count())
            label = self.comboBox.itemText(newIndex)
            tabIndex = self.tabWidget.currentIndex()
            cellIndex = self.tabWidget.getCurrentSelectedCell()
            labelChangedCommand = LabelChangedCommand(label, tabIndex,
                                                      cellIndex,
                                                      self.tabWidget,
                                                      self.graphicsScene,
                                                      self.comboBox)
            self.undoStack.push(labelChangedCommand)

    @Slot(int)
    def labelChanged(self, index):
        label = self.comboBox.itemText(index)
        tabIndex = self.tabWidget.currentIndex()
        cellIndex = self.tabWidget.getCurrentSelectedCell()
        labelChangedCommand = LabelChangedCommand(label, tabIndex, cellIndex,
                                                  self.tabWidget,
                                                  self.graphicsScene,
                                                  self.comboBox)
        self.undoStack.push(labelChangedCommand)

    @Slot()
    def saveDataToDisk(self):
        self.undoStack.setClean()
        for name, model in zip(self.tabWidget.filenames(),
                               self.tabWidget.models()):
            model.save(name)

    @Slot(bool)
    def cleanChanged(self, clean):
        self.setWindowModified(not clean)

    @Slot(int, int)
    def selectBox(self, tabIndex, rowIndex):
        if tabIndex != self.tabWidget.currentIndex() or \
                rowIndex != self.tabWidget.getCurrentSelectedCell().row():
            selectBoxCommand = SelectBoxCommand(tabIndex, rowIndex,
                                                self.tabWidget,
                                                self.graphicsScene,
                                                self.comboBox)
            self.undoStack.push(selectBoxCommand)

    @Slot(int, int, QRectF)
    def changeBox(self, tabIndex, rowIndex, box):
        moveBoxCommand = MoveBoxCommand(tabIndex, rowIndex, box,
                                        self.tabWidget, self.graphicsScene)
        self.undoStack.push(moveBoxCommand)

    @Slot(QRectF, QRectF)
    def viewportMoved(self, rect, prevRect):
        viewportMovedCommand = ViewportMovedCommand(rect, prevRect,
                                                    self.graphicsView)
        self.undoStack.push(viewportMovedCommand)

    @Slot()
    def updateStopWatchLabel(self):
        elapsed = QTime(0, 0).addMSecs(self.time.elapsed())
        self.stopwatchLabel.setText(elapsed.toString())

    @Slot()
    def startStopWatch(self):
        self.time.start()
        self.stopwatch.start(1000)

    @Slot()
    def stopStopWatch(self):
        self.stopwatch.stop()

    @Slot()
    def resetStopWatch(self):
        self.time.start()
        self.stopwatchLabel.setText(QTime(0, 0).toString())

    @Slot()
    def deleteItem(self):
        tabIndex = self.tabWidget.currentIndex()
        cellIndex = self.tabWidget.getCurrentSelectedCell()
        self.undoStack.beginMacro(f"Delete item {tabIndex}:{cellIndex.row()}")
        deleteItemCommand = DeleteItemCommand(tabIndex, cellIndex,
                                              self.tabWidget,
                                              self.graphicsView,
                                              self.graphicsScene,
                                              self.comboBox)
        self.undoStack.push(deleteItemCommand)
        cellIndex = cellIndex.model().index(cellIndex.row(),
                                            cellIndex.column())
        if cellIndex.isValid():
            self.cellClicked(tabIndex, cellIndex, tabIndex, cellIndex)
        self.undoStack.endMacro()

    @Slot(ResizableRect)
    def createItem(self, rect):
        self.undoStack.beginMacro(
            f"Create item {rect.tabIndex}:{rect.rowIndex}")
        createItemCommand = CreateItemCommand(rect, self.tabWidget,
                                              self.graphicsScene,
                                              self.comboBox)
        self.undoStack.push(createItemCommand)
        self.selectBox(rect.tabIndex, rect.rowIndex)
        self.undoStack.endMacro()

    @Slot()
    def tabItemForward(self):
        changeTabItemZValueCommand = ChangeTabItemZValueCommand(
            self.tabWidget.currentIndex(), 1, self.graphicsScene)
        self.undoStack.push(changeTabItemZValueCommand)

    @Slot()
    def tabItemBackward(self):
        changeTabItemZValueCommand = ChangeTabItemZValueCommand(
            self.tabWidget.currentIndex(), -1, self.graphicsScene)
        self.undoStack.push(changeTabItemZValueCommand)

    @Slot()
    def copy(self):
        tabIndex = self.tabWidget.currentIndex()
        cellIndex = self.tabWidget.getCurrentSelectedCell()
        box = self.graphicsScene.box(tabIndex, cellIndex.row())
        copyCommand = CopyCommand(box, self.copyList)
        self.undoStack.push(copyCommand)

    @Slot()
    def paste(self):
        pos = self.graphicsView.mapFromGlobal(QCursor.pos())
        scenePos = self.graphicsView.mapToScene(pos)
        prop = self.copyList[-1]
        self.undoStack.beginMacro(f"paste item {prop.box}")
        pasteCommand = PasteCommand(scenePos, prop, self.tabWidget,
                                    self.graphicsScene)
        self.undoStack.push(pasteCommand)
        self.selectBox(prop.tabIndex, prop.rowIndex)
        self.undoStack.endMacro()
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()
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
Beispiel #13
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
Beispiel #14
0
class Scene(QGraphicsScene):
    def __init__(self, parent=None, view=None):
        QGraphicsScene.__init__(self, parent)
        self.undoStack = QUndoStack(self)
        # super(Scene, self).__init__(parent)
        self.view = view
        self.imgPosibleConnectH = QGraphicsPixmapItem(
            os.path.join(PATHBLOCKSIMG, "ConnectH.png"))
        super(Scene, self).addItem(self.imgPosibleConnectH)
        self.imgPosibleConnectH.setVisible(False)
        self.imgPosibleConnectV = QGraphicsPixmapItem(
            os.path.join(PATHBLOCKSIMG, "ConnectV.png"))
        super(Scene, self).addItem(self.imgPosibleConnectV)
        self.imgPosibleConnectV.setVisible(False)
        self.oldPos = None
        self.createActions()

    def createActions(self):
        self.popMenu = QMenu()
        self.undoAction = self.undoStack.createUndoAction(
            self, self.tr("&Undo"))
        self.undoAction.setShortcuts(QKeySequence.Undo)
        self.popMenu.addAction(self.undoAction)

        self.redoAction = self.undoStack.createRedoAction(
            self, self.tr("&Redo"))
        self.redoAction.setShortcuts(QKeySequence.Redo)
        self.popMenu.addAction(self.redoAction)

        self.disableAction = QAction(self.tr("&Disable"), self)
        self.disableAction.triggered.connect(self.setEnabledMain)
        self.popMenu.addAction(self.disableAction)

    def setEnabledMain(self):
        for item in self.items():
            if isinstance(item, QGraphicsBlockItem) and item.isBlockDef():
                if item.functionname == "main":
                    item.setEnabled(not item.isEnabled())
                elif item.functionname == "when":
                    item.setEnabled(not item.isEnabled())

    def addItem(self, item: QGraphicsItem, fromStack=False):
        if fromStack:
            super(Scene, self).addItem(item)
        else:
            self.undoStack.push(AddCommand(item, self))

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent):

        movingItem = self.itemAt(event.scenePos(), self.view.transform())
        if movingItem is None and event.button() is Qt.MouseButton.RightButton:
            self.popMenu.exec_(event.screenPos())
        if movingItem is not None and event.button(
        ) is Qt.MouseButton.LeftButton:
            self.oldPos = movingItem.pos()
        self.clearSelection()
        super(Scene, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent):
        if len(self.selectedItems()) is not 0:
            movingItem = self.selectedItems()[0]
        else:
            movingItem = None
        if movingItem is not None and event.button(
        ) is Qt.MouseButton.LeftButton:
            if self.oldPos is not movingItem.pos():
                self.undoStack.push(MoveCommand(movingItem, self.oldPos, self))
        super(Scene, self).mouseReleaseEvent(event)

    def getListInstructions(self):
        list = []
        for item in self.items():
            if isinstance(item, QGraphicsBlockItem) and item.isBlockDef(
            ) and item.isEnabled():
                inst = item.getInstructions()
                list.append(inst)
        return list
Beispiel #15
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)

    # Private 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 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)
Beispiel #16
0
class NodeGraph(QtCore.QObject):
  
    node_selected = QtCore.Signal(list)

    def __init__(self, parent=None):
        super(NodeGraph, self).__init__(parent)
        self._model = NodeGraphModel()
        self._viewer = NodeViewer()
        self._undo_stack = QUndoStack(self)
        self._init_actions()
        self._wire_signals()
        self.patch_context_menu()

    def _wire_signals(self):
        self._viewer.moved_nodes.connect(self._on_nodes_moved)
        self._viewer.search_triggered.connect(self._on_search_triggered)
        self._viewer.connection_changed.connect(self._on_connection_changed)
        self._viewer.node_selected.connect(self._on_node_selected)

    def _init_actions(self):
        # setup tab search shortcut.
        tab = QAction('Search Nodes', self)
        tab.setShortcut('tab')
        tab.triggered.connect(self._toggle_tab_search)
        self._viewer.addAction(tab)
        setup_actions(self)

    def _toggle_tab_search(self):
        """
        toggle the tab search widget.
        """
        self._viewer.tab_search_set_nodes(NodeVendor.names)
        self._viewer.tab_search_toggle()

    def _on_nodes_moved(self, node_data):
        """
        called when selected nodes in the viewer has changed position.

        Args:
            node_data (dict): {<node_view>: <previous_pos>}
        """
        self._undo_stack.beginMacro('moved nodes')
        for node_view, prev_pos in node_data.items():
            node = self._model.nodes[node_view.id]
            self._undo_stack.push(NodeMovedCmd(node, node.pos(), prev_pos))
        self._undo_stack.endMacro()

    def _on_nodes_moved(self, node_data):
        """
        called when a node in the viewer is selected on left click.

        """
        nodes = self.selected_nodes()
        self.node_selected.emit(nodes)

    def _on_search_triggered(self, node_type, pos):
        """
        called when the tab search widget is triggered in the viewer.

        Args:
            node_type (str): node identifier.
            pos (tuple): x,y position for the node.
        """
        self.create_node(node_type, pos=pos)

    def _on_connection_changed(self, disconnected, connected):
        """
        called when a pipe connection has been changed in the viewer.

        Args:
            disconnected (list[list[widgets.port.PortItem]):
                pair list of port view items.
            connected (list[list[widgets.port.PortItem]]):
                pair list of port view items.
        """
        if not (disconnected or connected):
            return

        label = 'connected node(s)' if connected else 'disconnected node(s)'
        ptypes = {'in': 'inputs', 'out': 'outputs'}

        self._undo_stack.beginMacro(label)
        for p1_view, p2_view in disconnected:
            node1 = self._model.nodes[p1_view.node.id]
            node2 = self._model.nodes[p2_view.node.id]
            port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name]
            port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name]
            port1.disconnect_from(port2)
        for p1_view, p2_view in connected:
            node1 = self._model.nodes[p1_view.node.id]
            node2 = self._model.nodes[p2_view.node.id]
            port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name]
            port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name]
            port1.connect_to(port2)
        self._undo_stack.endMacro()

    @property
    def model(self):
        """
        Return the node graph model.

        Returns:
            NodeGraphModel: model object.
        """
        return self._model

    def show(self):
        """
        Show node graph viewer widget.
        """
        self._viewer.show()

    def hide(self):
        """
        Hide node graph viewer widget.
        """
        self._viewer.hide()

    def close(self):
        """
        Close node graph viewer widget.
        """
        self._viewer.close()

    def viewer(self):
        """
        Return the node graph viewer widget object.

        Returns:
            NodeGraphQt.widgets.viewer.NodeViewer: viewer widget.
        """
        return self._viewer

    def scene(self):
        """
        Return the scene object.

        Returns:
            NodeGraphQt.widgets.scene.NodeScene: node scene.
        """
        return self._viewer.scene()

    def undo_stack(self):
        """
        Returns the undo stack used in the node graph

        Returns:
            QUndoStack: undo stack.
        """
        return self._undo_stack

    def begin_undo(self, name='undo'):
        """
        Start of an undo block followed by a end_undo().

        Args:
            name (str): name for the undo block.
        """
        self._undo_stack.beginMacro(name)

    def end_undo(self):
        """
        End of an undo block started by begin_undo().
        """
        self._undo_stack.endMacro()

    def context_menu(self):
        """
        Returns a node graph context menu object.

        Returns:
            ContextMenu: node graph context menu object instance.
        """
        return self._viewer.context_menu()
      
    @staticmethod
	  def modify_context_menu(viewer):