Windows dark mode (#483)

* Add Dark Mode support for Windows

* Only use dark mode if enabled

* Actions: debug Windows build

* Don't error if dark mode is unavailable

* Revert Actions: debug Windows build

* darkdetect: update vendor copy to 0.5.1

* Revert "Don't error if dark mode is unavailable"

This reverts commit 5a9e871c12312c55355a9eb75df15c66bb4238ac.

* Set up tmate

* Stronger patching of darkdetect is needed

* Revert "Set up tmate"

This reverts commit 4012051f9328c52ab6d1297ede111ab48fad1f2d.

* Fix EXE by using Windows darkdetect directly

* Add try wrap to Windows darkmode check

* darkdetect: update vendor copy to 0.5.2

* Revert to public darkdetect API

Co-authored-by: Alberto Sottile <asottile@gmail.com>
Co-authored-by: Alberto Sottile <alby128@gmail.com>
This commit is contained in:
Etoh 2021-12-09 16:24:40 +00:00 committed by GitHub
parent c0e8652c8c
commit d60efabbad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 21 deletions

View File

@ -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()

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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'
if theme() is not None:
return theme() == 'Light'