diff --git a/ui/bin/opensnitch-ui b/ui/bin/opensnitch-ui
index 16863c1082..e2e82ad060 100755
--- a/ui/bin/opensnitch-ui
+++ b/ui/bin/opensnitch-ui
@@ -22,6 +22,7 @@ if dist_path not in sys.path:
from opensnitch.service import UIService
from opensnitch.config import Config
+from opensnitch.utils import Themes
import opensnitch.version
import opensnitch.ui_pb2
from opensnitch.ui_pb2_grpc import add_UIServicer_to_server
@@ -40,6 +41,15 @@ def supported_qt_version(major, medium, minor):
q = QtCore.QT_VERSION_STR.split(".")
return int(q[0]) >= major and int(q[1]) >= medium and int(q[2]) >= minor
+def load_translations():
+ locale = QtCore.QLocale.system()
+ i18n_path = os.path.dirname(os.path.realpath(opensnitch.__file__)) + "/i18n"
+ print("Loading translations:", i18n_path, "locale:", locale.name())
+ translator = QtCore.QTranslator()
+ translator.load(i18n_path + "/" + locale.name() + "/opensnitch-" + locale.name() + ".qm")
+
+ return translator
+
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OpenSnitch UI service.')
parser.add_argument("--socket", dest="socket", default="unix:///tmp/osui.sock", help="Path of the unix socket for the gRPC service (https://github.com/grpc/grpc/blob/master/doc/naming.md).", metavar="FILE")
@@ -55,13 +65,11 @@ if __name__ == '__main__':
except Exception:
pass
- locale = QtCore.QLocale.system()
- i18n_path = os.path.dirname(os.path.realpath(opensnitch.__file__)) + "/i18n"
- print("Loading translations:", i18n_path, "locale:", locale.name())
- translator = QtCore.QTranslator()
- translator.load(i18n_path + "/" + locale.name() + "/opensnitch-" + locale.name() + ".qm")
+ translator = load_translations()
app = QtWidgets.QApplication(sys.argv)
app.installTranslator(translator)
+ thm = Themes.instance()
+ thm.load_theme(app)
service = UIService(app, on_exit)
# @doc: https://grpc.github.io/grpc/python/grpc.html#server-object
diff --git a/ui/opensnitch/config.py b/ui/opensnitch/config.py
index b963c4e46b..87163a22f1 100644
--- a/ui/opensnitch/config.py
+++ b/ui/opensnitch/config.py
@@ -41,6 +41,7 @@ class Config:
POPUP_TOP_LEFT = 3
POPUP_BOTTOM_LEFT = 4
+ DEFAULT_THEME = "global/theme"
DEFAULT_DISABLE_POPUPS = "global/disable_popups"
DEFAULT_TIMEOUT_KEY = "global/default_timeout"
DEFAULT_ACTION_KEY = "global/default_action"
@@ -65,7 +66,6 @@ class Config:
NOTIFICATION_TYPE_SYSTEM = 0
NOTIFICATION_TYPE_QT = 1
-
STATS_GEOMETRY = "statsDialog/geometry"
STATS_LAST_TAB = "statsDialog/last_tab"
STATS_FILTER_TEXT = "statsDialog/general_filter_text"
diff --git a/ui/opensnitch/dialogs/preferences.py b/ui/opensnitch/dialogs/preferences.py
index cb3f2b7b8e..af28ac70ac 100644
--- a/ui/opensnitch/dialogs/preferences.py
+++ b/ui/opensnitch/dialogs/preferences.py
@@ -9,7 +9,7 @@
from opensnitch.config import Config
from opensnitch.nodes import Nodes
from opensnitch.database import Database
-from opensnitch.utils import Message, QuickHelp
+from opensnitch.utils import Message, QuickHelp, Themes
from opensnitch.notifications import DesktopNotifications
from opensnitch import ui_pb2
@@ -32,6 +32,9 @@ class PreferencesDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
def __init__(self, parent=None, appicon=None):
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
+ self._themes = Themes.instance()
+ self._saved_theme = ""
+
self._cfg = Config.get()
self._nodes = Nodes.instance()
self._db = Database.instance()
@@ -115,10 +118,28 @@ def showEvent(self, event):
# True when any node option changes
self._node_needs_update = False
+ def _load_themes(self):
+ theme_idx, self._saved_theme = self._themes.get_saved_theme()
+
+ self.labelThemeError.setVisible(False)
+ self.labelThemeError.setText("")
+ self.comboUITheme.clear()
+ self.comboUITheme.addItem(QC.translate("preferences", "System"))
+ if self._themes.available():
+ themes = self._themes.list_themes()
+ self.comboUITheme.addItems(themes)
+ else:
+ self._saved_theme = ""
+ self.labelThemeError.setStyleSheet('color: red')
+ self.labelThemeError.setVisible(True)
+ self.labelThemeError.setText(QC.translate("preferences", "Themes not available. Install qt-material: pip3 install qt-material"))
+
+ self.comboUITheme.setCurrentIndex(theme_idx)
+
def _load_settings(self):
self._default_action = self._cfg.getInt(self._cfg.DEFAULT_ACTION_KEY)
- self._default_target = self._cfg.getSettings(self._cfg.DEFAULT_TARGET_KEY)
- self._default_timeout = self._cfg.getSettings(self._cfg.DEFAULT_TIMEOUT_KEY)
+ self._default_target = self._cfg.getInt(self._cfg.DEFAULT_TARGET_KEY, 0)
+ self._default_timeout = self._cfg.getInt(self._cfg.DEFAULT_TIMEOUT_KEY, 15)
self._disable_popups = self._cfg.getBool(self._cfg.DEFAULT_DISABLE_POPUPS)
if self._cfg.hasKey(self._cfg.DEFAULT_DURATION_KEY):
@@ -140,8 +161,8 @@ def _load_settings(self):
)
self.comboUIAction.setCurrentIndex(self._default_action)
- self.comboUITarget.setCurrentIndex(int(self._default_target))
- self.spinUITimeout.setValue(int(self._default_timeout))
+ self.comboUITarget.setCurrentIndex(self._default_target)
+ self.spinUITimeout.setValue(self._default_timeout)
self.spinUITimeout.setEnabled(not self._disable_popups)
self.popupsCheck.setChecked(self._disable_popups)
@@ -171,6 +192,7 @@ def _load_settings(self):
self.spinDBMaxDays.setValue(dbMaxDays)
self.spinDBPurgeInterval.setValue(dbPurgeInterval)
+ self._load_themes()
self._load_node_settings()
self._load_ui_columns_config()
@@ -360,6 +382,13 @@ def _save_ui_config(self):
self._cfg.setSettings(self._cfg.NOTIFICATIONS_TYPE,
int(Config.NOTIFICATION_TYPE_SYSTEM if self.radioSysNotifs.isChecked() else Config.NOTIFICATION_TYPE_QT))
+ self._themes.save_theme(self.comboUITheme.currentIndex(), self.comboUITheme.currentText())
+ if self._themes.available() and self._saved_theme != self.comboUITheme.currentText():
+ Message.ok(
+ QC.translate("preferences", "UI theme changed"),
+ QC.translate("preferences", "Restart the GUI in order to apply the new theme"),
+ QtWidgets.QMessageBox.Warning)
+
# this is a workaround for not display pop-ups.
# see #79 for more information.
if self.popupsCheck.isChecked():
diff --git a/ui/opensnitch/res/preferences.ui b/ui/opensnitch/res/preferences.ui
index edce03bef6..fffe654a5d 100644
--- a/ui/opensnitch/res/preferences.ui
+++ b/ui/opensnitch/res/preferences.ui
@@ -7,7 +7,7 @@
0
0
626
- 405
+ 442
@@ -472,21 +472,7 @@
UI
- -
-
-
-
-
- any temporary rules
-
-
- -
-
- once
-
-
-
-
- -
+
-
<html><head/><body><p>When this option is selected, the rules of the selected duration won't be added to the list of temporary rules in the GUI.</p><p><br/></p><p>Temporary rules will still be valid, and you can use them when prompted to allow/deny a new connection.</p></body></html>
@@ -496,7 +482,7 @@
- -
+
-
@@ -577,7 +563,16 @@
- -
+
-
+
+
-
+
+ System
+
+
+
+
+ -
@@ -704,6 +699,34 @@
+ -
+
+
+ Theme
+
+
+
+ -
+
+
-
+
+ any temporary rules
+
+
+ -
+
+ once
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/ui/opensnitch/res/stats.ui b/ui/opensnitch/res/stats.ui
index f0afe1e28d..0c979b6630 100644
--- a/ui/opensnitch/res/stats.ui
+++ b/ui/opensnitch/res/stats.ui
@@ -233,8 +233,8 @@
../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup
-
- false
+
+ true
@@ -279,13 +279,7 @@
-
-
-
-
-
- 32
- 32
-
-
+
Save to CSV.
@@ -299,28 +293,19 @@
Ctrl+S
-
- Qt::ToolButtonFollowStyle
-
-
+
true
-
-
+
0
0
-
-
- 32
- 32
-
-
@@ -328,25 +313,19 @@
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup
-
+
true
-
-
+
32
32
-
-
- 32
- 32
-
-
Create a new rule
@@ -357,7 +336,7 @@
../../../../../../../../.designer/backup../../../../../../../../.designer/backup
-
+
true
@@ -453,13 +432,7 @@
-
-
-
-
- 32
- 32
-
-
+
Start or Stop interception
@@ -476,7 +449,7 @@
false
-
+
true
diff --git a/ui/opensnitch/utils.py b/ui/opensnitch/utils.py
index 94db93bf9e..509f148cba 100644
--- a/ui/opensnitch/utils.py
+++ b/ui/opensnitch/utils.py
@@ -9,7 +9,7 @@
import fcntl
import struct
import array
-import os
+import os, sys, glob
class AsnDB():
__instance = None
@@ -91,6 +91,91 @@ def get_asn(self, ip):
except Exception:
return ""
+class Themes():
+ """Change GUI's appearance using qt-material lib.
+ https://github.com/UN-GCPDS/qt-material
+ """
+ THEMES_PATH = [
+ os.path.expanduser("~/.config/opensnitch/"),
+ os.path.dirname(sys.modules[__name__].__file__)
+ ]
+ __instance = None
+
+ AVAILABLE = False
+ try:
+ from qt_material import apply_stylesheet as qtmaterial_apply_stylesheet
+ from qt_material import list_themes as qtmaterial_themes
+ AVAILABLE = True
+ except Exception:
+ print("Themes not available. Install qt-material if you want to change GUI's appearance: pip3 install qt-material.")
+
+ @staticmethod
+ def instance():
+ if Themes.__instance == None:
+ Themes.__instance = Themes()
+ return Themes.__instance
+
+ def __init__(self):
+ self._cfg = Config.get()
+ theme = self._cfg.getInt(self._cfg.DEFAULT_THEME, 0)
+
+ def available(self):
+ return Themes.AVAILABLE
+
+ def get_saved_theme(self):
+ if not Themes.AVAILABLE:
+ return 0, ""
+
+ theme = self._cfg.getSettings(self._cfg.DEFAULT_THEME)
+ if theme != "" and theme != None:
+ # 0 == System
+ return self.list_themes().index(theme)+1, theme
+ return 0, ""
+
+ def save_theme(self, theme_idx, theme):
+ if not Themes.AVAILABLE:
+ return
+
+ if theme_idx == 0:
+ self._cfg.setSettings(self._cfg.DEFAULT_THEME, "")
+ else:
+ self._cfg.setSettings(self._cfg.DEFAULT_THEME, theme)
+
+ def load_theme(self, app):
+ if not Themes.AVAILABLE:
+ return
+
+ try:
+ theme_idx, theme_name = self.get_saved_theme()
+ if theme_name != "":
+ invert = "light" in theme_name
+ print("Using theme:", theme_idx, theme_name, "inverted:", invert)
+ # TODO: load {theme}.xml.extra and .xml.css for further
+ # customizations.
+ Themes.qtmaterial_apply_stylesheet(app, theme=theme_name, invert_secondary=invert)
+ except Exception as e:
+ print("Themes.load_theme() exception:", e)
+
+ def list_local_themes(self):
+ themes = []
+ if not Themes.AVAILABLE:
+ return themes
+
+ try:
+ for tdir in self.THEMES_PATH:
+ themes += glob.glob(tdir + "/themes/*.xml")
+ except Exception:
+ pass
+ finally:
+ return themes
+
+ def list_themes(self):
+ themes = self.list_local_themes()
+ if not Themes.AVAILABLE:
+ return themes
+
+ themes += Themes.qtmaterial_themes()
+ return themes
class CleanerTask(Thread):
interval = 1