From 46f978e6f0714ca8ed95a26243e2c3a98aba5c1f Mon Sep 17 00:00:00 2001 From: Garrett Robinson Date: Thu, 28 Sep 2023 16:21:13 +0300 Subject: [PATCH] Detect OS color mode and set as property for stylesheets Sets the detected OS color mode (dark/light) as a property on the QApplication so it can be referenced in stylesheets to select style rules suited to the OS color mode. --- dangerzone/gui/__init__.py | 30 ++++++++++++++++++++++++++++++ dangerzone/gui/logic.py | 4 +++- dangerzone/gui/main_window.py | 5 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/dangerzone/gui/__init__.py b/dangerzone/gui/__init__.py index c35d232..655b13b 100644 --- a/dangerzone/gui/__init__.py +++ b/dangerzone/gui/__init__.py @@ -1,3 +1,4 @@ +import enum import functools import logging import os @@ -33,6 +34,18 @@ from .updater import UpdaterThread log = logging.getLogger(__name__) +class OSColorMode(enum.Enum): + """ + Operating system color mode, e.g. Light or Dark Mode on macOS 10.14+ or Windows 10+. + + The enum values are used as the names of Qt properties that will be selected by QSS + property selectors to set color-mode-specific style rules. + """ + + LIGHT = "light" + DARK = "dark" + + class Application(QtWidgets.QApplication): document_selected = QtCore.Signal(list) application_activated = QtCore.Signal() @@ -61,6 +74,23 @@ class Application(QtWidgets.QApplication): self.event = monkeypatch_event # type: ignore [method-assign] + self.os_color_mode = self.infer_os_color_mode() + log.debug(f"Inferred system color scheme as {self.os_color_mode}") + + def infer_os_color_mode(self) -> OSColorMode: + """ + Qt 6.5+ explicitly provides the OS color scheme via QStyleHints.colorScheme(), + but we still need to support PySide2/Qt 5, so instead we infer the OS color + scheme from the default palette. + """ + text_color, window_color = ( + self.palette().color(role) + for role in (QtGui.QPalette.WindowText, QtGui.QPalette.Window) + ) + if text_color.lightness() > window_color.lightness(): + return OSColorMode.DARK + return OSColorMode.LIGHT + @click.command() @click.option( diff --git a/dangerzone/gui/logic.py b/dangerzone/gui/logic.py index 3832c56..74d7d1e 100644 --- a/dangerzone/gui/logic.py +++ b/dangerzone/gui/logic.py @@ -12,6 +12,8 @@ from colorama import Fore # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. if typing.TYPE_CHECKING: from PySide2 import QtCore, QtGui, QtWidgets + + from . import Application else: try: from PySide6 import QtCore, QtGui, QtWidgets @@ -35,7 +37,7 @@ class DangerzoneGui(DangerzoneCore): """ def __init__( - self, app: QtWidgets.QApplication, isolation_provider: IsolationProvider + self, app: "Application", isolation_provider: IsolationProvider ) -> None: super().__init__(isolation_provider) diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index 0f7253d..acbf6c4 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -162,6 +162,11 @@ class MainWindow(QtWidgets.QMainWindow): central_widget.setLayout(layout) self.setCentralWidget(central_widget) + # Set the OS color mode as a property on the MainWindow, which is the closest + # thing we have to a top-level container element akin to an HTML ``. + # This allows us to make QSS rules conditional on the OS color mode. + self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value) + self.show() def load_svg_image(self, filename: str) -> QtGui.QPixmap: