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 9cae0ba359..fafca5c814 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