# -*- 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 . """ Main window for the GUI. """ import sys import os import time import logging from .local_config import LocalConfig from .local_server import LocalServer from .modules import MODULES from .qt import QtGui, QtCore, QtWidgets, qslot from .controller import Controller from .node import Node from .ui.main_window_ui import Ui_MainWindow from .style import Style from .dialogs.about_dialog import AboutDialog from .dialogs.project_dialog import ProjectDialog from .dialogs.preferences_dialog import PreferencesDialog from .dialogs.snapshots_dialog import SnapshotsDialog from .dialogs.export_debug_dialog import ExportDebugDialog from .dialogs.doctor_dialog import DoctorDialog from .dialogs.edit_project_dialog import EditProjectDialog from .dialogs.setup_wizard import SetupWizard from .settings import GENERAL_SETTINGS from .items.node_item import NodeItem from .items.link_item import LinkItem, SvgIconItem from .items.shape_item import ShapeItem from .items.label_item import LabelItem from .topology import Topology from .http_client import HTTPClient from .progress import Progress from .update_manager import UpdateManager from .dialogs.appliance_wizard import ApplianceWizard from .dialogs.new_template_wizard import NewTemplateWizard from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler from .status_bar import StatusBarHandler from .registry.appliance import ApplianceError from .template_manager import TemplateManager from .appliance_manager import ApplianceManager log = logging.getLogger(__name__) class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): """ Main window implementation. :param parent: parent widget """ # signal to tell the view if the user is adding a link or not adding_link_signal = QtCore.Signal(bool) # Signal of settings updates settings_updated_signal = QtCore.Signal() def __init__(self, parent=None, open_file=None): """ :param open_file: Open this file instead of asking for a new project """ super().__init__(parent) self._settings = {} self.setupUi(self) self.setUnifiedTitleAndToolBarOnMac(True) # These widgets will be disabled when no project is loaded self.disableWhenNoProjectWidgets = [ self.uiGraphicsView, self.uiAnnotateMenu, self.uiAnnotationToolBar, self.uiControlToolBar, self.uiControlMenu, self.uiSaveProjectAsAction, self.uiExportProjectAction, self.uiScreenshotAction, self.uiSnapshotAction, self.uiEditProjectAction, self.uiDeleteProjectAction, self.uiImportExportConfigsAction, self.uiLockAllAction ] self._notif_dialog = NotifDialog(self) # Setup logger logging.getLogger().addHandler(NotifDialogHandler(self._notif_dialog)) logging.getLogger().addHandler(StatusBarHandler(self.uiStatusBar)) self._open_file_at_startup = open_file MainWindow._instance = self topology = Topology.instance() topology.setMainWindow(self) topology.project_changed_signal.connect(self._projectChangedSlot) Controller.instance().setParent(self) LocalServer.instance().setParent(self) HTTPClient.setProgressCallback(Progress.instance(self)) self._first_file_load = True self._open_project_path = None self._loadSettings() self._connections() self._maxrecent_files = 5 self._project_dialog = None self.recent_file_actions = [] self.recent_project_actions = [] self._start_time = time.time() local_config = LocalConfig.instance() #local_config.config_changed_signal.connect(self._localConfigChangedSlot) self._local_config_timer = QtCore.QTimer(self) self._local_config_timer.timeout.connect(local_config.checkConfigChanged) self._local_config_timer.start(1000) # milliseconds self._template_manager = TemplateManager().instance() self._appliance_manager = ApplianceManager().instance() # restore the geometry and state of the main window. self._save_gui_state_geometry = True self.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["geometry"].encode())) self.restoreState(QtCore.QByteArray().fromBase64(self._settings["state"].encode())) # populate the view -> docks menu self.uiDocksMenu.addAction(self.uiTopologySummaryDockWidget.toggleViewAction()) self.uiDocksMenu.addAction(self.uiComputeSummaryDockWidget.toggleViewAction()) self.uiDocksMenu.addAction(self.uiConsoleDockWidget.toggleViewAction()) action = self.uiNodesDockWidget.toggleViewAction() action.setIconText("All devices") self.uiDocksMenu.addAction(action) # Sometimes the parent seem invalid https://github.com/GNS3/gns3-gui/issues/2182 self.uiNodesDockWidget.setParent(self) # make sure the dock widget is not open self.uiNodesDockWidget.setVisible(False) # default directories for QFileDialog self._import_configs_from_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation) self._export_configs_to_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation) self._screenshots_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.PicturesLocation) self._pictures_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.PicturesLocation) self._appliance_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DownloadLocation) self._portable_project_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DownloadLocation) self._project_dir = None # add recent file actions to the File menu for i in range(0, self._maxrecent_files): action = QtGui.QAction(self.uiFileMenu) action.setVisible(False) action.triggered.connect(self.openRecentFileSlot) self.recent_file_actions.append(action) self.uiFileMenu.insertActions(self.uiQuitAction, self.recent_file_actions) self.recent_file_actions_separator = self.uiFileMenu.insertSeparator(self.uiQuitAction) self.recent_file_actions_separator.setVisible(False) self.updateRecentFileActions() # add recent projects to the File menu for i in range(0, self._maxrecent_files): action = QtGui.QAction(self.uiFileMenu) action.setVisible(False) action.triggered.connect(self.openRecentProjectSlot) self.recent_project_actions.append(action) self.recent_project_actions_separator = self.uiFileMenu.addSeparator() self.recent_project_actions_separator.setVisible(False) self.uiFileMenu.addActions(self.recent_project_actions) # set the window icon self.setWindowIcon(QtGui.QIcon(":/images/gns3.ico")) # restore the style self._setStyle(self._settings.get("style")) if self._settings.get("hide_new_template_button"): self.uiNewTemplatePushButton.hide() self.setWindowTitle("[*] GNS3") # load initial stuff once the event loop isn't busy self.run_later(0, self.startupLoading) def _connections(self): """ Connect widgets to slots """ # file menu connections self.uiNewProjectAction.triggered.connect(self._newProjectActionSlot) self.uiOpenProjectAction.triggered.connect(self.openProjectActionSlot) self.uiOpenApplianceAction.triggered.connect(self.openApplianceActionSlot) self.uiNewTemplateAction.triggered.connect(self._newTemplateActionSlot) self.uiSaveProjectAsAction.triggered.connect(self._saveProjectAsActionSlot) self.uiExportProjectAction.triggered.connect(self._exportProjectActionSlot) self.uiImportProjectAction.triggered.connect(self._importProjectActionSlot) self.uiImportExportConfigsAction.triggered.connect(self._importExportConfigsActionSlot) self.uiScreenshotAction.triggered.connect(self._screenshotActionSlot) self.uiSnapshotAction.triggered.connect(self._snapshotActionSlot) self.uiEditProjectAction.triggered.connect(self._editProjectActionSlot) self.uiDeleteProjectAction.triggered.connect(self._deleteProjectActionSlot) # edit menu connections self.uiSelectAllAction.triggered.connect(self._selectAllActionSlot) self.uiSelectNoneAction.triggered.connect(self._selectNoneActionSlot) self.uiPreferencesAction.triggered.connect(self.preferencesActionSlot) # view menu connections self.uiActionFullscreen.triggered.connect(self._fullScreenActionSlot) self.uiZoomInAction.triggered.connect(self._zoomInActionSlot) self.uiZoomOutAction.triggered.connect(self._zoomOutActionSlot) self.uiZoomResetAction.triggered.connect(self._zoomResetActionSlot) self.uiFitInViewAction.triggered.connect(self._fitInViewActionSlot) self.uiShowLayersAction.triggered.connect(self._showLayersActionSlot) self.uiResetPortLabelsAction.triggered.connect(self._resetPortLabelsActionSlot) self.uiShowPortNamesAction.triggered.connect(self._showPortNamesActionSlot) self.uiShowGridAction.triggered.connect(self._showGridActionSlot) self.uiSnapToGridAction.triggered.connect(self._snapToGridActionSlot) self.uiLockAllAction.triggered.connect(self._lockActionSlot) self.uiResetGUIStateAction.triggered.connect(self._resetGUIState) self.uiResetDocksAction.triggered.connect(self._resetDocksSlot) # tool menu connections self.uiWebUIAction.triggered.connect(self._openWebInterfaceActionSlot) # control menu connections self.uiStartAllAction.triggered.connect(self._startAllActionSlot) self.uiSuspendAllAction.triggered.connect(self._suspendAllActionSlot) self.uiStopAllAction.triggered.connect(self._stopAllActionSlot) self.uiReloadAllAction.triggered.connect(self._reloadAllActionSlot) self.uiAuxConsoleAllAction.triggered.connect(self._auxConsoleAllActionSlot) self.uiConsoleAllAction.triggered.connect(self._consoleAllActionSlot) self.uiResetConsoleAllAction.triggered.connect(self._consoleResetAllActionSlot) # device menu is contextual and is build on-the-fly self.uiDeviceMenu.aboutToShow.connect(self._deviceMenuActionSlot) # annotate menu connections self.uiAddNoteAction.triggered.connect(self._addNoteActionSlot) self.uiInsertImageAction.triggered.connect(self._insertImageActionSlot) self.uiDrawRectangleAction.triggered.connect(self._drawRectangleActionSlot) self.uiDrawEllipseAction.triggered.connect(self._drawEllipseActionSlot) self.uiDrawLineAction.triggered.connect(self._drawLineActionSlot) self.uiEditReadmeAction.triggered.connect(self._editReadmeActionSlot) # help menu connections self.uiOnlineHelpAction.triggered.connect(self._onlineHelpActionSlot) self.uiCheckForUpdateAction.triggered.connect(self._checkForUpdateActionSlot) self.uiSetupWizard.triggered.connect(self._setupWizardActionSlot) self.uiAboutQtAction.triggered.connect(self._aboutQtActionSlot) self.uiAboutAction.triggered.connect(self._aboutActionSlot) self.uiExportDebugInformationAction.triggered.connect(self._exportDebugInformationSlot) self.uiDoctorAction.triggered.connect(self._doctorSlot) self.uiAcademyAction.triggered.connect(self._academyActionSlot) self.uiShortcutsAction.triggered.connect(self._shortcutsActionSlot) # browsers tool bar connections self.uiBrowseRoutersAction.triggered.connect(self._browseRoutersActionSlot) self.uiBrowseSwitchesAction.triggered.connect(self._browseSwitchesActionSlot) self.uiBrowseEndDevicesAction.triggered.connect(self._browseEndDevicesActionSlot) self.uiBrowseSecurityDevicesAction.triggered.connect(self._browseSecurityDevicesActionSlot) self.uiBrowseAllDevicesAction.triggered.connect(self._browseAllDevicesActionSlot) self.uiAddLinkAction.triggered.connect(self._addLinkActionSlot) # new template button self.uiNewTemplatePushButton.clicked.connect(self._newTemplateActionSlot) # connect the signal to the view self.adding_link_signal.connect(self.uiGraphicsView.addingLinkSlot) # connect to the signal when settings change self.settings_updated_signal.connect(self.settingsChangedSlot) def _loadSettings(self): """ Loads the settings from the persistent settings file. """ local_config = LocalConfig.instance() self._settings = local_config.loadSectionSettings(self.__class__.__name__, GENERAL_SETTINGS) def settings(self): """ Returns the general settings. :returns: settings dictionary """ return self._settings def setSettings(self, new_settings): """ Set new general settings. :param new_settings: settings dictionary """ # change the GUI style style = new_settings.get("style") if style and new_settings["style"] != self._settings["style"]: self._setStyle(style) self._settings.update(new_settings) # save the settings LocalConfig.instance().saveSectionSettings(self.__class__.__name__, self._settings) self.settings_updated_signal.emit() def _openWebInterfaceActionSlot(self): if Controller.instance().connected(): base_url = Controller.instance().httpClient().fullUrl() webui_url = "{}/static/web-ui/bundled".format(base_url) QtGui.QDesktopServices.openUrl(QtCore.QUrl(webui_url)) def _showGridActionSlot(self): """ Called when we ask to display the grid """ self.showGrid(self.uiShowGridAction.isChecked()) # save settings project = Topology.instance().project() if project is not None: project.setShowGrid(self.uiShowGridAction.isChecked()) project.update() def _snapToGridActionSlot(self): """ Called when user click on the snap to grid menu item :return: None """ self.snapToGrid(self.uiSnapToGridAction.isChecked()) # save settings project = Topology.instance().project() if project is not None: project.setSnapToGrid(self.uiSnapToGridAction.isChecked()) project.update() def _lockActionSlot(self): """ Called when user click on the lock menu item :return: None """ if self.uiGraphicsView.isEnabled(): for item in self.uiGraphicsView.items(): if not isinstance(item, LinkItem) and not isinstance(item, LabelItem) and not isinstance(item, SvgIconItem): if self.uiLockAllAction.isChecked() and not item.locked(): item.setLocked(True) elif not self.uiLockAllAction.isChecked() and item.locked(): item.setLocked(False) if item.parentItem() is None: item.updateNode() item.update() def _resetGUIState(self): """ Reset the GUI state. """ self._save_gui_state_geometry = False self.close() if hasattr(sys, "frozen"): QtCore.QProcess.startDetached(os.path.abspath(sys.executable), sys.argv) else: QtWidgets.QMessageBox.information(self, "GUI state","The GUI state has been reset, please restart the application") def _resetDocksSlot(self): """ Reset the dock widgets. """ self.uiTopologySummaryDockWidget.setFloating(False) self.uiComputeSummaryDockWidget.setFloating(False) self.uiConsoleDockWidget.setFloating(False) self.uiNodesDockWidget.setFloating(False) def _newProjectActionSlot(self): """ Slot called to create a new project. """ # prevents race condition if self._project_dialog is not None: return self._project_dialog = ProjectDialog(self) self._project_dialog.show() create_new_project = self._project_dialog.exec() if create_new_project: Topology.instance().createLoadProject(self._project_dialog.getProjectSettings()) self._project_dialog = None def _newTemplateActionSlot(self): """ Called when user want to create a new template. """ dialog = NewTemplateWizard(self) dialog.show() dialog.exec() @qslot def openApplianceActionSlot(self, *args): """ Slot called to open an appliance. """ directory = self._appliance_dir if not os.path.exists(self._appliance_dir): directory = Topology.instance().projectsDirPath() path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import appliance", directory, "All files (*);;GNS3 Appliance (*.gns3appliance *.gns3a)", "GNS3 Appliance (*.gns3appliance *.gns3a)") if path: self.loadPath(path) self._appliance_dir = os.path.dirname(path) def openProjectActionSlot(self): """ Slot called to open a project. """ if Controller.instance().isRemote(): # If the server is remote we use the new project windows with the project library self._newProjectActionSlot() else: directory = self._project_dir if self._project_dir is None or not os.path.exists(self._project_dir): directory = Topology.instance().projectsDirPath() path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open project", directory, "All files (*);;GNS3 Project (*.gns3);;GNS3 Portable Project (*.gns3project *.gns3p);;NET files (*.net)", "GNS3 Project (*.gns3)") if path: self.loadPath(path) self._project_dir = os.path.dirname(path) def openRecentFileSlot(self): """ Slot called to open recent file from the File menu. """ action = self.sender() if action: path = action.data() if not os.path.isfile(path): QtWidgets.QMessageBox.critical(self, "Recent file", "{}: no such file".format(path)) return self.loadPath(path) def openRecentProjectSlot(self): """ Slot called to open recent project from the project menu. """ action = self.sender() if action and action.data(): if len(action.data()) == 2: project_id, project_path = action.data() Topology.instance().createLoadProject({ "project_path": project_path, "project_id": project_id}) else: (project_id, ) = action.data() Topology.instance().createLoadProject({"project_id": project_id}) def loadPath(self, path): """Open a file and close the previous project""" if not path: return if self._first_file_load is True: self._first_file_load = False time.sleep(0.5) # give some time to the server to initialize if self._project_dialog: self._project_dialog.reject() self._project_dialog = None if path.endswith(".gns3project") or path.endswith(".gns3p"): # Portable GNS3 project Topology.instance().importProject(path) elif path.endswith(".net"): QtWidgets.QMessageBox.critical(self, "Open project", "Importing legacy project is not supported in 2.0.\nYou must open it using GNS3 1.x in order to convert it or manually run the gns3 converter.") return elif path.endswith(".gns3appliance") or path.endswith(".gns3a"): # GNS3 appliance try: self._appliance_wizard = ApplianceWizard(self, path) except ApplianceError as e: QtWidgets.QMessageBox.critical(self, "Appliance", "Error while importing appliance {}: {}".format(path, str(e))) return self._appliance_wizard.show() self._appliance_wizard.exec() elif path.endswith(".gns3"): if Controller.instance().isRemote(): QtWidgets.QMessageBox.critical(self, "Open project", "Cannot open a .gns3 file on a remote server, please use a portable project (.gns3p) instead") return else: Topology.instance().loadProject(path) else: try: extension = path.split('.')[1] QtWidgets.QMessageBox.critical(self, "File open", "Unsupported file extension {} for {}".format(extension, path)) except IndexError: QtWidgets.QMessageBox.critical(self, "File open", "Missing file extension for {}".format(path)) @qslot def _projectChangedSlot(self, *args): """ Called when a project finish to load """ project = Topology.instance().project() if project is not None and self._project_dialog: self._project_dialog.reject() self._project_dialog = None self._refreshVisibleWidgets() @qslot def settingsChangedSlot(self, *args): """ Called when settings are updated """ # It covers case when project is not set # and we need to refresh template manager # and appliance manager project = Topology.instance().project() if project is None: self._template_manager.instance().refresh() self._appliance_manager.instance().refresh() def _refreshVisibleWidgets(self): """ Refresh widgets that should be visible or not """ # No projects if Topology.instance().project() is None: for widget in self.disableWhenNoProjectWidgets: widget.setEnabled(False) else: for widget in self.disableWhenNoProjectWidgets: widget.setEnabled(True) def _saveProjectAsActionSlot(self): """ Slot called to save a project to another location/name. """ Topology.instance().saveProjectAs() def _importExportConfigsActionSlot(self): """ Slot called when importing and exporting configs for the entire project. """ options = ["Export configs to a directory", "Import configs from a directory"] selection, ok = QtWidgets.QInputDialog.getItem(self, "Import/Export configs", "Please choose an option:", options, 0, False) if ok: if selection == options[0]: self._exportConfigs() else: self._importConfigs() def _exportConfigs(self): """ Exports all configs to a directory. """ path = QtWidgets.QFileDialog.getExistingDirectory(self, "Export directory", self._export_configs_to_dir, QtWidgets.QFileDialog.Option.ShowDirsOnly) if path: self._export_configs_to_dir = os.path.dirname(path) for module in MODULES: instance = module.instance() if hasattr(instance, "exportConfigs"): instance.exportConfigs(path) def _importConfigs(self): """ Imports all configs from a directory. """ path = QtWidgets.QFileDialog.getExistingDirectory(self, "Import directory", self._import_configs_from_dir, QtWidgets.QFileDialog.Option.ShowDirsOnly) if path: self._import_configs_from_dir = os.path.dirname(path) for module in MODULES: instance = module.instance() if hasattr(instance, "importConfigs"): instance.importConfigs(path) def createScreenshot(self, path): """ Create a screenshot of the scene. :returns: True if the image was successfully saved; otherwise returns False """ scene = self.uiGraphicsView.scene() scene.clearSelection() source = scene.itemsBoundingRect().adjusted(-20.0, -20.0, 20.0, 20.0) image = QtGui.QImage(source.size().toSize(), QtGui.QImage.Format.Format_RGB32) image.fill(QtCore.Qt.GlobalColor.white) painter = QtGui.QPainter(image) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True) painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) scene.render(painter, source=source) painter.end() # TODO: quality option return image.save(path) def showLayers(self, show_layers): """ Shows layers in GUI :param show_layers: boolean :return: None """ NodeItem.show_layer = show_layers ShapeItem.show_layer = show_layers for item in self.uiGraphicsView.items(): item.update() def showGrid(self, show_grid): """ Shows grid in GUI :param show_grid: boolean :return: None """ self.uiGraphicsView.viewport().update() def snapToGrid(self, snap_to_grid): """ Snap to grid in GUI :param snap_to_grid: boolean :return: None """ self.uiGraphicsView.viewport().update() def showInterfaceLabels(self, show_interface_labels): """ Show interface labels in GUI :param show_interface_labels: boolean :return: None """ LinkItem.showPortLabels(show_interface_labels) for item in self.uiGraphicsView.scene().items(): if isinstance(item, LinkItem): item.adjust() def _updateZoomSettings(self, zoom=None): """ Updates zoom settings :param zoom integer optional, when not provided then calculated from current view :return: None """ if zoom is None: zoom = round(self.uiGraphicsView.transform().m11() * 100) # save settings project = Topology.instance().project() if project is not None: project.setZoom(zoom) project.update() def _screenshotActionSlot(self): """ Slot called to take a screenshot of the scene. """ # supported image file formats file_formats = "PNG File (*.png);;JPG File (*.jpeg *.jpg);;BMP File (*.bmp);;XPM File (*.xpm *.xbm);;PPM File (*.ppm);;TIFF File (*.tiff)" path, selected_filter = QtWidgets.QFileDialog.getSaveFileName(self, "Screenshot", self._screenshots_dir, file_formats) if not path: return self._screenshots_dir = os.path.dirname(path) # add the extension if missing (Mac OS automatically adds an extension already) if not sys.platform.startswith("darwin"): file_format = "." + selected_filter[:4].lower().strip() if not path.endswith(file_format): path += file_format if not self.createScreenshot(path): QtWidgets.QMessageBox.critical(self, "Screenshot", "Could not create screenshot file {}".format(path)) def _snapshotActionSlot(self): """ Slot called to open the snapshot dialog. """ project = Topology.instance().project() dialog = SnapshotsDialog(self, project) dialog.show() dialog.exec() def _selectAllActionSlot(self): """ Slot called to select all the items on the scene. """ scene = self.uiGraphicsView.scene() for item in scene.items(): item.setSelected(True) def _selectNoneActionSlot(self): """ Slot called to none of the items on the scene. """ scene = self.uiGraphicsView.scene() for item in scene.items(): item.setSelected(False) def _fullScreenActionSlot(self): """ Slot to switch to full screen. """ if not self.windowState() & QtCore.Qt.WindowState.WindowFullScreen: # switch to full screen self.setWindowState(self.windowState() | QtCore.Qt.WindowState.WindowFullScreen) else: # switch back to normal self.setWindowState(self.windowState() & ~QtCore.Qt.WindowState.WindowFullScreen) def _zoomInActionSlot(self): """ Slot called to scale in the view. """ factor_in = pow(2.0, 60 / 240.0) self.uiGraphicsView.scaleView(factor_in) self._updateZoomSettings() def _zoomOutActionSlot(self): """ Slot called to scale out the view. """ factor_out = pow(2.0, -60 / 240.0) self.uiGraphicsView.scaleView(factor_out) self._updateZoomSettings() def _zoomResetActionSlot(self): """ Slot called to reset the zoom. """ self.uiGraphicsView.resetTransform() self._updateZoomSettings() def _fitInViewActionSlot(self): """ Slot called to fit the topology in the view. """ view = self.uiGraphicsView bounding_rect = view.scene().itemsBoundingRect().adjusted(-20.0, -20.0, 20.0, 20.0) view.ensureVisible(bounding_rect) view.fitInView(bounding_rect, QtCore.Qt.AspectRatioMode.KeepAspectRatio) def _showLayersActionSlot(self): """ Slot called to show the layer positions on the scene. """ self.showLayers(self.uiShowLayersAction.isChecked()) # save settings project = Topology.instance().project() if project is not None: project.setShowLayers(self.uiShowLayersAction.isChecked()) project.update() def _resetPortLabelsActionSlot(self): """ Slot called to reset the port labels on the scene. """ for item in self.uiGraphicsView.scene().items(): if isinstance(item, LinkItem): item.resetPortLabels() item.adjust() def _showPortNamesActionSlot(self): """ Slot called to show the port names on the scene. """ self.showInterfaceLabels(self.uiShowPortNamesAction.isChecked()) # save settings project = Topology.instance().project() if project is not None: project.setShowInterfaceLabels(self.uiShowPortNamesAction.isChecked()) project.update() def _startAllActionSlot(self): """ Slot called when starting all the nodes. """ reply = QtWidgets.QMessageBox.question(self, "Confirm Start All", "Are you sure you want to start all devices?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return project = Topology.instance().project() if project is not None: project.start_all_nodes() def _suspendAllActionSlot(self): """ Slot called when suspending all the nodes. """ reply = QtWidgets.QMessageBox.question(self, "Confirm Suspend All", "Are you sure you want to suspend all devices?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return project = Topology.instance().project() if project is not None: project.suspend_all_nodes() def _stopAllActionSlot(self): """ Slot called when stopping all the nodes. """ reply = QtWidgets.QMessageBox.question(self, "Confirm Stop All", "Are you sure you want to stop all devices?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return project = Topology.instance().project() if project is not None: project.stop_all_nodes() def _reloadAllActionSlot(self): """ Slot called when reloading all the nodes. """ reply = QtWidgets.QMessageBox.question(self, "Confirm Reload All", "Are you sure you want to reload all devices?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return project = Topology.instance().project() if project is not None: project.reload_all_nodes() def _consoleResetAllActionSlot(self): """ Slot called when reset all console connections. """ project = Topology.instance().project() if project is not None: project.reset_console_all_nodes() def _deviceMenuActionSlot(self): """ Slot to contextually show the device menu. """ self.uiDeviceMenu.clear() self.uiGraphicsView.populateDeviceContextualMenu(self.uiDeviceMenu) def _auxConsoleAllActionSlot(self): """ Slot called when connecting to all the nodes using the AUX console. """ self.uiGraphicsView.auxConsoleFromItems(self.uiGraphicsView.scene().items()) def _consoleAllActionSlot(self): """ Slot called when connecting to all the nodes using the console. """ self.uiGraphicsView.consoleFromAllItems() def _addNoteActionSlot(self): """ Slot called when adding a new note on the scene. """ self.uiGraphicsView.addNote(self.uiAddNoteAction.isChecked()) def _insertImageActionSlot(self): """ Slot called when inserting an image on the scene. """ # supported image file formats file_formats = "Image files (*.svg *.bmp *.jpeg *.jpg *.gif *.pbm *.pgm *.png *.ppm *.xbm *.xpm);;All files (*)" path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Image", self._pictures_dir, file_formats) if not path: return self._pictures_dir = os.path.dirname(path) QtGui.QPixmap(path) self.uiGraphicsView.addImage(path) def _drawRectangleActionSlot(self): """ Slot called when adding a rectangle on the scene. """ self.uiGraphicsView.addRectangle(self.uiDrawRectangleAction.isChecked()) def _drawEllipseActionSlot(self): """ Slot called when adding a ellipse on the scene. """ self.uiGraphicsView.addEllipse(self.uiDrawEllipseAction.isChecked()) def _drawLineActionSlot(self): """ Slot called when adding a line on the scene. """ self.uiGraphicsView.addLine(self.uiDrawLineAction.isChecked()) def _onlineHelpActionSlot(self): """ Slot to launch a browser pointing to the documentation page. """ QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://docs.gns3.com/")) def _checkForUpdateActionSlot(self, silent=False): """ Slot to check if a newer version is available. :param silent: do not display any message """ self._update_manager = UpdateManager() self._update_manager.checkForUpdate(self, silent) def _setupWizardActionSlot(self): """ Slot to open the setup wizard. """ with Progress.instance().context(min_duration=0): setup_wizard = SetupWizard(self) setup_wizard.show() res = setup_wizard.exec() # start and connect to the local server if needed LocalServer.instance().localServerAutoStartIfRequired() def _shortcutsActionSlot(self): shortcuts_text = "" for action in self.findChildren(QtGui.QAction): shortcut = action.shortcut().toString() if shortcut: shortcuts_text += f"{action.toolTip()}: {shortcut}\n" QtWidgets.QMessageBox.information(self, "Shortcuts", shortcuts_text) def _aboutQtActionSlot(self): """ Slot to display the Qt About dialog. """ QtWidgets.QMessageBox.aboutQt(self) def _aboutActionSlot(self): """ Slot to display the GNS3 About dialog. """ dialog = AboutDialog(self) dialog.show() dialog.exec() def _exportDebugInformationSlot(self): """ Slot to display a window for exporting debug information """ dialog = ExportDebugDialog(self, Topology.instance().project()) dialog.show() dialog.exec() def _doctorSlot(self): """ Slot to display a window for exporting debug information """ dialog = DoctorDialog(self) dialog.show() dialog.exec() def _academyActionSlot(self): """ Slot to launch a browser pointing to the courses page. """ QtGui.QDesktopServices.openUrl(QtCore.QUrl("http://academy.gns3.com/")) def _showNodesDockWidget(self, title, category): """ Makes the NodesDockWidget appear with the appropriate title and the devices from the specified category listed. Makes the dock disappear if the same category is selected. :param title: NodesDockWidget title :param category: category of device to list """ if self.uiNodesDockWidget.windowTitle() == title: self.uiNodesDockWidget.setVisible(False) self.uiNodesDockWidget.setWindowTitle("") else: self.uiNodesDockWidget.setWindowTitle(title) self.uiNodesDockWidget.setVisible(True) self.uiNodesDockWidget.populateNodesView(category) def _localConfigChangedSlot(self): """ Called when the local config change """ self.uiNodesView.refresh() def _browseRoutersActionSlot(self): """ Slot to browse all the routers. """ self._showNodesDockWidget("Routers", Node.routers) def _browseSwitchesActionSlot(self): """ Slot to browse all the switches. """ self._showNodesDockWidget("Switches", Node.switches) def _browseEndDevicesActionSlot(self): """ Slot to browse all the end devices. """ self._showNodesDockWidget("End devices", Node.end_devices) def _browseSecurityDevicesActionSlot(self): """ Slot to browse all the security devices. """ self._showNodesDockWidget("Security devices", Node.security_devices) def _browseAllDevicesActionSlot(self): """ Slot to browse all the devices. """ self._showNodesDockWidget("All devices", None) def _addLinkActionSlot(self): """ Slot to receive events from the add a link action. """ if not self.uiAddLinkAction.isChecked(): self.uiAddLinkAction.setText("Add a link") self.adding_link_signal.emit(False) else: self.uiAddLinkAction.setText("Cancel") self.adding_link_signal.emit(True) def preferencesActionSlot(self): """ Slot to show the preferences dialog. """ with Progress.instance().context(min_duration=0): dialog = PreferencesDialog(self) #dialog.restoreGeometry(QtCore.QByteArray().fromBase64(self._settings["preferences_dialog_geometry"].encode())) dialog.show() dialog.exec() #self._settings["preferences_dialog_geometry"] = bytes(dialog.saveGeometry().toBase64()).decode() #self.setSettings(self._settings) def _editReadmeActionSlot(self): """ Slot to edit the README file """ Topology.instance().editReadme() def resizeEvent(self, event): self._notif_dialog.resize() super().resizeEvent(event) def keyPressEvent(self, event): """ Handles all key press events for the main window. :param event: QKeyEvent """ key = event.key() # if the user is adding a link and press Escape, then cancel the link addition. if self.uiAddLinkAction.isChecked() and key == QtCore.Qt.Key.Key_Escape: self.uiAddLinkAction.setChecked(False) self._addLinkActionSlot() elif key == QtCore.Qt.Key.Key_C and (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier): status_bar_message = self.uiStatusBar.currentMessage() if status_bar_message: QtWidgets.QApplication.clipboard().setText(status_bar_message) else: super().keyPressEvent(event) def closeEvent(self, event): """ Handles the event when the main window is closed. :param event: QCloseEvent """ if Topology.instance().project(): reply = QtWidgets.QMessageBox.question(self, "Confirm Exit", "Are you sure you want to exit GNS3?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: event.ignore() return progress = Progress.instance() progress.setAllowCancelQuery(True) progress.setCancelButtonText("Force quit") log.debug("Close the Main Window") self._finish_application_closing(close_windows=False) event.accept() self.uiConsoleTextEdit.closeIO() def _finish_application_closing(self, close_windows=True): """ Handles the event when the main window is closed. And project closed. :params closing: True the windows is currently closing do not try to reclose it """ log.debug("_finish_application_closing") if self._save_gui_state_geometry: self._settings["geometry"] = bytes(self.saveGeometry().toBase64()).decode() self._settings["state"] = bytes(self.saveState().toBase64()).decode() else: self._settings["geometry"] = "" self._settings["state"] = "" self.setSettings(self._settings) Controller.instance().stopListenNotifications() server = LocalServer.instance() server.stopLocalServer(wait=True) time_spent = "{:.0f}".format(time.time() - self._start_time) log.debug("Time spend in the software is {}".format(time_spent)) if close_windows: self.close() def _nodeRunning(self): """ Display a warning to user :returns: False is a device is still running """ # check if any node is running topology = Topology.instance() topology.project = Topology.instance().project() for node in topology.nodes(): if not node.isAlwaysOn() and node.status() == Node.started: return True return False def startupLoading(self): """ Called by QTimer.singleShot to load everything needed at startup. """ if not LocalConfig.instance().isMainGui(): reply = QtWidgets.QMessageBox.warning(self, "GNS3", "Another GNS3 GUI is already running. Continue?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: sys.exit(1) return run_as_root_path = LocalConfig.instance().runAsRootPath() if not sys.platform.startswith("win") and os.geteuid() == 0: # touches file to know that user has run GNS3 as root and to prevent # from running as user if not os.path.exists(run_as_root_path): try: open(run_as_root_path, 'a').close() except OSError as e: log.warning("Cannot write `run_as_root` file due to: {}".format(str(e))) QtWidgets.QMessageBox.warning(self, "Root", "Running GNS3 as root is not recommended and could be dangerous") if not sys.platform.startswith("win") and os.geteuid() != 0 and os.path.exists(run_as_root_path): QtWidgets.QMessageBox.critical( self, "Run as user", "GNS3 has been previously run as root. It is not possible " "to change to another user and GNS3 will be shutdown. Please delete the '{}' file " "and start the program again.".format(run_as_root_path)) sys.exit(1) # restore debug level if self._settings["debug_level"]: print("Activating debugging (use command 'debug 0' to deactivate)") root = logging.getLogger() root.setLevel(logging.DEBUG) # restore the style self._setStyle(self._settings.get("style")) Controller.instance().connected_signal.connect(self._controllerConnectedSlot) Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions) self.uiGraphicsView.setEnabled(False) # show the setup wizard if not self._settings["hide_setup_wizard"]: self._setupWizardActionSlot() else: # start and connect to the local server if needed LocalServer.instance().localServerAutoStartIfRequired() if self._open_file_at_startup: self.loadPath(self._open_file_at_startup) self._open_file_at_startup = None elif Topology.instance().project() is None: self._newProjectActionSlot() if self._settings["check_for_update"]: # automatic check for update every week (604800 seconds) current_epoch = int(time.mktime(time.localtime())) if current_epoch - self._settings["last_check_for_update"] >= 604800: # let's check for an update self._checkForUpdateActionSlot(silent=True) self._settings["last_check_for_update"] = current_epoch self.setSettings(self._settings) def updateRecentProjectsSettings(self, project_id, project_name, project_path): """ Updates the recent project settings. :param project_id: The ID of the project :param project_name: The name of the project :param project_path: The project path """ # Projects are stored as a list of project_id:project_name key = "{}:{}:{}".format(project_id, project_name, project_path) recent_projects = [] for project in self._settings["recent_projects"]: recent_projects.append(project) # Because the name can change we compare only the project id and path for project_key in list(recent_projects): if project_key.split(":")[0] == project_id: recent_projects.remove(project_key) for project_key in list(recent_projects): try: if project_key.split(":")[2] == project_path: recent_projects.remove(project_key) # 2.0.0 alpha1 compatible except IndexError: pass recent_projects.insert(0, key) if len(recent_projects) > self._maxrecent_files: recent_projects.pop() # write the recent file list self._settings["recent_projects"] = recent_projects self.setSettings(self._settings) def updateRecentProjectActions(self): """ Updates recent project actions. """ index = 0 size = len(self._settings["recent_projects"]) for project in self._settings["recent_projects"]: # Projects are stored as a list of project_id:project_name try: project_id, project_name, project_path = project.split(":", maxsplit=2) except ValueError: # Compatible with 2.0.0a1 project_path = None project_id, project_name = project.split(":", maxsplit=1) if project_id not in [p["project_id"] for p in Controller.instance().projects()]: size -= 1 continue action = self.recent_project_actions[index] if project_path and os.path.exists(project_path): action.setText(" {}. {} [{}]".format(index + 1, project_name, project_path)) action.setData((project_id, project_path, )) else: action.setText(" {}. {}".format(index + 1, project_name)) action.setData((project_id, )) index += 1 if Controller.instance().isRemote(): for index in range(0, size): self.recent_project_actions[index].setVisible(True) for index in range(size + 1, self._maxrecent_files): self.recent_project_actions[index].setVisible(False) if size: self.recent_project_actions_separator.setVisible(True) else: for action in self.recent_project_actions: action.setVisible(False) self.recent_project_actions_separator.setVisible(False) def updateRecentFileSettings(self, path): """ Updates the recent file settings. :param path: path to the new file """ recent_files = [] for file_path in self._settings["recent_files"]: if file_path: file_path = os.path.normpath(file_path) if file_path not in recent_files and os.path.exists(file_path): recent_files.append(file_path) # update the recent file list if path in recent_files: recent_files.remove(path) recent_files.insert(0, path) if len(recent_files) > self._maxrecent_files: recent_files.pop() # write the recent file list self._settings["recent_files"] = recent_files self.setSettings(self._settings) def updateRecentFileActions(self): """ Updates recent file actions. """ index = 0 size = len(self._settings["recent_files"]) for file_path in self._settings["recent_files"]: try: if file_path and os.path.exists(file_path): action = self.recent_file_actions[index] duplicate = False for file_path_2 in self._settings["recent_files"]: if file_path != file_path_2 and os.path.basename(file_path) == os.path.basename(file_path_2): duplicate = True break if duplicate: action.setText(" {}. {} [{}]".format(index + 1, os.path.basename(file_path), file_path)) else: action.setText(" {}. {}".format(index + 1, os.path.basename(file_path))) action.setData(file_path) action.setVisible(True) index += 1 # We can have this error if user save a file with unicode char # and change his system locale. except UnicodeEncodeError: pass if not Controller.instance().isRemote(): for index in range(size + 1, self._maxrecent_files): self.recent_file_actions[index].setVisible(False) if size: self.recent_file_actions_separator.setVisible(True) else: for index in range(0, self._maxrecent_files): self.recent_file_actions[index].setVisible(False) self.recent_file_actions_separator.setVisible(False) def _controllerConnectedSlot(self): self.updateRecentFileActions() self._refreshVisibleWidgets() def run_later(self, counter, callback): """ Run a function after X milliseconds :params counter: Time to wait before fire the callback (in milliseconds) :params callback: Function to run """ QtCore.QTimer.singleShot(counter, callback) def _exportProjectActionSlot(self): """ Slot called to export a portable project """ Topology.instance().exportProject() def _importProjectActionSlot(self): """ Slot called to import a portable project """ directory = self._portable_project_dir if not os.path.exists(directory): directory = Topology.instance().projectsDirPath() path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open portable project", directory, "All files (*);;GNS3 Portable Project (*.gns3project *.gns3p)", "GNS3 Portable Project (*.gns3project *.gns3p)") if path: Topology.instance().importProject(path) self._portable_project_dir = os.path.dirname(path) def _editProjectActionSlot(self): if Topology.instance().project() is None: return dialog = EditProjectDialog(self) dialog.show() dialog.exec() def _deleteProjectActionSlot(self): if Topology.instance().project() is None: return reply = QtWidgets.QMessageBox.warning( self, "GNS3", "The project will be deleted from disk. All files will be removed including the project subdirectories. Continue?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.Yes: Topology.instance().deleteProject() def _setStyle(self, style_name): """ Applies a style. :param style_name: Style name """ style = Style(self) if style_name.startswith("Charcoal"): style.setCharcoalStyle() elif style_name == "Classic": style.setClassicStyle() else: style.setLegacyStyle() @staticmethod def instance(): """ Singleton to return only one instance of MainWindow. :returns: instance of MainWindow """ if not hasattr(MainWindow, "_instance"): MainWindow._instance = MainWindow() return MainWindow._instance