# -*- coding: utf-8 -*- # # Copyright (C) 2014 GNS3 Technologies Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Graphical view on the scene where items are drawn. """ import logging import os from .qt import sip import sys from .qt import QtCore, QtGui, QtNetwork, QtWidgets, qpartial, qslot from .items.node_item import NodeItem from .dialogs.node_properties_dialog import NodePropertiesDialog from .link import Link from .node import Node from .modules import MODULES from .modules.module_error import ModuleError from .modules.builtin import Builtin from .settings import GRAPHICS_VIEW_SETTINGS from .topology import Topology from .template_manager import TemplateManager from .dialogs.style_editor_dialog import StyleEditorDialog from .dialogs.text_editor_dialog import TextEditorDialog from .dialogs.symbol_selection_dialog import SymbolSelectionDialog from .dialogs.idlepc_dialog import IdlePCDialog from .dialogs.console_command_dialog import ConsoleCommandDialog from .dialogs.file_editor_dialog import FileEditorDialog from .dialogs.node_info_dialog import NodeInfoDialog from .local_config import LocalConfig from .progress import Progress from .utils.server_select import server_select from .compute_manager import ComputeManager from .utils.get_icon import get_icon # link items from .items.link_item import LinkItem, SvgIconItem from .items.ethernet_link_item import EthernetLinkItem from .items.serial_link_item import SerialLinkItem # other items from .items.label_item import LabelItem from .items.text_item import TextItem from .items.shape_item import ShapeItem from .items.drawing_item import DrawingItem from .items.rectangle_item import RectangleItem from .items.line_item import LineItem from .items.ellipse_item import EllipseItem from .items.image_item import ImageItem from .items.logo_item import LogoItem log = logging.getLogger(__name__) class GraphicsView(QtWidgets.QGraphicsView): """ Graphics view that displays the scene. :param parent: parent widget """ # Class-level constants for default colors DEFAULT_DRAWING_GRID_COLOR = QtGui.QColor(208, 208, 208) # #D0D0D0 DEFAULT_NODE_GRID_COLOR = QtGui.QColor(190, 190, 190) # #BEBEBE def __init__(self, parent): # Our parent is the central widget which parent is the main window. self._main_window = parent.parent() super().__init__(parent) self._settings = {} self._loadSettings() self._adding_link = False self._adding_note = False self._adding_rectangle = False self._adding_ellipse = False self._adding_line = False self._newlink = None self._dragging = False self._grid_size = 75 self._drawing_grid_size = 25 self._drawing_grid_color = self.DEFAULT_DRAWING_GRID_COLOR self._node_grid_color = self.DEFAULT_NODE_GRID_COLOR self._last_mouse_position = None self._topology = Topology.instance() self._background_warning_msgbox = QtWidgets.QErrorMessage(self) self._background_warning_msgbox.setWindowTitle("Layer position") # set the scene scene = QtWidgets.QGraphicsScene(parent=self) width = self._settings["scene_width"] height = self._settings["scene_height"] self.setScene(scene) self.setSceneSize(width, height) # set the custom flags for this view self.setDragMode(QtWidgets.QGraphicsView.DragMode.RubberBandDrag) self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) self.setTransformationAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setResizeAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorViewCenter) # default directories for QFileDialog self._import_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation) self._export_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation) self._local_addresses = ['0.0.0.0', '127.0.0.1', 'localhost', '::1', '0:0:0:0:0:0:0:1', '::', QtNetwork.QHostInfo.localHostName()] def setSceneSize(self, width, height): self.scene().setSceneRect(-(width / 2), -(height / 2), width, height) def setZoom(self, zoom): """ Sets zoom of the Graphics View """ if zoom: factor = zoom / 100. self.scale(factor, factor) def setNodeGridSize(self, grid_size): """ Sets the grid size for nodes. """ self._grid_size = grid_size def nodeGridSize(self): """ Returns the grid size for nodes. :return: integer """ return self._grid_size def setDrawingGridSize(self, grid_size): """ Sets the grid size for drawings """ self._drawing_grid_size = grid_size def drawingGridSize(self): """ Returns the grid size for drawings :return: integer """ return self._drawing_grid_size def setEnabled(self, enabled): if enabled is False: self.reset() item = QtWidgets.QGraphicsTextItem("Please create a project") item.setPos(0, 0) self.scene().addItem(item) super().setEnabled(enabled) self.toggleUiDeviceMenu() def reset(self): """ Remove all the items from the scene and reset the instances count. """ # reset the modules for module in MODULES: instance = module.instance() instance.reset() # reset instance IDs for # nodes, links and ports Node.reset() Link.reset() # reset the topology self._topology.reset() # clear the topology summary self._main_window.uiTopologySummaryTreeWidget.clear() # reset the lock button self._main_window.uiLockAllAction.setChecked(False) # clear all objects on the scene self.scene().clear() # reset zoom / scale self.resetTransform() def _loadSettings(self): """ Loads the settings from the persistent settings file. """ self._settings = LocalConfig.instance().loadSectionSettings(self.__class__.__name__, GRAPHICS_VIEW_SETTINGS) def settings(self): """ Returns the graphics view settings. :returns: settings dictionary """ return self._settings def setSettings(self, new_settings): """ Set new graphics view settings. :param new_settings: settings dictionary """ # save the settings self._settings.update(new_settings) LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings) def addingLinkSlot(self, enabled): """ Slot to receive events from MainWindow when a user has clicked on "Add a link" button. :param enable: either the user is adding a link or not (boolean) """ if enabled: self.setCursor(QtCore.Qt.CursorShape.CrossCursor) else: if self._newlink and self._newlink in self.scene().items(): self.scene().removeItem(self._newlink) self._newlink = None self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) self._adding_link = enabled def addNote(self, state): """ Adds a note. :param state: boolean """ if state: self._adding_note = True self.setCursor(QtCore.Qt.CursorShape.IBeamCursor) else: self._adding_note = False self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) def addRectangle(self, state): """ Adds a rectangle. :param state: boolean """ if state: self._adding_rectangle = True self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) else: self._adding_rectangle = False self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) def addEllipse(self, state): """ Adds an ellipse. :param state: boolean """ if state: self._adding_ellipse = True self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) else: self._adding_ellipse = False self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) def addLine(self, state): """ Adds a line. :param state: boolean """ if state: self._adding_line = True self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) else: self._adding_line = False self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) def addImage(self, image_path): """ Adds an image. :param image_path: path to the image """ image_item = ImageItem(image_path=image_path, project=self._topology.project()) image_item.create() self.scene().addItem(image_item) self._topology.addDrawing(image_item) def addLogo(self, logo_path, logo_url): logo_item = LogoItem(logo_path, logo_url, self._topology.project()) self.scene().addItem(logo_item) def addLink(self, source_node, source_port, destination_node, destination_port, **link_data): """ Creates a Link instance representing a connection between 2 devices. :param source_node: source Node instance :param source_port: source Port instance :param destination_node: destination Node instance :param destination_port: destination Port instance :param link_data: information about link from the API :returns: Link """ link = Link(source_node, source_port, destination_node, destination_port, **link_data) # connect the signals that let the graphics view knows about events such as # a new link creation or deletion. if self._topology.addLink(link): source_node.addLink(link) destination_node.addLink(link) link.add_link_signal.connect(self.addLinkSlot) link.delete_link_signal.connect(self.deleteLinkSlot) if link.initialized(): self.addLinkSlot(link.id()) return link def addLinkSlot(self, link_id): """ Slot to receive events from Link instances when a link has been created. :param link_id: link identifier """ link = self._topology.getLink(link_id) if not link: return source_item = None destination_item = None source_port = link.sourcePort() destination_port = link.destinationPort() # find the correct source and destination node items for item in self.scene().items(): if isinstance(item, NodeItem): if item.node().id() == link.sourceNode().id(): source_item = item if item.node().id() == link.destinationNode().id(): destination_item = item if source_item and destination_item: break if not source_item or not destination_item: log.error("Could not find a source or destination item for the link!") self.deleteLinkSlot(link_id) return if link.sourcePort().linkType() == "Serial": link_item = SerialLinkItem(source_item, source_port, destination_item, destination_port, link) else: link_item = EthernetLinkItem(source_item, source_port, destination_item, destination_port, link) self.scene().addItem(link_item) def deleteLinkSlot(self, link_id): """ Slot to receive events from Link instances when a link has been deleted. :param link_id: link identifier """ link = self._topology.getLink(link_id) self._topology.removeLink(link) def _userNodeLinking(self, event, item): """ Handles node linking by the user. :param event: QMouseEvent instance :param item: NodeItem instance """ # link addition code if not self._newlink: source_item = item source_port = source_item.connectToPort(event.globalPosition().toPoint()) if not source_port: return if source_port.link() is not None: QtWidgets.QMessageBox.warning(self, "Create link", "Can't create the link the port is not free") return if source_port.linkType() == "Serial": self._newlink = SerialLinkItem(source_item, source_port, self.mapToScene(event.position().toPoint()), None, adding_flag=True) else: self._newlink = EthernetLinkItem(source_item, source_port, self.mapToScene(event.position().toPoint()), None, adding_flag=True) self.scene().addItem(self._newlink) else: source_item = self._newlink.sourceItem() source_port = self._newlink.sourcePort() destination_item = item destination_port = destination_item.connectToPort(event.globalPosition().toPoint()) if not destination_port: return if destination_port.link() is not None: QtWidgets.QMessageBox.warning(self, "Create link", "Can't create the link the destination port is not free") return if self._newlink in self.scene().items(): self.scene().removeItem(self._newlink) self._newlink = None self.addLink(source_item.node(), source_port, destination_item.node(), destination_port) def mousePressEvent(self, event): """ Handles all mouse press events. :param event: QMouseEvent instance """ is_not_link = True is_not_logo = True item = self.itemAt(event.position().toPoint()) if item and sip.isdeleted(item): return elif not item: self._main_window.uiStatusBar.clearMessage() # reset the status bar message when clicking on the scene if item and (isinstance(item, LinkItem) or isinstance(item.parentItem(), LinkItem)): is_not_link = False if item and (isinstance(item, LogoItem) or isinstance(item.parentItem(), LogoItem)): is_not_logo = False else: for it in self.scene().items(): if isinstance(it, LinkItem): it.setHovered(False) if (event.buttons() == QtCore.Qt.MouseButton.LeftButton and event.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier) or event.buttons() == QtCore.Qt.MouseButton.MiddleButton: # checks to see if either the middle mouse is pressed # or a combination of left mouse button and SHIT key are pressed to start dragging the view self._last_mouse_position = self.mapFromGlobal(event.globalPosition()) self._dragging = True self.setCursor(QtCore.Qt.CursorShape.ClosedHandCursor) return if is_not_link and item and event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier and event.button() == QtCore.Qt.MouseButton.LeftButton and item and not self._adding_link: # manual selection using CTRL if item.isSelected(): item.setSelected(False) else: item.setSelected(True) elif is_not_link and is_not_logo and event.button() == QtCore.Qt.MouseButton.RightButton and not self._adding_link: pass #TODO: remove this without creating a bug... elif is_not_link and self._adding_link and event.button() == QtCore.Qt.MouseButton.RightButton: # send a escape key to the main window to cancel the link addition key = QtGui.QKeyEvent(QtCore.QEvent.Type.KeyPress, QtCore.Qt.Key.Key_Escape, QtCore.Qt.KeyboardModifier.NoModifier) QtWidgets.QApplication.sendEvent(self._main_window, key) elif item and isinstance(item, NodeItem) and self._adding_link and event.button() == QtCore.Qt.MouseButton.LeftButton: self._userNodeLinking(event, item) #context_event = QtGui.QContextMenuEvent(QtGui.QContextMenuEvent.Reason.Mouse, event.pos()) #QtWidgets.QApplication.sendEvent(self, context_event) elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_note: pos = self.mapToScene(event.position().toPoint()) note = self.createDrawingItem("text", int(pos.x()), int(pos.y()), 2) pos_x = note.pos().x() pos_y = note.pos().y() - (note.boundingRect().height() / 2) note.setPos(pos_x, pos_y) note.editText() self._main_window.uiAddNoteAction.setChecked(False) self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) self._adding_note = False elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_rectangle: pos = self.mapToScene(event.position().toPoint()) self.createDrawingItem("rect", int(pos.x()), int(pos.y()), 1) self._main_window.uiDrawRectangleAction.setChecked(False) self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) self._adding_rectangle = False elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_ellipse: pos = self.mapToScene(event.position().toPoint()) self.createDrawingItem("ellipse", int(pos.x()), int(pos.y()), 1) self._main_window.uiDrawEllipseAction.setChecked(False) self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) self._adding_ellipse = False elif event.button() == QtCore.Qt.MouseButton.LeftButton and self._adding_line: pos = self.mapToScene(event.position().toPoint()) self.createDrawingItem("line", int(pos.x()), int(pos.y()), 1) self._main_window.uiDrawLineAction.setChecked(False) self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) self._adding_line = False else: super().mousePressEvent(event) self.toggleUiDeviceMenu() def contextMenuEvent(self, event): """ Handles all context menu events. :param event: QContextMenuEvent instance """ is_not_link = True is_not_logo = True item = self.itemAt(event.pos()) if item and sip.isdeleted(item): return if item and (isinstance(item, LinkItem) or isinstance(item.parentItem(), LinkItem)): is_not_link = False if item and (isinstance(item, LogoItem) or isinstance(item.parentItem(), LogoItem)): is_not_logo = False else: for it in self.scene().items(): if isinstance(it, LinkItem): it.setHovered(False) if is_not_link and is_not_logo and not self._adding_link: if item and not sip.isdeleted(item): # Prevent right clicking on a selected item from de-selecting all other items if not item.isSelected(): if not (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier): for it in self.scene().items(): it.setSelected(False) item.setSelected(True) self._showDeviceContextualMenu(event.globalPos()) # when more than one item is selected display the contextual menu even if mouse is not above an item elif len(self.scene().selectedItems()) > 1: self._showDeviceContextualMenu(event.globalPos()) #elif item and isinstance(item, NodeItem) and self._adding_link: # self._userNodeLinking(event, item) else: super().contextMenuEvent(event) def mouseReleaseEvent(self, event): """ Handles all mouse release events. :param: QMouseEvent instance """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem): item.mouseRelease() # If the left mouse button is not still pressed TOGETHER with the SHIFT key and neither is the middle button # this means the user is no longer trying to drag the view if self._dragging and not (event.buttons() == QtCore.Qt.MouseButton.LeftButton and event.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier) and not (event.buttons() & QtCore.Qt.MouseButton.MiddleButton): self._dragging = False self.setCursor(QtCore.Qt.CursorShape.ArrowCursor) else: item = self.itemAt(event.position().toPoint()) if item is not None and not (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier): item.setSelected(True) super().mouseReleaseEvent(event) self.toggleUiDeviceMenu() def wheelEvent(self, event): """ Handles zoom in or out using the mouse wheel. :param: QWheelEvent instance """ if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier: delta = event.angleDelta() if delta is not None and delta.x() == 0: # CTRL is pressed then use the mouse wheel to zoom in or out. self.scaleView(pow(2.0, (delta.y()/2) / 240.0)) self._topology.project().setZoom(round(self.transform().m11() * 100)) self._topology.project().update() else: super().wheelEvent(event) def scaleView(self, scale_factor): """ Scales the view (zoom in and out). """ factor = self.transform().scale(scale_factor, scale_factor).mapRect(QtCore.QRectF(0, 0, 1, 1)).width() if factor < 0.10 or factor > 10: return self.scale(scale_factor, scale_factor) self._main_window.uiStatusBar.showMessage("Zoom: {}%".format(round(self.transform().m11() * 100)), 5000) def keyPressEvent(self, event): """ Handles all key press events for this view. :param event: QKeyEvent """ if event.key() == QtCore.Qt.Key.Key_Delete: # check if we are editing an LabelItem instance, then send the delete key event to it for item in self.scene().selectedItems(): if (isinstance(item, LabelItem) or isinstance(item, TextItem)) and item.hasFocus(): super().keyPressEvent(event) return self.deleteActionSlot() super().keyPressEvent(event) def mouseMoveEvent(self, event): """ Handles all mouse move events (mouse tracking has been enabled). :param event: QMouseEvent instance """ if self._dragging: # This if statement event checks to see if the user is dragging the scene # if so it sets the value of the scene scroll bars based on the change between # the previous and current mouse position mapped_global_pos = self.mapFromGlobal(event.globalPosition()) hBar = self.horizontalScrollBar() vBar = self.verticalScrollBar() delta = mapped_global_pos - self._last_mouse_position hBar.setValue(int(hBar.value() + (delta.x() if QtWidgets.QApplication.isRightToLeft() else -delta.x()))) vBar.setValue(int(vBar.value() - delta.y())) self._last_mouse_position = mapped_global_pos if self._adding_link and self._newlink and self._newlink in self.scene().items(): # update the mouse position when the user is adding a link. self._newlink.setMousePoint(self.mapToScene(event.position().toPoint())) event.ignore() else: item = self.itemAt(event.position().toPoint()) if item: # show item coords in the status bar coords = "X: {} Y: {} Z: {}".format(item.x(), item.y(), item.zValue()) self._main_window.uiStatusBar.showMessage(coords) # force the children to redraw because of a problem with QGraphicsEffect for item in self.scene().selectedItems(): for child in item.childItems(): child.update() super().mouseMoveEvent(event) def mouseDoubleClickEvent(self, event): """ Handles all mouse double click events. :param event: QMouseEvent instance """ item = self.itemAt(event.position().toPoint()) if not self._adding_link: if isinstance(item, NodeItem) and item.node().initialized(): item.setSelected(True) if item.node().status() == Node.stopped or item.node().consoleType() == "none" or item.node().consoleType() is None: self.configureSlot() return else: if item.node().bringToFront(): return self.consoleFromItems(self.scene().selectedItems()) return elif isinstance(item, LabelItem) and isinstance(item.parentItem(), NodeItem): if item.parentItem().node().initialized(): item.parentItem().setSelected(True) self.changeHostnameActionSlot() return super().mouseDoubleClickEvent(event) def configureSlot(self, items=None): """ Opens the node properties dialog. """ if not items: items = [] for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().initialized() and hasattr(item.node(), "configPage"): items.append(item) with Progress.instance().context(min_duration=0): node_properties = NodePropertiesDialog(items, self._main_window) node_properties.setModal(True) node_properties.show() node_properties.exec() def dragMoveEvent(self, event): """ Handles all drag move events. :param event: QDragMoveEvent instance """ # check if what is dragged is handled by this view if event.mimeData().hasFormat("text/uri-list") \ or event.mimeData().hasFormat("application/x-gns3-template"): event.acceptProposedAction() event.accept() else: event.ignore() def dropEvent(self, event): """ Handles all drop events. :param event: QDropEvent instance """ log.debug("Drop event received with mime data: {}".format(event.mimeData().formats())) # check if what has been dropped is handled by this view if event.mimeData().hasFormat("application/x-gns3-template"): template_id = event.mimeData().data("application/x-gns3-template").data().decode() event.setDropAction(QtCore.Qt.DropAction.CopyAction) event.accept() if event.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier: max_nodes_per_line = 10 # max number of nodes on a single line offset = 100 # spacing between elements integer, ok = QtWidgets.QInputDialog.getInt(self, "Nodes", "Number of nodes:", 2, 1, 100, 1) if ok: for node_number in range(integer): x = event.position().x() - (150 // 2) + (node_number % max_nodes_per_line) * offset y = event.position().y() - (70 // 2) + (node_number // max_nodes_per_line) * offset if self.createNodeFromTemplateId(template_id, QtCore.QPointF(x, y)) is False: event.ignore() break else: if self.createNodeFromTemplateId(template_id, event.position()) is False: event.ignore() elif event.mimeData().hasFormat("text/uri-list") and event.mimeData().hasUrls(): # This should not arrive but we received bug report with it... if len(event.mimeData().urls()) == 0: return if len(event.mimeData().urls()) > 1: QtWidgets.QMessageBox.critical(self, "Project files", "Please drop only one file") return path = event.mimeData().urls()[0].toLocalFile() if os.path.isfile(path): self._main_window.loadPath(path) event.acceptProposedAction() else: event.ignore() def _showDeviceContextualMenu(self, pos): """ Create and display the device contextual menu on the view. :param pos: position where to display the menu """ menu = QtWidgets.QMenu(parent=self) self.populateDeviceContextualMenu(menu) menu.exec(pos) menu.clear() # Make sure to deselect all items. # This is to prevent a bug on Windows # see https://github.com/GNS3/gns3-gui/issues/2986 for it in self.scene().items(): it.setSelected(False) def populateDeviceContextualMenu(self, menu): """ Adds device actions to the device contextual menu. :param menu: QMenu instance """ items = self.scene().selectedItems() if not items: return if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "configPage"), items)): # Action: Configure node configure_action = QtGui.QAction("Configure", menu) configure_action.setIcon(get_icon("configuration.svg")) configure_action.triggered.connect(self.configureActionSlot) menu.addAction(configure_action) if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)): console_action = QtGui.QAction("Console", menu) console_action.setIcon(get_icon("console.svg")) console_action.triggered.connect(self.consoleActionSlot) menu.addAction(console_action) if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)): aux_console_action = QtGui.QAction("Auxiliary console", menu) aux_console_action.setIcon(get_icon("aux-console.svg")) aux_console_action.triggered.connect(self.auxConsoleActionSlot) menu.addAction(aux_console_action) if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)): start_action = QtGui.QAction("Start", menu) start_action.setIcon(get_icon("start.svg")) start_action.triggered.connect(self.startActionSlot) menu.addAction(start_action) if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)): suspend_action = QtGui.QAction("Suspend", menu) suspend_action.setIcon(get_icon("pause.svg")) suspend_action.triggered.connect(self.suspendActionSlot) menu.addAction(suspend_action) if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)): stop_action = QtGui.QAction("Stop", menu) stop_action.setIcon(get_icon("stop.svg")) stop_action.triggered.connect(self.stopActionSlot) menu.addAction(stop_action) if True in list(map(lambda item: isinstance(item, NodeItem) and not item.node().isAlwaysOn(), items)): reload_action = QtGui.QAction("Reload", menu) reload_action.setIcon(get_icon("reload.svg")) reload_action.triggered.connect(self.reloadActionSlot) menu.addAction(reload_action) if True in list(map(lambda item: isinstance(item, NodeItem) and item.node().console() is not None, items)): console_edit_action = QtGui.QAction("Custom console", menu) console_edit_action.setIcon(get_icon("console_edit.svg")) console_edit_action.triggered.connect(self.customConsoleActionSlot) menu.addAction(console_edit_action) if True in list(map(lambda item: isinstance(item, NodeItem), items)): # Action: Change hostname change_hostname_action = QtGui.QAction("Change hostname", menu) change_hostname_action.setIcon(get_icon("show-hostname.svg")) change_hostname_action.triggered.connect(self.changeHostnameActionSlot) menu.addAction(change_hostname_action) if True in list(map(lambda item: isinstance(item, NodeItem), items)): # Action: Change symbol change_symbol_action = QtGui.QAction("Change symbol", menu) change_symbol_action.setIcon(get_icon("node_conception.svg")) change_symbol_action.triggered.connect(self.changeSymbolActionSlot) menu.addAction(change_symbol_action) if True in list(map(lambda item: isinstance(item, DrawingItem) or isinstance(item, NodeItem), items)): duplicate_action = QtGui.QAction("Duplicate", menu) duplicate_action.setIcon(get_icon("duplicate.svg")) duplicate_action.triggered.connect(self.duplicateActionSlot) menu.addAction(duplicate_action) if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "info"), items)): # Action: Show node information show_node_info_action = QtGui.QAction("Show node information", menu) show_node_info_action.setIcon(get_icon("help.svg")) show_node_info_action.triggered.connect(self.showNodeInfoSlot) menu.addAction(show_node_info_action) if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir"), items)): # Action: Show in file manager show_in_file_manager_action = QtGui.QAction("Show in file manager", menu) show_in_file_manager_action.setIcon(get_icon("open.svg")) show_in_file_manager_action.triggered.connect(self.showInFileManagerSlot) menu.addAction(show_in_file_manager_action) if not sys.platform.startswith("darwin") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)): # Action: bring console or window to front (Windows and Linux only) bring_to_front_action = QtGui.QAction("Bring to front", menu) bring_to_front_action.setIcon(get_icon("front.svg")) bring_to_front_action.triggered.connect(self.bringToFrontSlot) menu.addAction(bring_to_front_action) if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configFiles()), items)): import_config_action = QtGui.QAction("Import config", menu) import_config_action.setIcon(get_icon("import.svg")) import_config_action.triggered.connect(self.importConfigActionSlot) menu.addAction(import_config_action) if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configFiles()), items)): export_config_action = QtGui.QAction("Export config", menu) export_config_action.setIcon(get_icon("export.svg")) export_config_action.triggered.connect(self.exportConfigActionSlot) menu.addAction(export_config_action) if True in list(map(lambda item: isinstance(item, NodeItem) and bool(item.node().configTextFiles()), items)): export_config_action = QtGui.QAction("Edit config", menu) export_config_action.setIcon(get_icon("edit.svg")) export_config_action.triggered.connect(self.editConfigActionSlot) menu.addAction(export_config_action) if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)): idlepc_action = QtGui.QAction("Idle-PC", menu) idlepc_action.setIcon(get_icon("calculate.svg")) idlepc_action.triggered.connect(self.idlepcActionSlot) menu.addAction(idlepc_action) if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "idlepc"), items)): auto_idlepc_action = QtGui.QAction("Auto Idle-PC", menu) auto_idlepc_action.setIcon(get_icon("calculate.svg")) auto_idlepc_action.triggered.connect(self.autoIdlepcActionSlot) menu.addAction(auto_idlepc_action) if True in list(map(lambda item: isinstance(item, LabelItem), items)): text_edit_action = QtGui.QAction("Text edit", menu) text_edit_action.setIcon(get_icon("show-hostname.svg")) text_edit_action.triggered.connect(self.textEditActionSlot) menu.addAction(text_edit_action) if True in list(map(lambda item: isinstance(item, TextItem), items)): text_edit_action = QtGui.QAction("Text edit", menu) text_edit_action.setIcon(get_icon("edit.svg")) text_edit_action.triggered.connect(self.textEditActionSlot) menu.addAction(text_edit_action) if True in list(map(lambda item: isinstance(item, ShapeItem) or isinstance(item, LineItem), items)): style_action = QtGui.QAction("Style", menu) style_action.setIcon(get_icon("node_conception.svg")) style_action.triggered.connect(self.styleActionSlot) menu.addAction(style_action) if True in list(map(lambda item: isinstance(item, LabelItem), items)) and False in list(map(lambda item: item.parentItem() is None, items)): # action only for port labels reset_label_position_action = QtGui.QAction("Reset position", menu) reset_label_position_action.setIcon(get_icon("reset.svg")) reset_label_position_action.triggered.connect(self.resetLabelPositionActionSlot) menu.addAction(reset_label_position_action) # item must have no parent if True in list(map(lambda item: item.parentItem() is None, items)): if len(items) > 1: horizontal_align_action = QtGui.QAction("Align horizontally", menu) horizontal_align_action.setIcon(get_icon("horizontally.svg")) horizontal_align_action.triggered.connect(self.horizontalAlignmentSlot) menu.addAction(horizontal_align_action) vertical_align_action = QtGui.QAction("Align vertically", menu) vertical_align_action.setIcon(get_icon("vertically.svg")) vertical_align_action.triggered.connect(self.verticalAlignmentSlot) menu.addAction(vertical_align_action) raise_layer_action = QtGui.QAction("Raise one layer", menu) raise_layer_action.setIcon(get_icon("raise_z_value.svg")) raise_layer_action.triggered.connect(self.raiseLayerActionSlot) menu.addAction(raise_layer_action) lower_layer_action = QtGui.QAction("Lower one layer", menu) lower_layer_action.setIcon(get_icon("lower_z_value.svg")) lower_layer_action.triggered.connect(self.lowerLayerActionSlot) menu.addAction(lower_layer_action) if len(items) > 1: lock_action = QtGui.QAction("Lock or unlock items", menu) lock_action.setIcon(get_icon("lock.svg")) else: item = items[0] if item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable: lock_action = QtGui.QAction("Lock item", menu) lock_action.setIcon(get_icon("lock.svg")) else: lock_action = QtGui.QAction("Unlock item", menu) lock_action.setIcon(get_icon("unlock.svg")) lock_action.triggered.connect(self.lockActionSlot) menu.addAction(lock_action) delete_action = QtGui.QAction("Delete", menu) delete_action.setIcon(get_icon("delete.svg")) delete_action.triggered.connect(self.deleteActionSlot) menu.addAction(delete_action) def startActionSlot(self): """ Slot to receive events from the start action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and hasattr(item.node(), "start") and item.node().initialized(): item.node().start() def stopActionSlot(self): """ Slot to receive events from the stop action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and hasattr(item.node(), "stop") and item.node().initialized(): item.node().stop() def suspendActionSlot(self): """ Slot to receive events from the suspend action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and hasattr(item.node(), "suspend") and item.node().initialized(): item.node().suspend() def reloadActionSlot(self): """ Slot to receive events from the reload action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and hasattr(item.node(), "reload") and item.node().initialized(): item.node().reload() def configureActionSlot(self): """ Slot to receive events from the configure action in the contextual menu. """ items = [] for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().initialized() and hasattr(item.node(), "configPage"): items.append(item) if items: self.configureSlot(items) def changeHostnameActionSlot(self): """ Slot to receive events from the change hostname action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().initialized(): new_hostname, ok = QtWidgets.QInputDialog.getText(self, "Change hostname", "Hostname:", QtWidgets.QLineEdit.EchoMode.Normal, item.node().name()) if ok: if not new_hostname.strip(): QtWidgets.QMessageBox.critical(self, "Change hostname", "Hostname cannot be blank") continue if hasattr(item.node(), "validateHostname") and not LocalConfig.instance().experimental(): if not item.node().validateHostname(new_hostname): QtWidgets.QMessageBox.critical(self, "Change hostname", "Invalid name detected for this node: {}".format(new_hostname)) continue item.node().update({"name": new_hostname}) def changeSymbolActionSlot(self): """ Slot to receive events from the change symbol action in the contextual menu. """ items = [] for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().initialized(): items.append(item) if items: dialog = SymbolSelectionDialog(self, items) dialog.show() dialog.exec() def showInFileManagerSlot(self): """ Slot to receive events from the show in file manager action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and hasattr(item.node(), "nodeDir") and item.node().initialized(): node = item.node() node_dir = node.nodeDir() if node_dir is None: QtWidgets.QMessageBox.critical(self, "Show in file manager", "This node has no working directory") break if os.path.exists(node_dir): log.debug("Open %s in file manager") if QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(node_dir)) is False: QtWidgets.QMessageBox.critical(self, "Show in file manager", "Failed to open {}".format(node_dir)) break else: reply = QtWidgets.QMessageBox.information(self, "Show in file manager", "The device directory is located in {} on {}\n\nCopy path to clipboard?".format(node_dir, node.compute().name()), QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.Yes: QtWidgets.QApplication.clipboard().setText(node_dir) break def consoleToNode(self, node, aux=False): """ Start a console application to connect to a node. :param node: Node instance :param aux: auxiliary console mode :returns: False if the console application could not be started """ if not hasattr(node, "console") or not node.initialized() or node.status() != Node.started: # returns True to ignore this node. return True if aux and not hasattr(node, "auxConsole"): # returns True to ignore this node. return True # TightVNC has lack support of IPv6 host at this moment if "vncviewer" in node.consoleCommand() and ":" in node.consoleHost(): QtWidgets.QMessageBox.warning(self, "TightVNC", "TightVNC (vncviewer) may not start because of lack of IPv6 support.") try: node.openConsole(aux=aux) except (OSError, ValueError) as e: QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e)) return False return True def consoleFromItems(self, items): """ Console from scene items. :param items: Item instances """ nodes = {} node_initialized = False for item in items: if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized(): node_initialized = True if item.node().status() == Node.started: node = item.node() nodes[node.name()] = node if not nodes and node_initialized: if len(items) > 1: QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened") else: QtWidgets.QMessageBox.warning(self, "Console", "This node must be started before a console can be opened") delay = self._main_window.settings()["delay_console_all"] counter = 0 for name in sorted(nodes.keys(), key=str.casefold): node = nodes[name] callback = qpartial(self.consoleToNode, node) self._main_window.run_later(counter, callback) counter += delay def consoleFromAllItems(self): """ Console from all scene items with console type different than "none" """ items = [item for item in self.scene().items() if isinstance(item, NodeItem) and item.node().consoleType() != "none"] nb_items = len(items) if nb_items > 10: proceed = QtWidgets.QMessageBox.question(self, "Console to all nodes", "You are about to open console windows to {} nodes. Are you sure?".format(nb_items), QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No) if proceed == QtWidgets.QMessageBox.StandardButton.No: return self.consoleFromItems(items) def consoleActionSlot(self): """ Slot to receive events from the console action in the contextual menu. """ self.consoleFromItems(self.scene().selectedItems()) def customConsoleActionSlot(self): """ Allow user to use a custom console for this VM """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized(): if item.node().consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"): continue current_cmd = item.node().consoleCommand() console_type = item.node().consoleType() (ok, cmd) = ConsoleCommandDialog.getCommand(self, console_type=console_type, current=current_cmd) if ok: try: if item.node().status() != Node.started: QtWidgets.QMessageBox.warning(self, "Console", "This node must be started before a console can be opened") continue item.node().openConsole(command=cmd) except (OSError, ValueError) as e: QtWidgets.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e)) def auxConsoleFromItems(self, items): """ Aux console from scene items. :param items: Item instances """ nodes = {} node_initialized = False for item in items: if isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole") and item.node().initialized(): node_initialized = True if item.node().status() == Node.started: node = item.node() nodes[node.name()] = node if not nodes and node_initialized: if len(items) > 1: QtWidgets.QMessageBox.warning(self, "Console", "At least one node must be started before a console can be opened") else: QtWidgets.QMessageBox.warning(self, "Console", "This node must be started before a console can be opened") delay = self._main_window.settings()["delay_console_all"] counter = 0 for name in sorted(nodes.keys(), key=str.casefold): node = nodes[name] callback = qpartial(self.consoleToNode, node, aux=True) self._main_window.run_later(counter, callback) counter += delay def auxConsoleActionSlot(self): """ Slot to receive events from the auxiliary console action in the contextual menu. """ self.auxConsoleFromItems(self.scene().selectedItems()) def importConfigActionSlot(self): """ Slot to receive events from the import config action in the contextual menu. """ items = [] for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().configFiles() and item.node().initialized(): items.append(item) if not items: return for item in items: if len(item.node().configFiles()) == 1: config_file = item.node().configFiles()[0] else: config_file, ok = QtWidgets.QInputDialog.getItem(self, "Import file", "File to import?", item.node().configFiles(), 0, False) if not ok: continue path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import {}".format(os.path.basename(config_file)), self._import_config_directory, "All files (*);;Config files (*.cfg)", "Config files (*.cfg)") if not path: continue self._import_config_directory = os.path.dirname(path) item.node().importFile(config_file, path) def editConfigActionSlot(self): """ Slot to receive event to edit the configuration file """ items = [] for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().configTextFiles() and item.node().initialized(): items.append(item) if not items: return for item in items: if len(item.node().configTextFiles()) == 1: config_file = item.node().configTextFiles()[0] else: config_file, ok = QtWidgets.QInputDialog.getItem(self, "Edit file", "File to edit?", item.node().configTextFiles(), 0, False) if not ok: continue dialog = FileEditorDialog(item.node(), config_file, parent=self) dialog.show() dialog.exec() def exportConfigActionSlot(self): """ Slot to receive events from the export config action in the contextual menu. """ items = [] for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and item.node().configFiles() and item.node().initialized(): items.append(item) if not items: return for item in items: for config_file in item.node().configFiles(): path, ok = QtWidgets.QFileDialog.getSaveFileName(self, "Export file", os.path.join(self._export_config_directory, item.node().name() + "_" + os.path.basename(config_file)), "All files (*);;Config files (*.cfg)") if not path: continue self._export_config_directory = os.path.dirname(path) item.node().exportFile(config_file, path) def showNodeInfoSlot(self): """ Slot to receive events from the show node info action in the contextual menu. """ items = self.scene().selectedItems() if len(items) != 1: QtWidgets.QMessageBox.critical(self, "Show node information", "Please select only one node") return item = items[0] if isinstance(item, NodeItem): dialog = NodeInfoDialog(item.node(), parent=self) dialog.show() dialog.exec() def bringToFrontSlot(self): """ Slot to receive events from the bring to front action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"): item.node().bringToFront() def idlepcActionSlot(self): """ Slot to receive events from the idlepc action in the contextual menu. """ items = self.scene().selectedItems() if len(items) != 1: QtWidgets.QMessageBox.critical(self, "Idle-PC", "Please select only one router") return item = items[0] if isinstance(item, NodeItem) and hasattr(item.node(), "idlepc") and item.node().initialized(): router = item.node() router.computeIdlepcs(self._idlepcCallback) def _idlepcCallback(self, result, error=False, context={}, **kwargs): """ Slot to allow the user to select an idle-pc value. """ if error: QtWidgets.QMessageBox.critical(self, "Idle-PC", "Error: {}".format(result["message"])) else: router = context["router"] log.debug("{} has received Idle-PC proposals".format(router.name())) idlepcs = result if idlepcs and idlepcs[0] != "0x0": dialog = IdlePCDialog(router, idlepcs, parent=self) dialog.show() dialog.exec() else: QtWidgets.QMessageBox.critical(self, "Idle-PC", "Sorry no Idle-PC values could be computed, please check again with Cisco IOS in a different state") def autoIdlepcActionSlot(self): """ Slot to receive events from the auto idlepc action in the contextual menu. """ items = self.scene().selectedItems() if len(items) != 1: QtWidgets.QMessageBox.critical(self, "Auto Idle-PC", "Please select only one router") return item = items[0] if isinstance(item, NodeItem) and hasattr(item.node(), "idlepc") and item.node().initialized(): router = item.node() router.computeAutoIdlepc(self._autoIdlepcCallback) def _autoIdlepcCallback(self, result, error=False, context={}, **kwargs): """ Slot to allow the user to select an idlepc value. """ if error: QtWidgets.QMessageBox.critical(self, "Auto Idle-PC", "Error: {}".format(result["message"])) else: router = context["router"] idlepc = result["idlepc"] log.debug("{} has received the auto idle-pc value: {}".format(router.name(), idlepc)) router.setIdlepc(idlepc) # apply Idle-PC to all routers with the same IOS image ios_image = os.path.basename(router.settings()["image"]) for node in Topology.instance().nodes(): if hasattr(node, "idlepc") and node.settings()["image"] == ios_image: node.setIdlepc(idlepc) # apply the idle-pc to templates with the same IOS image router.module().updateImageIdlepc(ios_image, idlepc) QtWidgets.QMessageBox.information(self, "Auto Idle-PC", "Idle-PC value {} has been applied on {} and all templates with IOS image {}".format(idlepc, router.name(), ios_image)) def duplicateActionSlot(self): """ Slot to receive events from the duplicate action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, DrawingItem): if isinstance(item, EllipseItem): type = "ellipse" elif isinstance(item, TextItem): type = "text" elif isinstance(item, RectangleItem): type = "rect" else: type = "image" self.createDrawingItem( type, int(item.pos().x()) + 20, int(item.pos().y()) + 20, item.zValue(), rotation=item.rotation(), svg=item.toSvg() ) elif isinstance(item, NodeItem): item.node().duplicate(item.pos().x() + 20, item.pos().y() + 20, item.zValue()) def styleActionSlot(self): """ Slot to receive events from the style action in the contextual menu. """ items = [] for item in self.scene().selectedItems(): if isinstance(item, ShapeItem) or isinstance(item, LineItem): items.append(item) if items: style_dialog = StyleEditorDialog(self._main_window, items) style_dialog.show() style_dialog.exec() def textEditActionSlot(self): """ Slot to receive events from the text edit action in the contextual menu. """ items = [] for item in self.scene().selectedItems(): if isinstance(item, LabelItem) or isinstance(item, TextItem): items.append(item) if items: text_edit_dialog = TextEditorDialog(self._main_window, items) text_edit_dialog.show() text_edit_dialog.exec() def resetLabelPositionActionSlot(self): """ Slot to receive events from the reset label position action in the contextual menu. """ for item in self.scene().selectedItems(): if isinstance(item, LabelItem) and item.parentItem(): links = item.parentItem().links() for port in item.parentItem().node().ports(): # find the correct port associated with the label if port.label() == item: port.deleteLabel() break # adjust all node links to force to re-display the label for link in links: link.adjust() def horizontalAlignmentSlot(self): """ Slot to receive events from the horizontal align action in the contextual menu. """ horizontal_pos = None for item in self.scene().selectedItems(): if item.parentItem() is None: if horizontal_pos is None: horizontal_pos = item.y() + item.boundingRect().height() / 2 item.setX(item.x()) item.setY(horizontal_pos - item.boundingRect().height() / 2) item.updateNode() def verticalAlignmentSlot(self): """ Slot to receive events from the vertical align action in the contextual menu. """ vertical_position = None for item in self.scene().selectedItems(): if item.parentItem() is None: if vertical_position is None: vertical_position = item.x() + item.boundingRect().width() / 2 item.setX(vertical_position - item.boundingRect().width() / 2) item.setY(item.y()) item.updateNode() def raiseLayerActionSlot(self): """ Slot to receive events from the raise one layer action in the contextual menu. """ for item in self.scene().selectedItems(): if item.parentItem() is None: if not (item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable): log.error("Cannot move object to a upper layer because it is locked") continue item.setZValue(item.zValue() + 1) item.updateNode() item.update() def lowerLayerActionSlot(self): """ Slot to receive events from the lower one layer action in the contextual menu. """ for item in self.scene().selectedItems(): if item.parentItem() is None: if not (item.flags() & QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable): log.error("Cannot move object to a lower layer because it is locked") continue item.setZValue(item.zValue() - 1) item.updateNode() item.update() def lockActionSlot(self): """ Slot to receive events from the lock action in the contextual menu. """ for item in self.scene().selectedItems(): if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem): if item.locked() is True: item.setLocked(False) else: item.setLocked(True) if item.parentItem() is None: item.updateNode() item.update() def deleteActionSlot(self): """ Slot to receive events from the delete action in the contextual menu. """ selected_nodes = [] for item in self.scene().selectedItems(): if isinstance(item, NodeItem): node = item.node() if node.locked(): QtWidgets.QMessageBox.critical(self, "Delete", "Cannot delete node '{}' because it is locked".format(node.name())) return selected_nodes.append(node) if isinstance(item, DrawingItem) and item.locked(): QtWidgets.QMessageBox.critical(self, "Delete", "Cannot delete drawing because it is locked") return if selected_nodes: if len(selected_nodes) > 1: question = "Do you want to permanently delete these {} nodes?".format(len(selected_nodes)) else: question = "Do you want to permanently delete {}?".format(selected_nodes[0].name()) reply = QtWidgets.QMessageBox.question(self, "Delete", question, QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return for item in self.scene().selectedItems(): if isinstance(item, NodeItem): item.node().delete() self._topology.removeNode(item.node()) elif item.parentItem() is None: item.delete() self.scene().clearSelection() self.toggleUiDeviceMenu() def allocateCompute(self, node_data, module_instance): """ Allocates a server. :returns: allocated compute node """ from .main_window import MainWindow mainwindow = MainWindow.instance() if "compute_id" in node_data: try: return ComputeManager.instance().getCompute(node_data["compute_id"]) except KeyError: raise ModuleError("Compute {} doesn't exists".format(node_data["compute_id"])) server = server_select(mainwindow, node_data.get("node_type")) if server is None: raise ModuleError("Please select a server") return server def createNodeFromTemplateId(self, template_id, pos): """ Ask the server to create a node using this template """ pos = self.mapToScene(pos.toPoint()) return TemplateManager().instance().createNodeFromTemplateId(self._topology.project(), template_id, pos.x(), pos.y()) def createNodeItem(self, node, symbol, x, y): node.setSymbol(symbol) node.setPos(x, y) node_item = NodeItem(node) self.scene().addItem(node_item) self._topology.addNode(node) node.error_signal.connect(self._displayNodeErrorSlot) node.server_error_signal.connect(self._displayNodeErrorSlot) return node_item @qslot def _displayNodeErrorSlot(self, node_id, message, *args): """ Show error send by a node to the user """ node = Topology.instance().getNode(node_id) name = "Node" if node and node.name(): name = node.name() if self._main_window and not sip.isdeleted(self._main_window): QtWidgets.QMessageBox.critical(self._main_window, name, message.strip()) def createDrawingItem(self, type, x, y, z, locked=False, rotation=0, svg=None, drawing_id=None): if type == "ellipse": item = EllipseItem(pos=QtCore.QPointF(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg) elif type == "rect": item = RectangleItem(pos=QtCore.QPointF(x, y), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg) elif type == "line": item = LineItem(pos=QtCore.QPointF(x, y), dst=QtCore.QPoint(200, 0), z=z, locked=locked, rotation=rotation, project=self._topology.project(), drawing_id=drawing_id, svg=svg) elif type == "image": item = ImageItem(pos=QtCore.QPointF(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg) elif type == "text": item = TextItem(pos=QtCore.QPointF(x, y), z=z, rotation=rotation, locked=locked, project=self._topology.project(), drawing_id=drawing_id, svg=svg) if drawing_id is None: item.create() self.scene().addItem(item) self._topology.addDrawing(item) return item @QtCore.Property(QtGui.QColor) def drawingGridColor(self): """Returns the drawing grid color""" return self._drawing_grid_color @drawingGridColor.setter def drawingGridColor(self, color): """Sets the drawing grid color""" self._drawing_grid_color = color self.viewport().update() @QtCore.Property(QtGui.QColor) def nodeGridColor(self): """Returns the node grid color""" return self._node_grid_color @nodeGridColor.setter def nodeGridColor(self, color): """Sets the node grid color""" self._node_grid_color = color self.viewport().update() def resetGridColors(self): """Reset grid colors to defaults""" self._drawing_grid_color = self.DEFAULT_DRAWING_GRID_COLOR self._node_grid_color = self.DEFAULT_NODE_GRID_COLOR self.viewport().update() def drawBackground(self, painter, rect): super().drawBackground(painter, rect) if self._main_window.uiShowGridAction.isChecked(): grids = [(self.drawingGridSize(), self._drawing_grid_color), (self.nodeGridSize(), self._node_grid_color)] painter.save() for (grid, colour) in grids: if not grid: continue painter.setPen(QtGui.QPen(colour)) left = int(rect.left()) - (int(rect.left()) % grid) top = int(rect.top()) - (int(rect.top()) % grid) x = left while x < rect.right(): painter.drawLine(x, int(rect.top()), x, int(rect.bottom())) x += grid y = top while y < rect.bottom(): painter.drawLine(int(rect.left()), y, int(rect.right()), y) y += grid painter.restore() def toggleUiDeviceMenu(self): """ Hook which enables/disables uiDeviceMenu based on the current items selection""" items = self.scene().selectedItems() if len(items) > 0: self._main_window.uiDeviceMenu.setEnabled(True) else: self._main_window.uiDeviceMenu.setEnabled(False)