mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 00:46:01 +03:00
Merge pull request #1814 from GNS3/show_error
Display an overlay popup with log messages
This commit is contained in:
@@ -21,7 +21,6 @@ Handles commands typed in the GNS3 console.
|
||||
|
||||
import sys
|
||||
import cmd
|
||||
import logging
|
||||
import struct
|
||||
import sip
|
||||
import json
|
||||
@@ -30,6 +29,9 @@ from .node import Node
|
||||
from .qt import QtCore
|
||||
from .version import __version__
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleCmd(cmd.Cmd):
|
||||
|
||||
@@ -177,6 +179,24 @@ class ConsoleCmd(cmd.Cmd):
|
||||
print("Cannot console to {}".format(device))
|
||||
break
|
||||
|
||||
def do_log(self, args):
|
||||
"""
|
||||
Log a message
|
||||
|
||||
log level message
|
||||
"""
|
||||
|
||||
args = args.split()
|
||||
if len(args) == 0:
|
||||
return
|
||||
level = args.pop(0)
|
||||
if level == "info":
|
||||
log.info(" ".join(args))
|
||||
elif level == "warning":
|
||||
log.warning(" ".join(args))
|
||||
else:
|
||||
log.error(" ".join(args))
|
||||
|
||||
def _start_console(self, node):
|
||||
"""
|
||||
Starts a console application for a specific node.
|
||||
|
||||
186
gns3/dialogs/notif_dialog.py
Normal file
186
gns3/dialogs/notif_dialog.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2016 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/>.
|
||||
|
||||
"""
|
||||
Display error to the user in an overlay popup
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from gns3.qt import QtWidgets, QtCore, qslot
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MAX_ELEMENTS = 3
|
||||
DISPLAY_DURATION = {
|
||||
"CRITICAL": 120,
|
||||
"ERROR": 120,
|
||||
"WARNING": 20,
|
||||
"INFO": 5
|
||||
}
|
||||
|
||||
|
||||
class NotifDialogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self, dialog):
|
||||
super().__init__()
|
||||
self._dialog = dialog
|
||||
self.setLevel(logging.INFO)
|
||||
self._dialog.show()
|
||||
|
||||
def emit(self, record):
|
||||
self._dialog.addNotif(record.levelname, record.getMessage())
|
||||
|
||||
|
||||
class NotifDialog(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self._notifs = []
|
||||
|
||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
|
||||
QtCore.Qt.WindowDoesNotAcceptFocus |
|
||||
QtCore.Qt.SubWindow)
|
||||
# QtCore.Qt.Tool)
|
||||
# QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) # | QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
self._layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
self._timer = QtCore.QTimer()
|
||||
self._timer.setInterval(1000)
|
||||
self._timer.timeout.connect(self._refreshSlot)
|
||||
self._timer.start()
|
||||
|
||||
for i in range(0, MAX_ELEMENTS):
|
||||
l = QtWidgets.QLabel()
|
||||
l.setAlignment(QtCore.Qt.AlignTop)
|
||||
l.setWordWrap(True)
|
||||
l.hide()
|
||||
self._layout.addWidget(l)
|
||||
self.setLayout(self._layout)
|
||||
|
||||
@qslot
|
||||
def addNotif(self, level, message):
|
||||
if not self.parent().settings()["overlay_notifications"]:
|
||||
return
|
||||
|
||||
# This unicode char prevent the wordwrap at /
|
||||
message = message.replace("/", "\u2060/\u2060")
|
||||
if len(self._notifs) == MAX_ELEMENTS:
|
||||
self._notifs.pop(0)
|
||||
self._notifs.append((level, message, time.time()))
|
||||
self.update()
|
||||
|
||||
@qslot
|
||||
def _refreshSlot(self):
|
||||
"""
|
||||
Hide the notifs after some delay
|
||||
"""
|
||||
notifs = []
|
||||
for (i, (level, message, when)) in enumerate(self._notifs):
|
||||
if when + DISPLAY_DURATION[level] > time.time():
|
||||
notifs.append((level, message, when))
|
||||
if notifs != self._notifs:
|
||||
self._notifs = notifs
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
if len(self._notifs) == 0:
|
||||
self.hide()
|
||||
else:
|
||||
for (i, (level, message, when)) in enumerate(self._notifs):
|
||||
w = self._layout.itemAt(i).widget()
|
||||
w.setText(message)
|
||||
if level == "ERROR" or level == "CRITICAL":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: rgb(247, 205, 198);
|
||||
border-left: 10px solid red;
|
||||
""")
|
||||
elif level == "WARNING":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: #f4f2b5;
|
||||
border-left: 10px solid orange;
|
||||
""")
|
||||
elif level == "INFO":
|
||||
w.setStyleSheet("""
|
||||
color: black;
|
||||
padding-left: 12px;
|
||||
background-color: #cfffc9;
|
||||
border-left: 10px solid green;
|
||||
""")
|
||||
|
||||
w.show()
|
||||
for i in range(i + 1, MAX_ELEMENTS):
|
||||
w = self._layout.itemAt(i).widget()
|
||||
w.hide()
|
||||
|
||||
x = self.parent().width() - self.width() - 10
|
||||
y = 10
|
||||
self.setGeometry(x, y, self.sizeHint().width(), self.sizeHint().height())
|
||||
self.show()
|
||||
|
||||
@qslot
|
||||
def mousePressEvent(self, event):
|
||||
self._notifs.clear()
|
||||
self.update()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
A demo main for testing the features
|
||||
"""
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class MainWindow(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
l1 = QtWidgets.QLabel()
|
||||
l1.setText("Hello World")
|
||||
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
vbox.addWidget(l1)
|
||||
self.setLayout(vbox)
|
||||
self.setStyleSheet("background-color:blue;")
|
||||
self._dialog = NotifDialog(self)
|
||||
log.addHandler(NotifDialogHandler(self._dialog))
|
||||
log.info("test")
|
||||
|
||||
def moveEvent(self, event):
|
||||
log.error("An error")
|
||||
log.info("An info with an url http://test")
|
||||
log.warning("A warning with a long long long longlong longlong longlong longlong longlong longlong longlong long message")
|
||||
self._dialog.update()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._dialog.update()
|
||||
|
||||
main = MainWindow()
|
||||
main.setMinimumWidth(600)
|
||||
main.setMinimumHeight(600)
|
||||
main.show()
|
||||
exit_code = app.exec_()
|
||||
@@ -281,7 +281,7 @@ class HTTPClient(QtCore.QObject):
|
||||
self._query_waiting_connections.append((request, callback))
|
||||
# If we are not connected and we enqueue the first query we open the conection
|
||||
if len(self._query_waiting_connections) == 1:
|
||||
log.info("Connection to {}".format(self.url()))
|
||||
log.debug("Connection to {}".format(self.url()))
|
||||
self._executeHTTPQuery("GET", "/version", self._callbackConnect, {}, server=server, timeout=5)
|
||||
|
||||
def _connectionError(self, callback, msg="", server=None):
|
||||
|
||||
@@ -241,7 +241,7 @@ class LocalConfig(QtCore.QObject):
|
||||
Read the configuration file.
|
||||
"""
|
||||
|
||||
log.info("Load config from %s", config_path)
|
||||
log.debug("Load config from %s", config_path)
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
self._last_config_changed = os.stat(config_path).st_mtime
|
||||
@@ -268,7 +268,7 @@ class LocalConfig(QtCore.QObject):
|
||||
with open(temporary, "w", encoding="utf-8") as f:
|
||||
json.dump(self._settings, f, sort_keys=True, indent=4)
|
||||
shutil.move(temporary, self._config_file)
|
||||
log.info("Configuration save to %s", self._config_file)
|
||||
log.debug("Configuration save to %s", self._config_file)
|
||||
self._last_config_changed = os.stat(self._config_file).st_mtime
|
||||
except (ValueError, OSError) as e:
|
||||
log.error("Could not write the config file {}: {}".format(self._config_file, e))
|
||||
@@ -373,9 +373,8 @@ class LocalConfig(QtCore.QObject):
|
||||
self._settings[section] = settings
|
||||
|
||||
if changed:
|
||||
log.info("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
log.debug("Section %s has missing default values. Adding keys %s Saving configuration", section, ','.join(set(default_settings.keys()) - set(settings.keys())))
|
||||
self.writeConfig()
|
||||
|
||||
return copy.deepcopy(settings)
|
||||
|
||||
def saveSectionSettings(self, section, settings):
|
||||
@@ -391,7 +390,7 @@ class LocalConfig(QtCore.QObject):
|
||||
|
||||
if self._settings[section] != settings:
|
||||
self._settings[section].update(copy.deepcopy(settings))
|
||||
log.info("Section %s has changed. Saving configuration", section)
|
||||
log.debug("Section %s has changed. Saving configuration", section)
|
||||
self.writeConfig()
|
||||
else:
|
||||
log.debug("Section %s has not changed. Skip saving configuration", section)
|
||||
|
||||
@@ -323,7 +323,7 @@ class LocalServer(QtCore.QObject):
|
||||
return True
|
||||
|
||||
if self.isLocalServerRunning():
|
||||
log.info("A local server already running on this host")
|
||||
log.debug("A local server already running on this host")
|
||||
# Try to kill the server. The server can be still running after
|
||||
# if the server was started by hand
|
||||
self._killAlreadyRunningServer()
|
||||
|
||||
@@ -51,6 +51,7 @@ from .update_manager import UpdateManager
|
||||
from .utils.analytics import AnalyticsClient
|
||||
from .dialogs.appliance_wizard import ApplianceWizard
|
||||
from .dialogs.new_appliance_dialog import NewApplianceDialog
|
||||
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
|
||||
from .registry.appliance import ApplianceError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -75,6 +76,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
# Setup logger
|
||||
logging.getLogger().addHandler(NotifDialogHandler(NotifDialog(self)))
|
||||
|
||||
self._open_file_at_startup = open_file
|
||||
|
||||
MainWindow._instance = self
|
||||
|
||||
@@ -255,6 +255,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
self.uiImagesPathLineEdit.setText(local_server["images_path"])
|
||||
self.uiConfigsPathLineEdit.setText(local_server["configs_path"])
|
||||
self.uiStatsCheckBox.setChecked(settings["send_stats"])
|
||||
self.uiOverlayNotificationsCheckBox.setChecked(settings["overlay_notifications"])
|
||||
self.uiCrashReportCheckBox.setChecked(local_server["report_errors"])
|
||||
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
|
||||
self.uiExperimentalFeaturesCheckBox.setChecked(settings["experimental_features"])
|
||||
@@ -329,6 +330,7 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
|
||||
"style": self.uiStyleComboBox.currentText(),
|
||||
"experimental_features": self.uiExperimentalFeaturesCheckBox.isChecked(),
|
||||
"check_for_update": self.uiCheckForUpdateCheckBox.isChecked(),
|
||||
"overlay_notifications": self.uiOverlayNotificationsCheckBox.isChecked(),
|
||||
"telnet_console_command": self.uiTelnetConsoleCommandLineEdit.text(),
|
||||
"vnc_console_command": self.uiVNCConsoleCommandLineEdit.text(),
|
||||
"delay_console_all": self.uiDelayConsoleAllSpinBox.value(),
|
||||
|
||||
@@ -216,6 +216,7 @@ else:
|
||||
GENERAL_SETTINGS = {
|
||||
"style": DEFAULT_STYLE,
|
||||
"check_for_update": True,
|
||||
"overlay_notifications": True,
|
||||
"experimental_features": False,
|
||||
"send_stats": True,
|
||||
"stats_visitor_id": str(uuid.uuid4()), # An anonymous id for stats
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>633</width>
|
||||
<width>634</width>
|
||||
<height>643</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -773,6 +773,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="uiOverlayNotificationsCheckBox">
|
||||
<property name="text">
|
||||
<string>Display error, warning and info in a overlay popup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="uiExperimentalFeaturesCheckBox">
|
||||
<property name="text">
|
||||
|
||||
@@ -13,7 +13,7 @@ class Ui_GeneralPreferencesPageWidget(object):
|
||||
|
||||
def setupUi(self, GeneralPreferencesPageWidget):
|
||||
GeneralPreferencesPageWidget.setObjectName("GeneralPreferencesPageWidget")
|
||||
GeneralPreferencesPageWidget.resize(633, 643)
|
||||
GeneralPreferencesPageWidget.resize(634, 643)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(GeneralPreferencesPageWidget)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.uiMiscTabWidget = QtWidgets.QTabWidget(GeneralPreferencesPageWidget)
|
||||
@@ -337,6 +337,9 @@ class Ui_GeneralPreferencesPageWidget(object):
|
||||
self.uiStatsCheckBox.setChecked(True)
|
||||
self.uiStatsCheckBox.setObjectName("uiStatsCheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiStatsCheckBox)
|
||||
self.uiOverlayNotificationsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
|
||||
self.uiOverlayNotificationsCheckBox.setObjectName("uiOverlayNotificationsCheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiOverlayNotificationsCheckBox)
|
||||
self.uiExperimentalFeaturesCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
|
||||
self.uiExperimentalFeaturesCheckBox.setObjectName("uiExperimentalFeaturesCheckBox")
|
||||
self.verticalLayout_2.addWidget(self.uiExperimentalFeaturesCheckBox)
|
||||
@@ -415,6 +418,7 @@ class Ui_GeneralPreferencesPageWidget(object):
|
||||
self.uiCheckForUpdateCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Automatically check for update"))
|
||||
self.uiCrashReportCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous crash reports"))
|
||||
self.uiStatsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous usage statistics"))
|
||||
self.uiOverlayNotificationsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Display error, warning and info in a overlay popup"))
|
||||
self.uiExperimentalFeaturesCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable experimental features (dangerous, restart required)"))
|
||||
self.uiMultiProfilesCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Ask for settings profile at application startup (work profile / home profile)"))
|
||||
self.uiMiscTabWidget.setTabText(self.uiMiscTabWidget.indexOf(self.uiMiscTab), _translate("GeneralPreferencesPageWidget", "Miscellaneous"))
|
||||
|
||||
Reference in New Issue
Block a user