From 085862b062b4abcf7127cffcc919389826c17dd8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 4 Feb 2024 11:52:10 +0530 Subject: [PATCH] Change to using a fixed standard light palette This fixes all manner of subtle issues introduced by Qt's broken attempts to map system colors to a QPalette. 1) Highlight color being same as Windows' accent color (this is anyway reverted in Qt 6.6) 2) Window titlebars not following system light/dark changes on Windows. 3) All the custom code that I wrote for tracking system light/dark changes can be dropped in favor of the new QStyleHints::colorScheme property 4) We had a fixed dark mode palette but not a fixed light mode palette. I am sure people, especially on Linux, are going to complain about this, but there are simply too many upsides. I will perhaps add a way to configure the details of the light and dark palettes for people that need it. This dialog could theoretically have a button to try to import the standard system palette automatically to create the custom palette. --- src/calibre/gui2/palette.py | 244 +++++++----------------------------- 1 file changed, 48 insertions(+), 196 deletions(-) diff --git a/src/calibre/gui2/palette.py b/src/calibre/gui2/palette.py index 9334013e5b..7a9f279355 100644 --- a/src/calibre/gui2/palette.py +++ b/src/calibre/gui2/palette.py @@ -5,8 +5,8 @@ import os import sys from contextlib import contextmanager from qt.core import ( - QAbstractNativeEventFilter, QApplication, QColor, QIcon, QPalette, QSettings, QProxyStyle, - QStyle, Qt, QTimer, pyqtSlot, QObject, QDataStream, QByteArray, QIODeviceBase + QApplication, QByteArray, QColor, QDataStream, QIcon, QIODeviceBase, QObject, + QPalette, QProxyStyle, QStyle, Qt ) from calibre.constants import DEBUG, dark_link_color, ismacos, iswindows @@ -14,7 +14,7 @@ from calibre.constants import DEBUG, dark_link_color, ismacos, iswindows dark_link_color = QColor(dark_link_color) dark_color = QColor(45,45,45) dark_text_color = QColor('#ddd') -light_color = QColor(0xef, 0xef, 0xef) +light_color = QColor(0xf0, 0xf0, 0xf0) light_text_color = QColor(0,0,0) light_link_color = QColor(0, 0, 255) @@ -28,49 +28,6 @@ class UseCalibreIcons(QProxyStyle): return ic -if iswindows: - import ctypes - - class WinEventFilter(QAbstractNativeEventFilter): - - def nativeEventFilter(self, eventType, message): - if eventType == b"windows_generic_MSG": - msg = ctypes.wintypes.MSG.from_address(message.__int__()) - # https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-settingchange - if msg.message == 0x001A and msg.lParam: # WM_SETTINGCHANGE - try: - s = ctypes.wstring_at(msg.lParam) - except OSError: - pass - else: - if s == 'ImmersiveColorSet': - QApplication.instance().palette_manager.check_for_windows_palette_change() - # prevent Qt from handling this event - return True, 0 - return False, 0 -if not iswindows and not ismacos: - from qt.dbus import QDBusConnection, QDBusMessage, QDBusVariant - - -def windows_is_system_dark_mode_enabled(): - s = QSettings(r"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", QSettings.Format.NativeFormat) - if s.status() == QSettings.Status.NoError: - return s.value("AppsUseLightTheme") == 0 - 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 palette_is_dark(self): col = self.color(QPalette.ColorRole.Window) return max(col.getRgb()[:3]) < 115 @@ -107,17 +64,6 @@ QPalette.serialize_as_python = serialize_palette_as_python QPalette.unserialize_from_bytes = unserialize_palette -def fix_palette_colors(p): - if iswindows: - # On Windows the highlighted colors for inactive widgets are the - # same as non highlighted colors. This is a regression from Qt 4. - # https://bugreports.qt-project.org/browse/QTBUG-41060 - for role in (QPalette.ColorRole.Highlight, QPalette.ColorRole.HighlightedText, QPalette.ColorRole.Base, QPalette.ColorRole.AlternateBase): - p.setColor(QPalette.ColorGroup.Inactive, role, p.color(QPalette.ColorGroup.Active, role)) - return True - return False - - def dark_palette(): p = QPalette() disabled_color = QColor(127,127,127) @@ -135,6 +81,7 @@ def dark_palette(): p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_color) p.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) p.setColor(QPalette.ColorRole.Link, dark_link_color) + p.setColor(QPalette.ColorRole.LinkVisited, Qt.GlobalColor.darkMagenta) p.setColor(QPalette.ColorRole.Highlight, QColor(0x0b, 0x45, 0xc4)) p.setColor(QPalette.ColorRole.HighlightedText, dark_text_color) @@ -144,93 +91,30 @@ def dark_palette(): def light_palette(): # {{{ - # generated by serializing the light palette on my Linux system - self = QPalette() - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.WindowText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Button, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Light, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Midlight, QColor(202, 202, 202, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Dark, QColor(159, 159, 159, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Mid, QColor(184, 184, 184, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Text, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.BrightText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.ButtonText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Base, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Window, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Shadow, QColor(118, 118, 118, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Highlight, QColor(48, 140, 198, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.HighlightedText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Link, QColor(0, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.LinkVisited, QColor(255, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.AlternateBase, QColor(247, 247, 247, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.ToolTipBase, QColor(255, 255, 220, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.ToolTipText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.PlaceholderText, QColor(0, 0, 0, 128)) - self.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.NoRole, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor(190, 190, 190, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Button, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Light, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Midlight, QColor(202, 202, 202, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Dark, QColor(190, 190, 190, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Mid, QColor(184, 184, 184, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(190, 190, 190, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.BrightText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(190, 190, 190, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Window, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Shadow, QColor(177, 177, 177, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Highlight, QColor(145, 145, 145, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Link, QColor(0, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.LinkVisited, QColor(255, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.AlternateBase, QColor(247, 247, 247, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ToolTipBase, QColor(255, 255, 220, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ToolTipText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.PlaceholderText, QColor(0, 0, 0, 128)) - self.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.NoRole, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.WindowText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Button, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Light, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Midlight, QColor(202, 202, 202, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Dark, QColor(159, 159, 159, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Mid, QColor(184, 184, 184, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Text, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.BrightText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ButtonText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Base, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Window, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Shadow, QColor(118, 118, 118, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Highlight, QColor(48, 140, 198, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.HighlightedText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Link, QColor(0, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.LinkVisited, QColor(255, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.AlternateBase, QColor(247, 247, 247, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ToolTipBase, QColor(255, 255, 220, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ToolTipText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.PlaceholderText, QColor(0, 0, 0, 128)) - self.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.NoRole, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.WindowText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Button, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Light, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Midlight, QColor(202, 202, 202, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Dark, QColor(159, 159, 159, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Mid, QColor(184, 184, 184, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Text, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.BrightText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.ButtonText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Base, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Window, QColor(239, 239, 239, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Shadow, QColor(118, 118, 118, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Highlight, QColor(48, 140, 198, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.HighlightedText, QColor(255, 255, 255, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.Link, QColor(0, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.LinkVisited, QColor(255, 0, 255, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.AlternateBase, QColor(247, 247, 247, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.ToolTipBase, QColor(255, 255, 220, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.ToolTipText, QColor(0, 0, 0, 255)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.PlaceholderText, QColor(0, 0, 0, 128)) - self.setColor(QPalette.ColorGroup.Current, QPalette.ColorRole.NoRole, QColor(0, 0, 0, 255)) - return self + p = QPalette() + disabled_color = QColor(120,120,120) + p.setColor(QPalette.ColorRole.Window, light_color) + p.setColor(QPalette.ColorRole.WindowText, light_text_color) + p.setColor(QPalette.ColorRole.PlaceholderText, disabled_color) + p.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.white) + p.setColor(QPalette.ColorRole.AlternateBase, QColor(245, 245, 245)) + p.setColor(QPalette.ColorRole.ToolTipBase, light_color) + p.setColor(QPalette.ColorRole.ToolTipText, light_text_color) + p.setColor(QPalette.ColorRole.Text, light_text_color) + p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_color) + p.setColor(QPalette.ColorRole.Button, light_color) + p.setColor(QPalette.ColorRole.ButtonText, light_text_color) + p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_color) + p.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) + p.setColor(QPalette.ColorRole.Link, light_link_color) + p.setColor(QPalette.ColorRole.LinkVisited, Qt.GlobalColor.magenta) + + p.setColor(QPalette.ColorRole.Highlight, QColor(48, 140, 198)) + p.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.white) + p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, disabled_color) + + return p + # }}} @@ -272,7 +156,6 @@ class PaletteManager(QObject): color_palette: str has_fixed_palette: bool using_calibre_style: bool - original_palette_modified: bool is_dark_theme: bool def __init__(self, color_palette, ui_style, force_calibre_style, headless): @@ -291,13 +174,6 @@ class PaletteManager(QObject): self.has_fixed_palette = self.color_palette != 'system' and self.using_calibre_style args = [] - if iswindows: - # passing darkmode=1 turns on dark window frames when windows - # is dark and darkmode=2 makes everything dark, but we have our - # own dark mode implementation when using calibre style so - # prefer that and use darkmode=1 - args.append('-platform') - args.append('windows:darkmode=' + ('1' if self.using_calibre_style else '2')) self.args_to_qt = tuple(args) if ismacos and not headless and self.has_fixed_palette: from calibre_extensions.cocoa import set_appearance @@ -309,33 +185,29 @@ class PaletteManager(QObject): if not self.using_calibre_style and app.style().objectName() == 'fusion': # Since Qt is using the fusion style anyway, specialize it self.using_calibre_style = True - self.original_palette = QPalette(app.palette()) - self.original_palette_modified = fix_palette_colors(self.original_palette) - if iswindows: - self.win_event_filter = WinEventFilter() - app.installNativeEventFilter(self.win_event_filter) def setup_styles(self): if self.using_calibre_style: + app = QApplication.instance() + system_is_dark = app.styleHints().colorScheme() == Qt.ColorScheme.Dark + app.styleHints().colorSchemeChanged.connect(self.color_scheme_changed) if iswindows: - use_dark_palette = self.color_palette == 'dark' or (self.color_palette == 'system' and windows_is_system_dark_mode_enabled()) + use_dark_palette = self.color_palette == 'dark' or (self.color_palette == 'system' and system_is_dark) elif ismacos: use_dark_palette = self.color_palette == 'dark' else: - use_dark_palette = self.color_palette == 'dark' or (self.color_palette == 'system' and 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) + use_dark_palette = self.color_palette == 'dark' or (self.color_palette == 'system' and system_is_dark) if use_dark_palette: self.set_dark_mode_palette() - elif self.original_palette_modified: - self.set_palette(self.original_palette) + else: + self.set_light_mode_palette() if self.has_fixed_palette and (self.color_palette == 'dark') != QApplication.instance().palette().is_dark_theme(): if self.color_palette == 'dark': self.set_dark_mode_palette() else: self.set_light_mode_palette() + if self.has_fixed_palette: + QApplication.instance().setAttribute(Qt.ApplicationAttribute.AA_SetPalette, True) if DEBUG: print('Using calibre Qt style:', self.using_calibre_style, file=sys.stderr) @@ -418,30 +290,18 @@ QTabBar::tab:only-one { def set_light_mode_palette(self): self.set_palette(light_palette()) - if not iswindows and not ismacos: - @pyqtSlot(str, str, QDBusVariant) - def linux_desktop_setting_changed(self, namespace, key, val): - if (namespace, key) == ('org.freedesktop.appearance', 'color-scheme'): - if self.has_fixed_palette: - return - 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): + def color_scheme_changed(self, new_color_scheme): + if DEBUG: + print('System Color Scheme changed to:', new_color_scheme, file=sys.stderr) if self.has_fixed_palette: return - use_dark_palette = bool(windows_is_system_dark_mode_enabled()) - if bool(self.is_dark_theme) != use_dark_palette: - if use_dark_palette: - self.set_dark_mode_palette() - else: - self.set_palette(self.original_palette) - self.on_palette_change() + if new_color_scheme == Qt.ColorScheme.Dark: + self.set_dark_mode_palette() + elif new_color_scheme == Qt.ColorScheme.Light: + self.set_light_mode_palette() + elif new_color_scheme == Qt.ColorScheme.Unknown: + self.set_light_mode_palette() + self.on_palette_change() @contextmanager def changing_palette(self): @@ -455,14 +315,6 @@ QTabBar::tab:only-one { def set_palette(self, pal): with self.changing_palette(): QApplication.instance().setPalette(pal) - # Needed otherwise Qt does not emit the paletteChanged signal when - # appearance is changed. And it has to be after current event - # processing finishes as of Qt 5.14 otherwise the palette change is - # ignored. - QTimer.singleShot(1000, self.mark_palette_as_unchanged_for_qt) - - def mark_palette_as_unchanged_for_qt(self): - QApplication.instance().setAttribute(Qt.ApplicationAttribute.AA_SetPalette, False) def on_qt_palette_change(self): if self.ignore_palette_changes: @@ -472,7 +324,7 @@ QTabBar::tab:only-one { if DEBUG: print('ApplicationPaletteChange event received', file=sys.stderr) if self.has_fixed_palette: - pal = dark_palette() if self.color_palette == 'dark' else self.original_palette + pal = dark_palette() if self.color_palette == 'dark' else light_palette() if QApplication.instance().palette().color(QPalette.ColorRole.Window) != pal.color(QPalette.ColorRole.Window): if DEBUG: print('Detected a spontaneous palette change by Qt, reverting it', file=sys.stderr)