From bc221d6c8f470e1258ed12700a5c103b080c42aa Mon Sep 17 00:00:00 2001 From: seird Date: Sun, 21 May 2023 11:41:01 +0200 Subject: [PATCH] Automatic theme (#29) * add "automatic" theme set the theme and icons based on the system theme * update the default icons based on system theme * update pyqt to 6.5.0 to get the colorSchemeChanged callback * rename style to theme * remove unused svg files for default theme * ServerInfoDialog: update feedback colors in dark mode --- gotify_tray/database/default_settings.py | 2 +- gotify_tray/gui/MainApplication.py | 6 ++- gotify_tray/gui/themes/__init__.py | 46 ++++++++++++----- gotify_tray/gui/themes/dark_purple/style.qss | 16 ++++++ gotify_tray/gui/themes/default/refresh.svg | 45 ----------------- .../gui/themes/default/status_active.svg | 49 ------------------- .../gui/themes/default/status_connecting.svg | 49 ------------------- .../gui/themes/default/status_error.svg | 49 ------------------- .../gui/themes/default/status_inactive.svg | 49 ------------------- gotify_tray/gui/themes/default/trashcan.svg | 1 - gotify_tray/gui/themes/light_purple/style.qss | 16 ++++++ gotify_tray/gui/widgets/MainWindow.py | 13 +++-- gotify_tray/gui/widgets/MessageWidget.py | 4 +- gotify_tray/gui/widgets/ServerInfoDialog.py | 20 ++++++-- gotify_tray/gui/widgets/SettingsDialog.py | 5 +- gotify_tray/gui/widgets/StatusWidget.py | 5 +- requirements.txt | 2 +- 17 files changed, 105 insertions(+), 272 deletions(-) delete mode 100644 gotify_tray/gui/themes/default/refresh.svg delete mode 100644 gotify_tray/gui/themes/default/status_active.svg delete mode 100644 gotify_tray/gui/themes/default/status_connecting.svg delete mode 100644 gotify_tray/gui/themes/default/status_error.svg delete mode 100644 gotify_tray/gui/themes/default/status_inactive.svg delete mode 100644 gotify_tray/gui/themes/default/trashcan.svg diff --git a/gotify_tray/database/default_settings.py b/gotify_tray/database/default_settings.py index fa980e2..0b951cc 100644 --- a/gotify_tray/database/default_settings.py +++ b/gotify_tray/database/default_settings.py @@ -5,7 +5,7 @@ from ..__version__ import __title__ DEFAULT_SETTINGS = { - "theme": "default", + "theme": "automatic", "message/check_missed/notify": True, "logging/level": "Disabled", "export/path": os.path.join( diff --git a/gotify_tray/gui/MainApplication.py b/gotify_tray/gui/MainApplication.py index 63adcbf..c36115d 100644 --- a/gotify_tray/gui/MainApplication.py +++ b/gotify_tray/gui/MainApplication.py @@ -76,7 +76,7 @@ class MainApplication(QtWidgets.QApplication): self.messages_model = MessagesModel() self.application_model = ApplicationModel() - self.main_window = MainWindow(self.application_model, self.messages_model) + self.main_window = MainWindow(self, self.application_model, self.messages_model) self.main_window.show() # The initial .show() is necessary to get the correct sizes when adding MessageWigets QtCore.QTimer.singleShot(0, self.main_window.hide) @@ -381,7 +381,7 @@ class MainApplication(QtWidgets.QApplication): message_widget.set_icons() def settings_callback(self): - settings_dialog = SettingsDialog() + settings_dialog = SettingsDialog(self) settings_dialog.quit_requested.connect(self.quit) settings_dialog.theme_change_requested.connect( self.theme_change_requested_callback @@ -435,6 +435,8 @@ class MainApplication(QtWidgets.QApplication): self.main_window.image_popup.connect(self.image_popup_callback) self.main_window.hidden.connect(self.main_window_hidden_callback) self.main_window.activated.connect(self.tray.revert_icon) + + self.styleHints().colorSchemeChanged.connect(lambda _: self.theme_change_requested_callback(settings.value("theme", type=str))) self.watchdog.closed.connect(lambda: self.listener_closed_callback(None, None)) diff --git a/gotify_tray/gui/themes/__init__.py b/gotify_tray/gui/themes/__init__.py index 822b575..c4565db 100644 --- a/gotify_tray/gui/themes/__init__.py +++ b/gotify_tray/gui/themes/__init__.py @@ -1,5 +1,5 @@ import logging -from PyQt6 import QtGui, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets from gotify_tray.utils import get_abs_path from . import default, dark_purple, light_purple from gotify_tray.database import Settings @@ -9,28 +9,50 @@ settings = Settings("gotify-tray") logger = logging.getLogger("gotify-tray") -styles = { +themes = { "default": default, + "automatic": None, "dark purple": dark_purple, "light purple": light_purple, } -def set_theme(app: QtWidgets.QApplication, style: str = "default"): - if style not in styles.keys(): - logger.error(f"set_style: style {style} is unsupported.") - return +def get_themes(): + return themes.keys() + + +def is_dark_mode(app: QtWidgets.QApplication) -> bool: + return app.styleHints().colorScheme() == QtCore.Qt.ColorScheme.Dark + + +def is_valid_theme(theme: str) -> bool: + return theme in get_themes() + + +def set_theme(app: QtWidgets.QApplication, theme: str = "automatic"): + if not is_valid_theme(theme): + logger.warning(f"set_theme: theme {theme} is unsupported.") + theme = "automatic" + + if theme == "automatic": + theme = "dark purple" if is_dark_mode(app) else "light purple" stylesheet = "" - with open(get_abs_path(f"gotify_tray/gui/themes/{style.replace(' ', '_')}/style.qss"), "r") as f: + with open(get_abs_path(f"gotify_tray/gui/themes/{theme.replace(' ', '_')}/style.qss"), "r") as f: stylesheet += f.read() - app.setPalette(styles[style].get_palette()) + app.setPalette(themes[theme].get_palette()) app.setStyleSheet(stylesheet) -def get_themes(): - return styles.keys() - -def get_theme_file(file: str, theme: str = None) -> str: + +def get_theme_file(app: QtWidgets.QApplication, file: str, theme: str = None) -> str: theme = settings.value("theme", type=str) if not theme else theme + + if not is_valid_theme(theme): + logger.warning(f"set_theme: theme {theme} is unsupported.") + theme = "automatic" + + if theme in ("automatic", "default"): + theme = "dark purple" if is_dark_mode(app) else "light purple" + return get_abs_path(f"gotify_tray/gui/themes/{theme.replace(' ', '_')}/{file}") diff --git a/gotify_tray/gui/themes/dark_purple/style.qss b/gotify_tray/gui/themes/dark_purple/style.qss index c2d5162..d33b80c 100644 --- a/gotify_tray/gui/themes/dark_purple/style.qss +++ b/gotify_tray/gui/themes/dark_purple/style.qss @@ -6,6 +6,22 @@ QPushButton:default:hover, QPushButton:checked:hover { background: #441b85; } +QPushButton[state="success"] { + background-color: #960b7a0b; + color: white; +} + +QPushButton[state="failed"] { + background-color: #8ebb2929; + color: white; +} + +QLineEdit[state="success"] {} + +QLineEdit[state="failed"] { + border: 1px solid red; +} + QToolTip { color: #BFBFBF; background-color: #5522a8; diff --git a/gotify_tray/gui/themes/default/refresh.svg b/gotify_tray/gui/themes/default/refresh.svg deleted file mode 100644 index 3257974..0000000 --- a/gotify_tray/gui/themes/default/refresh.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gotify_tray/gui/themes/default/status_active.svg b/gotify_tray/gui/themes/default/status_active.svg deleted file mode 100644 index 7af0e8e..0000000 --- a/gotify_tray/gui/themes/default/status_active.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - diff --git a/gotify_tray/gui/themes/default/status_connecting.svg b/gotify_tray/gui/themes/default/status_connecting.svg deleted file mode 100644 index 8b1a364..0000000 --- a/gotify_tray/gui/themes/default/status_connecting.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - diff --git a/gotify_tray/gui/themes/default/status_error.svg b/gotify_tray/gui/themes/default/status_error.svg deleted file mode 100644 index 2f89ca1..0000000 --- a/gotify_tray/gui/themes/default/status_error.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - diff --git a/gotify_tray/gui/themes/default/status_inactive.svg b/gotify_tray/gui/themes/default/status_inactive.svg deleted file mode 100644 index 2d0eba9..0000000 --- a/gotify_tray/gui/themes/default/status_inactive.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - diff --git a/gotify_tray/gui/themes/default/trashcan.svg b/gotify_tray/gui/themes/default/trashcan.svg deleted file mode 100644 index 94b811f..0000000 --- a/gotify_tray/gui/themes/default/trashcan.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/gotify_tray/gui/themes/light_purple/style.qss b/gotify_tray/gui/themes/light_purple/style.qss index 196a85c..5e811b2 100644 --- a/gotify_tray/gui/themes/light_purple/style.qss +++ b/gotify_tray/gui/themes/light_purple/style.qss @@ -6,6 +6,22 @@ QPushButton:default:hover, QPushButton:checked:hover { background: #5c24b6; } +QPushButton[state="success"] { + background-color: #6400FF00; + color: black; +} + +QPushButton[state="failed"] { + background-color: #64FF0000; + color: black; +} + +QLineEdit[state="success"] {} + +QLineEdit[state="failed"] { + border: 1px solid red; +} + QToolTip { color: #BFBFBF; background-color: #5522a8; diff --git a/gotify_tray/gui/widgets/MainWindow.py b/gotify_tray/gui/widgets/MainWindow.py index a4c095b..e502949 100644 --- a/gotify_tray/gui/widgets/MainWindow.py +++ b/gotify_tray/gui/widgets/MainWindow.py @@ -26,7 +26,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): activated = QtCore.pyqtSignal() def __init__( - self, application_model: ApplicationModel, messages_model: MessagesModel + self, app: QtWidgets.QApplication, + application_model: ApplicationModel, messages_model: MessagesModel ): super(MainWindow, self).__init__() self.setupUi(self) @@ -35,6 +36,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.setWindowTitle(__title__) + self.app = app + self.application_model = application_model self.messages_model = messages_model @@ -47,7 +50,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # Do not collapse the message list self.splitter.setCollapsible(1, False) - self.status_widget = StatusWidget() + self.status_widget = StatusWidget(app) self.horizontalLayout.insertWidget(0, self.status_widget) self.set_icons() @@ -70,8 +73,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): def set_icons(self): # Set button icons - self.pb_refresh.setIcon(QtGui.QIcon(get_theme_file("refresh.svg"))) - self.pb_delete_all.setIcon(QtGui.QIcon(get_theme_file("trashcan.svg"))) + self.pb_refresh.setIcon(QtGui.QIcon(get_theme_file(self.app, "refresh.svg"))) + self.pb_delete_all.setIcon(QtGui.QIcon(get_theme_file(self.app, "trashcan.svg"))) # Resize the labels and icons size = settings.value("MainWindow/label/size", type=int) @@ -105,7 +108,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self, message_item: MessagesModelItem, image_path: str = "" ): message_widget = MessageWidget( - self.listView_messages, message_item, image_path=image_path + self.app, self.listView_messages, message_item, image_path=image_path ) self.listView_messages.setIndexWidget( self.messages_model.indexFromItem(message_item), message_widget diff --git a/gotify_tray/gui/widgets/MessageWidget.py b/gotify_tray/gui/widgets/MessageWidget.py index 4147e6f..0f87eae 100644 --- a/gotify_tray/gui/widgets/MessageWidget.py +++ b/gotify_tray/gui/widgets/MessageWidget.py @@ -20,11 +20,13 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form): def __init__( self, + app: QtWidgets.QApplication, parent: QtWidgets.QWidget, message_item: MessagesModelItem, image_path: str = "", ): super(MessageWidget, self).__init__() + self.app = app self.parent = parent self.setupUi(self) self.setAutoFillBackground(True) @@ -104,7 +106,7 @@ class MessageWidget(QtWidgets.QWidget, Ui_Form): self.label_message.setFont(font_content) def set_icons(self): - self.pb_delete.setIcon(QtGui.QIcon(get_theme_file("trashcan.svg"))) + self.pb_delete.setIcon(QtGui.QIcon(get_theme_file(self.app, "trashcan.svg"))) self.pb_delete.setIconSize(QtCore.QSize(24, 24)) def set_message_image(self, filename: str): diff --git a/gotify_tray/gui/widgets/ServerInfoDialog.py b/gotify_tray/gui/widgets/ServerInfoDialog.py index b87e039..aac812a 100644 --- a/gotify_tray/gui/widgets/ServerInfoDialog.py +++ b/gotify_tray/gui/widgets/ServerInfoDialog.py @@ -47,10 +47,18 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog): self.task.incorrect_url.connect(self.incorrect_url_callback) self.task.start() + def update_widget_state(self, widget: QtWidgets.QWidget, state: str): + widget.setProperty("state", state) + widget.style().unpolish(widget) + widget.style().polish(widget) + widget.update() + def server_info_success(self, version: GotifyVersionModel): self.pb_test.setEnabled(True) self.label_server_info.setText(f"Version: {version.version}") - self.pb_test.setStyleSheet("background-color: rgba(0, 255, 0, 100);") + self.update_widget_state(self.pb_test, "success") + self.update_widget_state(self.line_token, "success") + self.update_widget_state(self.line_url, "success") self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) @@ -59,15 +67,17 @@ class ServerInfoDialog(QtWidgets.QDialog, Ui_Dialog): def incorrect_token_callback(self, version: GotifyVersionModel): self.pb_test.setEnabled(True) self.label_server_info.setText(f"Version: {version.version}") - self.pb_test.setStyleSheet("background-color: rgba(255, 0, 0, 100);") - self.line_token.setStyleSheet("border: 1px solid red;") + self.update_widget_state(self.pb_test, "failed") + self.update_widget_state(self.line_token, "failed") + self.update_widget_state(self.line_url, "success") self.line_token.setFocus() def incorrect_url_callback(self): self.pb_test.setEnabled(True) self.label_server_info.clear() - self.pb_test.setStyleSheet("background-color: rgba(255, 0, 0, 100);") - self.line_url.setStyleSheet("border: 1px solid red;") + self.update_widget_state(self.pb_test, "failed") + self.update_widget_state(self.line_token, "success") + self.update_widget_state(self.line_url, "failed") self.line_url.setFocus() def import_success_callback(self): diff --git a/gotify_tray/gui/widgets/SettingsDialog.py b/gotify_tray/gui/widgets/SettingsDialog.py index 7dded76..10c47db 100644 --- a/gotify_tray/gui/widgets/SettingsDialog.py +++ b/gotify_tray/gui/widgets/SettingsDialog.py @@ -27,10 +27,12 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): quit_requested = QtCore.pyqtSignal() theme_change_requested = QtCore.pyqtSignal(str) - def __init__(self): + def __init__(self, app: QtWidgets.QApplication): super(SettingsDialog, self).__init__() self.setupUi(self) self.setWindowTitle("Settings") + + self.app = app self.settings_changed = False self.changes_applied = False @@ -104,6 +106,7 @@ class SettingsDialog(QtWidgets.QDialog, Ui_Dialog): def add_message_widget(self): self.message_widget = MessageWidget( + self.app, self, MessagesModelItem( GotifyMessageModel( diff --git a/gotify_tray/gui/widgets/StatusWidget.py b/gotify_tray/gui/widgets/StatusWidget.py index b16938a..2d57bd0 100644 --- a/gotify_tray/gui/widgets/StatusWidget.py +++ b/gotify_tray/gui/widgets/StatusWidget.py @@ -8,8 +8,9 @@ settings = Settings("gotify-tray") class StatusWidget(QtWidgets.QLabel): - def __init__(self): + def __init__(self, app: QtWidgets.QApplication): super(StatusWidget, self).__init__() + self.app = app self.setFixedSize(QtCore.QSize(20, 20)) self.setScaledContents(True) self.set_connecting() @@ -17,7 +18,7 @@ class StatusWidget(QtWidgets.QLabel): def set_status(self, image: str): self.image = image - self.setPixmap(QtGui.QPixmap(get_theme_file(image))) + self.setPixmap(QtGui.QPixmap(get_theme_file(self.app, image))) def set_active(self): self.setToolTip("Listening for new messages") diff --git a/requirements.txt b/requirements.txt index b4d1061..ecb2b9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ requests==2.28.2 websocket-client==1.5.1 -pyqt6==6.4.2 +pyqt6==6.5.0 python-dateutil==2.8.2