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()
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 __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 __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()
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
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 setup_widgets(self): # Buttons self.filterLabel.mouseDoubleClickEvent = self.sort_all_headers self.expandBtn.pressed.connect(self.expand_all_items) # Work Timer self.item_worker.timeout.connect(self.add_widget_item) self.progressBar.hide() # Menu self.actionOpen.triggered.connect(self.open_file_window) self.actionBeenden.triggered.connect(self.close) self.actionExport.triggered.connect(self.export.export_selection) self.actionExportPos.triggered.connect(self.export.export_updated_pos_xml) # File display self.file_name_box: QGroupBox self.file_name_box.setTitle(_('Dateien Alt - Neu')) # Filter Line Edit self.lineEditFilter: QLineEdit self.lineEditFilter.setPlaceholderText(_('Zum filtern im Baum tippen. Leerzeichen separierte Begriffe werden ' 'mit UND gefunden. zB. t_mirko ks_bunt findet alle bunten Mirkos.')) for widget in self.widget_list: widget.clear() widget.undo_stack = QUndoStack(self.undo_grp) widget.setItemDelegate(KnechtValueDelegate(widget)) # Overlay widget.info_overlay = InfoOverlay(widget) # Setup Filtering widget.filter = TreeWidgetFilter(self, widget, self.lineEditFilter) widget.setAlternatingRowColors(True) self.intro_timer.timeout.connect(self.show_intro_msg) self.intro_timer.start() # Exporter signals self.err_sig.connect(self.error_msg) self.export_sig.connect(self.export_success) # Tab Changed self.widgetTabs.currentChanged.connect(self.tab_changed)
def __init__(self, history, stack: QUndoStack, target_idx: int): """ Walks across undo indices :param DocHistoryWidget history: The history widget to manipulate :param QUndoStack stack: Undostack to call :param int target_idx: the index to walk too """ super(TimeMachine, self).__init__() self.history = history self.stack = stack if target_idx >= stack.index(): target_idx += 1 self.target_idx = target_idx self.work_timer.timeout.connect(self.work) self.finished.connect(self.history.time_traveler_arrived)
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))
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__stack__ = QUndoStack() self._macroRunning = False
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.stack = QUndoStack()
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
def __init__(self): self.stack = QUndoStack()
def get_undo_stack(self, resource_index): if resource_index not in self.undo_stacks: self.undo_stacks[resource_index] = stack = QUndoStack(self.undo_group) stack.cleanChanged.connect(self.update_window_modified) return self.undo_stacks[resource_index]
self._old_value = dictionary[key] self._new_value = value self.setText(" modify command") def undo(self, dictionary): #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)
def __buildUndoCommand(self): self.undoStack = QUndoStack() self.addAction(self.ui.actionUndo) self.addAction(self.ui.actionRedo) self.ui.undoView.setStack(self.undoStack)
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 KnechtTreeView(QTreeView): view_cleared = Signal(object) view_refreshed = Signal() clean_changed = Signal(bool, object) reset_missing = Signal() file_dropped = Signal(Path) block_timeout = 60000 def __init__(self, parent: QWidget, undo_group: QUndoGroup): super(KnechtTreeView, self).__init__(parent) # -- Setup progress overlay self.progress_overlay = ProgressOverlay(self) self.progress = self.progress_overlay.progress # -- Setup tree view progress bar helper self.progress_msg = ShowTreeViewProgressMessage(self) # -- Add an undo stack to the view self.undo_stack = QUndoStack(undo_group) self.undo_stack.setUndoLimit(UNDO_LIMIT) self.undo_stack.cleanChanged.connect(self.view_clean_changed) # -- Item Edit undo self.edit_undo = ViewItemEditUndo(self) # -- Setup tree settings self.setAllColumnsShowFocus(True) self.setUniformRowHeights(True) self.setSortingEnabled(False) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setAlternatingRowColors(True) # -- Drag n Drop self.setAcceptDrops(True) self.setDragDropMode(QTreeView.DragDrop) self.supports_drop = True self.supports_drag_move = True self.drag_drop = KnechtDragDrop(self) # -- Filter items types this view accepts on paste/drop actions self.accepted_item_types = [] # Default accept all # Model Editor self.editor = KnechtEditor(self) # Reset missing signal self.editor.collect.reset_missing.connect(self.reset_missing) # Item Delegate for Value edits self.setItemDelegateForColumn(Kg.VALUE, KnechtValueDelegate(self)) # Info Overlay self.info_overlay = InfoOverlay(self) # Context Menu self.context = QMenu(self) # Filter line edit widget to send keyboard input to self._filter_text_widget: QLineEdit = None self.filter_bgr_animation: BgrAnimation = None # Filter Expand timer, expand filtered items after timeout self.filter_expand_timer = QTimer() self.filter_expand_timer.setSingleShot(True) self.filter_expand_timer.setInterval(300) self.filter_expand_timer.timeout.connect(self.filter_expand_results) # Filter typing time self.filter_timer = QTimer() self.filter_timer.setSingleShot(True) self.filter_timer.setInterval(500) self.filter_timer.timeout.connect(self._set_filter_from_timer) # Cache last applied filter self._cached_filter = str() # Setup view properties # Permanent type filter for eg. renderTree self.__permanent_type_filter = [] self.__permanent_type_filter_column = Kg.TYPE # Render Tree self.__is_render_view = False # Preset Wizard Preset Tree self.__is_wizard_preset_view = False @property def is_render_view(self): return self.__is_render_view @is_render_view.setter def is_render_view(self, val: bool): self.__is_render_view = val @property def is_wizard_preset_view(self): return self.__is_wizard_preset_view @is_wizard_preset_view.setter def is_wizard_preset_view(self, val: bool): self.__is_wizard_preset_view = val @property def permanent_type_filter_column(self): return self.__permanent_type_filter_column @permanent_type_filter_column.setter def permanent_type_filter_column(self, val: int): self.__permanent_type_filter_column = val @property def permanent_type_filter(self): """ Apply a permanent item type description white filter to this view """ return self.__permanent_type_filter @permanent_type_filter.setter def permanent_type_filter(self, val: List[str]): self.__permanent_type_filter = val if self.model() is not None: self.model( ).type_filter_column = self.__permanent_type_filter_column if val: # Apply filtering if filter has values self.apply_permanent_type_filter(True, self.__permanent_type_filter) else: # Disable filtering if filter no values self.apply_permanent_type_filter(False, self.__permanent_type_filter) @permanent_type_filter.deleter def permanent_type_filter(self): self.__permanent_type_filter = list() self.apply_permanent_type_filter(False, self.__permanent_type_filter) @property def filter_text_widget(self): return self._filter_text_widget @filter_text_widget.setter def filter_text_widget(self, widget: QLineEdit): self._filter_text_widget = widget self._filter_text_widget.textEdited.connect( self.set_filter_widget_text) bg_color = (255, 255, 255, 255) if KnechtSettings.app['app_style'] == 'fusion-dark': bg_color = KnechtSettings.dark_style['bg_color'] self.filter_bgr_animation = BgrAnimation(self._filter_text_widget, bg_color) def current_filter_text(self) -> str: if not self.filter_text_widget: return '' return self.filter_text_widget.text() def refresh(self): if not self.model(): return src_model = self.model().sourceModel() src_model.refreshData() if self.is_render_view: src_model.is_render_view_model = True if src_model.id_mgr.has_invalid_references(): self.editor.selection.highlight_invalid_references() if src_model.id_mgr.has_recursive_items(): self.editor.selection.highlight_recursive_indices() if self.permanent_type_filter: self.model().set_type_filter(self.permanent_type_filter) self.view_refreshed.emit() def clear_tree(self): if not self.model(): return src_model = self.model().sourceModel() if src_model.rowCount(): # Replace with empty tree model undoable self.editor.clear_tree() # Make sure we have focus after model update self.setFocus(Qt.OtherFocusReason) def sort_tree(self): setup_header_layout(self) @Slot(str) def set_filter_widget_text(self, filter_text: str): if not self.filter_text_widget: return if filter_text == self._cached_filter: # Skip filtering if eg. view tab changed but re-applied filter text is identical to last used filter return self.filter_text_widget.setText(filter_text) self.filter_timer.start() @Slot() def _set_filter_from_timer(self): self._set_filter(self.filter_text_widget.text()) def _set_filter(self, txt: str): self.filter_bgr_animation.blink() txt = txt.replace(' ', '|') self.model().setFilterRegExp(txt) self._cached_filter = txt self.filter_expand_timer.start() def quick_view_filter(self, enabled: bool): self.apply_permanent_type_filter(enabled, Kg.QUICK_VIEW_FILTER) def apply_permanent_type_filter(self, enabled: bool, white_filter_list: list): prx_model = self.model() if enabled: prx_model.set_type_filter(white_filter_list) else: prx_model.clear_type_filter() # Re-apply filter to de-/activate type filtering prx_model.clear_filter() prx_model.apply_last_filter() @Slot() def filter_expand_results(self): prx_model = self.model() if prx_model.filterRegExp().isEmpty(): return for row in range(0, prx_model.rowCount()): idx = prx_model.index(row, 1, QModelIndex()) if not self.isExpanded(idx): self.expand(idx) def clear_filter(self, collapse: bool = True): """ Clear the current filter, collapse all items, expand selections and current index. Re-Apply permanent type filtering. If called a second time without a prior filter set. Collapse items and do not highlight selections. Eg. when user hits Esc twice to collapse all items. """ LOGGER.debug('Clearing filter: %s %s', self.model().filterRegExp(), type(self.model())) if not self.model().filterRegExp().isEmpty(): # Expand and highlight current selection if we return from a filter action highlight_selection = True else: # Collapse all items if no previous filter set highlight_selection = False if type(self.model()) == KnechtSortFilterProxyModel: self.model().clear_filter() if collapse: self.collapseAll() if highlight_selection: self.editor.selection.highlight_selection() if self.filter_text_widget: self.filter_text_widget.setText('') if self.__permanent_type_filter and type( self.model()) == KnechtSortFilterProxyModel: self.model().set_type_filter(self.__permanent_type_filter) self.model().clear_filter() self.model().apply_last_filter() @Slot(bool) def view_clean_changed(self, clean: bool): LOGGER.debug('Reporting changed Tree View Clean state') self.clean_changed.emit(clean, self) def block_until_editor_finished(self): """ When adding or removing items via undo_chain this method can block until the editor returned from the undo chain. """ start = time.time() while not self.editor.enabled: QApplication.processEvents() if time.time() - start > self.block_timeout: break
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
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()
def create_ui(self): """Setup main UI elements, dock widgets, UI-related elements, etc. """ log.debug('Loading UI') # Undo Stack self.undo_stack = QUndoStack(self) self.undo_stack.setUndoLimit(100) # Object navigation history self.obj_history = deque([], config.MAX_OBJ_HISTORY) app = QApplication.instance() base_font = QFont() base_font.fromString(self.prefs['base_font']) app.setFont(base_font) # Object class table widget # classTable = QTableView(self) classTable = classtable.TableView(self) classTable.setObjectName("classTable") classTable.setAlternatingRowColors(True) classTable.setFrameShape(QFrame.StyledPanel) classTable_font = QFont() classTable_font.fromString(self.prefs['class_table_font']) classTable.setFont(classTable_font) fm = classTable.fontMetrics() classTable.setWordWrap(True) classTable.setEditTriggers(QAbstractItemView.EditKeyPressed | QAbstractItemView.DoubleClicked | QAbstractItemView.AnyKeyPressed | QAbstractItemView.SelectedClicked) # classTable.horizontalHeader().setMovable(True) # classTable.verticalHeader().setMovable(False) classTable.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) classTable.verticalHeader().setSectionResizeMode(QHeaderView.Interactive) classTable.horizontalHeader().setDefaultSectionSize(self.prefs['default_column_width']) classTable.verticalHeader().setDefaultSectionSize(fm.height() + 0) classTable.setSelectionMode(QAbstractItemView.ExtendedSelection) classTable.setContextMenuPolicy(Qt.CustomContextMenu) classTable.customContextMenuRequested.connect(self.custom_table_context_menu) # Create table model and proxy layers for transposing and filtering self.classTableModel = classtable.IDFObjectTableModel(classTable) self.transposeableModel = classtable.TransposeProxyModel(self.classTableModel) self.transposeableModel.setSourceModel(self.classTableModel) self.sortableModel = classtable.SortFilterProxyModel(self.transposeableModel) self.sortableModel.setSourceModel(self.transposeableModel) # Assign model to table (enable sorting FIRST) # table.setSortingEnabled(True) # Disable for now, CRUD actions won't work! classTable.setModel(self.sortableModel) # Connect some signals selection_model = classTable.selectionModel() selection_model.selectionChanged.connect(self.table_selection_changed) scroll_bar = classTable.verticalScrollBar() scroll_bar.valueChanged.connect(self.scroll_changed) # These are currently broken # classTable.horizontalHeader().sectionMoved.connect(self.moveObject) # classTable.verticalHeader().sectionMoved.connect(self.moveObject) # Object class tree widget classTreeDockWidget = QDockWidget("Object Classes and Counts", self) classTreeDockWidget.setObjectName("classTreeDockWidget") classTreeDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) classTree = QTreeView(classTreeDockWidget) classTree.setUniformRowHeights(True) classTree.setAllColumnsShowFocus(True) classTree.setRootIsDecorated(False) classTree.setExpandsOnDoubleClick(True) classTree.setIndentation(15) classTree.setAnimated(True) classTree_font = QFont() classTree_font.fromString(self.prefs['class_tree_font']) classTree.setFont(classTree_font) classTree.setAlternatingRowColors(True) classTree.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) palette = classTree.palette() palette.setColor(QPalette.Highlight, Qt.darkCyan) classTree.setPalette(palette) class_tree_window = QWidget(classTreeDockWidget) class_tree_dock_layout_v = QVBoxLayout() class_tree_dock_layout_h = QHBoxLayout() class_tree_dock_layout_v.setContentsMargins(0, 8, 0, 0) class_tree_dock_layout_h.setContentsMargins(0, 0, 0, 0) class_tree_filter_edit = QLineEdit(classTreeDockWidget) class_tree_filter_edit.setPlaceholderText("Filter Classes") class_tree_filter_edit.textChanged.connect(self.treeFilterRegExpChanged) class_tree_filter_cancel = QPushButton("Clear", classTreeDockWidget) class_tree_filter_cancel.setMaximumWidth(45) class_tree_filter_cancel.clicked.connect(self.clearTreeFilterClicked) class_tree_dock_layout_h.addWidget(class_tree_filter_edit) class_tree_dock_layout_h.addWidget(class_tree_filter_cancel) class_tree_dock_layout_v.addLayout(class_tree_dock_layout_h) class_tree_dock_layout_v.addWidget(classTree) class_tree_window.setLayout(class_tree_dock_layout_v) classTreeDockWidget.setWidget(class_tree_window) classTreeDockWidget.setContentsMargins(0,0,0,0) # Comments widget commentDockWidget = QDockWidget("Comments", self) commentDockWidget.setObjectName("commentDockWidget") commentDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) commentView = UndoRedoTextEdit(commentDockWidget, self) commentView.setLineWrapMode(QTextEdit.FixedColumnWidth) commentView.setLineWrapColumnOrWidth(499) commentView.setFrameShape(QFrame.StyledPanel) commentView_font = QFont() commentView_font.fromString(self.prefs['comments_font']) commentView.setFont(commentView_font) commentDockWidget.setWidget(commentView) # Info and help widget infoDockWidget = QDockWidget("Info", self) infoDockWidget.setObjectName("infoDockWidget") infoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) infoView = QTextEdit(infoDockWidget) infoView.setFrameShape(QFrame.StyledPanel) infoView.setReadOnly(True) infoDockWidget.setWidget(infoView) # Node list and jump menu widget refDockWidget = QDockWidget("Field References", self) refDockWidget.setObjectName("refDockWidget") refDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) ref_model = reftree.ReferenceTreeModel(None, refDockWidget) refView = QTreeView(refDockWidget) refView.setModel(ref_model) refView.setUniformRowHeights(True) refView.setRootIsDecorated(False) refView.setIndentation(15) refView.setColumnWidth(0, 160) refView.setFrameShape(QFrame.StyledPanel) refDockWidget.setWidget(refView) refView.doubleClicked.connect(self.ref_tree_double_clicked) # Logging and debugging widget logDockWidget = QDockWidget("Log Viewer", self) logDockWidget.setObjectName("logDockWidget") logDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) logView = QPlainTextEdit(logDockWidget) logView.setLineWrapMode(QPlainTextEdit.NoWrap) logView.setReadOnly(True) logView_font = QFont() logView_font.fromString(self.prefs['base_font']) logView.setFont(logView_font) logView.ensureCursorVisible() logDockWidget.setWidget(logView) # Undo view widget undoDockWidget = QDockWidget("Undo History", self) undoDockWidget.setObjectName("undoDockWidget") undoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) undoView = QUndoView(self.undo_stack) undoDockWidget.setWidget(undoView) # Define corner docking behaviour self.setDockNestingEnabled(True) self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea) self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea) # Assign main widget and dock widgets to QMainWindow self.setCentralWidget(classTable) self.addDockWidget(Qt.LeftDockWidgetArea, classTreeDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, commentDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, infoDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, refDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, logDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, undoDockWidget) # Store widgets for access by other objects self.classTable = classTable self.commentView = commentView self.infoView = infoView self.classTree = classTree self.logView = logView self.undoView = undoView self.refView = refView self.filterTreeBox = class_tree_filter_edit # Store docks for access by other objects self.commentDockWidget = commentDockWidget self.infoDockWidget = infoDockWidget self.classTreeDockWidget = classTreeDockWidget self.logDockWidget = logDockWidget self.undoDockWidget = undoDockWidget self.refDockWidget = refDockWidget # Perform other UI-related initialization tasks self.center() self.setUnifiedTitleAndToolBarOnMac(True) self.setWindowIcon(QIcon(':/images/logo.png')) # Status bar setup self.statusBar().showMessage('Status: Ready') self.unitsLabel = QLabel() self.unitsLabel.setAlignment(Qt.AlignCenter) self.unitsLabel.setMinimumSize(self.unitsLabel.sizeHint()) self.unitsLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.unitsLabel) self.pathLabel = QLabel() self.pathLabel.setAlignment(Qt.AlignCenter) self.pathLabel.setMinimumSize(self.pathLabel.sizeHint()) self.pathLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.pathLabel) self.versionLabel = QLabel() self.versionLabel.setAlignment(Qt.AlignCenter) self.versionLabel.setMinimumSize(self.versionLabel.sizeHint()) self.versionLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.versionLabel) self.progressBarIDF = QProgressBar() self.progressBarIDF.setAlignment(Qt.AlignCenter) self.progressBarIDF.setMaximumWidth(200) self.statusBar().addPermanentWidget(self.progressBarIDF) self.clipboard = QApplication.instance().clipboard() self.obj_clipboard = [] self.setStyleSheet(""" QToolTip { background-color: gray; color: white; border: black solid 1px } # QMenu { # background-color: rgbf(0.949020, 0.945098, 0.941176); # color: rgb(255,255,255); # } # QMenu::item::selected { # background-color: rgbf(0.949020, 0.945098, 0.941176); # } """)
class UIMainWindow(object): """Container class to hold all UI-related creation methods. Must be sublcassed. """ def create_ui(self): """Setup main UI elements, dock widgets, UI-related elements, etc. """ log.debug('Loading UI') # Undo Stack self.undo_stack = QUndoStack(self) self.undo_stack.setUndoLimit(100) # Object navigation history self.obj_history = deque([], config.MAX_OBJ_HISTORY) app = QApplication.instance() base_font = QFont() base_font.fromString(self.prefs['base_font']) app.setFont(base_font) # Object class table widget # classTable = QTableView(self) classTable = classtable.TableView(self) classTable.setObjectName("classTable") classTable.setAlternatingRowColors(True) classTable.setFrameShape(QFrame.StyledPanel) classTable_font = QFont() classTable_font.fromString(self.prefs['class_table_font']) classTable.setFont(classTable_font) fm = classTable.fontMetrics() classTable.setWordWrap(True) classTable.setEditTriggers(QAbstractItemView.EditKeyPressed | QAbstractItemView.DoubleClicked | QAbstractItemView.AnyKeyPressed | QAbstractItemView.SelectedClicked) # classTable.horizontalHeader().setMovable(True) # classTable.verticalHeader().setMovable(False) classTable.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) classTable.verticalHeader().setSectionResizeMode(QHeaderView.Interactive) classTable.horizontalHeader().setDefaultSectionSize(self.prefs['default_column_width']) classTable.verticalHeader().setDefaultSectionSize(fm.height() + 0) classTable.setSelectionMode(QAbstractItemView.ExtendedSelection) classTable.setContextMenuPolicy(Qt.CustomContextMenu) classTable.customContextMenuRequested.connect(self.custom_table_context_menu) # Create table model and proxy layers for transposing and filtering self.classTableModel = classtable.IDFObjectTableModel(classTable) self.transposeableModel = classtable.TransposeProxyModel(self.classTableModel) self.transposeableModel.setSourceModel(self.classTableModel) self.sortableModel = classtable.SortFilterProxyModel(self.transposeableModel) self.sortableModel.setSourceModel(self.transposeableModel) # Assign model to table (enable sorting FIRST) # table.setSortingEnabled(True) # Disable for now, CRUD actions won't work! classTable.setModel(self.sortableModel) # Connect some signals selection_model = classTable.selectionModel() selection_model.selectionChanged.connect(self.table_selection_changed) scroll_bar = classTable.verticalScrollBar() scroll_bar.valueChanged.connect(self.scroll_changed) # These are currently broken # classTable.horizontalHeader().sectionMoved.connect(self.moveObject) # classTable.verticalHeader().sectionMoved.connect(self.moveObject) # Object class tree widget classTreeDockWidget = QDockWidget("Object Classes and Counts", self) classTreeDockWidget.setObjectName("classTreeDockWidget") classTreeDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) classTree = QTreeView(classTreeDockWidget) classTree.setUniformRowHeights(True) classTree.setAllColumnsShowFocus(True) classTree.setRootIsDecorated(False) classTree.setExpandsOnDoubleClick(True) classTree.setIndentation(15) classTree.setAnimated(True) classTree_font = QFont() classTree_font.fromString(self.prefs['class_tree_font']) classTree.setFont(classTree_font) classTree.setAlternatingRowColors(True) classTree.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) palette = classTree.palette() palette.setColor(QPalette.Highlight, Qt.darkCyan) classTree.setPalette(palette) class_tree_window = QWidget(classTreeDockWidget) class_tree_dock_layout_v = QVBoxLayout() class_tree_dock_layout_h = QHBoxLayout() class_tree_dock_layout_v.setContentsMargins(0, 8, 0, 0) class_tree_dock_layout_h.setContentsMargins(0, 0, 0, 0) class_tree_filter_edit = QLineEdit(classTreeDockWidget) class_tree_filter_edit.setPlaceholderText("Filter Classes") class_tree_filter_edit.textChanged.connect(self.treeFilterRegExpChanged) class_tree_filter_cancel = QPushButton("Clear", classTreeDockWidget) class_tree_filter_cancel.setMaximumWidth(45) class_tree_filter_cancel.clicked.connect(self.clearTreeFilterClicked) class_tree_dock_layout_h.addWidget(class_tree_filter_edit) class_tree_dock_layout_h.addWidget(class_tree_filter_cancel) class_tree_dock_layout_v.addLayout(class_tree_dock_layout_h) class_tree_dock_layout_v.addWidget(classTree) class_tree_window.setLayout(class_tree_dock_layout_v) classTreeDockWidget.setWidget(class_tree_window) classTreeDockWidget.setContentsMargins(0,0,0,0) # Comments widget commentDockWidget = QDockWidget("Comments", self) commentDockWidget.setObjectName("commentDockWidget") commentDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) commentView = UndoRedoTextEdit(commentDockWidget, self) commentView.setLineWrapMode(QTextEdit.FixedColumnWidth) commentView.setLineWrapColumnOrWidth(499) commentView.setFrameShape(QFrame.StyledPanel) commentView_font = QFont() commentView_font.fromString(self.prefs['comments_font']) commentView.setFont(commentView_font) commentDockWidget.setWidget(commentView) # Info and help widget infoDockWidget = QDockWidget("Info", self) infoDockWidget.setObjectName("infoDockWidget") infoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) infoView = QTextEdit(infoDockWidget) infoView.setFrameShape(QFrame.StyledPanel) infoView.setReadOnly(True) infoDockWidget.setWidget(infoView) # Node list and jump menu widget refDockWidget = QDockWidget("Field References", self) refDockWidget.setObjectName("refDockWidget") refDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) ref_model = reftree.ReferenceTreeModel(None, refDockWidget) refView = QTreeView(refDockWidget) refView.setModel(ref_model) refView.setUniformRowHeights(True) refView.setRootIsDecorated(False) refView.setIndentation(15) refView.setColumnWidth(0, 160) refView.setFrameShape(QFrame.StyledPanel) refDockWidget.setWidget(refView) refView.doubleClicked.connect(self.ref_tree_double_clicked) # Logging and debugging widget logDockWidget = QDockWidget("Log Viewer", self) logDockWidget.setObjectName("logDockWidget") logDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) logView = QPlainTextEdit(logDockWidget) logView.setLineWrapMode(QPlainTextEdit.NoWrap) logView.setReadOnly(True) logView_font = QFont() logView_font.fromString(self.prefs['base_font']) logView.setFont(logView_font) logView.ensureCursorVisible() logDockWidget.setWidget(logView) # Undo view widget undoDockWidget = QDockWidget("Undo History", self) undoDockWidget.setObjectName("undoDockWidget") undoDockWidget.setAllowedAreas(Qt.AllDockWidgetAreas) undoView = QUndoView(self.undo_stack) undoDockWidget.setWidget(undoView) # Define corner docking behaviour self.setDockNestingEnabled(True) self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea) self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea) # Assign main widget and dock widgets to QMainWindow self.setCentralWidget(classTable) self.addDockWidget(Qt.LeftDockWidgetArea, classTreeDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, commentDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, infoDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, refDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, logDockWidget) self.addDockWidget(Qt.RightDockWidgetArea, undoDockWidget) # Store widgets for access by other objects self.classTable = classTable self.commentView = commentView self.infoView = infoView self.classTree = classTree self.logView = logView self.undoView = undoView self.refView = refView self.filterTreeBox = class_tree_filter_edit # Store docks for access by other objects self.commentDockWidget = commentDockWidget self.infoDockWidget = infoDockWidget self.classTreeDockWidget = classTreeDockWidget self.logDockWidget = logDockWidget self.undoDockWidget = undoDockWidget self.refDockWidget = refDockWidget # Perform other UI-related initialization tasks self.center() self.setUnifiedTitleAndToolBarOnMac(True) self.setWindowIcon(QIcon(':/images/logo.png')) # Status bar setup self.statusBar().showMessage('Status: Ready') self.unitsLabel = QLabel() self.unitsLabel.setAlignment(Qt.AlignCenter) self.unitsLabel.setMinimumSize(self.unitsLabel.sizeHint()) self.unitsLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.unitsLabel) self.pathLabel = QLabel() self.pathLabel.setAlignment(Qt.AlignCenter) self.pathLabel.setMinimumSize(self.pathLabel.sizeHint()) self.pathLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.pathLabel) self.versionLabel = QLabel() self.versionLabel.setAlignment(Qt.AlignCenter) self.versionLabel.setMinimumSize(self.versionLabel.sizeHint()) self.versionLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.statusBar().addPermanentWidget(self.versionLabel) self.progressBarIDF = QProgressBar() self.progressBarIDF.setAlignment(Qt.AlignCenter) self.progressBarIDF.setMaximumWidth(200) self.statusBar().addPermanentWidget(self.progressBarIDF) self.clipboard = QApplication.instance().clipboard() self.obj_clipboard = [] self.setStyleSheet(""" QToolTip { background-color: gray; color: white; border: black solid 1px } # QMenu { # background-color: rgbf(0.949020, 0.945098, 0.941176); # color: rgb(255,255,255); # } # QMenu::item::selected { # background-color: rgbf(0.949020, 0.945098, 0.941176); # } """) def create_tray_menu(self): """Creates an icon and menu for the system tray """ # Menu for the system tray self.trayIconMenu = QMenu(self) self.trayIconMenu.addAction(self.minimizeAction) self.trayIconMenu.addAction(self.maximizeAction) self.trayIconMenu.addAction(self.restoreAction) self.trayIconMenu.addSeparator() self.trayIconMenu.addAction(self.exitAct) # System tray itself self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setContextMenu(self.trayIconMenu) self.trayIcon.setIcon(QIcon(':/images/logo.png')) self.trayIcon.setToolTip('IDF+') self.trayIcon.show() def create_actions(self): """Creates appropriate actions for use in menus and toolbars. """ self.newAct = QAction(QIcon(':/images/new1.png'), "&New", self, shortcut=QKeySequence.New, statusTip="Create a new file", iconVisibleInMenu=True, triggered=self.new_file) self.openAct = QAction(QIcon(':/images/open.png'), "&Open...", self, shortcut=QKeySequence.Open, statusTip="Open an existing file", iconVisibleInMenu=True, triggered=self.open_file) self.saveAct = QAction(QIcon(':/images/save.png'), "&Save", self, shortcut=QKeySequence.Save, statusTip="Save the document to disk", iconVisibleInMenu=True, triggered=self.save) self.saveFormatAct = QAction(QIcon(':/images/save.png'), "&Format && Save", self, shortcut=QKeySequence('Ctrl+Shift+F'), statusTip="Format File and Save to disk", iconVisibleInMenu=True, triggered=self.format_save) self.saveAsAct = QAction(QIcon(':/images/saveas.png'), "Save &As...", self, shortcut=QKeySequence.SaveAs, statusTip="Save the document under a new name", iconVisibleInMenu=True, triggered=self.save_as) self.exitAct = QAction(QIcon(':/images/quit.png'), "E&xit", self, shortcut=QKeySequence('Ctrl+Q'), iconVisibleInMenu=True, statusTip="Exit the application", triggered=self.closeAllWindows) self.cutObjAct = QAction(QIcon(':/images/cut.png'), "Cu&t Object", self, shortcut=QKeySequence.Cut, statusTip="Cut current selection's contents to clipboard", iconVisibleInMenu=True, triggered=self.cutObject, iconText='Cut Obj') self.copyAct = QAction(QIcon(':/images/copy.png'), "&Copy Selected Values", self, statusTip="Copy current selection's contents to clipboard", iconVisibleInMenu=True, triggered=self.copySelected) self.pasteAct = QAction(QIcon(':/images/paste.png'), "&Paste Selected Values", self, statusTip="Paste clipboard into current selection", iconVisibleInMenu=True, triggered=self.pasteSelected) self.pasteExtAct = QAction(QIcon(':/images/paste.png'), "&Paste from External", self, shortcut=QKeySequence('Ctrl+Shift+v'), statusTip="Paste from external program", iconVisibleInMenu=True, triggered=self.paste_from_external) self.transposeAct = QAction("Transpose", self, shortcut=QKeySequence('Ctrl+t'), statusTip="Transpose rows and columns in object display", triggered=self.transpose_table) self.newObjAct = QAction(QIcon(':/images/new2.png'), "New Object", self, shortcut=QKeySequence('Ctrl+Shift+n'), statusTip="Create new object in current class", iconVisibleInMenu=True, triggered=self.newObject, iconText='New Obj') self.copyObjAct = QAction(QIcon(':/images/copy.png'), "Copy Object", self, shortcut=QKeySequence.Copy, statusTip="Copy the current Object(s)", iconVisibleInMenu=True, triggered=self.copyObject, iconText='Copy Obj') self.pasteObjAct = QAction(QIcon(':/images/paste.png'), "Paste Object", self, shortcut=QKeySequence.Paste, statusTip="Paste the currently copies Object(s)", iconVisibleInMenu=True, triggered=self.pasteObject, iconText='Paste Obj') self.dupObjAct = QAction(QIcon(':/images/copy.png'), "Duplicate Object", self, shortcut=QKeySequence('Shift+Ctrl+d'), statusTip="Duplicate the current Object(s)", iconVisibleInMenu=True, triggered=self.duplicateObject, iconText='Dup Obj') self.delObjAct = QAction(QIcon(':/images/delete.png'), "Delete Object", self, shortcut=QKeySequence.Delete, statusTip="Delete the current Object(s)", iconVisibleInMenu=True, triggered=self.deleteObject, iconText='Del Obj') self.undoAct = QAction(QIcon(':/images/undo.png'), "&Undo", self, shortcut=QKeySequence.Undo, statusTip="Undo previous action", iconVisibleInMenu=True, triggered=self.undo_stack.undo) self.redoAct = QAction(QIcon(':/images/redo.png'), "&Redo", self, shortcut=QKeySequence.Redo, statusTip="Redo previous action", iconVisibleInMenu=True, triggered=self.undo_stack.redo) self.groupAct = QAction("Hide Groups in Class Tree", self, shortcut=QKeySequence('Ctrl+g'), triggered=self.toggle_groups, checkable=True) # self.navForwardAct = QAction("Forward", self, # shortcut=QKeySequence('Ctrl+Plus'), # statusTip="Go forward to the next object", # triggered=self.navForward) # # self.navBackAct = QAction("Back", self, # shortcut=QKeySequence('Ctrl+Minus'), # statusTip="Go back to the previous object", # triggered=self.navBack) self.showInFolderAct = QAction(QIcon(':/images/new.png'), "&Show in folder", self, shortcut=QKeySequence('Ctrl+Shift+t'), statusTip="Open location of current file", iconVisibleInMenu=True) self.showInFolderAct.triggered.connect(lambda: self.show_in_folder()) self.epDocGettingStartedAction = QAction("EnergyPlus Getting Started", self, triggered=self.energy_plus_docs) self.epDocIORefAction = QAction("EnergyPlus I/O Reference", self, triggered=self.energy_plus_docs) self.epDocOutputDetailsAction = QAction("EnergyPlus Output Details and Examples", self, triggered=self.energy_plus_docs) self.epDocEngineeringRefAction = QAction("EnergyPlus Engineering Reference", self, triggered=self.energy_plus_docs) self.epDocAuxiliaryProgsAction = QAction("EnergyPlus Auxiliary Programs", self, triggered=self.energy_plus_docs) self.epDocEMSGuideAction = QAction("EnergyPlus EMS Application Guide", self, triggered=self.energy_plus_docs) self.epDocComplianceAction = QAction("Using EnergyPlus for Compliance", self, triggered=self.energy_plus_docs) self.epDocInterfaceAction = QAction("External Interface Application Guide", self, triggered=self.energy_plus_docs) self.epDocTipsTricksAction = QAction("Tips and Tricks Using EnergyPlus", self, triggered=self.energy_plus_docs) self.epDocPlantGuideAction = QAction("EnergyPlus Plant Application Guide", self, triggered=self.energy_plus_docs) self.epDocAcknowledgmentsAction = QAction("EnergyPlus Acknowledgments", self, triggered=self.energy_plus_docs) self.openInEditorAct = QAction(QIcon(':/images/new.png'), "&Open in text editor", self, shortcut=QKeySequence('Ctrl+e'), statusTip="Open current file in default editor", iconVisibleInMenu=True, triggered=self.open_in_text_editor) self.helpAct = QAction("&EnergyPlus Help (Online)", self, statusTip="Show the EnergyPlus' help", triggered=self.energyplus_help) self.aboutAct = QAction("&About IDF+", self, statusTip="Show the application's About box", triggered=self.about) self.clearRecentAct = QAction("Clear Recent", self, statusTip="Clear recent files", triggered=self.clear_recent) self.minimizeAction = QAction("Mi&nimize", self, triggered=self.hide) self.maximizeAction = QAction("Ma&ximize", self, triggered=self.showMaximized) self.restoreAction = QAction("&Restore", self, triggered=self.showNormal) self.showPrefsAction = QAction("&Preferences", self, triggered=self.show_prefs_dialog) self.showSearchAction = QAction("&Search && Replace", self, shortcut=QKeySequence('Ctrl+f'), triggered=self.show_search_dialog) self.findThisAct = QAction("Find This", self, triggered=self.find_this) self.jumpFilterGeometry = QAction("Include Geometry", self, triggered=self.jump_to_filter_geometry, checkable=True) self.setIPUnitsAction = QAction("&IP Units", self, triggered=self.toggle_units, checkable=True) self.setSIUnitsAction = QAction("&SI Units", self, triggered=self.toggle_units, checkable=True) self.classWithObjsAction = QAction("Show Only Classes With Objects", self, shortcut=QKeySequence('Ctrl+l'), statusTip="Show Only Classes With Objects", triggered=self.toggle_full_tree, checkable=True) self.fillRightAction = QAction("Fill right", self, shortcut=QKeySequence('Ctrl+d'), statusTip="Fill right", triggered=self.fill_right) self.logDockWidgetAct = self.logDockWidget.toggleViewAction() self.transposeAct.setEnabled(False) self.setSIUnitsAction.setChecked(True) self.undoAct.setEnabled(False) self.redoAct.setEnabled(False) self.saveAct.setEnabled(False) self.undo_stack.canUndoChanged.connect(self.toggle_can_undo) self.undo_stack.canRedoChanged.connect(self.toggle_can_redo) self.logDockWidgetAct.toggled.connect(self.start_log_watcher) def toggle_can_undo(self): if self.undo_stack.canUndo(): new_state = True else: new_state = False self.undoAct.setEnabled(new_state) self.set_dirty(new_state) def toggle_can_redo(self): if self.undo_stack.canRedo(): new_state = True else: new_state = False self.redoAct.setEnabled(new_state) def create_menus(self): """Create all required items for menus. """ # File Menu self.fileMenu = self.menuBar().addMenu("&File") self.fileMenuActions = (self.newAct, self.openAct, self.saveAct, self.saveAsAct, self.saveFormatAct, None, self.exitAct) self.update_file_menu() self.fileMenu.aboutToShow.connect(self.update_file_menu) # Edit Menu self.editMenu = self.menuBar().addMenu("&Edit") self.editMenu.addAction(self.undoAct) self.editMenu.addAction(self.redoAct) self.editMenu.addSeparator().setText('Objects') self.editMenu.addAction(self.newObjAct) self.editMenu.addAction(self.dupObjAct) self.editMenu.addSeparator() self.editMenu.addAction(self.cutObjAct) self.editMenu.addAction(self.copyObjAct) self.editMenu.addAction(self.pasteObjAct) self.editMenu.addAction(self.pasteExtAct) self.editMenu.addSeparator() self.editMenu.addAction(self.delObjAct) self.editMenu.addSeparator().setText('Values') self.editMenu.addAction(self.copyAct) self.editMenu.addAction(self.pasteAct) self.editMenu.addSeparator() self.editMenu.addAction(self.fillRightAction) self.editMenu.addSeparator() self.editMenu.addAction(self.showSearchAction) # Tools Menu self.toolsMenu = self.menuBar().addMenu("&Tools") self.toolsMenu.addAction(self.showInFolderAct) self.toolsMenu.addAction(self.openInEditorAct) self.toolsMenu.addSeparator() self.toolsMenu.addAction(self.showPrefsAction) # View Menu self.viewMenu = self.menuBar().addMenu("&View") action_group = QActionGroup(self) self.viewMenu.addAction(action_group.addAction(self.setSIUnitsAction)) self.viewMenu.addAction(action_group.addAction(self.setIPUnitsAction)) self.viewMenu.addSeparator().setText('Dockable Widgets') self.viewMenu.addAction(self.classTreeDockWidget.toggleViewAction()) self.viewMenu.addAction(self.infoView.parent().toggleViewAction()) self.viewMenu.addAction(self.commentView.parent().toggleViewAction()) self.viewMenu.addAction(self.logDockWidgetAct) self.viewMenu.addAction(self.undoView.parent().toggleViewAction()) self.viewMenu.addSeparator().setText('Toolbars') self.viewMenu.addAction(self.fileToolBar.toggleViewAction()) self.viewMenu.addAction(self.editToolBar.toggleViewAction()) # self.viewMenu.addAction(self.navToolBar.toggleViewAction()) self.viewMenu.addAction(self.filterToolBar.toggleViewAction()) self.viewMenu.addSeparator() self.viewMenu.addAction(self.classWithObjsAction) self.viewMenu.addAction(self.groupAct) self.viewMenu.addSeparator() self.viewMenu.addAction(self.transposeAct) # Jump Menu self.jumpToMenu = self.menuBar().addMenu("&Jump") self.update_jump_menu() self.jumpToMenu.aboutToShow.connect(self.update_jump_menu) self.jumpFilterGeometry.setEnabled(False) # Help Menu self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.helpAct) self.helpMenu.addAction(self.aboutAct) self.helpMenu.addSeparator() self.helpMenu.addAction(self.epDocGettingStartedAction) self.helpMenu.addAction(self.epDocIORefAction) self.helpMenu.addAction(self.epDocOutputDetailsAction) self.helpMenu.addAction(self.epDocEngineeringRefAction) self.helpMenu.addAction(self.epDocAuxiliaryProgsAction) self.helpMenu.addAction(self.epDocEMSGuideAction) self.helpMenu.addAction(self.epDocComplianceAction) self.helpMenu.addAction(self.epDocInterfaceAction) self.helpMenu.addAction(self.epDocTipsTricksAction) self.helpMenu.addAction(self.epDocPlantGuideAction) self.helpMenu.addAction(self.epDocAcknowledgmentsAction) def create_tool_bars(self): """Creates the necessary toolbars. """ # File Toolbar self.fileToolBar = self.addToolBar("File Toolbar") self.fileToolBar.setObjectName('fileToolbar') self.fileToolBar.addAction(self.newAct) self.fileToolBar.addAction(self.openAct) self.fileToolBar.addAction(self.saveAct) self.fileToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Edit Toolbar self.editToolBar = self.addToolBar("Edit Toolbar") self.editToolBar.setObjectName('editToolbar') self.editToolBar.addAction(self.undoAct) self.editToolBar.addAction(self.redoAct) self.editToolBar.addAction(self.newObjAct) self.editToolBar.addAction(self.dupObjAct) self.editToolBar.addAction(self.delObjAct) self.editToolBar.addAction(self.cutObjAct) self.editToolBar.addAction(self.copyObjAct) self.editToolBar.addAction(self.pasteObjAct) self.editToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Object history navigation toolbar # self.navToolBar = self.addToolBar("Navigation Toolbar") # self.navToolBar.setObjectName('viewToolBar') # self.navToolBar.addAction(self.navForwardAct) # self.navToolBar.addAction(self.navBackAct) # self.navToolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) # Object filter toolbar self.filterToolBar = self.addToolBar("Filter Toolbar") self.filterToolBar.setObjectName('filterToolBar') self.filterBox = QLineEdit() self.filterBox.setPlaceholderText("Filter Objects") self.filterBox.setMaximumWidth(160) self.filterBox.setFixedWidth(160) # filterLabel = QLabel("Filter Obj:", self) # filterLabel.setBuddy(self.filterBox) # self.filterToolBar.addWidget(filterLabel) self.filterBox.textChanged.connect(self.tableFilterRegExpChanged) self.filterBox.textChanged.connect(self.treeFilterRegExpChanged) clearFilterButton = QPushButton('Clear') clearFilterButton.setMaximumWidth(45) clearFilterButton.clicked.connect(self.clearFilterClicked) self.filterToolBar.addWidget(self.filterBox) self.filterToolBar.addWidget(clearFilterButton) self.caseSensitivity = QCheckBox('Case Sensitive') self.caseSensitivity.stateChanged.connect(self.caseSensitivityChanged) self.filterToolBar.addWidget(self.caseSensitivity) self.filterToolBar.addSeparator() self.filterToolBar.addAction(self.transposeAct) def create_shortcuts(self): """Creates keyboard shortcuts. """ # QShortcut(QKeySequence('Ctrl+l'), self).activated.connect(self.toggle_full_tree) # QShortcut(QKeySequence('Ctrl+d'), self).activated.connect(self.fill_right) # QShortcut(QKeySequence('Ctrl+d'), self).activated.connect(self.fill_right) # def createAction(self, text, slot=None, shortcut=None, icon=None, # tip=None, checkable=False, signal="triggered()"): # action = QAction(text, self) # if icon is not None: # action.setIcon(QIcon(":/%s.png" % icon)) # if shortcut is not None: # action.setShortcut(shortcut) # if tip is not None: # action.setToolTip(tip) # action.setStatusTip(tip) # if slot is not None: # self.connect(action, QtCore.SIGNAL(signal), slot) # if checkable: # action.setCheckable(True) # return action # def custom_table_context_menu(self, position): # Create a menu and populate it with actions menu = QMenu(self) menu.addAction(self.undoAct) menu.addAction(self.redoAct) menu.addSeparator() menu.addAction(self.copyObjAct) menu.addAction(self.dupObjAct) menu.addAction(self.delObjAct) menu.addAction(self.newObjAct) menu.addAction(self.cutObjAct) menu.addSeparator() menu.addMenu(self.jumpToMenu) menu.addSeparator() menu.addAction(self.findThisAct) menu.popup(self.classTable.viewport().mapToGlobal(position)) self.mouse_position = position def reset_progress_bar(self): self.progressBarIDF.hide() def center(self): """Called to center the window on the screen on startup. """ screen = QDesktopWidget().screenGeometry() size = self.geometry() self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2) def show_prefs_dialog(self): """Handles showing the settings dialog and setting its values. """ dlg = PrefsDialog(self, self.prefs) if dlg.exec_(): # Refresh the table view to take into account any new prefs self.load_table_view(self.current_obj_class) # Clear the idd cache if requested if self.prefs.get('clear_idd_cache', False) == True: self.clear_idd_cache() # Update preferences if various flags apply if not self.idf: return options_dict = dict() if self.prefs.get('apply_default_save_behaviour', False) == True: options_dict.update({'sort_order': self.prefs.get('sort_order'), 'special_formatting': self.prefs.get('special_formatting')}) # if self.prefs.get('apply_default_units_behaviour', False) == True: # options_dict.update({'save_units': self.prefs.get('save_units')}) # if self.prefs.get('apply_default_hidden_class_behaviour', False) == True: # options_dict.update({'save_hidden_classes': self.prefs.get('save_hidden_classes')}) if options_dict: self.idf.set_options(options_dict) self.set_dirty(True) def show_search_dialog(self): """Opens the search dialog. """ SearchReplaceDialog(self).show() def find_this(self): """Searches for fields with similar content. """ index = self.classTable.indexAt(self.mouse_position) text = self.classTable.model().data(index, Qt.EditRole) if text: SearchReplaceDialog(self, initial_query=text).show()
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()
def __init__(self, parent: QWidget, undo_group: QUndoGroup): super(KnechtTreeView, self).__init__(parent) # -- Setup progress overlay self.progress_overlay = ProgressOverlay(self) self.progress = self.progress_overlay.progress # -- Setup tree view progress bar helper self.progress_msg = ShowTreeViewProgressMessage(self) # -- Add an undo stack to the view self.undo_stack = QUndoStack(undo_group) self.undo_stack.setUndoLimit(UNDO_LIMIT) self.undo_stack.cleanChanged.connect(self.view_clean_changed) # -- Item Edit undo self.edit_undo = ViewItemEditUndo(self) # -- Setup tree settings self.setAllColumnsShowFocus(True) self.setUniformRowHeights(True) self.setSortingEnabled(False) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setAlternatingRowColors(True) # -- Drag n Drop self.setAcceptDrops(True) self.setDragDropMode(QTreeView.DragDrop) self.supports_drop = True self.supports_drag_move = True self.drag_drop = KnechtDragDrop(self) # -- Filter items types this view accepts on paste/drop actions self.accepted_item_types = [] # Default accept all # Model Editor self.editor = KnechtEditor(self) # Reset missing signal self.editor.collect.reset_missing.connect(self.reset_missing) # Item Delegate for Value edits self.setItemDelegateForColumn(Kg.VALUE, KnechtValueDelegate(self)) # Info Overlay self.info_overlay = InfoOverlay(self) # Context Menu self.context = QMenu(self) # Filter line edit widget to send keyboard input to self._filter_text_widget: QLineEdit = None self.filter_bgr_animation: BgrAnimation = None # Filter Expand timer, expand filtered items after timeout self.filter_expand_timer = QTimer() self.filter_expand_timer.setSingleShot(True) self.filter_expand_timer.setInterval(300) self.filter_expand_timer.timeout.connect(self.filter_expand_results) # Filter typing time self.filter_timer = QTimer() self.filter_timer.setSingleShot(True) self.filter_timer.setInterval(500) self.filter_timer.timeout.connect(self._set_filter_from_timer) # Cache last applied filter self._cached_filter = str() # Setup view properties # Permanent type filter for eg. renderTree self.__permanent_type_filter = [] self.__permanent_type_filter_column = Kg.TYPE # Render Tree self.__is_render_view = False # Preset Wizard Preset Tree self.__is_wizard_preset_view = False
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()
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
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)
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)
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)
def set_active_stack(self, undo_stack: QUndoStack): if isinstance(undo_stack, QUndoStack) and undo_stack is not self.active_stack: LOGGER.debug('Setting Active Stack: %s %s', undo_stack, isinstance(undo_stack, QUndoStack)) undo_stack.setActive(True) self.active_stack = undo_stack