mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 00:46:01 +03:00
1508 lines
55 KiB
Python
1508 lines
55 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/>.
|
|
|
|
"""
|
|
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")
|
|
|
|
# detect if the SVG module is correctly installed
|
|
supported_image_formats = [fmt.data().decode('utf-8') for fmt in QtGui.QImageReader().supportedImageFormats()]
|
|
log.debug("Supported image formats: %s", ", ".join(supported_image_formats))
|
|
if "svg" not in supported_image_formats:
|
|
log.warning("SVG image format is not supported, is the Qt SVG module installed?")
|
|
|
|
# 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
|