diff --git a/gns3/controller.py b/gns3/controller.py
index fa55ff16..b9758dba 100644
--- a/gns3/controller.py
+++ b/gns3/controller.py
@@ -19,6 +19,7 @@ import os
import hashlib
import tempfile
import json
+import pathlib
from .qt import QtCore, QtNetwork, QtGui, QtWidgets, QtWebSockets, qpartial, qslot
from .symbol import Symbol
@@ -46,7 +47,7 @@ class Controller(QtCore.QObject):
self._connected = False
self._connecting = False
self._version = None
- self._cache_directory = tempfile.mkdtemp()
+ self._cache_directory = tempfile.TemporaryDirectory(suffix="-gns3")
self._http_client = None
self._first_error = True
self._error_dialog = None
@@ -274,12 +275,11 @@ class Controller(QtCore.QObject):
if not self._http_client:
return
-
path = self.getStaticCachedPath(url)
if os.path.exists(path):
callback(path)
- elif path in self._static_asset_download_queue:
+ if path in self._static_asset_download_queue:
self._static_asset_download_queue[path].append((callback, fallback, ))
else:
self._static_asset_download_queue[path] = [(callback, fallback, )]
@@ -313,8 +313,8 @@ class Controller(QtCore.QObject):
def getStaticCachedPath(self, url):
"""
Returns static cached (hashed) path
+
:param url:
- :return:
"""
m = hashlib.md5()
m.update(url.encode())
@@ -322,9 +322,22 @@ class Controller(QtCore.QObject):
extension = ".svg"
else:
extension = ".png"
- path = os.path.join(self._cache_directory, m.hexdigest() + extension)
+ path = os.path.join(self._cache_directory.name, m.hexdigest() + extension)
return path
+ def clearStaticCache(self):
+ """
+ Clear the cache directory.
+ """
+
+ for filename in os.listdir(self._cache_directory.name):
+ if filename.endswith(".svg") or filename.endswith(".png"):
+ try:
+ os.remove(os.path.join(self._cache_directory.name, filename))
+ except OSError as e:
+ log.debug("Error deleting cached symbol '{}':{}".format(filename, e))
+ continue
+
def getSymbolIcon(self, symbol_id, callback, fallback=None):
"""
Get a QIcon for a symbol from the controller
@@ -341,10 +354,31 @@ class Controller(QtCore.QObject):
self.getStatic(Symbol(symbol_id).url(), qpartial(self._getIconCallback, callback), fallback=fallback)
def _getIconCallback(self, callback, path):
+
+ pixmap = QtGui.QPixmap(path)
+ if pixmap.isNull():
+ log.debug("Invalid symbol {}".format(path))
+ path = ":/icons/cancel.svg"
icon = QtGui.QIcon()
icon.addFile(path)
callback(icon)
+ def uploadSymbol(self, symbol_id, path):
+
+ self.post("/symbols/" + symbol_id + "/raw",
+ qpartial(self._finishSymbolUpload, path),
+ body=pathlib.Path(path), progressText="Uploading {}".format(symbol_id), timeout=None)
+
+ def _finishSymbolUpload(self, path, result, error=False, **kwargs):
+
+ if error:
+ log.error("Error while uploading symbol: {}: {}".format(path, result.get("message", "unknown")))
+ return
+
+ # Refresh the templates list
+ from .template_manager import TemplateManager
+ TemplateManager.instance().templates_changed_signal.emit()
+
def getSymbols(self, callback):
self.get('/symbols', callback=callback)
diff --git a/gns3/dialogs/new_template_wizard.py b/gns3/dialogs/new_template_wizard.py
index e9901a7a..89632d2d 100644
--- a/gns3/dialogs/new_template_wizard.py
+++ b/gns3/dialogs/new_template_wizard.py
@@ -19,6 +19,7 @@ import sys
import tempfile
import json
import sip
+import os
from gns3.qt import QtCore, QtWidgets, qpartial
from gns3.controller import Controller
@@ -59,6 +60,7 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
"""
ApplianceManager.instance().refresh(update=True)
+ Controller.instance().clearStaticCache()
def _appliancesChangedSlot(self):
"""
@@ -266,6 +268,7 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
super().done(result)
if result:
+ ApplianceManager.instance().appliances_changed_signal.disconnect(self._appliancesChangedSlot)
from gns3.main_window import MainWindow
if self.currentPage() == self.uiApplianceFromServerWizardPage:
items = self.uiAppliancesTreeWidget.selectedItems()
@@ -274,6 +277,10 @@ class NewTemplateWizard(QtWidgets.QWizard, Ui_NewTemplateWizard):
json.dump(item.data(0, QtCore.Qt.UserRole), f)
f.close()
MainWindow.instance().loadPath(f.name)
+ try:
+ os.remove(f.name)
+ except OSError:
+ pass
elif self.uiCreateTemplateManuallyRadioButton.isChecked():
MainWindow.instance().preferencesActionSlot()
elif self.uiImportApplianceFromFileRadioButton.isChecked():
diff --git a/gns3/dialogs/preferences_dialog.py b/gns3/dialogs/preferences_dialog.py
index 2390999d..9e24eb2e 100644
--- a/gns3/dialogs/preferences_dialog.py
+++ b/gns3/dialogs/preferences_dialog.py
@@ -128,7 +128,7 @@ class PreferencesDialog(QtWidgets.QDialog, Ui_PreferencesDialog):
# Class name, changed signal
widget_to_watch = {
- QtWidgets.QLineEdit: "textChanged",
+ #QtWidgets.QLineEdit: "textChanged",
QtWidgets.QPlainTextEdit: "textChanged",
# QtWidgets.QTreeWidget: "itemChanged",
QtWidgets.QComboBox: "currentIndexChanged",
diff --git a/gns3/dialogs/symbol_selection_dialog.py b/gns3/dialogs/symbol_selection_dialog.py
index 57df9234..dbc4d5ae 100644
--- a/gns3/dialogs/symbol_selection_dialog.py
+++ b/gns3/dialogs/symbol_selection_dialog.py
@@ -25,7 +25,6 @@ import pathlib
from ..qt import QtCore, QtGui, QtWidgets, qpartial, sip_is_deleted
from ..qt.qimage_svg_renderer import QImageSvgRenderer
from ..ui.symbol_selection_dialog_ui import Ui_SymbolSelectionDialog
-from ..local_server import LocalServer
from ..controller import Controller
from ..symbol import Symbol
@@ -56,7 +55,6 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self.uiCustomSymbolRadioButton.toggled.connect(self._customSymbolToggledSlot)
self.uiBuiltInSymbolRadioButton.toggled.connect(self._builtInSymbolToggledSlot)
self.uiSearchLineEdit.textChanged.connect(self._searchTextChangedSlot)
- self.uiBuiltinSymbolOnlyCheckBox.toggled.connect(self._builtinSymbolOnlyToggledSlot)
if not SymbolSelectionDialog._symbols_dir:
SymbolSelectionDialog._symbols_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.PicturesLocation)
@@ -64,9 +62,10 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self.uiButtonBox.button(QtWidgets.QDialogButtonBox.Apply).hide()
self.uiBuiltInSymbolRadioButton.setChecked(True)
- self.uiSymbolListWidget.setFocus()
- self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
+ self.uiSymbolTreeWidget.setFocus()
+ self.uiSymbolTreeWidget.setIconSize(QtCore.QSize(64, 64))
self._symbol_items = []
+ self._parents = {}
Controller.instance().get("/symbols", self._listSymbolsCallback)
@@ -78,17 +77,24 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
self._symbol_items = []
for symbol in result:
symbol = Symbol(**symbol)
- name = os.path.splitext(symbol.filename())[0]
- item = QtWidgets.QListWidgetItem(self.uiSymbolListWidget)
- item.setData(QtCore.Qt.UserRole, symbol)
- self._symbol_items.append(item)
- item.setText(name)
+ theme = symbol.theme()
+ if theme not in self._parents:
+ parent = QtWidgets.QTreeWidgetItem(self.uiSymbolTreeWidget)
+ parent.setText(0, theme)
+ font = parent.font(0)
+ font.setBold(True)
+ parent.setFont(0, font)
+ parent.setFlags(parent.flags() & ~QtCore.Qt.ItemIsSelectable)
+ self._parents[theme] = parent
+ else:
+ parent = self._parents[theme]
- image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
- # Set the ARGB to 0 to prevent rendering artifacts
- image.fill(0x00000000)
- icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
- item.setIcon(icon)
+ name = os.path.splitext(symbol.filename())[0]
+ item = QtWidgets.QTreeWidgetItem(parent)
+ item.setData(0, QtCore.Qt.UserRole, symbol)
+ item.setToolTip(0, symbol.id())
+ self._symbol_items.append(item)
+ item.setText(0, name)
def render(item, path):
if sip_is_deleted(item):
@@ -99,14 +105,11 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
image.fill(0x00000000)
svg_renderer.render(QtGui.QPainter(image))
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
- item.setIcon(icon)
+ item.setIcon(0, icon)
Controller.instance().getStatic(symbol.url(), qpartial(render, item))
self.adjustSize()
- def _builtinSymbolOnlyToggledSlot(self, checked):
- self._filter()
-
def _searchTextChangedSlot(self, text):
self._filter()
@@ -116,10 +119,10 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
"""
text = self.uiSearchLineEdit.text()
for item in self._symbol_items:
- if self.uiBuiltinSymbolOnlyCheckBox.isChecked() and not item.data(QtCore.Qt.UserRole).builtin():
+ if not item.data(0, QtCore.Qt.UserRole).builtin():
item.setHidden(True)
else:
- if len(text.strip()) == 0 or text.strip().lower() in item.text().lower():
+ if len(text.strip()) == 0 or text.strip().lower() in item.text(0).lower():
item.setHidden(False)
else:
item.setHidden(True)
@@ -164,10 +167,10 @@ class SymbolSelectionDialog(QtWidgets.QDialog, Ui_SymbolSelectionDialog):
def getSymbol(self):
- if self.uiSymbolListWidget.isEnabled():
- current = self.uiSymbolListWidget.currentItem()
+ if self.uiSymbolTreeWidget.isEnabled():
+ current = self.uiSymbolTreeWidget.currentItem()
if current:
- return current.data(QtCore.Qt.UserRole).id()
+ return current.data(0, QtCore.Qt.UserRole).id()
else:
return os.path.basename(self.uiSymbolLineEdit.text())
return None
diff --git a/gns3/items/link_item.py b/gns3/items/link_item.py
index 188df69b..7da331da 100644
--- a/gns3/items/link_item.py
+++ b/gns3/items/link_item.py
@@ -36,7 +36,8 @@ class SvgIconItem(QtSvg.QGraphicsSvgItem):
def mousePressEvent(self, event):
- self.parentItem().mousePressEvent(event)
+ if self.parentItem():
+ self.parentItem().mousePressEvent(event)
event.accept()
diff --git a/gns3/registry/appliance_to_template.py b/gns3/registry/appliance_to_template.py
index a8f25df2..dcf6b92d 100644
--- a/gns3/registry/appliance_to_template.py
+++ b/gns3/registry/appliance_to_template.py
@@ -168,37 +168,38 @@ class ApplianceToTemplate:
return os.path.basename(path)
- def _set_symbol(self, symbol, controller_symbols):
+ def _set_symbol(self, symbol_id, controller_symbols):
"""
Check if exists on controller or download symbol from the web if needed
"""
# GNS3 builtin symbol
- if symbol.startswith(":/symbols/"):
- return symbol
+ if symbol_id.startswith(":/symbols/"):
+ return symbol_id
- path = os.path.join(Config().symbols_dir, symbol)
+ path = os.path.join(Config().symbols_dir, symbol_id)
if os.path.exists(path):
return os.path.basename(path)
if controller_symbols:
- is_symbol_on_controller = len([s for s in controller_symbols
- if s['symbol_id'] == symbol]) > 0
+ is_symbol_on_controller = len([s for s in controller_symbols if s['symbol_id'] == symbol_id]) > 0
if is_symbol_on_controller:
- cached = Controller.instance().getStaticCachedPath(symbol)
+ cached = Controller.instance().getStaticCachedPath(symbol_id)
if os.path.exists(cached):
try:
shutil.copy(cached, path)
except IOError as e:
- log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(
- cached, path, str(e)
- ))
- return symbol
+ log.warning("Cannot copy cached symbol from `{}` to `{}` due `{}`".format(cached, path, e))
+ return symbol_id
- url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol)
+ url = "https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{}".format(symbol_id)
try:
urllib.request.urlretrieve(url, path)
+ controller = Controller.instance()
+ controller.clearStaticCache()
+ if controller.isRemote():
+ controller.uploadSymbol(symbol_id, path)
return os.path.basename(path)
except (OSError, CertificateError):
return None
diff --git a/gns3/symbol.py b/gns3/symbol.py
index bb1b7fe7..9c6ba137 100644
--- a/gns3/symbol.py
+++ b/gns3/symbol.py
@@ -19,10 +19,11 @@ import urllib.parse
class Symbol:
- def __init__(self, symbol_id=None, builtin=False, filename=None):
+ def __init__(self, symbol_id=None, builtin=False, filename=None, theme=None):
self._id = symbol_id
self._builtin = builtin
self._filename = filename
+ self._theme = theme
def id(self):
return self._id
@@ -33,6 +34,9 @@ class Symbol:
def builtin(self):
return self._builtin
+ def theme(self):
+ return self._theme
+
def url(self):
return urllib.parse.quote("/symbols/" + self._id + "/raw")
diff --git a/gns3/ui/new_template_wizard.ui b/gns3/ui/new_template_wizard.ui
index 38021aba..406ffd5e 100644
--- a/gns3/ui/new_template_wizard.ui
+++ b/gns3/ui/new_template_wizard.ui
@@ -70,6 +70,9 @@
Command line replacements:
%c = capture file (PCAP format)
")) self.uiAutoStartCheckBox.setText(_translate("PacketCapturePreferencesPageWidget", "Automatically start the packet capture application")) self.uiCaptureAnalyzerCommandLabel.setText(_translate("PacketCapturePreferencesPageWidget", "Packet capture analyzer command:")) + self.uiCaptureReaderCommandLineEdit.setToolTip(_translate("PacketCapturePreferencesPageWidget", "Command line replacements:
%c = capture file (PCAP format)
")) self.uiRestoreDefaultsPushButton.setText(_translate("PacketCapturePreferencesPageWidget", "Restore defaults")) diff --git a/gns3/ui/symbol_selection_dialog.ui b/gns3/ui/symbol_selection_dialog.ui index 781c4713..b2e4dbac 100755 --- a/gns3/ui/symbol_selection_dialog.ui +++ b/gns3/ui/symbol_selection_dialog.ui @@ -6,8 +6,8 @@