diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index 78b2ba1..8be4d7d 100755 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -10,7 +10,7 @@ from configparser import SafeConfigParser, DEFAULTSECT from syncplay import constants, utils, version, milestone from syncplay.messages import getMessage, setLanguage, isValidLanguage from syncplay.players.playerFactory import PlayerFactory -from syncplay.utils import isMacOS +from syncplay.utils import isMacOS, isWindows class InvalidConfigValue(Exception): @@ -514,13 +514,22 @@ class ConfigurationGetter(object): self._overrideConfigWithArgs(args) if not self._config['noGui']: try: - from syncplay.vendor.Qt import QtWidgets, IsPySide, IsPySide2 + from syncplay.vendor.Qt import QtWidgets, IsPySide, IsPySide2, QtGui from syncplay.vendor.Qt.QtCore import QCoreApplication from syncplay.vendor import qt5reactor if not (IsPySide2 or IsPySide): raise ImportError if QCoreApplication.instance() is None: self.app = QtWidgets.QApplication(sys.argv) + if isWindows(): + try: + from syncplay.vendor import darkdetect + isDarkMode = darkdetect.isDark() + except: + isDarkMode = False + if isDarkMode: + self.app.setStyle(QtWidgets.QStyleFactory.create("fusion")) + self.app.setPalette(self.getDarkPalette(QtGui)) qt5reactor.install() if isMacOS(): import appnope @@ -550,6 +559,29 @@ class ConfigurationGetter(object): setLanguage(self._config['language']) return self._config + def getDarkPalette(self, QtGui): + # Based on https://gist.github.com/lschmierer/443b8e21ad93e2a2d7eb#gistcomment-3503395 + darkPalette = QtGui.QPalette() + darkPalette.setColor(QtGui.QPalette.Window, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.WindowText, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.Base, QtGui.QColor(35, 35, 35)) + darkPalette.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.ToolTipBase, QtGui.QColor(25, 25, 25)) + darkPalette.setColor(QtGui.QPalette.ToolTipText, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.Text, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.Button, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.ButtonText, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.BrightText, QtGui.QColor(255, 0, 0)) + darkPalette.setColor(QtGui.QPalette.Link, QtGui.QColor(42, 130, 218)) + darkPalette.setColor(QtGui.QPalette.Highlight, QtGui.QColor(42, 130, 218)) + darkPalette.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(35, 35, 35)) + darkPalette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Button, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtGui.QColor(128, 128, 128)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtGui.QColor(128, 128, 128)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(128, 128, 128)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Light, QtGui.QColor(53, 53, 53)) + return darkPalette + def setConfigOption(self, option, value): path = self._getConfigurationFilePath() backup = self._config.copy() diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index a6b4e12..7068db6 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -40,7 +40,7 @@ if isMacOS() and IsPySide: from Cocoa import NSString, NSUTF8StringEncoding lastCheckedForUpdates = None from syncplay.vendor import darkdetect -if isMacOS(): +if isMacOS() or isWindows(): isDarkMode = darkdetect.isDark() else: isDarkMode = None diff --git a/syncplay/vendor/darkdetect/__init__.py b/syncplay/vendor/darkdetect/__init__.py index bb1df11..fc68343 100644 --- a/syncplay/vendor/darkdetect/__init__.py +++ b/syncplay/vendor/darkdetect/__init__.py @@ -4,7 +4,7 @@ # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- -__version__ = '0.5.0' +__version__ = '0.5.2' import sys import platform @@ -16,10 +16,10 @@ if sys.platform == "darwin": else: from ._mac_detect import * del V -elif sys.platform == "win32" and platform.release() == "10": - # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) or higher. The getwindowsversion method returns a tuple. +elif sys.platform == "win32" and int(platform.release()) >= 10: + # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) OR HIGHER. The getwindowsversion method returns a tuple. # The third item is the build number that we can use to check if the user has a new enough version of Windows. - winver = int(sys.getwindowsversion()[2]) + winver = int(platform.version().split('.')[2]) if winver >= 14393: from ._windows_detect import * else: diff --git a/syncplay/vendor/darkdetect/_mac_detect.py b/syncplay/vendor/darkdetect/_mac_detect.py index 322582f..fbe6a35 100644 --- a/syncplay/vendor/darkdetect/_mac_detect.py +++ b/syncplay/vendor/darkdetect/_mac_detect.py @@ -6,17 +6,15 @@ import ctypes import ctypes.util -import platform -from distutils.version import LooseVersion as V - -if V(platform.mac_ver()[0]) < V("10.16"): - appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) - objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) -else: +try: + # macOS Big Sur+ use "a built-in dynamic linker cache of all system-provided libraries" appkit = ctypes.cdll.LoadLibrary('AppKit.framework/AppKit') objc = ctypes.cdll.LoadLibrary('libobjc.dylib') -del V +except OSError: + # revert to full path for older OS versions and hardened programs + appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) + objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) void_p = ctypes.c_void_p ull = ctypes.c_uint64 diff --git a/syncplay/vendor/darkdetect/_windows_detect.py b/syncplay/vendor/darkdetect/_windows_detect.py index 61409a7..a976885 100644 --- a/syncplay/vendor/darkdetect/_windows_detect.py +++ b/syncplay/vendor/darkdetect/_windows_detect.py @@ -5,14 +5,20 @@ def theme(): # Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode. This dictionary converts that output into the text that the program is expecting. valueMeaning = {0: "Dark", 1: "Light"} # In HKEY_CURRENT_USER, get the Personalisation Key. - key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") - # In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. - # The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. - subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] + try: + key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") + # In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. + # The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. + subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] + except FileNotFoundError: + # some headless Windows instances (e.g. GitHub Actions or Docker images) do not have this key + return None return valueMeaning[subkey] def isDark(): - return theme() == 'Dark' + if theme() is not None: + return theme() == 'Dark' def isLight(): - return theme() == 'Light' \ No newline at end of file + if theme() is not None: + return theme() == 'Light' \ No newline at end of file