Files
gns3-gui/gns3/graphics_view.py

1621 lines
65 KiB
Python

# -*- 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 <http://www.gnu.org/licenses/>.
"""
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
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
"""
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._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.RubberBandDrag)
self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
# default directories for QFileDialog
self._import_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation)
self._export_config_directory = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.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
:param zoom:
:return:
"""
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()
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.CrossCursor)
else:
if self._newlink and self._newlink in self.scene().items():
self.scene().removeItem(self._newlink)
self._newlink = None
self.setCursor(QtCore.Qt.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.IBeamCursor)
else:
self._adding_note = False
self.setCursor(QtCore.Qt.ArrowCursor)
def addRectangle(self, state):
"""
Adds a rectangle.
:param state: boolean
"""
if state:
self._adding_rectangle = True
self.setCursor(QtCore.Qt.PointingHandCursor)
else:
self._adding_rectangle = False
self.setCursor(QtCore.Qt.ArrowCursor)
def addEllipse(self, state):
"""
Adds an ellipse.
:param state: boolean
"""
if state:
self._adding_ellipse = True
self.setCursor(QtCore.Qt.PointingHandCursor)
else:
self._adding_ellipse = False
self.setCursor(QtCore.Qt.ArrowCursor)
def addLine(self, state):
"""
Adds a line.
:param state: boolean
"""
if state:
self._adding_line = True
self.setCursor(QtCore.Qt.PointingHandCursor)
else:
self._adding_line = False
self.setCursor(QtCore.Qt.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):
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()
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.pos()), None, adding_flag=True)
else:
self._newlink = EthernetLinkItem(source_item, source_port, self.mapToScene(event.pos()), 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()
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.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 (event.buttons() == QtCore.Qt.LeftButton and event.modifiers() == QtCore.Qt.ShiftModifier) or event.buttons() == QtCore.Qt.MidButton:
# 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.globalPos())
self._dragging = True
self.setCursor(QtCore.Qt.ClosedHandCursor)
return
if is_not_link and item and event.modifiers() == QtCore.Qt.ControlModifier and event.button() == QtCore.Qt.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.RightButton 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.ControlModifier:
for it in self.scene().items():
it.setSelected(False)
item.setSelected(True)
self._showDeviceContextualMenu(QtGui.QCursor.pos())
else:
self._showDeviceContextualMenu(QtGui.QCursor.pos())
# 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(QtGui.QCursor.pos())
elif is_not_link and self._adding_link and event.button() == QtCore.Qt.RightButton:
# send a escape key to the main window to cancel the link addition
key = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_Escape, QtCore.Qt.NoModifier)
QtWidgets.QApplication.sendEvent(self._main_window, key)
elif item and isinstance(item, NodeItem) and self._adding_link and event.button() == QtCore.Qt.LeftButton:
self._userNodeLinking(event, item)
elif event.button() == QtCore.Qt.LeftButton and self._adding_note:
pos = self.mapToScene(event.pos())
note = self.createDrawingItem("text", pos.x(), 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.ArrowCursor)
self._adding_note = False
elif event.button() == QtCore.Qt.LeftButton and self._adding_rectangle:
pos = self.mapToScene(event.pos())
self.createDrawingItem("rect", pos.x(), pos.y(), 1)
self._main_window.uiDrawRectangleAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_rectangle = False
elif event.button() == QtCore.Qt.LeftButton and self._adding_ellipse:
pos = self.mapToScene(event.pos())
self.createDrawingItem("ellipse", pos.x(), pos.y(), 1)
self._main_window.uiDrawEllipseAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_ellipse = False
elif event.button() == QtCore.Qt.LeftButton and self._adding_line:
pos = self.mapToScene(event.pos())
self.createDrawingItem("line", pos.x(), pos.y(), 1)
self._main_window.uiDrawLineAction.setChecked(False)
self.setCursor(QtCore.Qt.ArrowCursor)
self._adding_line = False
else:
super().mousePressEvent(event)
self.toggleUiDeviceMenu()
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.LeftButton and event.modifiers() == QtCore.Qt.ShiftModifier) and not event.buttons() & QtCore.Qt.MidButton:
self._dragging = False
self.setCursor(QtCore.Qt.ArrowCursor)
else:
item = self.itemAt(event.pos())
if item is not None and not event.modifiers() & QtCore.Qt.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.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() / 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)), 2000)
def keyPressEvent(self, event):
"""
Handles all key press events for this view.
:param event: QKeyEvent
"""
if event.key() == QtCore.Qt.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.globalPos())
hBar = self.horizontalScrollBar()
vBar = self.verticalScrollBar()
delta = mapped_global_pos - self._last_mouse_position
hBar.setValue(hBar.value() + (delta.x() if QtWidgets.QApplication.isRightToLeft() else -delta.x()))
vBar.setValue(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.pos()))
event.ignore()
else:
item = self.itemAt(event.pos())
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, 2000)
# 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.pos())
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().isAlwaysOn():
self.configureSlot()
return
else:
if sys.platform.startswith("win") and 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
"""
# 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.CopyAction)
event.accept()
if event.keyboardModifiers() == QtCore.Qt.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.pos().x() - (150 / 2) + (node_number % max_nodes_per_line) * offset
y = event.pos().y() - (70 / 2) + (node_number // max_nodes_per_line) * offset
if self.createNodeFromTemplateId(template_id, QtCore.QPoint(x, y)) is False:
event.ignore()
break
else:
if self.createNodeFromTemplateId(template_id, event.pos()) 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()
self.populateDeviceContextualMenu(menu)
menu.exec_(pos)
menu.clear()
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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 sys.platform.startswith("win") and True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "bringToFront"), items)):
# Action: bring console or window to front (Windows only)
bring_to_front_action = QtWidgets.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 hasattr(item.node(), "configFiles"), items)):
import_config_action = QtWidgets.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 hasattr(item.node(), "configFiles"), items)):
export_config_action = QtWidgets.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 hasattr(item.node(), "configFiles"), items)):
export_config_action = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.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 = QtWidgets.QAction("Lock or unlock items", menu)
lock_action.setIcon(get_icon("lock.svg"))
else:
item = items[0]
if item.flags() & QtWidgets.QGraphicsItem.ItemIsMovable:
lock_action = QtWidgets.QAction("Lock item", menu)
lock_action.setIcon(get_icon("lock.svg"))
else:
lock_action = QtWidgets.QAction("Unlock item", menu)
lock_action.setIcon(get_icon("unlock.svg"))
lock_action.triggered.connect(self.lockActionSlot)
menu.addAction(lock_action)
delete_action = QtWidgets.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.Normal, item.node().name())
if ok:
if hasattr(item.node(), "validateHostname"):
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.Yes | QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.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()):
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, except builtin devices.
"""
items = [item for item in self.scene().items()
if not (isinstance(item, NodeItem) and isinstance(item.node().module(), Builtin))]
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
"""
current_cmd = None
console_type = "telnet"
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized() and item.node().status() == Node.started:
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:
for item in self.scene().selectedItems():
if isinstance(item, NodeItem) and item.node().console() is not None and item.node().initialized() and item.node().status() == Node.started:
node = item.node()
if node.consoleType() not in ("telnet", "serial", "vnc", "spice", "spice+agent"):
continue
try:
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()):
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 hasattr(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_dir,
"All files (*.*);;Config files (*.cfg)",
"Config files (*.cfg)")
if not path:
continue
self._import_config_dir = 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 hasattr(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, "Edit file", "File to edit?", item.node().configFiles(), 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 hasattr(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, item.pos().x() + 20, 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:
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:
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):
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):
selected_nodes.append(item.node())
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.Yes, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.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)
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.QPoint(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.QPoint(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.QPoint(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.QPoint(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.QPoint(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
def drawBackground(self, painter, rect):
super().drawBackground(painter, rect)
if self._main_window.uiShowGridAction.isChecked():
grids = [(self.drawingGridSize(),QtGui.QColor(208, 208, 208)),
(self.nodeGridSize(),QtGui.QColor(190, 190, 190))]
painter.save()
for (grid,colour) in grids:
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, rect.top(), x, rect.bottom())
x += grid
y = top
while y < rect.bottom():
painter.drawLine(rect.left(), y, 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)