diff --git a/napari/_vendor/darkdetect/__init__.py b/napari/_vendor/darkdetect/__init__.py index d8ab5940ac3..bb8c76590d5 100644 --- a/napari/_vendor/darkdetect/__init__.py +++ b/napari/_vendor/darkdetect/__init__.py @@ -4,7 +4,7 @@ # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- -__version__ = '0.5.2' +__version__ = '0.6.0' import sys import platform diff --git a/napari/_vendor/darkdetect/_dummy.py b/napari/_vendor/darkdetect/_dummy.py index 1e99668edf9..1e8211770cc 100644 --- a/napari/_vendor/darkdetect/_dummy.py +++ b/napari/_vendor/darkdetect/_dummy.py @@ -4,6 +4,8 @@ # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- +import typing + def theme(): return None @@ -12,3 +14,6 @@ def isDark(): def isLight(): return None + +def listener(callback: typing.Callable[[str], None]) -> None: + raise NotImplementedError() diff --git a/napari/_vendor/darkdetect/_linux_detect.py b/napari/_vendor/darkdetect/_linux_detect.py index 3110ae88cb5..e55ee2e5f97 100644 --- a/napari/_vendor/darkdetect/_linux_detect.py +++ b/napari/_vendor/darkdetect/_linux_detect.py @@ -5,7 +5,7 @@ #----------------------------------------------------------------------------- import subprocess - +import typing def theme(): # Here we just triage to GTK settings for now @@ -28,3 +28,12 @@ def isDark(): def isLight(): return theme() == 'Light' + +def listener(callback: typing.Callable[[str], None]) -> None: + with subprocess.Popen( + ('gsettings', 'monitor', 'org.gnome.desktop.interface', 'gtk-theme'), + stdout=subprocess.PIPE, + universal_newlines=True, + ) as p: + for line in p.stdout: + callback('Dark' if line.strip().removeprefix("gtk-theme: '").removesuffix("'").endswith('-dark') else 'Light') diff --git a/napari/_vendor/darkdetect/_mac_detect.py b/napari/_vendor/darkdetect/_mac_detect.py index fbe6a35623d..dfee7877dcb 100644 --- a/napari/_vendor/darkdetect/_mac_detect.py +++ b/napari/_vendor/darkdetect/_mac_detect.py @@ -6,6 +6,7 @@ import ctypes import ctypes.util +import typing try: # macOS Big Sur+ use "a built-in dynamic linker cache of all system-provided libraries" @@ -68,3 +69,6 @@ def isDark(): def isLight(): return theme() == 'Light' + +def listener(callback: typing.Callable[[str], None]) -> None: + raise NotImplementedError() diff --git a/napari/_vendor/darkdetect/_windows_detect.py b/napari/_vendor/darkdetect/_windows_detect.py index a9768850c16..40967c114ca 100644 --- a/napari/_vendor/darkdetect/_windows_detect.py +++ b/napari/_vendor/darkdetect/_windows_detect.py @@ -1,5 +1,61 @@ from winreg import HKEY_CURRENT_USER as hkey, QueryValueEx as getSubkeyValue, OpenKey as getKey +import ctypes +import ctypes.wintypes +import typing + +advapi32 = ctypes.windll.advapi32 + +# LSTATUS RegOpenKeyExA( +# HKEY hKey, +# LPCSTR lpSubKey, +# DWORD ulOptions, +# REGSAM samDesired, +# PHKEY phkResult +# ); +advapi32.RegOpenKeyExA.argtypes = ( + ctypes.wintypes.HKEY, + ctypes.wintypes.LPCSTR, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.POINTER(ctypes.wintypes.HKEY), +) +advapi32.RegOpenKeyExA.restype = ctypes.wintypes.LONG + +# LSTATUS RegQueryValueExA( +# HKEY hKey, +# LPCSTR lpValueName, +# LPDWORD lpReserved, +# LPDWORD lpType, +# LPBYTE lpData, +# LPDWORD lpcbData +# ); +advapi32.RegQueryValueExA.argtypes = ( + ctypes.wintypes.HKEY, + ctypes.wintypes.LPCSTR, + ctypes.wintypes.LPDWORD, + ctypes.wintypes.LPDWORD, + ctypes.wintypes.LPBYTE, + ctypes.wintypes.LPDWORD, +) +advapi32.RegQueryValueExA.restype = ctypes.wintypes.LONG + +# LSTATUS RegNotifyChangeKeyValue( +# HKEY hKey, +# WINBOOL bWatchSubtree, +# DWORD dwNotifyFilter, +# HANDLE hEvent, +# WINBOOL fAsynchronous +# ); +advapi32.RegNotifyChangeKeyValue.argtypes = ( + ctypes.wintypes.HKEY, + ctypes.wintypes.BOOL, + ctypes.wintypes.DWORD, + ctypes.wintypes.HANDLE, + ctypes.wintypes.BOOL, +) +advapi32.RegNotifyChangeKeyValue.restype = ctypes.wintypes.LONG + def theme(): """ Uses the Windows Registry to detect if the user is using Dark Mode """ # 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. @@ -21,4 +77,46 @@ def isDark(): def isLight(): if theme() is not None: - return theme() == 'Light' \ No newline at end of file + return theme() == 'Light' + +def listener(callback: typing.Callable[[str], None]) -> None: + hKey = ctypes.wintypes.HKEY() + advapi32.RegOpenKeyExA( + ctypes.wintypes.HKEY(0x80000001), # HKEY_CURRENT_USER + ctypes.wintypes.LPCSTR(b'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize'), + ctypes.wintypes.DWORD(), + ctypes.wintypes.DWORD(0x00020019), # KEY_READ + ctypes.byref(hKey), + ) + + dwSize = ctypes.wintypes.DWORD(ctypes.sizeof(ctypes.wintypes.DWORD)) + queryValueLast = ctypes.wintypes.DWORD() + queryValue = ctypes.wintypes.DWORD() + advapi32.RegQueryValueExA( + hKey, + ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), + ctypes.wintypes.LPDWORD(), + ctypes.wintypes.LPDWORD(), + ctypes.cast(ctypes.byref(queryValueLast), ctypes.wintypes.LPBYTE), + ctypes.byref(dwSize), + ) + + while True: + advapi32.RegNotifyChangeKeyValue( + hKey, + ctypes.wintypes.BOOL(True), + ctypes.wintypes.DWORD(0x00000004), # REG_NOTIFY_CHANGE_LAST_SET + ctypes.wintypes.HANDLE(None), + ctypes.wintypes.BOOL(False), + ) + advapi32.RegQueryValueExA( + hKey, + ctypes.wintypes.LPCSTR(b'AppsUseLightTheme'), + ctypes.wintypes.LPDWORD(), + ctypes.wintypes.LPDWORD(), + ctypes.cast(ctypes.byref(queryValue), ctypes.wintypes.LPBYTE), + ctypes.byref(dwSize), + ) + if queryValueLast.value != queryValue.value: + queryValueLast.value = queryValue.value + callback('Light' if queryValue.value else 'Dark')