diff --git a/mods/accent-color-sync.cpp b/mods/accent-color-sync.cpp new file mode 100644 index 000000000..52c4f6893 --- /dev/null +++ b/mods/accent-color-sync.cpp @@ -0,0 +1,642 @@ +// ==WindhawkMod== +// @id accent-color-sync +// @name Accent Color Sync +// @description Synchronises OpenGlass and Control Panel color settings +// @description:fr-FR Synchronisation des couleurs d'OpenGlass et du Panneau de configuration +// @description:es-ES Sincroniza los colores de OpenGlass y del Panel de control +// @version 1.31 +// @author CatmanFan / Mr._Lechkar +// @github https://github.com/CatmanFan +// @include explorer.exe +// @architecture x86 +// @architecture x86-64 +// @compilerOptions -lcomdlg32 +// ==/WindhawkMod== + +// ==WindhawkModReadme== +/* +Brings back the functionality of the 'Color intensity' slider to Windows 10 using OpenGlass. The mod synchronises OpenGlass's Aero settings with the slider value. + +## ⚠️ Requirements +**This mod will not function properly without [kfh83](https://github.com/kfh83)'s OpenGlass-legacy fork installed.** This is because the mod writes values to the registry's DWM section that are used only by this specific software. This requires the GlassType value in the registry to be set to 0x01 (Aero), and it will be automatically changed as such if it isn't. + +To get this mod to function fully, perform the following steps: +1. Install the OpenGlass-legacy fork; this can be done by compiling the source code from **[the official repo](https://github.com/ALTaleX531/OpenGlass/tree/legacy)**, or by getting an existing binary version from **[this GitHub post](https://github.com/ALTaleX531/OpenGlass/pull/14#issue-2415161314)**. + * If you are updating this mod from version 1.0, it is required to disable or uninstall any other existing DWM shader software (such as regular OpenGlass or DWMBlurGlass). +2. Afterwards, go to *HKCU\SOFTWARE\Microsoft\Windows\DWM* in the registry and add any one of the three DWORD values of *og_ColorizationColorBalance*, *og_ColorizationAfterglowBalance* and *og_ColorizationBlurBalance* before applying this mod. These will be handled automatically. + +If you are updating this mod from version 1.3, it is recommended to also enable the *Sync with DWM* option from the mod's settings, although this can have some minor bugs (see below). You may need to try changing the accent color manually if changes do not automatically take effect. + +## Current known bugs +* **Sync with DWM** option: In the Control Panel, the theme preview's color icon's opacity does not always reflect the value set by the color intensity slider if it is changed while the *Sync with DWM* mod setting is enabled, unless after the theme preview is regenerated by changing the color RGB values or the desktop background. +* When toggling transparency, because the GlassType value used by OpenGlass is changed, the taskbar and windows visually glitch after DWM is refreshed. A hotfix is currently implemented which logs out the current session if this setting is toggled. +* Actually closing the Personalization window does not produce the same behaviour as clicking "Cancel" (i.e. the RGB color is changed but the OpenGlass opacity stays the same). +* When changing the intensity slider, there is a very rare chance of DWM crashing. +*/ +// ==/WindhawkModReadme== + +// ==WindhawkModSettings== +/* +- syncDWM: TRUE + $name: Sync with DWM + $name:fr-FR: Synchroniser avec DWM + $name:es-ES: Sincronizar con DWM + $description: Writes the opacity value to DWM's color and afterglow variables. This makes it so that the opacity is also written to the theme alongside the color's RGB. Otherwise, Windows automatically sets it to remain stationary at 0xc4 (196 of 255). + $description:fr-FR: Sauvegarder la valeur d'opacité aux options de DWM. Cela permet de définir également l'opacité dans le thème simultanément avec les valeurs du RVB. Sinon, Windows le définit en permanence à 0xc4 (196 sur 255). + $description:es-ES: Guarda el valor de opacidad de OpenGlass en DWM. Esto le permite guardar la opacidad del tema, simultáneamente con los valores RGB. Windows normalmente establece la opacidad siempre en 0xc4 (196 de 255). +*/ +// ==/WindhawkModSettings== + +#if _WIN64 +#define THISCALL __cdecl +#define STHISCALL L"__cdecl" +#define STDCALL __cdecl +#define SSTDCALL L"__cdecl" +#define STRTOID_NAME L"StrToID" +#else +#define THISCALL __thiscall +#define STHISCALL L"__thiscall" +#define STDCALL __stdcall +#define SSTDCALL L"__stdcall" +#define STRTOID_NAME L"_StrToID@4" +#endif + +#include +#include +#include +#include +#include +#include +#include + +struct +{ + int opacity; + bool boolTransparency; + bool boolSyncDWM; +} settings; + +const std::wstring dwmKey = L"SOFTWARE\\Microsoft\\Windows\\DWM"; +const std::wstring opacityValue = L"og_Opacity"; + +#pragma region ## Registry functions ## +/** + * Reads a string value from a registry key within HKCU. + * + * @param sk The path to the key, not including "HKCU\". + * @param v The name of the value. + * @return The string value if it is found, otherwise `NULL`. + */ +std::wstring read_SZ(std::wstring sk, std::wstring v, std::wstring defaultValue) +{ + const LPCTSTR subkey = sk.c_str(); + const LPCTSTR value = v.c_str(); + WCHAR szBuffer[512]; + DWORD size(sizeof(szBuffer)); + + HKEY hKey; + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, subkey, 0, KEY_READ, &hKey); + if (openRes == ERROR_SUCCESS) + { + LONG setRes = RegQueryValueEx(hKey, value, 0, NULL, (LPBYTE)&szBuffer, &size); + RegCloseKey(hKey); + + if (setRes == ERROR_SUCCESS) + { + defaultValue = szBuffer; + } + } + + return defaultValue; +} + +/** + * Reads a DWORD value from a registry key within HKCU. + * + * @param sk The path to the key, not including "HKCU\". + * @param v The name of the value. + * @return The DWORD value if it is found, otherwise `NULL`. + */ +DWORD read_DWORD(std::wstring sk, std::wstring v) +{ + const LPCTSTR subkey = sk.c_str(); + const LPCTSTR value = v.c_str(); + DWORD data(0); + DWORD size(sizeof(DWORD)); + + HKEY hKey; + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, subkey, 0, KEY_READ, &hKey); + if (openRes != ERROR_SUCCESS) + { + return NULL; + } + + LONG setRes = RegQueryValueEx(hKey, value, 0, NULL, reinterpret_cast(&data), &size); + RegCloseKey(hKey); + + if (setRes == ERROR_SUCCESS) + { + return data; + } + else + { + return NULL; + } +} + +/** + * Checks for the existence of a DWORD value within an HKCU registry key. + * + * @param sk The path to the key, not including "HKCU\". + * @param v The name of the value. + * @return `TRUE` if found, otherwise `FALSE`. + */ +BOOL exists_DWORD(std::wstring sk, std::wstring v) +{ + const LPCTSTR subkey = sk.c_str(); + const LPCTSTR value = v.c_str(); + DWORD data(0); + DWORD size(sizeof(DWORD)); + + HKEY hKey; + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, subkey, 0, KEY_ALL_ACCESS, &hKey); + LONG setRes = RegQueryValueEx(hKey, value, 0, NULL, reinterpret_cast(&data), &size); + RegCloseKey(hKey); + + if (openRes != ERROR_SUCCESS || setRes != ERROR_SUCCESS) + { + return FALSE; + } + else + { + return TRUE; + } +} + +/** + * Checks for the existence of a registry key within HKCU. + * + * @param sk The path to the key, not including "HKCU\". + * @return `TRUE` if found, otherwise `FALSE`. + */ +BOOL exists_Key(std::wstring sk) +{ + const LPCTSTR subkey = sk.c_str(); + + HKEY hKey; + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, subkey, 0, KEY_ALL_ACCESS, &hKey); + if (openRes != ERROR_SUCCESS) + { + return FALSE; + } + else + { + return TRUE; + } +} + +/** + * Writes a DWORD value to a registry key within HKCU. + * + * @param sk The path to the key, not including "HKCU\". + * @param v The name of the value. + * @param data The DWORD value to write. + * @return `TRUE` if the operation succeeded, otherwise `FALSE`. + */ +BOOL set_DWORD(std::wstring sk, std::wstring v, unsigned long data) +{ + const LPCTSTR subkey = sk.c_str(); + const LPCTSTR value = v.c_str(); + + HKEY hKey; + LONG openRes = RegOpenKeyEx(HKEY_CURRENT_USER, subkey, 0, KEY_ALL_ACCESS, &hKey); + if (openRes != ERROR_SUCCESS) + { + Wh_Log(L"Failed to open registry key"); + return FALSE; + } + + LONG setRes = data < 0 ? RegDeleteValue(hKey, value) : RegSetValueEx(hKey, value, 0, REG_DWORD, (const BYTE*)&data, sizeof(data)); + RegCloseKey(hKey); + + if (setRes == ERROR_SUCCESS) + { + return TRUE; + } + else + { + Wh_Log(L"Failed writing to registry"); + return FALSE; + } +} +#pragma endregion + +#pragma region ## Windows7 opacity ## +int getOpacityFromBlur(int bB, bool returnOutOf100 = TRUE) +{ + bool found = FALSE; + if (bB == 28) found = TRUE; + int x = 0; + + x = (bB - 103.6842f) / -0.526316f; + if (x >= 26 && x < 102) found = TRUE; + + x = (bB - 76.093f) / -0.255814f; + if (x >= 102 && x < 188) found = TRUE; + + x = (bB - 131.25f) / -0.535714f; + if (x >= 189 && x <= 217) found = TRUE; + + if (found) + { + if (returnOutOf100) x = (x - 26.0) / (217.0 - 26.0) * 100.0; + return x; + } + return 0; +} + +void getColorBalances(int sliderPosition, int &primaryBalance, int &secondaryBalance, int &blurBalance) +{ + int pB = 0, sB = 0, bB = 0; + int x = sliderPosition; + + //primary + if (x >= 26 && x < 103) + pB = 5; + else if (x >= 103 && x < 188) + pB = 0.776471*x - 74.9765f + 1.5f; + else if (x == 188) + pB = 71; + else if (x >= 189 && x <= 217) + pB = 0.535714*x - 31.25f + 1.5f; + + //secondary + if (x >= 26 && x < 102) + sB = 0.526316*x - 8.68421f; + else if (x >= 102 && x < 189) + sB = -0.517241*x + 97.7586f; + else if(x >= 189 && x <= 217) + sB = 0; + + //blur + if (x >= 26 && x < 102) + bB = -0.526316*x + 103.6842f; + else if (x >= 102 && x < 188) + bB = -0.255814f*x + 76.093f; + else if (x == 188) + bB = 28; + else if (x >= 189 && x <= 217) + bB = -0.535714f*x + 131.25f; + + primaryBalance = pB; + secondaryBalance = sB; + blurBalance = bB; +} +#pragma endregion + +int intensitySliderMin = 10; +int intensitySliderMax = 85; +int valueTo100(int x) { return x - intensitySliderMin / (intensitySliderMax - intensitySliderMin) * 100; } +int valueFrom100(int x) { return (x / 100) * (intensitySliderMax - intensitySliderMin) + intensitySliderMin; } +int valueTo255(int x) { return round(2.5497*x + 0.0449); } +int getOpacityFromColor(DWORD color, bool rounded) +{ + std::stringstream ssA; + ssA << std::hex << std::setfill('0') << std::setw(8) << color; + unsigned long x = std::stoul(ssA.str().substr(0, 2), nullptr, 16); + return rounded ? round((x - 0.0732) / 2.5488) : x; +} + +/** + * Writes the OpenGlass opacity value to DWM's color and afterglow values in registry. + */ +void SyncOpacity() +{ + if (!settings.boolSyncDWM) return; + if (settings.opacity < 0 || settings.opacity > 100) + { + settings.opacity = getOpacityFromBlur(0x31); + set_DWORD(dwmKey, opacityValue, settings.opacity); + } + + const std::wstring value = L"ColorizationColor"; + if (!exists_DWORD(dwmKey, value)) return; + DWORD x = read_DWORD(dwmKey, value); + int sysOpacity = getOpacityFromColor(x, false); + + int ogOpacity = valueTo255(settings.opacity); + + // Hotfixes to force match with Windows 7 accent colors, since the calculation above is janky + if (ogOpacity == 0x69 || ogOpacity == 0x6a) ogOpacity = 0x6b; + if (ogOpacity == 0xa6 || ogOpacity == 0xa7) ogOpacity = 0xa8; + if (ogOpacity == 0x67) ogOpacity = 0x66; + if (ogOpacity == 0x6e || ogOpacity == 0x6f) ogOpacity = 0x70; + if (x == 0x52fadc0e) x = 0x54fadc0e; + if (x == 0x54fadc0e) ogOpacity = 0x54; + + if (sysOpacity != ogOpacity) + { + std::stringstream ssA; + ssA << std::hex << std::setfill('0') << std::setw(8) << x; + std::stringstream ssB; + ssB << std::hex << std::setfill('0') << std::setw(2) << ogOpacity; + DWORD newColor = std::stoul("0x" + ssB.str() + ssA.str().substr(2), nullptr, 16); + + set_DWORD(dwmKey, L"ColorizationColor", newColor); + set_DWORD(dwmKey, L"ColorizationAfterglow", newColor); + } +} + +int opacity = -1; +/** + * Calculates the color, afterglow and blur intensity values from an integer out of 100, then writes them to the registry for use with OpenGlass. + * @param sync Determines whether to also sync opacity. + */ +void WriteNewColor(bool sync) +{ + if (!settings.boolTransparency) + opacity = 100; + else + { + if (opacity < 0 || opacity > 100) + opacity = settings.opacity; + if (opacity < 0 || opacity > 100) + { + settings.opacity = opacity = getOpacityFromBlur(0x31); + } + } + set_DWORD(dwmKey, opacityValue, opacity); + + // ********************************************* + // Create Aero intensity values + // ********************************************* + int colour = 100; + int afterglow = 0; + int blur = 0; + + if (settings.boolTransparency) + { + getColorBalances + ( + valueTo255(opacity), + colour, + afterglow, + blur + ); + + // Changing the ColorizationColor and Afterglow also affects the intensity slider + } + + // ********************************************* + // Actually do the registry editing + // ********************************************* + set_DWORD(dwmKey, L"og_ColorizationColorBalance", colour); + set_DWORD(dwmKey, L"og_ColorizationAfterglowBalance", afterglow); + set_DWORD(dwmKey, L"og_ColorizationBlurBalance", blur); + + // Other registry values + set_DWORD(dwmKey, L"GlassOpacity", settings.boolTransparency ? 0 : 100); + set_DWORD(dwmKey, L"GlassType", settings.boolTransparency ? 1 : 0); + + if (sync) SyncOpacity(); + PostMessage(FindWindow(TEXT("dwm"), nullptr), WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0); +} + +#pragma region ## DirectUI hooks ## +typedef ATOM WINAPI (*StrToId_T)(unsigned const short*); +StrToId_T StrToID; + +// Pointers to DUI elements +intptr_t intensitySlider = 0; +intptr_t okButton = 0; +intptr_t cancelButton = 0; + +typedef unsigned short(*THISCALL Element_GetID_T)(class Element*, void*); +Element_GetID_T Element_GetID; + +typedef void(*THISCALL Element_OnPropertyChanged_T)(class Element*, class PropertyInfo const *,int,class Value *,class Value *); +Element_OnPropertyChanged_T Element_OnPropertyChanged; + +void THISCALL Element_OnPropertyChanged_hook(class Element* This, class PropertyInfo const *prop, int integer, class Value *valueA, class Value *valueB) +{ + Element_OnPropertyChanged(This, prop, integer, valueA, valueB); + + ATOM id = Element_GetID(This, &This); + if (intensitySlider != id && id == StrToID((unsigned const short*)L"IntensitySlider")) + { + intensitySlider = reinterpret_cast(This); + } + else if (okButton != id && id == StrToID((unsigned const short*)L"OkButton")) + { + okButton = reinterpret_cast(This); + SyncOpacity(); + } + else if (cancelButton != id && id == StrToID((unsigned const short*)L"CancelButton")) + { + cancelButton = reinterpret_cast(This); + SyncOpacity(); + } +} + +long (*THISCALL CCTrackBar_SetThumbPosition)(class CCTrackBar*, int); +long THISCALL CCTrackBar_SetThumbPosition_hook(class CCTrackBar* This, int value) +{ + intptr_t current = reinterpret_cast(This); + + // Track bar value + if (current > 0 && current == intensitySlider) + { + opacity = value; + WriteNewColor(false); + } + + return CCTrackBar_SetThumbPosition(This, value); +} + +typedef void (*THISCALL CCPushButton_OnSelectedPropertyChanged_T)(class CCPushButton*, void*); +CCPushButton_OnSelectedPropertyChanged_T CCPushButton_OnSelectedPropertyChanged; +void THISCALL CCPushButton_OnSelectedPropertyChanged_hook(class CCPushButton* This, void* that) +{ + intptr_t current = reinterpret_cast(This); + + // OK button + if (current > 0 && current == okButton) + { + settings.opacity = opacity; + SyncOpacity(); + } + + // Cancel button + else if (current > 0 && current == cancelButton) + { + opacity = settings.opacity; + WriteNewColor(true); + if (intensitySlider > 0) CCTrackBar_SetThumbPosition(reinterpret_cast(intensitySlider), opacity); + } + + CCPushButton_OnSelectedPropertyChanged(This, that); +} +#pragma endregion + +WindhawkUtils::SYMBOL_HOOK dui70dll_hooks[] = { + { + {STRTOID_NAME}, + (void**)&StrToID, + }, + + { + {L"public: unsigned short " STHISCALL " DirectUI::Element::GetID(void)"}, + (void**)&Element_GetID + }, + + { + {L"public: virtual void " STHISCALL " DirectUI::Element::OnPropertyChanged(struct DirectUI::PropertyInfo const *,int,class DirectUI::Value *,class DirectUI::Value *)"}, + (void**)&Element_OnPropertyChanged, + (void*)Element_OnPropertyChanged_hook + }, + + { + {L"public: long " STHISCALL " DirectUI::CCTrackBar::SetThumbPosition(int)"}, + (void**)&CCTrackBar_SetThumbPosition, + (void*)CCTrackBar_SetThumbPosition_hook + }, + + { + {L"public: virtual void " STHISCALL " DirectUI::CCPushButton::OnSelectedPropertyChanged(void)"}, + (void**)&CCPushButton_OnSelectedPropertyChanged, + (void*)&CCPushButton_OnSelectedPropertyChanged_hook + } +}; + +#pragma region ## ThemeUI hooks ## +enum DWMPGLASSATTRIBUTE : INT +{ + DWMPGA_TRANSPARENCY_ALLOWED = 0x0, + DWMPGA_TRANSPARENCY_DISALLOWED = 0x1, + DWMPGA_NO_GLASS = 0x2, + DWMPGA_LAST = 0x3, +}; + +long (*STDCALL SetDwmColorizationColor)(unsigned long, enum DWMPGLASSATTRIBUTE,int); +long STDCALL SetDwmColorizationColor_hook(unsigned long color, enum DWMPGLASSATTRIBUTE attribute,int integer) +{ + if (settings.boolSyncDWM) + { + settings.opacity = opacity = getOpacityFromColor(color, true); + WriteNewColor(false); + } + return SetDwmColorizationColor(color, attribute, integer); +} +#pragma endregion + +WindhawkUtils::SYMBOL_HOOK themeuidll_hooks[] = { + { + {L"long " SSTDCALL " SetDwmColorizationColor(unsigned long,enum DWMPGLASSATTRIBUTE,int)"}, + (void**)&SetDwmColorizationColor, + (void*)SetDwmColorizationColor_hook + } +}; + +BOOL LoadSettings() +{ + bool regSetup = FALSE; + + if (!exists_DWORD(dwmKey, L"og_ColorizationColorBalance") + && !exists_DWORD(dwmKey, L"og_ColorizationAfterglowBalance") + && !exists_DWORD(dwmKey, L"og_ColorizationBlurBalance")) + { + Wh_Log(L"OpenGlass Legacy is not detected, cannot continue"); + return FALSE; + } + else if (exists_DWORD(dwmKey, L"og_ColorizationColorBalance") + && exists_DWORD(dwmKey, L"og_ColorizationAfterglowBalance") + && exists_DWORD(dwmKey, L"og_ColorizationBlurBalance")) + { + regSetup = FALSE; + } + else + { + regSetup = TRUE; + } + + if (!exists_DWORD(dwmKey, opacityValue)) regSetup = TRUE; + else settings.opacity = read_DWORD(dwmKey, opacityValue); + + if (regSetup) + { + Wh_Log(L"Setting up registry"); + set_DWORD(dwmKey, L"og_ColorizationColorBalance", 0x08); + set_DWORD(dwmKey, L"og_ColorizationAfterglowBalance", 0x2b); + set_DWORD(dwmKey, L"og_ColorizationBlurBalance", 0x31); + + settings.opacity = getOpacityFromBlur(0x31); + if (!set_DWORD(dwmKey, opacityValue, settings.opacity)) return FALSE; + if (!exists_DWORD(dwmKey, opacityValue)) return FALSE; + } + + settings.boolTransparency = TRUE; + settings.boolSyncDWM = Wh_GetIntSetting(L"syncDWM"); + + return TRUE; +} + +BOOL Wh_ModSettingsChanged(BOOL* bReload) { + Wh_Log(L"Settings changed"); + + *bReload = TRUE; + return TRUE; +} + +BOOL Wh_ModInit() { + if (!IsWindows10OrGreater()) + { + Wh_Log(L"Cannot run on Windows 8.1 or earlier"); + return FALSE; + } + + std::wstring username = read_SZ(L"Volatile Environment", L"USERNAME", L"???"); + if (username == L"???") + { + Wh_Log(L"Local username not detected, unloading"); + return FALSE; + } + + if (!LoadSettings()) + { + Wh_Log(L"Failed to load settings"); + return FALSE; + } + + HMODULE hDui = LoadLibraryW(L"dui70.dll"); + if (hDui) + { + if (!WindhawkUtils::HookSymbols(hDui, dui70dll_hooks, ARRAYSIZE(dui70dll_hooks))) + { + Wh_Log(L"Failed to hook symbols from dui70.dll"); + return FALSE; + } + } + else + { + Wh_Log(L"Failed to load dui70.dll"); + return FALSE; + } + + HMODULE hThemeUi = LoadLibraryW(L"themeui.dll"); + if (hThemeUi) + { + if (!WindhawkUtils::HookSymbols(hThemeUi, themeuidll_hooks, ARRAYSIZE(themeuidll_hooks))) + { + Wh_Log(L"Failed to hook symbols from themeui.dll"); + return FALSE; + } + } + else + { + Wh_Log(L"Failed to load themeui.dll"); + return FALSE; + } + + WriteNewColor(true); + return TRUE; +}