diff --git a/.gitignore b/.gitignore index 1899114..c978a44 100755 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,12 @@ _build/ ARM64/ Win32/ -x64/ *.d *.o *.user -NppSnippets.dll version_git.h +plugindemo-2025_May_21.10.1/.gitignore +.claude/settings.local.json +.claude/todo.txt +.gitignore +NppSnippets/ diff --git a/DarkMode.cpp b/DarkMode.cpp new file mode 100644 index 0000000..27838de --- /dev/null +++ b/DarkMode.cpp @@ -0,0 +1,338 @@ +#include + +#include "DarkMode.h" +#include "IatHook.h" + +#include +#include + +#include +#include + +#if defined(__GNUC__) && __GNUC__ > 8 +#define WINAPI_LAMBDA_RETURN(return_t) -> return_t WINAPI +#elif defined(__GNUC__) +#define WINAPI_LAMBDA_RETURN(return_t) WINAPI -> return_t +#else +#define WINAPI_LAMBDA_RETURN(return_t) -> return_t +#endif + +enum IMMERSIVE_HC_CACHE_MODE +{ + IHCM_USE_CACHED_VALUE, + IHCM_REFRESH +}; + +// 1903 18362 +enum class PreferredAppMode +{ + Default, + AllowDark, + ForceDark, + ForceLight, + Max +}; + +enum WINDOWCOMPOSITIONATTRIB +{ + WCA_UNDEFINED = 0, + WCA_NCRENDERING_ENABLED = 1, + WCA_NCRENDERING_POLICY = 2, + WCA_TRANSITIONS_FORCEDISABLED = 3, + WCA_ALLOW_NCPAINT = 4, + WCA_CAPTION_BUTTON_BOUNDS = 5, + WCA_NONCLIENT_RTL_LAYOUT = 6, + WCA_FORCE_ICONIC_REPRESENTATION = 7, + WCA_EXTENDED_FRAME_BOUNDS = 8, + WCA_HAS_ICONIC_BITMAP = 9, + WCA_THEME_ATTRIBUTES = 10, + WCA_NCRENDERING_EXILED = 11, + WCA_NCADORNMENTINFO = 12, + WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, + WCA_VIDEO_OVERLAY_ACTIVE = 14, + WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, + WCA_DISALLOW_PEEK = 16, + WCA_CLOAK = 17, + WCA_CLOAKED = 18, + WCA_ACCENT_POLICY = 19, + WCA_FREEZE_REPRESENTATION = 20, + WCA_EVER_UNCLOAKED = 21, + WCA_VISUAL_OWNER = 22, + WCA_HOLOGRAPHIC = 23, + WCA_EXCLUDED_FROM_DDA = 24, + WCA_PASSIVEUPDATEMODE = 25, + WCA_USEDARKMODECOLORS = 26, + WCA_LAST = 27 +}; + +struct WINDOWCOMPOSITIONATTRIBDATA +{ + WINDOWCOMPOSITIONATTRIB Attrib; + PVOID pvData; + SIZE_T cbData; +}; + +using fnRtlGetNtVersionNumbers = void (WINAPI *)(LPDWORD major, LPDWORD minor, LPDWORD build); +using fnSetWindowCompositionAttribute = BOOL (WINAPI *)(HWND hWnd, WINDOWCOMPOSITIONATTRIBDATA*); +using fnShouldAppsUseDarkMode = bool (WINAPI *)(); // ordinal 132 +using fnAllowDarkModeForWindow = bool (WINAPI *)(HWND hWnd, bool allow); // ordinal 133 +using fnAllowDarkModeForApp = bool (WINAPI *)(bool allow); // ordinal 135, in 1809 +using fnFlushMenuThemes = void (WINAPI *)(); // ordinal 136 +using fnRefreshImmersiveColorPolicyState = void (WINAPI *)(); // ordinal 104 +using fnIsDarkModeAllowedForWindow = bool (WINAPI *)(HWND hWnd); // ordinal 137 +using fnGetIsImmersiveColorUsingHighContrast = bool (WINAPI *)(IMMERSIVE_HC_CACHE_MODE mode); // ordinal 106 +using fnOpenNcThemeData = HTHEME(WINAPI *)(HWND hWnd, LPCWSTR pszClassList); // ordinal 49 +using fnSetPreferredAppMode = PreferredAppMode (WINAPI *)(PreferredAppMode appMode); // ordinal 135, in 1903 + +fnSetWindowCompositionAttribute _SetWindowCompositionAttribute = nullptr; +fnShouldAppsUseDarkMode _ShouldAppsUseDarkMode = nullptr; +fnAllowDarkModeForWindow _AllowDarkModeForWindow = nullptr; +fnAllowDarkModeForApp _AllowDarkModeForApp = nullptr; +fnFlushMenuThemes _FlushMenuThemes = nullptr; +fnRefreshImmersiveColorPolicyState _RefreshImmersiveColorPolicyState = nullptr; +fnIsDarkModeAllowedForWindow _IsDarkModeAllowedForWindow = nullptr; +fnGetIsImmersiveColorUsingHighContrast _GetIsImmersiveColorUsingHighContrast = nullptr; +fnOpenNcThemeData _OpenNcThemeData = nullptr; +fnSetPreferredAppMode _SetPreferredAppMode = nullptr; + +bool g_darkModeSupported = false; +bool g_darkModeEnabled = false; +DWORD g_buildNumber = 0; + +bool ShouldAppsUseDarkMode() +{ + if (!_ShouldAppsUseDarkMode) + { + return false; + } + + return _ShouldAppsUseDarkMode(); +} + +bool AllowDarkModeForWindow(HWND hWnd, bool allow) +{ + if (g_darkModeSupported && _AllowDarkModeForWindow) + return _AllowDarkModeForWindow(hWnd, allow); + return false; +} + +bool IsHighContrast() +{ + HIGHCONTRASTW highContrast{}; + highContrast.cbSize = sizeof(HIGHCONTRASTW); + if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(HIGHCONTRASTW), &highContrast, FALSE)) + return (highContrast.dwFlags & HCF_HIGHCONTRASTON) == HCF_HIGHCONTRASTON; + return false; +} + +void SetTitleBarThemeColor(HWND hWnd, BOOL dark) +{ + if (g_buildNumber < 18362) + SetPropW(hWnd, L"UseImmersiveDarkModeColors", reinterpret_cast(static_cast(dark))); + else if (_SetWindowCompositionAttribute) + { + WINDOWCOMPOSITIONATTRIBDATA data = { WCA_USEDARKMODECOLORS, &dark, sizeof(dark) }; + _SetWindowCompositionAttribute(hWnd, &data); + } +} + +void RefreshTitleBarThemeColor(HWND hWnd) +{ + BOOL dark = FALSE; + if (_IsDarkModeAllowedForWindow && _ShouldAppsUseDarkMode) + { + if (_IsDarkModeAllowedForWindow(hWnd) && _ShouldAppsUseDarkMode() && !IsHighContrast()) + { + dark = TRUE; + } + } + + SetTitleBarThemeColor(hWnd, dark); +} + +bool IsColorSchemeChangeMessage(LPARAM lParam) +{ + bool is = false; + if (lParam && (0 == lstrcmpi(reinterpret_cast(lParam), L"ImmersiveColorSet")) && _RefreshImmersiveColorPolicyState) + { + _RefreshImmersiveColorPolicyState(); + is = true; + } + if (_GetIsImmersiveColorUsingHighContrast) + _GetIsImmersiveColorUsingHighContrast(IHCM_REFRESH); + return is; +} + +bool IsColorSchemeChangeMessage(UINT message, LPARAM lParam) +{ + if (message == WM_SETTINGCHANGE) + return IsColorSchemeChangeMessage(lParam); + return false; +} + +void AllowDarkModeForApp(bool allow) +{ + if (_AllowDarkModeForApp) + _AllowDarkModeForApp(allow); + else if (_SetPreferredAppMode) + _SetPreferredAppMode(allow ? PreferredAppMode::ForceDark : PreferredAppMode::Default); +} + +void FlushMenuThemes() +{ + if (_FlushMenuThemes) + { + _FlushMenuThemes(); + } +} + +// limit dark scroll bar to specific windows and their children +std::unordered_set g_darkScrollBarWindows; +std::mutex g_darkScrollBarMutex; + +void EnableDarkScrollBarForWindowAndChildren(HWND hwnd) +{ + std::lock_guard lock(g_darkScrollBarMutex); + g_darkScrollBarWindows.insert(hwnd); +} + +bool IsWindowOrParentUsingDarkScrollBar(HWND hwnd) +{ + HWND hwndRoot = GetAncestor(hwnd, GA_ROOT); + + std::lock_guard lock(g_darkScrollBarMutex); + if (g_darkScrollBarWindows.count(hwnd)) { + return true; + } + if (hwnd != hwndRoot && g_darkScrollBarWindows.count(hwndRoot)) { + return true; + } + + return false; +} + +void FixDarkScrollBar() +{ + HMODULE hComctl = LoadLibraryEx(L"comctl32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (hComctl) + { + auto addr = FindDelayLoadThunkInModule(hComctl, "uxtheme.dll", 49); // OpenNcThemeData + if (addr) + { + DWORD oldProtect; + if (VirtualProtect(addr, sizeof(IMAGE_THUNK_DATA), PAGE_READWRITE, &oldProtect) && _OpenNcThemeData) + { + auto MyOpenThemeData = [](HWND hWnd, LPCWSTR classList) WINAPI_LAMBDA_RETURN(HTHEME) { + if (wcscmp(classList, L"ScrollBar") == 0) + { + if (IsWindowOrParentUsingDarkScrollBar(hWnd)) { + hWnd = nullptr; + classList = L"Explorer::ScrollBar"; + } + } + return _OpenNcThemeData(hWnd, classList); + }; + + addr->u1.Function = reinterpret_cast(static_cast(MyOpenThemeData)); + VirtualProtect(addr, sizeof(IMAGE_THUNK_DATA), oldProtect, &oldProtect); + } + } + } +} + +constexpr bool CheckBuildNumber(DWORD buildNumber) +{ + return (buildNumber == 17763 || // 1809 + buildNumber == 18362 || // 1903 + buildNumber == 18363 || // 1909 + buildNumber == 19041 || // 2004 + buildNumber == 19042 || // 20H2 + buildNumber == 19043 || // 21H1 + buildNumber == 19044 || // 21H2 + (buildNumber > 19044 && buildNumber < 22000) || // Windows 10 any version > 21H2 + buildNumber >= 22000); // Windows 11 builds +} + +bool IsWindows10() // or later OS version +{ + return (g_buildNumber >= 17763); +} + +bool IsWindows11() // or later OS version +{ + return (g_buildNumber >= 22000); +} + +DWORD GetWindowsBuildNumber() +{ + return g_buildNumber; +} + +void InitDarkMode() +{ + fnRtlGetNtVersionNumbers RtlGetNtVersionNumbers = nullptr; + HMODULE hNtdllModule = GetModuleHandle(L"ntdll.dll"); + if (hNtdllModule) + { + RtlGetNtVersionNumbers = reinterpret_cast(GetProcAddress(hNtdllModule, "RtlGetNtVersionNumbers")); + } + + if (RtlGetNtVersionNumbers) + { + DWORD major, minor; + RtlGetNtVersionNumbers(&major, &minor, &g_buildNumber); + g_buildNumber &= ~0xF0000000; + if (major == 10 && minor == 0 && CheckBuildNumber(g_buildNumber)) + { + HMODULE hUxtheme = LoadLibraryEx(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (hUxtheme) + { + _OpenNcThemeData = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(49))); + _RefreshImmersiveColorPolicyState = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(104))); + _GetIsImmersiveColorUsingHighContrast = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(106))); + _ShouldAppsUseDarkMode = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(132))); + _AllowDarkModeForWindow = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133))); + + auto ord135 = GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135)); + if (g_buildNumber < 18362) + _AllowDarkModeForApp = reinterpret_cast(ord135); + else + _SetPreferredAppMode = reinterpret_cast(ord135); + + _FlushMenuThemes = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(136))); + _IsDarkModeAllowedForWindow = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(137))); + + HMODULE hUser32Module = GetModuleHandleW(L"user32.dll"); + if (hUser32Module) + { + _SetWindowCompositionAttribute = reinterpret_cast(GetProcAddress(hUser32Module, "SetWindowCompositionAttribute")); + } + + if (_OpenNcThemeData && + _RefreshImmersiveColorPolicyState && + _ShouldAppsUseDarkMode && + _AllowDarkModeForWindow && + (_AllowDarkModeForApp || _SetPreferredAppMode) && + _FlushMenuThemes && + _IsDarkModeAllowedForWindow) + { + g_darkModeSupported = true; + } + } + } + } +} + +void SetDarkMode(bool useDark, bool fixDarkScrollbar) +{ + if (g_darkModeSupported) + { + AllowDarkModeForApp(useDark); + FlushMenuThemes(); + if (fixDarkScrollbar) + { + FixDarkScrollBar(); + } + g_darkModeEnabled = ShouldAppsUseDarkMode() && !IsHighContrast(); + } +} \ No newline at end of file diff --git a/DarkMode.h b/DarkMode.h new file mode 100644 index 0000000..e970e58 --- /dev/null +++ b/DarkMode.h @@ -0,0 +1,19 @@ +#pragma once + +extern bool g_darkModeSupported; +extern bool g_darkModeEnabled; + +bool ShouldAppsUseDarkMode(); +bool AllowDarkModeForWindow(HWND hWnd, bool allow); +bool IsHighContrast(); +void RefreshTitleBarThemeColor(HWND hWnd); +void SetTitleBarThemeColor(HWND hWnd, BOOL dark); +bool IsColorSchemeChangeMessage(LPARAM lParam); +bool IsColorSchemeChangeMessage(UINT message, LPARAM lParam); +void AllowDarkModeForApp(bool allow); +void EnableDarkScrollBarForWindowAndChildren(HWND hwnd); +void InitDarkMode(); +void SetDarkMode(bool useDarkMode, bool fixDarkScrollbar); +bool IsWindows10(); +bool IsWindows11(); +DWORD GetWindowsBuildNumber(); \ No newline at end of file diff --git a/DlgAbout.cpp b/DlgAbout.cpp index 10a5a0b..11a4192 100644 --- a/DlgAbout.cpp +++ b/DlgAbout.cpp @@ -39,6 +39,7 @@ static BOOL OnInitDialog(HWND hDlg) ///////////////////////////////////////////////////////////////////////////// // + static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); @@ -47,6 +48,13 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar { case WM_INITDIALOG: { + // Apply dark mode theming using official API + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + ::SendMessage(g_nppData._nppHandle, NPPM_DARKMODESUBCLASSANDTHEME, + static_cast(NppDarkMode::dmfInit), + reinterpret_cast(hDlg)); + } return OnInitDialog(hDlg); } case WM_NOTIFY: @@ -75,6 +83,7 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar } return FALSE; } + } return FALSE; } diff --git a/DlgConsole.cpp b/DlgConsole.cpp index ae8b535..cf07528 100644 --- a/DlgConsole.cpp +++ b/DlgConsole.cpp @@ -41,6 +41,8 @@ #include "Options.h" #include "WaitCursor.h" +extern NppData g_nppData; + #ifdef _MSC_VER #pragma comment(lib, "shlwapi.lib") #endif @@ -1416,6 +1418,19 @@ void InvalidateListbox() InvalidateRect(s_hList, NULL, TRUE); } +///////////////////////////////////////////////////////////////////////////// +// Handle dark mode changes + +void HandleConsoleDarkModeChange() +{ + if (s_bConsoleVisible && s_hDlg) + { + // For docking panels, Notepad++ handles dark mode automatically + // We just need to invalidate our custom listbox + InvalidateListbox(); + } +} + ///////////////////////////////////////////////////////////////////////////// // @@ -1488,6 +1503,33 @@ static BOOL CALLBACK DlgProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM break; } + case WM_CTLCOLORDLG: + case WM_CTLCOLORSTATIC: + { + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + static HBRUSH dlgBrush = nullptr; + if (!dlgBrush) dlgBrush = CreateSolidBrush(RGB(32, 32, 32)); + SetTextColor((HDC)wParam, RGB(224, 224, 224)); + SetBkColor((HDC)wParam, RGB(32, 32, 32)); + return (LRESULT)dlgBrush; + } + break; + } + + case WM_CTLCOLOREDIT: + { + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + static HBRUSH editBrush = nullptr; + if (!editBrush) editBrush = CreateSolidBrush(RGB(64, 64, 64)); + SetTextColor((HDC)wParam, RGB(224, 224, 224)); + SetBkColor((HDC)wParam, RGB(64, 64, 64)); + return (LRESULT)editBrush; + } + break; + } + case WM_CTLCOLORLISTBOX: return (UINT_PTR) OnCtlColorListbox(hWnd, (HWND) lParam, (HDC) wParam); @@ -1539,7 +1581,7 @@ void SnippetsConsole() tbd.pszModuleName = L"Snippets"; // name of the dll this dialog belongs to tbd.pszName = L"Snippets"; // Name for titlebar tbd.hClient = s_hDlg; // HWND Handle of window this dock belongs to - tbd.uMask = DWS_DF_CONT_RIGHT | DWS_ICONTAB | DWS_USEOWNDARKMODE; // Put it on the right + tbd.uMask = DWS_DF_CONT_RIGHT | DWS_ICONTAB; // Put it on the right tbd.hIconTab = s_hTabIcon; // Put the icon in SendMessage(g_nppData._nppHandle, NPPM_DMMREGASDCKDLG, 0, (LPARAM) &tbd); // Register it diff --git a/DlgConsole.h b/DlgConsole.h index c0fe9a5..7cbabbf 100644 --- a/DlgConsole.h +++ b/DlgConsole.h @@ -25,6 +25,7 @@ extern void SnippetsConsole(); extern void CreateConsoleDlg(); extern void UpdateSnippetsList(); extern void InvalidateListbox(); +extern void HandleConsoleDarkModeChange(); extern void FocusLibraryCombo(); extern void FocusFilterSnippets(); extern void FocusSnippetsList(); diff --git a/DlgEditLanguages.cpp b/DlgEditLanguages.cpp index 40b1d21..e704bb3 100644 --- a/DlgEditLanguages.cpp +++ b/DlgEditLanguages.cpp @@ -244,6 +244,7 @@ static BOOL OnInitDialog(HWND hDlg) SetWindowText(GetDlgItem(hDlg, IDC_NAME), s_pLibrary->WGetName()); s_hWndLangList = GetDlgItem(hDlg, IDC_LANG_LIST); ListView_SetExtendedListViewStyle(s_hWndLangList, LVS_EX_FULLROWSELECT | LVS_EX_CHECKBOXES); + // AddColumn(0, 95, L"Language"); @@ -302,6 +303,7 @@ static BOOL OnInitDialog(HWND hDlg) ///////////////////////////////////////////////////////////////////////////// // + static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); @@ -310,6 +312,13 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar { case WM_INITDIALOG: { + // Apply dark mode theming using official API + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + ::SendMessage(g_nppData._nppHandle, NPPM_DARKMODESUBCLASSANDTHEME, + static_cast(0x0000000BUL), + reinterpret_cast(hDlg)); + } return OnInitDialog(hDlg); } case WM_COMMAND: @@ -324,6 +333,7 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar } return FALSE; } + } return FALSE; } diff --git a/DlgEditLibrary.cpp b/DlgEditLibrary.cpp index 4ac0ee7..07a06c0 100644 --- a/DlgEditLibrary.cpp +++ b/DlgEditLibrary.cpp @@ -112,6 +112,7 @@ static BOOL OnInitDialog(HWND hDlg) ///////////////////////////////////////////////////////////////////////////// // + static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); @@ -120,6 +121,13 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar { case WM_INITDIALOG: { + // Apply dark mode theming using official API + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + ::SendMessage(g_nppData._nppHandle, NPPM_DARKMODESUBCLASSANDTHEME, + static_cast(0x0000000BUL), + reinterpret_cast(hDlg)); + } return OnInitDialog(hDlg); } case WM_COMMAND: @@ -134,6 +142,7 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar } return FALSE; } + } return FALSE; } diff --git a/DlgEditSnippet.cpp b/DlgEditSnippet.cpp index b5e1b07..96986db 100644 --- a/DlgEditSnippet.cpp +++ b/DlgEditSnippet.cpp @@ -229,6 +229,7 @@ static BOOL OnInitDialog(HWND hDlg) ///////////////////////////////////////////////////////////////////////////// // + static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); @@ -237,6 +238,13 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar { case WM_INITDIALOG: { + // Apply dark mode theming using official API + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + ::SendMessage(g_nppData._nppHandle, NPPM_DARKMODESUBCLASSANDTHEME, + static_cast(0x0000000BUL), + reinterpret_cast(hDlg)); + } return OnInitDialog(hDlg); } case WM_COMMAND: @@ -269,6 +277,7 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar } return FALSE; } + } return FALSE; } diff --git a/DlgImportLibrary.cpp b/DlgImportLibrary.cpp index 184754e..92232f7 100644 --- a/DlgImportLibrary.cpp +++ b/DlgImportLibrary.cpp @@ -150,6 +150,7 @@ static BOOL OnInitDialog(HWND hDlg) ///////////////////////////////////////////////////////////////////////////// // + static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); @@ -158,6 +159,13 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar { case WM_INITDIALOG: { + // Apply dark mode theming using official API + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + ::SendMessage(g_nppData._nppHandle, NPPM_DARKMODESUBCLASSANDTHEME, + static_cast(0x0000000BUL), + reinterpret_cast(hDlg)); + } return OnInitDialog(hDlg); } case WM_COMMAND: @@ -172,6 +180,7 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar } return FALSE; } + } return FALSE; } diff --git a/DlgOptions.cpp b/DlgOptions.cpp index 148d6de..31dbdc2 100644 --- a/DlgOptions.cpp +++ b/DlgOptions.cpp @@ -131,6 +131,7 @@ static BOOL OnInitDialog(HWND hDlg) ///////////////////////////////////////////////////////////////////////////// // + static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); @@ -139,6 +140,13 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar { case WM_INITDIALOG: { + // Apply dark mode theming using official API + if ((bool)::SendMessage(g_nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0)) + { + ::SendMessage(g_nppData._nppHandle, NPPM_DARKMODESUBCLASSANDTHEME, + static_cast(NppDarkMode::dmfInit), + reinterpret_cast(hDlg)); + } return OnInitDialog(hDlg); } case WM_COMMAND: @@ -156,6 +164,7 @@ static BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPar } return FALSE; } + } return FALSE; } diff --git a/IatHook.h b/IatHook.h new file mode 100644 index 0000000..368c665 --- /dev/null +++ b/IatHook.h @@ -0,0 +1,94 @@ +// This file contains code from +// https://github.com/stevemk14ebr/PolyHook_2_0/blob/master/sources/IatHook.cpp +// which is licensed under the MIT License. +// See PolyHook_2_0-LICENSE for more information. + +#pragma once + +#include + +template +constexpr T RVA2VA(T1 base, T2 rva) +{ + return reinterpret_cast(reinterpret_cast(base) + rva); +} + +template +constexpr T DataDirectoryFromModuleBase(void *moduleBase, size_t entryID) +{ + auto dosHdr = static_cast(moduleBase); + auto ntHdr = RVA2VA(moduleBase, dosHdr->e_lfanew); + auto dataDir = ntHdr->OptionalHeader.DataDirectory; + return RVA2VA(moduleBase, dataDir[entryID].VirtualAddress); +} + +PIMAGE_THUNK_DATA FindAddressByName(void *moduleBase, PIMAGE_THUNK_DATA impName, PIMAGE_THUNK_DATA impAddr, const char *funcName) +{ + for (; impName->u1.Ordinal; ++impName, ++impAddr) + { + if (IMAGE_SNAP_BY_ORDINAL(impName->u1.Ordinal)) + continue; + + auto import = RVA2VA(moduleBase, impName->u1.AddressOfData); + if (strcmp(reinterpret_cast(import->Name), funcName) != 0) + continue; + return impAddr; + } + return nullptr; +} + +PIMAGE_THUNK_DATA FindAddressByOrdinal(void *moduleBase, PIMAGE_THUNK_DATA impName, PIMAGE_THUNK_DATA impAddr, uint16_t ordinal) +{ + UNREFERENCED_PARAMETER(moduleBase); + for (; impName->u1.Ordinal; ++impName, ++impAddr) + { + if (IMAGE_SNAP_BY_ORDINAL(impName->u1.Ordinal) && IMAGE_ORDINAL(impName->u1.Ordinal) == ordinal) + return impAddr; + } + return nullptr; +} + +PIMAGE_THUNK_DATA FindIatThunkInModule(void *moduleBase, const char *dllName, const char *funcName) +{ + auto imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_IMPORT); + for (; imports->Name; ++imports) + { + if (_stricmp(RVA2VA(moduleBase, imports->Name), dllName) != 0) + continue; + + auto origThunk = RVA2VA(moduleBase, imports->OriginalFirstThunk); + auto thunk = RVA2VA(moduleBase, imports->FirstThunk); + return FindAddressByName(moduleBase, origThunk, thunk, funcName); + } + return nullptr; +} + +PIMAGE_THUNK_DATA FindDelayLoadThunkInModule(void *moduleBase, const char *dllName, const char *funcName) +{ + auto imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); + for (; imports->DllNameRVA; ++imports) + { + if (_stricmp(RVA2VA(moduleBase, imports->DllNameRVA), dllName) != 0) + continue; + + auto impName = RVA2VA(moduleBase, imports->ImportNameTableRVA); + auto impAddr = RVA2VA(moduleBase, imports->ImportAddressTableRVA); + return FindAddressByName(moduleBase, impName, impAddr, funcName); + } + return nullptr; +} + +PIMAGE_THUNK_DATA FindDelayLoadThunkInModule(void *moduleBase, const char *dllName, uint16_t ordinal) +{ + auto imports = DataDirectoryFromModuleBase(moduleBase, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT); + for (; imports->DllNameRVA; ++imports) + { + if (_stricmp(RVA2VA(moduleBase, imports->DllNameRVA), dllName) != 0) + continue; + + auto impName = RVA2VA(moduleBase, imports->ImportNameTableRVA); + auto impAddr = RVA2VA(moduleBase, imports->ImportAddressTableRVA); + return FindAddressByOrdinal(moduleBase, impName, impAddr, ordinal); + } + return nullptr; +} \ No newline at end of file diff --git a/Makefile b/Makefile index 5f21604..d6d30fc 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,8 @@ endif $(V_RES) $(WINDRES) -o $@ -i $< PROGRAM_SRCS_CPP = \ + DarkMode.cpp \ + NppDarkMode.cpp \ DlgAbout.cpp \ DlgConsole.cpp \ DlgEditSnippet.cpp \ diff --git a/NPP/Notepad_plus_msgs.h b/NPP/Notepad_plus_msgs.h index 7cbbc92..8152205 100644 --- a/NPP/Notepad_plus_msgs.h +++ b/NPP/Notepad_plus_msgs.h @@ -533,6 +533,21 @@ enum Platform { PF_UNKNOWN, PF_X86, PF_X64, PF_IA64, PF_ARM64 }; // Note: in the case of calling failure ("false" is returned), you may need to change NppDarkMode::Colors structure to: // https://github.com/notepad-plus-plus/notepad-plus-plus/blob/master/PowerEditor/src/NppDarkMode.h#L32 + #define NPPM_DARKMODESUBCLASSANDTHEME (NPPMSG + 112) + namespace NppDarkMode + { + // Standard flags for main parent after its children are initialized. + constexpr ULONG dmfInit = 0x0000000BUL; + + // Standard flags for main parent usually used in NPPN_DARKMODECHANGED. + constexpr ULONG dmfHandleChange = 0x0000000CUL; + }; + // ULONG NPPM_DARKMODESUBCLASSANDTHEME(ULONG dmFlags, HWND hwnd) + // Add support for generic dark mode to plugin dialog. Subclassing is applied automatically unless DWS_USEOWNDARKMODE flag is used. + // wParam[in]: dmFlags has 2 possible value dmfInit (0x0000000BUL) & dmfHandleChange (0x0000000CUL) + // lParam[in]: hwnd is the dialog handle of plugin - Docking panels don't need to call NPPM_DARKMODESUBCLASSANDTHEME + // Returns successful combinations of flags. + #define NPPM_GETCURRENTCMDLINE (NPPMSG + 109) // INT NPPM_GETCURRENTCMDLINE(size_t strLen, TCHAR *commandLineStr) // Get the Current Command Line string. diff --git a/NppDarkMode.cpp b/NppDarkMode.cpp new file mode 100644 index 0000000..2106002 --- /dev/null +++ b/NppDarkMode.cpp @@ -0,0 +1,650 @@ +// This file is part of Notepad++ project +// Copyright (c) 2021 adzm / Adam D. Walling + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// at your option any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include +#include +#include +#include + +#include "NppDarkMode.h" +#include "DarkMode.h" + +#include +#include + +#ifdef __MINGW64__ +#include +#endif + +#if defined(__GNUC__) && __GNUC__ > 8 +#define WINAPI_LAMBDA [](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) -> LRESULT WINAPI +#elif defined(__GNUC__) +#define WINAPI_LAMBDA [](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) WINAPI -> LRESULT +#else +#define WINAPI_LAMBDA [](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) -> LRESULT +#endif + +namespace NppDarkMode +{ + struct Brushes + { + HBRUSH background = nullptr; + HBRUSH ctrlBackground = nullptr; + HBRUSH hotBackground = nullptr; + HBRUSH dlgBackground = nullptr; + HBRUSH errorBackground = nullptr; + HBRUSH edgeBrush = nullptr; + HBRUSH hotEdgeBrush = nullptr; + HBRUSH disabledEdgeBrush = nullptr; + + Brushes(const Colors& colors) + { + change(colors); + } + + ~Brushes() + { + ::DeleteObject(background); + ::DeleteObject(ctrlBackground); + ::DeleteObject(hotBackground); + ::DeleteObject(dlgBackground); + ::DeleteObject(errorBackground); + ::DeleteObject(edgeBrush); + ::DeleteObject(hotEdgeBrush); + ::DeleteObject(disabledEdgeBrush); + } + + void change(const Colors& colors) + { + ::DeleteObject(background); + ::DeleteObject(ctrlBackground); + ::DeleteObject(hotBackground); + ::DeleteObject(dlgBackground); + ::DeleteObject(errorBackground); + ::DeleteObject(edgeBrush); + ::DeleteObject(hotEdgeBrush); + ::DeleteObject(disabledEdgeBrush); + + background = ::CreateSolidBrush(colors.background); + ctrlBackground = ::CreateSolidBrush(colors.softerBackground); + hotBackground = ::CreateSolidBrush(colors.hotBackground); + dlgBackground = ::CreateSolidBrush(colors.pureBackground); + errorBackground = ::CreateSolidBrush(colors.errorBackground); + edgeBrush = ::CreateSolidBrush(colors.edge); + hotEdgeBrush = ::CreateSolidBrush(colors.hotEdge); + disabledEdgeBrush = ::CreateSolidBrush(colors.disabledEdge); + } + }; + + struct Pens + { + HPEN darkerTextPen = nullptr; + HPEN edgePen = nullptr; + HPEN hotEdgePen = nullptr; + HPEN disabledEdgePen = nullptr; + + Pens(const Colors& colors) + { + change(colors); + } + + ~Pens() + { + ::DeleteObject(darkerTextPen); + ::DeleteObject(edgePen); + ::DeleteObject(hotEdgePen); + ::DeleteObject(disabledEdgePen); + } + + void change(const Colors& colors) + { + ::DeleteObject(darkerTextPen); + ::DeleteObject(edgePen); + ::DeleteObject(hotEdgePen); + ::DeleteObject(disabledEdgePen); + + darkerTextPen = ::CreatePen(PS_SOLID, 1, colors.darkerText); + edgePen = ::CreatePen(PS_SOLID, 1, colors.edge); + hotEdgePen = ::CreatePen(PS_SOLID, 1, colors.hotEdge); + disabledEdgePen = ::CreatePen(PS_SOLID, 1, colors.disabledEdge); + } + }; + + // Default dark color scheme + static const Colors darkColors{ + HEXRGB(0x202020), // background + HEXRGB(0x404040), // softerBackground + HEXRGB(0x404040), // hotBackground + HEXRGB(0x202020), // pureBackground + HEXRGB(0xB00000), // errorBackground + HEXRGB(0xE0E0E0), // textColor + HEXRGB(0xC0C0C0), // darkerTextColor + HEXRGB(0x808080), // disabledTextColor + HEXRGB(0xFFFF00), // linkTextColor + HEXRGB(0x646464), // edgeColor + HEXRGB(0x9B9B9B), // hotEdgeColor + HEXRGB(0x484848) // disabledEdgeColor + }; + + struct Theme + { + Colors _colors; + Brushes _brushes; + Pens _pens; + + Theme(const Colors& colors) + : _colors(colors) + , _brushes(colors) + , _pens(colors) + {} + + void change(const Colors& colors) + { + _colors = colors; + _brushes.change(colors); + _pens.change(colors); + } + }; + + Theme tCurrent(darkColors); + static boolean _isDarkModeEnabled = false; + + void initDarkMode() + { + ::InitDarkMode(); + _isDarkModeEnabled = false; // Start with false, call setDarkMode to enable + } + + void setDarkMode(bool useDarkMode) + { + ::SetDarkMode(useDarkMode, true); + // Force dark mode regardless of system settings + _isDarkModeEnabled = useDarkMode; + } + + bool isEnabled() + { + return _isDarkModeEnabled; + } + + bool isWindows10() + { + return ::IsWindows10(); + } + + bool isWindows11() + { + return ::IsWindows11(); + } + + DWORD getWindowsBuildNumber() + { + return ::GetWindowsBuildNumber(); + } + + COLORREF getBackgroundColor() { return tCurrent._colors.background; } + COLORREF getCtrlBackgroundColor() { return tCurrent._colors.softerBackground; } + COLORREF getHotBackgroundColor() { return tCurrent._colors.hotBackground; } + COLORREF getDlgBackgroundColor() { return tCurrent._colors.pureBackground; } + COLORREF getErrorBackgroundColor() { return tCurrent._colors.errorBackground; } + COLORREF getTextColor() { return tCurrent._colors.text; } + COLORREF getDarkerTextColor() { return tCurrent._colors.darkerText; } + COLORREF getDisabledTextColor() { return tCurrent._colors.disabledText; } + COLORREF getLinkTextColor() { return tCurrent._colors.linkText; } + COLORREF getEdgeColor() { return tCurrent._colors.edge; } + COLORREF getHotEdgeColor() { return tCurrent._colors.hotEdge; } + COLORREF getDisabledEdgeColor() { return tCurrent._colors.disabledEdge; } + + HBRUSH getBackgroundBrush() { return tCurrent._brushes.background; } + HBRUSH getCtrlBackgroundBrush() { return tCurrent._brushes.ctrlBackground; } + HBRUSH getHotBackgroundBrush() { return tCurrent._brushes.hotBackground; } + HBRUSH getDlgBackgroundBrush() { return tCurrent._brushes.dlgBackground; } + HBRUSH getErrorBackgroundBrush() { return tCurrent._brushes.errorBackground; } + + HBRUSH getEdgeBrush() { return tCurrent._brushes.edgeBrush; } + HBRUSH getHotEdgeBrush() { return tCurrent._brushes.hotEdgeBrush; } + HBRUSH getDisabledEdgeBrush() { return tCurrent._brushes.disabledEdgeBrush; } + + HPEN getDarkerTextPen() { return tCurrent._pens.darkerTextPen; } + HPEN getEdgePen() { return tCurrent._pens.edgePen; } + HPEN getHotEdgePen() { return tCurrent._pens.hotEdgePen; } + HPEN getDisabledEdgePen() { return tCurrent._pens.disabledEdgePen; } + + // from DarkMode.h + void allowDarkModeForApp(bool allow) + { + ::AllowDarkModeForApp(allow); + } + + bool allowDarkModeForWindow(HWND hWnd, bool allow) + { + return ::AllowDarkModeForWindow(hWnd, allow); + } + + void setTitleBarThemeColor(HWND hWnd) + { + ::RefreshTitleBarThemeColor(hWnd); + } + + // enhancements to DarkMode.h + void enableDarkScrollBarForWindowAndChildren(HWND hwnd) + { + ::EnableDarkScrollBarForWindowAndChildren(hwnd); + } + + inline void paintRoundFrameRect(HDC hdc, const RECT rect, const HPEN hpen, int width, int height) + { + auto holdBrush = ::SelectObject(hdc, ::GetStockObject(NULL_BRUSH)); + auto holdPen = ::SelectObject(hdc, hpen); + ::RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, width, height); + ::SelectObject(hdc, holdBrush); + ::SelectObject(hdc, holdPen); + } + + // Button subclassing + constexpr UINT_PTR g_buttonSubclassID = 42; + + struct ButtonData + { + bool isHot = false; + bool isFocused = false; + bool isPressed = false; + }; + + static LRESULT CALLBACK ButtonSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData + ) + { + auto pButtonData = reinterpret_cast(dwRefData); + + switch (uMsg) + { + case WM_MOUSEMOVE: + case WM_MOUSELEAVE: + { + pButtonData->isHot = (uMsg == WM_MOUSEMOVE); + ::InvalidateRect(hWnd, nullptr, FALSE); + break; + } + + case WM_PAINT: + { + if (!isEnabled()) + { + break; + } + + PAINTSTRUCT ps; + HDC hdc = ::BeginPaint(hWnd, &ps); + + RECT rcClient{}; + ::GetClientRect(hWnd, &rcClient); + + bool isDisabled = !::IsWindowEnabled(hWnd); + bool isPressed = (::GetKeyState(VK_LBUTTON) & 0x8000) && pButtonData->isHot; + + COLORREF bgColor = isPressed ? getHotBackgroundColor() : + pButtonData->isHot ? getCtrlBackgroundColor() : + getBackgroundColor(); + + HBRUSH bgBrush = ::CreateSolidBrush(bgColor); + ::FillRect(hdc, &rcClient, bgBrush); + ::DeleteObject(bgBrush); + + // Draw border + HPEN borderPen = ::CreatePen(PS_SOLID, 1, pButtonData->isHot ? getHotEdgeColor() : getEdgeColor()); + paintRoundFrameRect(hdc, rcClient, borderPen, 2, 2); + ::DeleteObject(borderPen); + + // Draw text + ::SetBkMode(hdc, TRANSPARENT); + ::SetTextColor(hdc, isDisabled ? getDisabledTextColor() : getTextColor()); + + wchar_t buttonText[256]; + ::GetWindowText(hWnd, buttonText, 256); + + ::DrawText(hdc, buttonText, -1, &rcClient, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + ::EndPaint(hWnd, &ps); + return 0; + } + + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, ButtonSubclass, uIdSubclass); + delete pButtonData; + break; + } + } + + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + } + + void subclassButtonControl(HWND hwnd) + { + DWORD_PTR pButtonData = reinterpret_cast(new ButtonData()); + SetWindowSubclass(hwnd, ButtonSubclass, g_buttonSubclassID, pButtonData); + } + + // ComboBox subclassing - minimal implementation + constexpr UINT_PTR g_comboBoxSubclassID = 43; + + static LRESULT CALLBACK ComboBoxSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData + ) + { + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, ComboBoxSubclass, uIdSubclass); + break; + } + } + + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + } + + void subclassComboBoxControl(HWND hwnd) + { + SetWindowSubclass(hwnd, ComboBoxSubclass, g_comboBoxSubclassID, 0); + } + + // GroupBox subclassing - minimal implementation + constexpr UINT_PTR g_groupboxSubclassID = 44; + + static LRESULT CALLBACK GroupboxSubclass( + HWND hWnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam, + UINT_PTR uIdSubclass, + DWORD_PTR dwRefData + ) + { + switch (uMsg) + { + case WM_NCDESTROY: + { + ::RemoveWindowSubclass(hWnd, GroupboxSubclass, uIdSubclass); + break; + } + } + + return ::DefSubclassProc(hWnd, uMsg, wParam, lParam); + } + + void subclassGroupboxControl(HWND hwnd) + { + SetWindowSubclass(hwnd, GroupboxSubclass, g_groupboxSubclassID, 0); + } + + // Helper function to get window class name + std::wstring getWndClassName(HWND hwnd) + { + const size_t classNameLen = 16; + TCHAR className[classNameLen]; + GetClassName(hwnd, className, classNameLen); + return std::wstring(className); + } + + void autoSubclassAndThemeChildControls(HWND hwndParent, bool subclass, bool theme) + { + struct Params + { + const wchar_t* themeClassName = nullptr; + bool subclass = false; + bool theme = false; + }; + + Params p{ + isEnabled() ? L"DarkMode_Explorer" : nullptr + , subclass + , theme + }; + + ::EnableThemeDialogTexture(hwndParent, theme && !isEnabled() ? ETDT_ENABLETAB : ETDT_DISABLE); + + EnumChildWindows(hwndParent, [](HWND hwnd, LPARAM lParam) -> BOOL { + auto& p = *reinterpret_cast(lParam); + const size_t classNameLen = 16; + TCHAR className[classNameLen] {}; + GetClassName(hwnd, className, classNameLen); + + if (wcscmp(className, WC_COMBOBOX) == 0) + { + auto style = ::GetWindowLongPtr(hwnd, GWL_STYLE); + + if ((style & CBS_DROPDOWNLIST) == CBS_DROPDOWNLIST || (style & CBS_DROPDOWN) == CBS_DROPDOWN) + { + COMBOBOXINFO cbi = {}; + cbi.cbSize = sizeof(COMBOBOXINFO); + if (GetComboBoxInfo(hwnd, &cbi) == TRUE) + { + if (p.theme && cbi.hwndList) + { + //dark scrollbar for listbox of combobox + SetWindowTheme(cbi.hwndList, p.themeClassName, nullptr); + } + } + + if (p.subclass) + { + HWND hParent = ::GetParent(hwnd); + if ((hParent == nullptr || getWndClassName(hParent) != WC_COMBOBOXEX)) + { + subclassComboBoxControl(hwnd); + } + } + } + return TRUE; + } + + if (wcscmp(className, WC_LISTBOX) == 0) + { + if (p.theme) + { + //dark scrollbar for listbox + SetWindowTheme(hwnd, p.themeClassName, nullptr); + } + return TRUE; + } + + if (wcscmp(className, WC_EDIT) == 0) + { + auto style = ::GetWindowLongPtr(hwnd, GWL_STYLE); + bool hasScrollBar = ((style & WS_HSCROLL) == WS_HSCROLL) || ((style & WS_VSCROLL) == WS_VSCROLL); + if (p.theme && hasScrollBar) + { + //dark scrollbar for edit control + SetWindowTheme(hwnd, p.themeClassName, nullptr); + } + return TRUE; + } + + if (wcscmp(className, WC_BUTTON) == 0) + { + auto nButtonStyle = ::GetWindowLongPtr(hwnd, GWL_STYLE); + switch (nButtonStyle & BS_TYPEMASK) + { + case BS_CHECKBOX: + case BS_AUTOCHECKBOX: + case BS_3STATE: + case BS_AUTO3STATE: + case BS_RADIOBUTTON: + case BS_AUTORADIOBUTTON: + { + if ((nButtonStyle & BS_PUSHLIKE) == BS_PUSHLIKE) + { + if (p.theme) + { + SetWindowTheme(hwnd, p.themeClassName, nullptr); + } + } + + if (p.subclass) + { + subclassButtonControl(hwnd); + } + break; + } + + case BS_GROUPBOX: + { + if (p.subclass) + { + subclassGroupboxControl(hwnd); + } + break; + } + + case BS_PUSHBUTTON: + case BS_DEFPUSHBUTTON: + case BS_SPLITBUTTON: + case BS_DEFSPLITBUTTON: + { + if (p.theme) + { + SetWindowTheme(hwnd, p.themeClassName, nullptr); + } + break; + } + + default: + { + break; + } + } + return TRUE; + } + + return TRUE; + }, reinterpret_cast(&p)); + } + + void autoThemeChildControls(HWND hwndParent) + { + autoSubclassAndThemeChildControls(hwndParent, false, true); + } + + void setDarkTitleBar(HWND hwnd) + { + allowDarkModeForWindow(hwnd, isEnabled()); + setTitleBarThemeColor(hwnd); + } + + void setDarkScrollBar(HWND hwnd) + { + if (isEnabled()) + { + enableDarkScrollBarForWindowAndChildren(hwnd); + } + } + + void disableVisualStyle(HWND hwnd, bool doDisable) + { + if (doDisable) + { + SetWindowTheme(hwnd, L"", L""); + } + else + { + SetWindowTheme(hwnd, nullptr, nullptr); + } + } + + void setBorder(HWND hwnd, bool border) + { + auto currentStyle = ::GetWindowLongPtr(hwnd, GWL_EXSTYLE); + auto newStyle = border ? + (currentStyle | WS_EX_CLIENTEDGE) : + (currentStyle & ~WS_EX_CLIENTEDGE); + + if (newStyle != currentStyle) + { + ::SetWindowLongPtr(hwnd, GWL_EXSTYLE, newStyle); + ::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } + } + + LRESULT onCtlColor(HDC hdc) + { + ::SetTextColor(hdc, getTextColor()); + ::SetBkColor(hdc, getBackgroundColor()); + return reinterpret_cast(getBackgroundBrush()); + } + + LRESULT onCtlColorSofter(HDC hdc) + { + ::SetTextColor(hdc, getTextColor()); + ::SetBkColor(hdc, getCtrlBackgroundColor()); + return reinterpret_cast(getCtrlBackgroundBrush()); + } + + LRESULT onCtlColorDarker(HDC hdc) + { + ::SetTextColor(hdc, getTextColor()); + ::SetBkColor(hdc, getDlgBackgroundColor()); + return reinterpret_cast(getDlgBackgroundBrush()); + } + + LRESULT onCtlColorError(HDC hdc) + { + ::SetTextColor(hdc, getTextColor()); + ::SetBkColor(hdc, getErrorBackgroundColor()); + return reinterpret_cast(getErrorBackgroundBrush()); + } + + LRESULT onCtlColorIfEnabled(HDC hdc, bool bEnabled) + { + LRESULT result{ FALSE }; + + if (isEnabled()) { + result = onCtlColorDarker(hdc); + SetTextColor(hdc, bEnabled ? getTextColor() : getDisabledTextColor()); + } + else { + SetTextColor(hdc, GetSysColor(bEnabled ? COLOR_WINDOWTEXT : COLOR_GRAYTEXT)); + } + + return result; + } + + INT_PTR onCtlColorListbox(WPARAM wParam, LPARAM lParam) + { + auto hdc = reinterpret_cast(wParam); + auto hwnd = reinterpret_cast(lParam); + + auto style = ::GetWindowLongPtr(hwnd, GWL_STYLE); + bool isComboBox = (style & LBS_COMBOBOX) == LBS_COMBOBOX; + if (!isComboBox && ::IsWindowEnabled(hwnd)) + { + return static_cast(onCtlColorSofter(hdc)); + } + return static_cast(onCtlColor(hdc)); + } +} \ No newline at end of file diff --git a/NppDarkMode.h b/NppDarkMode.h new file mode 100644 index 0000000..638f545 --- /dev/null +++ b/NppDarkMode.h @@ -0,0 +1,127 @@ +// This file is part of Notepad++ project +// Copyright (c) 2021 adzm / Adam D. Walling + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// at your option any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#pragma comment(lib, "dwmapi.lib") +#pragma comment(lib, "comctl32.lib") +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "uxtheme.lib") + +constexpr COLORREF HEXRGB(DWORD rrggbb) { + // from 0xRRGGBB like natural #RRGGBB + // to the little-endian 0xBBGGRR + return + ((rrggbb & 0xFF0000) >> 16) | + ((rrggbb & 0x00FF00) ) | + ((rrggbb & 0x0000FF) << 16); +} + +namespace NppDarkMode +{ + struct Colors + { + COLORREF background = 0; + COLORREF softerBackground = 0; // ctrl background color + COLORREF hotBackground = 0; + COLORREF pureBackground = 0; // dlg background color + COLORREF errorBackground = 0; + COLORREF text = 0; + COLORREF darkerText = 0; + COLORREF disabledText = 0; + COLORREF linkText = 0; + COLORREF edge = 0; + COLORREF hotEdge = 0; + COLORREF disabledEdge = 0; + }; + + struct Options + { + bool enable = false; + bool enableMenubar = false; + bool enablePlugin = false; + }; + + void initDarkMode(); + bool isEnabled(); + void setDarkMode(bool useDarkMode); + + bool isWindows10(); + bool isWindows11(); + DWORD getWindowsBuildNumber(); + + COLORREF getBackgroundColor(); + COLORREF getCtrlBackgroundColor(); + COLORREF getHotBackgroundColor(); + COLORREF getDlgBackgroundColor(); + COLORREF getErrorBackgroundColor(); + + COLORREF getTextColor(); + COLORREF getDarkerTextColor(); + COLORREF getDisabledTextColor(); + COLORREF getLinkTextColor(); + + COLORREF getEdgeColor(); + COLORREF getHotEdgeColor(); + COLORREF getDisabledEdgeColor(); + + HBRUSH getBackgroundBrush(); + HBRUSH getDlgBackgroundBrush(); + HBRUSH getCtrlBackgroundBrush(); + HBRUSH getHotBackgroundBrush(); + HBRUSH getErrorBackgroundBrush(); + + HBRUSH getEdgeBrush(); + HBRUSH getHotEdgeBrush(); + HBRUSH getDisabledEdgeBrush(); + + HPEN getDarkerTextPen(); + HPEN getEdgePen(); + HPEN getHotEdgePen(); + HPEN getDisabledEdgePen(); + + // from DarkMode.h + void allowDarkModeForApp(bool allow); + bool allowDarkModeForWindow(HWND hWnd, bool allow); + void setTitleBarThemeColor(HWND hWnd); + + // enhancements to DarkMode.h + void enableDarkScrollBarForWindowAndChildren(HWND hwnd); + + inline void paintRoundFrameRect(HDC hdc, const RECT rect, const HPEN hpen, int width = 0, int height = 0); + + void subclassButtonControl(HWND hwnd); + void subclassComboBoxControl(HWND hwnd); + void subclassGroupboxControl(HWND hwnd); + + void autoSubclassAndThemeChildControls(HWND hwndParent, bool subclass = true, bool theme = true); + void autoThemeChildControls(HWND hwndParent); + + void setDarkTitleBar(HWND hwnd); + void setDarkScrollBar(HWND hwnd); + + void disableVisualStyle(HWND hwnd, bool doDisable); + + void setBorder(HWND hwnd, bool border = true); + + LRESULT onCtlColor(HDC hdc); + LRESULT onCtlColorSofter(HDC hdc); + LRESULT onCtlColorDarker(HDC hdc); + LRESULT onCtlColorError(HDC hdc); + + LRESULT onCtlColorIfEnabled(HDC hdc, bool bEnabled); + INT_PTR onCtlColorListbox(WPARAM wParam, LPARAM lParam); +} \ No newline at end of file diff --git a/NppSnippets.cpp b/NppSnippets.cpp index 4ac6297..8abaa94 100644 --- a/NppSnippets.cpp +++ b/NppSnippets.cpp @@ -112,6 +112,13 @@ extern "C" __declspec(dllexport) void beNotified(SCNotification* notifyCode) break; } + case NPPN_DARKMODECHANGED: + { + // Update console dialog dark mode theming + HandleConsoleDarkModeChange(); + break; + } + case NPPN_TBMODIFICATION: { // First initialize the options diff --git a/NppSnippets.vcxproj b/NppSnippets.vcxproj index a30ac91..e02b7d0 100644 --- a/NppSnippets.vcxproj +++ b/NppSnippets.vcxproj @@ -264,6 +264,8 @@ + + diff --git a/x64/Release/NppSnippets.dll b/x64/Release/NppSnippets.dll new file mode 100644 index 0000000..dea73a5 Binary files /dev/null and b/x64/Release/NppSnippets.dll differ diff --git a/x64/Release/NppSnippets.exp b/x64/Release/NppSnippets.exp new file mode 100644 index 0000000..bf5c25f Binary files /dev/null and b/x64/Release/NppSnippets.exp differ diff --git a/x64/Release/NppSnippets.lib b/x64/Release/NppSnippets.lib new file mode 100644 index 0000000..a574cd4 Binary files /dev/null and b/x64/Release/NppSnippets.lib differ diff --git a/x64/Release/NppSnippets.pdb b/x64/Release/NppSnippets.pdb new file mode 100644 index 0000000..5fbfeba Binary files /dev/null and b/x64/Release/NppSnippets.pdb differ