From bc97485ff235a534c0c0841c86b009b50ab58ffe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 22 Mar 2022 18:12:47 +0530 Subject: [PATCH] Linux: Use the system dark mode setting This comes from the desktop settings portal implemented in KDE 5.24 and GNOME 42 --- manual/customize.rst | 2 +- src/calibre/gui2/__init__.py | 37 ++++++++++++++++++++++++++++++++++-- src/qt/__init__.py | 2 +- src/qt/__main__.py | 5 ++++- src/qt/dbus.py | 14 ++++++++++++++ src/qt/dbus.pyi | 20 +++++++++++++++++++ src/qt/dbus_name_map.py | 22 +++++++++++++++++++++ 7 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/qt/dbus.py create mode 100644 src/qt/dbus.pyi create mode 100644 src/qt/dbus_name_map.py diff --git a/manual/customize.rst b/manual/customize.rst index 482c6bf36b..0229e5451a 100644 --- a/manual/customize.rst +++ b/manual/customize.rst @@ -49,7 +49,7 @@ Environment variables the system theme -- beware of crashes and hangs. * ``CALIBRE_SHOW_DEPRECATION_WARNINGS`` - causes calibre to print deprecation warnings to stdout. Useful for calibre developers. * ``CALIBRE_NO_DEFAULT_PROGRAMS`` - prevent calibre from automatically registering the filetypes it is capable of handling with Windows. - * ``CALIBRE_USE_DARK_PALETTE`` - set it to ``1`` to have calibre use dark colors. Works on **Linux only**. + * ``CALIBRE_USE_DARK_PALETTE`` - set it to ``1`` to have calibre use dark colors and ``0`` for light colors. Works on **Linux only**. * ``SYSFS_PATH`` - Use if sysfs is mounted somewhere other than /sys * ``http_proxy``, ``https_proxy`` - used on Linux to specify an HTTP(S) proxy diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 5eb29b3e3e..9f124815d7 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -16,7 +16,7 @@ from qt.core import ( QFileIconProvider, QFileInfo, QFont, QFontDatabase, QFontInfo, QFontMetrics, QGuiApplication, QIcon, QIODevice, QLocale, QNetworkProxyFactory, QObject, QPalette, QResource, QSettings, QSocketNotifier, QStringListModel, QStyle, Qt, - QThread, QTimer, QTranslator, QUrl, pyqtSignal + QThread, QTimer, QTranslator, QUrl, pyqtSignal, pyqtSlot ) from threading import Lock, RLock @@ -47,6 +47,8 @@ try: NO_URL_FORMATTING = QUrl.UrlFormattingOption.None_ except AttributeError: NO_URL_FORMATTING = getattr(QUrl, 'None') +if islinux: + from qt.dbus import QDBusConnection, QDBusMessage, QDBusVariant class IconResourceManager: @@ -1234,7 +1236,14 @@ class Application(QApplication): elif ismacos: use_dark_palette = False else: - use_dark_palette = os.environ.get('CALIBRE_USE_DARK_PALETTE') == '1' + if 'CALIBRE_USE_DARK_PALETTE' in os.environ: + use_dark_palette = os.environ.get('CALIBRE_USE_DARK_PALETTE') == '1' + else: + use_dark_palette = linux_is_system_dark_mode_enabled() + bus = QDBusConnection.sessionBus() + bus.connect( + 'org.freedesktop.portal.Desktop', '/org/freedesktop/portal/desktop', + 'org.freedesktop.portal.Settings', 'SettingChanged', 'ssv', self.linux_desktop_setting_changed) if use_dark_palette: self.set_dark_mode_palette() elif self.original_palette_modified: @@ -1247,6 +1256,18 @@ class Application(QApplication): self.load_calibre_style() self.on_palette_change() + if islinux: + @pyqtSlot(str, str, QDBusVariant) + def linux_desktop_setting_changed(self, namespace, key, val): + if (namespace, key) == ('org.freedesktop.appearance', 'color-scheme'): + use_dark_palette = val.variant() == 1 + if use_dark_palette != bool(self.is_dark_theme): + if use_dark_palette: + self.set_dark_mode_palette() + else: + self.set_palette(self.original_palette) + self.on_palette_change() + def check_for_windows_palette_change(self): use_dark_palette = bool(windows_is_system_dark_mode_enabled()) if bool(self.is_dark_theme) != use_dark_palette: @@ -1670,6 +1691,18 @@ def windows_is_system_dark_mode_enabled(): return False +def linux_is_system_dark_mode_enabled(): + bus = QDBusConnection.sessionBus() + m = QDBusMessage.createMethodCall( + 'org.freedesktop.portal.Desktop', '/org/freedesktop/portal/desktop', + 'org.freedesktop.portal.Settings', 'Read' + ) + m.setArguments(['org.freedesktop.appearance', 'color-scheme']) + reply = bus.call(m, timeout=1000) + a = reply.arguments() + return len(a) and isinstance(a[0], int) and a[0] == 1 + + def make_view_use_window_background(view): p = view.palette() p.setColor(QPalette.ColorRole.Base, p.color(QPalette.ColorRole.Window)) diff --git a/src/qt/__init__.py b/src/qt/__init__.py index f6cdf02131..fa610e4a66 100644 --- a/src/qt/__init__.py +++ b/src/qt/__init__.py @@ -1,5 +1,5 @@ # autogenerated by __main__.py do not edit -top_level_module_names=('QtCore', 'QtGui', 'QtWidgets', 'QtNetwork', 'QtSvg', 'QtPrintSupport', 'QtWebEngineCore', 'QtWebEngineWidgets') +top_level_module_names=('QtCore', 'QtGui', 'QtWidgets', 'QtNetwork', 'QtSvg', 'QtPrintSupport', 'QtWebEngineCore', 'QtWebEngineWidgets', 'QtDBus') def __getattr__(name): diff --git a/src/qt/__main__.py b/src/qt/__main__.py index fd1b3747f1..35bfdcd148 100644 --- a/src/qt/__main__.py +++ b/src/qt/__main__.py @@ -22,6 +22,9 @@ module_lists = { 'QtWebEngineCore', 'QtWebEngineWidgets', ), + 'dbus': ( + 'QtDBus', + ) } @@ -54,7 +57,7 @@ def scan(name): top_level_module_names = () -for name in ('core', 'webengine'): +for name in ('core', 'webengine', 'dbus'): top_level_module_names += module_lists[name] scan(name) with open(f'{base}/__init__.py', 'w') as f: diff --git a/src/qt/dbus.py b/src/qt/dbus.py new file mode 100644 index 0000000000..3fb3f5dc3a --- /dev/null +++ b/src/qt/dbus.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2021, Kovid Goyal + + +from .loader import dynamic_load +from .dbus_name_map import name_map, module_names + +already_imported = {} +qt_modules = {} + + +def __getattr__(name): + return dynamic_load(name, name_map, already_imported, qt_modules, module_names) diff --git a/src/qt/dbus.pyi b/src/qt/dbus.pyi new file mode 100644 index 0000000000..f289d8c3b4 --- /dev/null +++ b/src/qt/dbus.pyi @@ -0,0 +1,20 @@ +# autogenerated by __main__.py do not edit +import PyQt6.QtDBus +QDBus = PyQt6.QtDBus.QDBus +QDBusAbstractAdaptor = PyQt6.QtDBus.QDBusAbstractAdaptor +QDBusAbstractInterface = PyQt6.QtDBus.QDBusAbstractInterface +QDBusArgument = PyQt6.QtDBus.QDBusArgument +QDBusConnection = PyQt6.QtDBus.QDBusConnection +QDBusConnectionInterface = PyQt6.QtDBus.QDBusConnectionInterface +QDBusError = PyQt6.QtDBus.QDBusError +QDBusInterface = PyQt6.QtDBus.QDBusInterface +QDBusMessage = PyQt6.QtDBus.QDBusMessage +QDBusObjectPath = PyQt6.QtDBus.QDBusObjectPath +QDBusPendingCall = PyQt6.QtDBus.QDBusPendingCall +QDBusPendingCallWatcher = PyQt6.QtDBus.QDBusPendingCallWatcher +QDBusPendingReply = PyQt6.QtDBus.QDBusPendingReply +QDBusReply = PyQt6.QtDBus.QDBusReply +QDBusServiceWatcher = PyQt6.QtDBus.QDBusServiceWatcher +QDBusSignature = PyQt6.QtDBus.QDBusSignature +QDBusUnixFileDescriptor = PyQt6.QtDBus.QDBusUnixFileDescriptor +QDBusVariant = PyQt6.QtDBus.QDBusVariant \ No newline at end of file diff --git a/src/qt/dbus_name_map.py b/src/qt/dbus_name_map.py new file mode 100644 index 0000000000..9a5a6e9a4a --- /dev/null +++ b/src/qt/dbus_name_map.py @@ -0,0 +1,22 @@ +# autogenerated by __main__.py do not edit +name_map = {'QDBus': 'PyQt6.QtDBus', + 'QDBusAbstractAdaptor': 'PyQt6.QtDBus', + 'QDBusAbstractInterface': 'PyQt6.QtDBus', + 'QDBusArgument': 'PyQt6.QtDBus', + 'QDBusConnection': 'PyQt6.QtDBus', + 'QDBusConnectionInterface': 'PyQt6.QtDBus', + 'QDBusError': 'PyQt6.QtDBus', + 'QDBusInterface': 'PyQt6.QtDBus', + 'QDBusMessage': 'PyQt6.QtDBus', + 'QDBusObjectPath': 'PyQt6.QtDBus', + 'QDBusPendingCall': 'PyQt6.QtDBus', + 'QDBusPendingCallWatcher': 'PyQt6.QtDBus', + 'QDBusPendingReply': 'PyQt6.QtDBus', + 'QDBusReply': 'PyQt6.QtDBus', + 'QDBusServiceWatcher': 'PyQt6.QtDBus', + 'QDBusSignature': 'PyQt6.QtDBus', + 'QDBusUnixFileDescriptor': 'PyQt6.QtDBus', + 'QDBusVariant': 'PyQt6.QtDBus', + 'QtDBus': 'PyQt6.QtDBus'} +module_names = frozenset(('QtDBus',) +)