From d9bf9c9df194de0b6323fbe3a709c469b0373cbf Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sat, 4 Jul 2026 00:45:52 -0300 Subject: [PATCH 01/24] Add a dark theme for the Options dialog Makes the Options property sheet (the "O" dialog) follow the dark look of the rest of the player instead of always rendering in the light system style. It covers the whole dialog: the navigation tree, the property pages, group boxes, edits/combos, list controls, spin buttons, sliders, tab headers, checkboxes and the scrollbars (flat, drawn through the bundled CoolSB, matching the playlist). The sheet also re-themes live when the setting is toggled while it is open. Everything is gated on the existing "Use the 'dark' theme" setting (bUseDarkTheme) and reuses the existing ThemeRGB() palette, so when the flag is off every control falls back to its original light appearance and there is no behavioural change. A new helper (controls/DarkTheme.*) centralises the theming so most of the ~26 pages are covered from CPPageBase / CPPageSheet. Small, flag-gated tweaks are made to the bundled coolsb (expose one global as extern so the header can be included from more than one TU) and to the TreePropSheet page frame (dark caption/background colours). Co-Authored-By: Claude Opus 4.8 --- .../ui/TreePropSheet/PropPageFrameDefault.cpp | 34 +- .../ui/TreePropSheet/PropPageFrameDefault.h | 9 + src/ExtLib/ui/coolsb/coolsblib.c | 4 + src/ExtLib/ui/coolsb/coolscroll.h | 2 +- src/apps/mplayerc/PPageAccelTbl.cpp | 14 +- src/apps/mplayerc/PPageBase.cpp | 86 +- src/apps/mplayerc/PPageBase.h | 7 + src/apps/mplayerc/PPageExternalFilters.h | 3 +- src/apps/mplayerc/PPageFormats.cpp | 198 ++- src/apps/mplayerc/PPageFullscreen.cpp | 113 +- src/apps/mplayerc/PPageFullscreen.h | 3 + src/apps/mplayerc/PPageInterface.cpp | 30 +- src/apps/mplayerc/PPageInternalFilters.cpp | 6 +- src/apps/mplayerc/PPageInternalFilters.h | 6 +- src/apps/mplayerc/PPageOSD.cpp | 12 +- src/apps/mplayerc/PPageSheet.cpp | 45 + src/apps/mplayerc/PPageSheet.h | 2 + src/apps/mplayerc/PPageSubStyle.cpp | 15 +- src/apps/mplayerc/PlayerListCtrl.cpp | 48 + src/apps/mplayerc/PlayerListCtrl.h | 11 + src/apps/mplayerc/RegFilterChooserDlg.cpp | 3 + src/apps/mplayerc/SelectMediaType.cpp | 3 + .../mplayerc/controls/DarkCheckListBox.cpp | 101 ++ src/apps/mplayerc/controls/DarkCheckListBox.h | 37 + src/apps/mplayerc/controls/DarkTabCtrl.cpp | 149 ++ src/apps/mplayerc/controls/DarkTabCtrl.h | 45 + src/apps/mplayerc/controls/DarkTheme.cpp | 1388 +++++++++++++++++ src/apps/mplayerc/controls/DarkTheme.h | 110 ++ src/apps/mplayerc/mpc-be.vcxproj | 6 + src/apps/mplayerc/mpc-be.vcxproj.filters | 18 + src/apps/mplayerc/mplayerc.rc | 38 +- 31 files changed, 2482 insertions(+), 64 deletions(-) create mode 100644 src/apps/mplayerc/controls/DarkCheckListBox.cpp create mode 100644 src/apps/mplayerc/controls/DarkCheckListBox.h create mode 100644 src/apps/mplayerc/controls/DarkTabCtrl.cpp create mode 100644 src/apps/mplayerc/controls/DarkTabCtrl.h create mode 100644 src/apps/mplayerc/controls/DarkTheme.cpp create mode 100644 src/apps/mplayerc/controls/DarkTheme.h diff --git a/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.cpp b/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.cpp index 8781486a7b..6ab1039b61 100644 --- a/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.cpp +++ b/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.cpp @@ -175,6 +175,12 @@ BEGIN_MESSAGE_MAP(CPropPageFrameDefault, CWnd) //}}AFX_MSG_MAP END_MESSAGE_MAP() +// +bool CPropPageFrameDefault::s_bDarkMode = false; +COLORREF CPropPageFrameDefault::s_clrFace = RGB(37, 42, 47); +COLORREF CPropPageFrameDefault::s_clrText = RGB(179, 184, 189); +// + CPropPageFrameDefault::CPropPageFrameDefault() { @@ -299,9 +305,22 @@ CRect CPropPageFrameDefault::CalcCaptionArea() void CPropPageFrameDefault::DrawCaption(CDC *pDc, CRect rect, LPCTSTR lpszCaption, HICON hIcon) { // - COLORREF clrLeft = GetSysColor(COLOR_ACTIVECAPTION); + auto Lighten = [](COLORREF c, int d) -> COLORREF { + int r = GetRValue(c) + d; if (r > 255) { r = 255; } + int g = GetGValue(c) + d; if (g > 255) { g = 255; } + int b = GetBValue(c) + d; if (b > 255) { b = 255; } + return RGB(r, g, b); + }; + + COLORREF clrLeft; + COLORREF clrRight; + if (s_bDarkMode) { + clrLeft = clrRight = Lighten(s_clrFace, 14); // subtle header band above the page + } else { + clrLeft = GetSysColor(COLOR_ACTIVECAPTION); + clrRight = pDc->GetPixel(rect.right-1, rect.top); + } // - COLORREF clrRight = pDc->GetPixel(rect.right-1, rect.top); FillGradientRectH(pDc, rect, clrLeft, clrRight); // draw icon @@ -317,7 +336,7 @@ void CPropPageFrameDefault::DrawCaption(CDC *pDc, CRect rect, LPCTSTR lpszCaptio // draw text rect.left += 2; - COLORREF clrPrev = pDc->SetTextColor(GetSysColor(COLOR_CAPTIONTEXT)); + COLORREF clrPrev = pDc->SetTextColor(s_bDarkMode ? s_clrText : GetSysColor(COLOR_CAPTIONTEXT)); int nBkStyle = pDc->SetBkMode(TRANSPARENT); CFont *pFont = (CFont*)pDc->SelectStockObject(SYSTEM_FONT); @@ -405,6 +424,15 @@ void CPropPageFrameDefault::OnPaint() BOOL CPropPageFrameDefault::OnEraseBkgnd(CDC* pDC) { + // + if (s_bDarkMode) { + CRect rect; + GetClientRect(rect); + pDC->FillSolidRect(rect, s_clrFace); + return TRUE; + } + // + if (g_ThemeLib.IsAvailable() && g_ThemeLib.IsThemeActive()) { HTHEME hTheme = g_ThemeLib.OpenThemeData(m_hWnd, L"Tab"); diff --git a/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.h b/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.h index 8063a29baf..b651bdeb25 100644 --- a/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.h +++ b/src/ExtLib/ui/TreePropSheet/PropPageFrameDefault.h @@ -46,6 +46,15 @@ class /*AFX_EXT_CLASS*/ CPropPageFrameDefault : public CWnd, CPropPageFrameDefault(); virtual ~CPropPageFrameDefault(); +// +// Dark theme palette, set by the host (CPPageSheet) before the frame paints. +// When s_bDarkMode is false the classic (system-colored) appearance is used. +public: + static bool s_bDarkMode; + static COLORREF s_clrFace; + static COLORREF s_clrText; +// + // operations public: diff --git a/src/ExtLib/ui/coolsb/coolsblib.c b/src/ExtLib/ui/coolsb/coolsblib.c index 2d0cb736ad..0d4e11a983 100644 --- a/src/ExtLib/ui/coolsb/coolsblib.c +++ b/src/ExtLib/ui/coolsb/coolsblib.c @@ -37,6 +37,10 @@ static TCHAR szPropStr[] = _T("CoolSBSubclassPtr"); +// Single definition of the theme-colour callback (declared extern in coolscroll.h so the +// header can be included from more than one translation unit without a duplicate symbol). +ptr_themeRGB fThemeRGB = NULL; + LRESULT CALLBACK CoolSBWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); SCROLLWND *GetScrollWndFromHwnd(HWND hwnd) diff --git a/src/ExtLib/ui/coolsb/coolscroll.h b/src/ExtLib/ui/coolsb/coolscroll.h index 682b0f2804..2c1b82994f 100644 --- a/src/ExtLib/ui/coolsb/coolscroll.h +++ b/src/ExtLib/ui/coolsb/coolscroll.h @@ -143,7 +143,7 @@ int WINAPI CoolSB_SetScrollPos (HWND hwnd, int nBar, int nPos, BOOL fRedraw); int WINAPI CoolSB_SetScrollRange (HWND hwnd, int nBar, int nMinPos, int nMaxPos, BOOL fRedraw); BOOL WINAPI CoolSB_ShowScrollBar (HWND hwnd, int wBar, BOOL fShow); -ptr_themeRGB fThemeRGB; +extern ptr_themeRGB fThemeRGB; // // Scrollbar dimension functions diff --git a/src/apps/mplayerc/PPageAccelTbl.cpp b/src/apps/mplayerc/PPageAccelTbl.cpp index 757f58d414..114a571f09 100644 --- a/src/apps/mplayerc/PPageAccelTbl.cpp +++ b/src/apps/mplayerc/PPageAccelTbl.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "PPageAccelTbl.h" +#include "controls/DarkTheme.h" //#define MASK_NUMBER 0xFFF #define DUP_KEY (1<<12) @@ -839,11 +840,20 @@ void CPPageAccelTbl::OnCustomdrawList( NMHDR* pNMHDR, LRESULT* pResult ) auto itemData = (ITEMDATA*)m_list.GetItemData(pLVCD->nmcd.dwItemSpec); auto dup = itemData->flag; - if (pLVCD->iSubItem == COL_CMD && dup + const bool isDup = (pLVCD->iSubItem == COL_CMD && dup || pLVCD->iSubItem == COL_KEY && (dup & DUP_KEY) || pLVCD->iSubItem == COL_APPCMD && (dup & DUP_APPCMD) - || pLVCD->iSubItem == COL_RMCMD && (dup & DUP_RMCMD)) { + || pLVCD->iSubItem == COL_RMCMD && (dup & DUP_RMCMD)); + + if (isDup) { pLVCD->clrTextBk = RGB(255, 130, 120); + if (DarkTheme::IsActive()) { + pLVCD->clrText = RGB(0, 0, 0); // keep the text readable on the highlight + } + } + else if (DarkTheme::IsActive()) { + pLVCD->clrTextBk = DarkTheme::FaceColor(); + pLVCD->clrText = DarkTheme::TextColor(); } else { pLVCD->clrTextBk = GetSysColor(COLOR_WINDOW); diff --git a/src/apps/mplayerc/PPageBase.cpp b/src/apps/mplayerc/PPageBase.cpp index 8388c1a4e9..bbfdaf2786 100644 --- a/src/apps/mplayerc/PPageBase.cpp +++ b/src/apps/mplayerc/PPageBase.cpp @@ -22,6 +22,7 @@ #include "stdafx.h" #include "MainFrm.h" #include "PPageBase.h" +#include "controls/DarkTheme.h" // CPPageBase dialog @@ -79,6 +80,9 @@ BOOL CPPageBase::PreTranslateMessage(MSG* pMsg) BEGIN_MESSAGE_MAP(CPPageBase, CCmdUIPropertyPage) ON_WM_DESTROY() + ON_WM_CTLCOLOR() + ON_WM_ERASEBKGND() + ON_WM_DRAWITEM() END_MESSAGE_MAP() // CPPageBase message handlers @@ -87,7 +91,87 @@ BOOL CPPageBase::OnSetActive() { AfxGetAppSettings().nLastUsedPage = (UINT)(ULONG_PTR)m_pPSP->pszTemplate; - return __super::OnSetActive(); + BOOL bRet = __super::OnSetActive(); + + // Re-apply the dark visual style on every activation. It is idempotent (controls + // already themed are skipped), and doing it each time avoids a race on the page + // shown first, whose controls may not have been ready the very first time. + DarkTheme::ApplyThemeToChildren(GetSafeHwnd()); + // Keep group boxes behind the controls they frame so their dark fill never paints over + // them (which made labels/combos vanish until invalidated, e.g. after Apply). + DarkTheme::FixGroupBoxes(GetSafeHwnd()); + + return bRet; +} + +HBRUSH CPPageBase::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) +{ + HBRUSH hbr = __super::OnCtlColor(pDC, pWnd, nCtlColor); + + if (HBRUSH hbrDark = DarkTheme::OnCtlColor(pDC, nCtlColor)) { + return hbrDark; + } + + return hbr; +} + +BOOL CPPageBase::OnEraseBkgnd(CDC* pDC) +{ + if (DarkTheme::IsActive()) { + CRect rc; + GetClientRect(rc); + pDC->FillSolidRect(rc, DarkTheme::FaceColor()); + return TRUE; + } + + return __super::OnEraseBkgnd(pDC); +} + +// The horizontal separator lines in the option pages are owner-drawn statics +// (SS_OWNERDRAW) so Windows does not paint its light 3D etched line. We draw them +// here: a flat line in the shared border colour when the dark theme is active, or +// the classic etched edge otherwise. +void CPPageBase::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) +{ + if (lpDrawItemStruct && lpDrawItemStruct->CtlType == ODT_STATIC) { + CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); + CRect rc(lpDrawItemStruct->rcItem); + if (DarkTheme::IsActive()) { + pDC->FillSolidRect(rc, DarkTheme::FaceColor()); + pDC->FillSolidRect(rc.left, rc.top + rc.Height() / 2, rc.Width(), 1, DarkTheme::CtrlBorderColor()); + } else { + pDC->FillSolidRect(rc, GetSysColor(COLOR_3DFACE)); + ::DrawEdge(lpDrawItemStruct->hDC, &rc, EDGE_ETCHED, BF_TOP); + } + return; + } + + __super::OnDrawItem(nIDCtl, lpDrawItemStruct); +} + +BOOL CPPageBase::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult) +{ + // For NM_CUSTOMDRAW, let any page-specific handler run first (e.g. colour-picker + // buttons that paint themselves with the selected colour, or list controls). Only + // when the page does not handle it do we apply the generic dark theming for the + // controls native dark mode leaves light: trackbar (slider) channels, checkbox/ + // radio-button captions and push buttons. (Group boxes are handled by subclassing, + // since they emit no NM_CUSTOMDRAW.) + NMHDR* pNMHDR = reinterpret_cast(lParam); + if (pNMHDR && pNMHDR->code == NM_CUSTOMDRAW) { + if (__super::OnNotify(wParam, lParam, pResult)) { + return TRUE; + } + if (DarkTheme::TrackbarCustomDraw(pNMHDR, pResult)) { + return TRUE; + } + if (DarkTheme::ButtonCustomDraw(pNMHDR, pResult)) { + return TRUE; + } + return FALSE; + } + + return __super::OnNotify(wParam, lParam, pResult); } void CPPageBase::OnDestroy() diff --git a/src/apps/mplayerc/PPageBase.h b/src/apps/mplayerc/PPageBase.h index 6cea299149..7f142b46f9 100644 --- a/src/apps/mplayerc/PPageBase.h +++ b/src/apps/mplayerc/PPageBase.h @@ -34,6 +34,9 @@ class CPPageBase : public CCmdUIPropertyPage CToolTipCtrl m_wndToolTip; void CreateToolTip(); + // dark theme: apply the dark visual style to child controls once, on first activation + bool m_bDarkThemeApplied = false; + public: CPPageBase(UINT nIDTemplate, UINT nIDCaption = 0); virtual ~CPPageBase(); @@ -43,6 +46,7 @@ class CPPageBase : public CCmdUIPropertyPage virtual BOOL PreTranslateMessage(MSG* pMsg); virtual BOOL OnSetActive(); virtual BOOL OnApply(); + virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult); int ScaleY(int y); @@ -50,4 +54,7 @@ class CPPageBase : public CCmdUIPropertyPage public: afx_msg void OnDestroy(); + afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + afx_msg void OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct); }; diff --git a/src/apps/mplayerc/PPageExternalFilters.h b/src/apps/mplayerc/PPageExternalFilters.h index a5da197fcf..947f8477bd 100644 --- a/src/apps/mplayerc/PPageExternalFilters.h +++ b/src/apps/mplayerc/PPageExternalFilters.h @@ -23,6 +23,7 @@ #include "PPageBase.h" #include "controls/FloatEdit.h" +#include "controls/DarkCheckListBox.h" bool IsSupportedExternalVideoRenderer(CLSID clsid); @@ -46,7 +47,7 @@ class CPPageExternalFilters : public CPPageBase enum { IDD = IDD_PPAGEEXTERNALFILTERS }; - CCheckListBox m_filters; + CDarkCheckListBox m_filters; int m_iLoadType = FilterOverride::PREFERRED; CHexEdit m_dwMerit; CTreeCtrl m_tree; diff --git a/src/apps/mplayerc/PPageFormats.cpp b/src/apps/mplayerc/PPageFormats.cpp index fb64da19f1..89a30dff84 100644 --- a/src/apps/mplayerc/PPageFormats.cpp +++ b/src/apps/mplayerc/PPageFormats.cpp @@ -27,11 +27,162 @@ #include "PPageFormats.h" #include "SvgHelper.h" #include "WindowsUserChoice.h" +#include "controls/DarkTheme.h" +#include +#include // BP_CHECKBOX / CBS_* +#pragma comment(lib, "uxtheme.lib") static constexpr auto previousRegistration = L"PreviousRegistration"; static constexpr auto registeredAppName = L"MPC-BE"; static constexpr auto registeredKey = L"Software\\Clients\\Media\\MPC-BE\\Capabilities"; +// Renders the native themed dark checkbox — the same celeste-accent glyph the rest of +// the dark Options dialog uses (Internal Filters, Fullscreen) — into a 3-state image +// list: index 0 = unchecked, 1 = checked, 2 = indeterminate. +// The glyph is drawn directly over the opaque dark row background (exactly like the +// Fullscreen page draws it with DrawThemeBackground). Going through buffered paint / +// premultiplied alpha instead darkened the celeste and left the unchecked box white. +// Because the app runs in force-dark mode, OpenThemeData(..., "BUTTON") resolves to the +// dark (celeste) checkbox. +static bool MakeThemedCheckImageList(CImageList& il, int h, HWND hRef) +{ + DarkTheme::AllowDarkModeForApp(); // force-dark app mode + DarkTheme::ApplyThemeToControl(hRef); // allow dark mode on the list *now* — this runs at + // OnInitDialog time, before the page's OnSetActive + // themes it, so without this OpenThemeData(hRef, + // "BUTTON") would resolve to the light checkbox + // (a darker blue with a white unchecked box). + + HTHEME hTheme = ::OpenThemeData(hRef, L"BUTTON"); + if (!hTheme) { + return false; + } + + il.DeleteImageList(); + if (!il.Create(h, h, ILC_COLOR32, 3, 0)) { + ::CloseThemeData(hTheme); + return false; + } + + // Buffered painting into a 32-bit top-down DIB keeps the glyph's per-pixel alpha, so + // the rounded corners stay transparent (a plain opaque draw left 4 white corner dots). + ::BufferedPaintInit(); + + const int states[3] = { CBS_UNCHECKEDNORMAL, CBS_CHECKEDNORMAL, CBS_MIXEDNORMAL }; + HDC hdcScreen = ::GetDC(nullptr); + HDC hdcMem = ::CreateCompatibleDC(hdcScreen); + bool ok = true; + + for (int i = 0; i < 3 && ok; ++i) { + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = h; + bmi.bmiHeader.biHeight = -h; // top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + void* pBits = nullptr; + HBITMAP hDib = ::CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0); + if (!hDib) { + ok = false; + break; + } + HBITMAP hOldBmp = (HBITMAP)::SelectObject(hdcMem, hDib); + + RECT rcFull = { 0, 0, h, h }; + BP_PAINTPARAMS pp = { sizeof(pp) }; + pp.dwFlags = BPPF_ERASE; + HDC hdcPaint = nullptr; + HPAINTBUFFER hbp = ::BeginBufferedPaint(hdcMem, &rcFull, BPBF_TOPDOWNDIB, &pp, &hdcPaint); + if (hbp) { + SIZE gsz = { h, h }; + ::GetThemePartSize(hTheme, hdcPaint, BP_CHECKBOX, states[i], nullptr, TS_DRAW, &gsz); + // Clamp to the cell so the glyph is never clipped (its natural size can be a + // couple of pixels larger than the cell, which cut off part of the checkbox). + int sz = std::min(std::min((int)gsz.cx, (int)gsz.cy), h); + if (sz < 1) { sz = h; } + RECT rg; + rg.left = (h - sz) / 2; + rg.top = (h - sz) / 2; + rg.right = rg.left + sz; + rg.bottom = rg.top + sz; + ::DrawThemeBackground(hTheme, hdcPaint, BP_CHECKBOX, states[i], &rg, nullptr); + ::EndBufferedPaint(hbp, TRUE); // copy the premultiplied bits into hDib + } else { + ok = false; + } + + ::SelectObject(hdcMem, hOldBmp); + if (ok) { + il.Add(CBitmap::FromHandle(hDib), (CBitmap*)nullptr); // 32bpp: keeps per-pixel alpha + } + ::DeleteObject(hDib); + } + + ::DeleteDC(hdcMem); + ::ReleaseDC(nullptr, hdcScreen); + ::BufferedPaintUnInit(); + ::CloseThemeData(hTheme); + + if (ok && il.GetImageCount() == 3) { + return true; + } + il.DeleteImageList(); + return false; +} + +// Fallback (used only if the themed glyph above is unavailable): a hand-drawn dark box +// with a celeste checkmark, matching the palette of the rest of the dark dialog. +static void MakeDarkCheckImageList(CImageList& il, int h, HWND hRef) +{ + if (MakeThemedCheckImageList(il, h, hRef)) { + return; + } + + const COLORREF mask = RGB(255, 0, 255); // transparent key colour + const COLORREF back = DarkTheme::CtrlBackColor(); + const COLORREF border = DarkTheme::CtrlBorderColor(); + const COLORREF mark = RGB(76, 194, 255); // celeste, Win11 dark-mode accent glyph + + CClientDC screen(nullptr); + CDC dc; + dc.CreateCompatibleDC(&screen); + CBitmap bmp; + bmp.CreateCompatibleBitmap(&screen, h * 3, h); + CBitmap* pOld = dc.SelectObject(&bmp); + + dc.FillSolidRect(0, 0, h * 3, h, mask); + + for (int i = 0; i < 3; ++i) { + CRect box(i * h, 0, i * h + h, h); + box.DeflateRect(1, 1); + dc.FillSolidRect(box, back); + dc.Draw3dRect(box, border, border); + + if (i == 1) { + // checkmark + CPen pen(PS_SOLID, 2, mark); + CPen* pp = dc.SelectObject(&pen); + const int l = box.left, t = box.top, w = box.Width(), hh = box.Height(); + dc.MoveTo(l + w * 3 / 10, t + hh * 5 / 10); + dc.LineTo(l + w * 45 / 100, t + hh * 7 / 10); + dc.LineTo(l + w * 75 / 100, t + hh * 28 / 100); + dc.SelectObject(pp); + } else if (i == 2) { + // partial: a small filled square + CRect inner = box; + inner.DeflateRect(box.Width() / 4, box.Height() / 4); + dc.FillSolidRect(inner, mark); + } + } + + dc.SelectObject(pOld); + il.DeleteImageList(); + il.Create(h, h, ILC_COLOR24 | ILC_MASK, 3, 0); + il.Add(&bmp, mask); +} + // CPPageFormats dialog CComPtr CPPageFormats::m_pAAR; @@ -677,23 +828,29 @@ BOOL CPPageFormats::OnInitDialog() m_list.InsertColumn(COL_CATEGORY, L"Category", LVCFMT_LEFT); - CSvgImage svgImage; - if (svgImage.Load(IDF_SVG_ONOFF)) { - int w = 0; - int h = 0; - if (CDPI* pDpi = dynamic_cast(AfxGetMainWnd())) { - h = pDpi->ScaleY(12); - } else { - // this panel can be created without the main window. - CDPI dpi; - h = dpi.ScaleY(12); - } - if (HBITMAP hBitmap = svgImage.Rasterize(w, h)) { - if (w == h * 3) { - m_onoff.Create(h, h, ILC_COLOR32 | ILC_MASK, 3, 0); - ImageList_Add(m_onoff.GetSafeHandle(), hBitmap, nullptr); + int chkH = 0; + if (CDPI* pDpi = dynamic_cast(AfxGetMainWnd())) { + chkH = pDpi->ScaleY(12); + } else { + // this panel can be created without the main window. + CDPI dpi; + chkH = dpi.ScaleY(12); + } + + if (DarkTheme::IsActive()) { + MakeDarkCheckImageList(m_onoff, chkH, m_list.GetSafeHwnd()); + } else { + CSvgImage svgImage; + if (svgImage.Load(IDF_SVG_ONOFF)) { + int w = 0; + int h = chkH; + if (HBITMAP hBitmap = svgImage.Rasterize(w, h)) { + if (w == h * 3) { + m_onoff.Create(h, h, ILC_COLOR32 | ILC_MASK, 3, 0); + ImageList_Add(m_onoff.GetSafeHandle(), hBitmap, nullptr); + } + DeleteObject(hBitmap); } - DeleteObject(hBitmap); } } @@ -713,6 +870,15 @@ BOOL CPPageFormats::OnInitDialog() } m_list.SetColumnWidth(COL_CATEGORY, LVSCW_AUTOSIZE); + { + // Stretch the single column to at least the list width so the content (and the + // full-row selection highlight) fills the control instead of stopping mid-way. + CRect rcList; + m_list.GetClientRect(rcList); + if (m_list.GetColumnWidth(COL_CATEGORY) < rcList.Width()) { + m_list.SetColumnWidth(COL_CATEGORY, rcList.Width()); + } + } m_list.SetSelectionMark(0); m_list.SetItemState(0, LVIS_SELECTED, LVIS_SELECTED); diff --git a/src/apps/mplayerc/PPageFullscreen.cpp b/src/apps/mplayerc/PPageFullscreen.cpp index 060231a3f3..c26fc67f5a 100644 --- a/src/apps/mplayerc/PPageFullscreen.cpp +++ b/src/apps/mplayerc/PPageFullscreen.cpp @@ -22,10 +22,14 @@ #include "stdafx.h" #include "MainFrm.h" #include "PPageFullscreen.h" +#include "controls/DarkTheme.h" #include "DSUtil/SysVersion.h" #include "DSUtil/std_helper.h" #include "MultiMonitor.h" #include +#include +#include // BP_CHECKBOX / CBS_* +#pragma comment(lib, "uxtheme.lib") static CString FormatModeString(const dispmode& dmod) { @@ -72,7 +76,7 @@ BEGIN_MESSAGE_MAP(CPPageFullscreen, CPPageBase) ON_NOTIFY(NM_CUSTOMDRAW, IDC_LIST1, OnCustomdrawList) ON_CLBN_CHKCHANGE(IDC_LIST1, OnCheckChangeList) - ON_UPDATE_COMMAND_UI(IDC_LIST1, OnUpdateFullscreenRes) + ON_UPDATE_COMMAND_UI(IDC_LIST1, OnUpdateFullscreenList) ON_UPDATE_COMMAND_UI(IDC_CHECK7, OnUpdateFullscreenRes) ON_UPDATE_COMMAND_UI(IDC_CHECK3, OnUpdateFullscreenRes) ON_UPDATE_COMMAND_UI(IDC_RESTORERESCHECK, OnUpdateFullscreenRes) @@ -211,6 +215,7 @@ BOOL CPPageFullscreen::OnInitDialog() m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER | LVS_EX_GRIDLINES | LVS_EX_BORDERSELECT | LVS_EX_ONECLICKACTIVATE | LVS_EX_CHECKBOXES | LVS_EX_FLATSB); + m_list.InsertColumn(COL_Z, ResStr(IDS_PPAGE_FS_CLN_ON_OFF), LVCFMT_LEFT, 60, -1, -1); m_list.InsertColumn(COL_VFR_F, ResStr(IDS_PPAGE_FS_CLN_FROM_FPS), LVCFMT_RIGHT, 60, -1, -1); m_list.InsertColumn(COL_VFR_T, ResStr(IDS_PPAGE_FS_CLN_TO_FPS), LVCFMT_RIGHT, 60, -1, -1); @@ -221,6 +226,10 @@ BOOL CPPageFullscreen::OnInitDialog() ModesUpdate(); UpdateData(FALSE); + // Set the initial locked state + matching checkbox images now (before the first + // paint), so the list does not briefly show the wrong set. + UpdateListLockState(); + return TRUE; } @@ -232,21 +241,71 @@ void CPPageFullscreen::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage ) { *pResult = CDRF_NOTIFYITEMDRAW; } else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage ) { - *pResult = CDRF_NOTIFYSUBITEMDRAW; + // Also ask for a post-paint so we can draw our own checkbox over the native one + // (dark theme only). + *pResult = CDRF_NOTIFYSUBITEMDRAW | (DarkTheme::IsActive() ? CDRF_NOTIFYPOSTPAINT : 0); + } else if ( CDDS_ITEMPOSTPAINT == pLVCD->nmcd.dwDrawStage ) { + // The native LVS_EX_CHECKBOXES glyph does not follow the dark theme: unchecked + // boxes look white, and it can't be reliably greyed for the inactive list. So we + // draw the checkbox ourselves over the native one: a dark box, a celeste tick when + // active or a grey tick when the list is inactive (auto-mode off). + if (DarkTheme::IsActive()) { + const int item = (int)pLVCD->nmcd.dwItemSpec; + CRect rcBounds, rcLabel; + if (m_list.GetItemRect(item, &rcBounds, LVIR_BOUNDS) + && m_list.GetItemRect(item, &rcLabel, LVIR_LABEL) + && rcLabel.left > rcBounds.left) { + CDC* pDC = CDC::FromHandle(pLVCD->nmcd.hdc); + + // Cover the whole native checkbox area with the row background. + CRect rcGap(rcBounds.left, rcBounds.top, rcLabel.left, rcBounds.bottom); + pDC->FillSolidRect(rcGap, DarkTheme::FaceColor()); + + const bool active = (m_listLocked == 0); + const bool checked = (m_list.GetCheck(item) != FALSE); + + // Draw the native themed checkbox glyph over the (hidden) native one: the + // normal (celeste) glyph when the list is active, or the greyed-out disabled + // glyph when inactive — the same grey as the standalone disabled checkboxes. + if (HTHEME hTheme = ::OpenThemeData(m_list.GetSafeHwnd(), L"BUTTON")) { + const int st = active + ? (checked ? CBS_CHECKEDNORMAL : CBS_UNCHECKEDNORMAL) + : (checked ? CBS_CHECKEDDISABLED : CBS_UNCHECKEDDISABLED); + SIZE gsz = { 14, 14 }; + ::GetThemePartSize(hTheme, pDC->GetSafeHdc(), BP_CHECKBOX, st, nullptr, TS_DRAW, &gsz); + RECT rg; + rg.left = rcGap.left + (rcGap.Width() - gsz.cx) / 2; + rg.top = rcGap.top + (rcGap.Height() - gsz.cy) / 2; + rg.right = rg.left + gsz.cx; + rg.bottom = rg.top + gsz.cy; + ::DrawThemeBackground(hTheme, pDC->GetSafeHdc(), BP_CHECKBOX, st, &rg, nullptr); + ::CloseThemeData(hTheme); + } + } + } + *pResult = CDRF_DODEFAULT; } else if ( (CDDS_ITEMPREPAINT | CDDS_SUBITEM) == pLVCD->nmcd.dwDrawStage ) { COLORREF crText, crBkgnd; m_fullScreenModes.bEnabled = m_bEnableAutoMode ? m_bEnableAutoMode + m_bBeforePlayback : 0; - if (m_fullScreenModes.bEnabled == FALSE) { - crText = RGB(128,128,128); - crBkgnd = RGB(240, 240, 240); + if (DarkTheme::IsActive()) { + crBkgnd = DarkTheme::FaceColor(); + crText = (m_fullScreenModes.bEnabled == FALSE) ? ThemeRGB(110, 115, 120) : DarkTheme::TextColor(); + if (m_list.GetCheck(pLVCD->nmcd.dwItemSpec) == false) { + crText = ThemeRGB(110, 115, 120); + } } else { - crText = RGB(0,0,0); - crBkgnd = RGB(255,255,255); - } - if (m_list.GetCheck(pLVCD->nmcd.dwItemSpec) == false) { - crText = RGB(128,128,128); + if (m_fullScreenModes.bEnabled == FALSE) { + crText = RGB(128,128,128); + crBkgnd = RGB(240, 240, 240); + } else { + crText = RGB(0,0,0); + crBkgnd = RGB(255,255,255); + } + if (m_list.GetCheck(pLVCD->nmcd.dwItemSpec) == false) { + crText = RGB(128,128,128); + } } pLVCD->clrText = crText; pLVCD->clrTextBk = crBkgnd; @@ -395,6 +454,40 @@ void CPPageFullscreen::OnUpdateFullscreenRes(CCmdUI* pCmdUI) pCmdUI->Enable(m_bEnableAutoMode); } +void CPPageFullscreen::OnUpdateFullscreenList(CCmdUI* pCmdUI) +{ + if (!DarkTheme::IsActive()) { + pCmdUI->Enable(m_bEnableAutoMode); + return; + } + + // A natively disabled list-view ignores our dark theming and repaints itself with the + // light system colours (white rows/borders/checkboxes). So instead of disabling the + // window we keep it enabled (it stays dark) but "locked": all interaction is blocked + // so it behaves like a disabled control. The row custom-draw dims the text when + // auto-mode is off to show it is inactive; the native (dark) checkboxes stay as they + // are (replacing the checkbox state image list fights LVS_EX_CHECKBOXES and glitches). + pCmdUI->Enable(TRUE); + UpdateListLockState(); +} + +void CPPageFullscreen::UpdateListLockState() +{ + if (!DarkTheme::IsActive()) { + return; + } + + // Read the real check state of "use autochange" (IDC_CHECK2) directly — the DDX + // member m_bEnableAutoMode is only synced on UpdateData() and lags a frame behind. + const int locked = (IsDlgButtonChecked(IDC_CHECK2) == BST_CHECKED) ? 0 : 1; + if (locked == m_listLocked) { + return; + } + m_listLocked = locked; + m_list.SetLocked(locked != 0); + m_list.Invalidate(); +} + void CPPageFullscreen::OnUpdateShowBarsWhenFullScreen(CCmdUI* pCmdUI) { pCmdUI->Enable(!AfxGetAppSettings().ExclusiveFSAllowed()); diff --git a/src/apps/mplayerc/PPageFullscreen.h b/src/apps/mplayerc/PPageFullscreen.h index b7df663384..686a1ce369 100644 --- a/src/apps/mplayerc/PPageFullscreen.h +++ b/src/apps/mplayerc/PPageFullscreen.h @@ -52,6 +52,7 @@ class CPPageFullscreen : public CPPageBase CString m_strFullScreenMonitorID; CPlayerListCtrl m_list; + int m_listLocked = -1; // -1 = unset, 0/1 = current locked state enum { COL_Z, COL_VFR_F, @@ -75,6 +76,7 @@ class CPPageFullscreen : public CPPageBase void ReindexList(); void GetCurDispModeString(CString& strMode); void ModesUpdate(); + void UpdateListLockState(); public: CPPageFullscreen(); ~CPPageFullscreen() = default; @@ -90,6 +92,7 @@ class CPPageFullscreen : public CPPageBase public: afx_msg void OnUpdateFullscreenRes(CCmdUI* pCmdUI); + afx_msg void OnUpdateFullscreenList(CCmdUI* pCmdUI); afx_msg void OnBeginlabeleditList(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnDolabeleditList(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnEndlabeleditList(NMHDR* pNMHDR, LRESULT* pResult); diff --git a/src/apps/mplayerc/PPageInterface.cpp b/src/apps/mplayerc/PPageInterface.cpp index d482950eed..4c45cbbcba 100644 --- a/src/apps/mplayerc/PPageInterface.cpp +++ b/src/apps/mplayerc/PPageInterface.cpp @@ -23,6 +23,8 @@ #include "MainFrm.h" #include "PPageInterface.h" #include "DSUtil/SysVersion.h" +#include "controls/DarkTheme.h" +#include // CPPageInterface dialog @@ -173,6 +175,23 @@ BOOL CPPageInterface::OnApply() //s.bDarkMenuBlurBehind = !!m_chkDarkMenuBlurBehind.GetCheck(); s.bDarkTitle = !!m_chkDarkTitle.GetCheck(); + // If the dark-theme toggle (or the theme colours) changed while the Options dialog is + // still open, re-theme the whole property sheet so it doesn't end up a mix of light and + // dark controls. GA_ROOT gives the sheet's top-level window (the pages live under it). + { + const bool bDarkToggled = (!!s.bUseDarkTheme != !!bUseDarkTheme); + const bool bColorsChanged = s.nThemeBrightness != m_nThemeBrightness_Old + || s.nThemeRed != m_nThemeRed_Old + || s.nThemeGreen != m_nThemeGreen_Old + || s.nThemeBlue != m_nThemeBlue_Old; + if (bDarkToggled || (s.bUseDarkTheme && bColorsChanged)) { + TreePropSheet::CPropPageFrameDefault::s_bDarkMode = DarkTheme::IsActive(); + TreePropSheet::CPropPageFrameDefault::s_clrFace = DarkTheme::FaceColor(); + TreePropSheet::CPropPageFrameDefault::s_clrText = DarkTheme::TextColor(); + DarkTheme::RefreshTheme(::GetAncestor(GetSafeHwnd(), GA_ROOT)); + } + } + s.fUseWin7TaskBar = !!m_fUseWin7TaskBar; s.fUseTimeTooltip = !!m_fUseTimeTooltip; s.nTimeTooltipPosition = m_TimeTooltipPosition.GetCurSel(); @@ -400,9 +419,15 @@ void CPPageInterface::OnCustomDrawBtns(NMHDR *pNMHDR, LRESULT *pResult) dc.Attach(pNMCD->hdc); CRect r; CopyRect(&r,&pNMCD->rc); - CPen penFrEnabled (PS_SOLID, 0, GetSysColor(COLOR_BTNTEXT)); - CPen penFrDisabled (PS_SOLID, 0, GetSysColor(COLOR_BTNSHADOW)); + const bool bDark = DarkTheme::IsActive(); + CPen penFrEnabled (PS_SOLID, 0, bDark ? DarkTheme::CtrlBorderColor() : GetSysColor(COLOR_BTNTEXT)); + CPen penFrDisabled (PS_SOLID, 0, bDark ? DarkTheme::CtrlBorderColor() : GetSysColor(COLOR_BTNSHADOW)); CPen *penOld = dc.SelectObject(&penFrEnabled); + CBrush brBack(bDark ? DarkTheme::FaceColor() : GetSysColor(COLOR_3DFACE)); + CBrush* pOldBrush = dc.SelectObject(&brBack); + if (bDark) { + dc.FillSolidRect(&r, DarkTheme::FaceColor()); // avoid a light ring around the rounded swatch + } if (CDIS_HOT == pNMCD->uItemState || CDIS_HOT + CDIS_FOCUS == pNMCD->uItemState || CDIS_DISABLED == pNMCD->uItemState) { dc.SelectObject(&penFrDisabled); @@ -418,6 +443,7 @@ void CPPageInterface::OnCustomDrawBtns(NMHDR *pNMHDR, LRESULT *pResult) } dc.SelectObject(&penOld); + dc.SelectObject(pOldBrush); dc.Detach(); *pResult = CDRF_SKIPDEFAULT; diff --git a/src/apps/mplayerc/PPageInternalFilters.cpp b/src/apps/mplayerc/PPageInternalFilters.cpp index 49df45cf47..28b68559ad 100644 --- a/src/apps/mplayerc/PPageInternalFilters.cpp +++ b/src/apps/mplayerc/PPageInternalFilters.cpp @@ -145,9 +145,9 @@ static filter_t s_audio_decoders[] = { {L"Other PCM/ADPCM", AUDIO_DECODER, ADEC_PCM_ADPCM, 0}, }; -IMPLEMENT_DYNAMIC(CPPageInternalFiltersListBox, CCheckListBox) +IMPLEMENT_DYNAMIC(CPPageInternalFiltersListBox, CDarkCheckListBox) CPPageInternalFiltersListBox::CPPageInternalFiltersListBox(int n) - : CCheckListBox() + : CDarkCheckListBox() , m_n(n) { for (int i = 0; i < FILTER_TYPE_NB; i++) { @@ -181,7 +181,7 @@ INT_PTR CPPageInternalFiltersListBox::OnToolHitTest(CPoint point, TOOLINFO* pTI) return pTI->uId; } -BEGIN_MESSAGE_MAP(CPPageInternalFiltersListBox, CCheckListBox) +BEGIN_MESSAGE_MAP(CPPageInternalFiltersListBox, CDarkCheckListBox) ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipNotify) ON_WM_RBUTTONDOWN() END_MESSAGE_MAP() diff --git a/src/apps/mplayerc/PPageInternalFilters.h b/src/apps/mplayerc/PPageInternalFilters.h index 2d9afcb003..d0ea4b8a8a 100644 --- a/src/apps/mplayerc/PPageInternalFilters.h +++ b/src/apps/mplayerc/PPageInternalFilters.h @@ -25,6 +25,8 @@ #include "PPageBase.h" #include "afxcmn.h" #include "controls/FloatEdit.h" +#include "controls/DarkCheckListBox.h" +#include "controls/DarkTabCtrl.h" enum { SOURCE, @@ -39,7 +41,7 @@ struct filter_t { UINT nHintID; }; -class CPPageInternalFiltersListBox : public CCheckListBox +class CPPageInternalFiltersListBox : public CDarkCheckListBox { DECLARE_DYNAMIC(CPPageInternalFiltersListBox) @@ -91,7 +93,7 @@ class CPPageInternalFilters : public CPPageBase CIntEdit m_edtBufferDuration; CSpinButtonCtrl m_spnBufferDuration; - CTabCtrl m_Tab; + CDarkTabCtrl m_Tab; void ShowPPage(CUnknown* (WINAPI * CreateInstance)(LPUNKNOWN lpunk, HRESULT* phr)); diff --git a/src/apps/mplayerc/PPageOSD.cpp b/src/apps/mplayerc/PPageOSD.cpp index 45723e70da..549f2f9914 100644 --- a/src/apps/mplayerc/PPageOSD.cpp +++ b/src/apps/mplayerc/PPageOSD.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "MainFrm.h" #include "PPageOSD.h" +#include "controls/DarkTheme.h" static int CALLBACK EnumFontProc(ENUMLOGFONT FAR* lf, NEWTEXTMETRIC FAR* tm, DWORD FontType, LPARAM dwData) { @@ -336,9 +337,15 @@ void CPPageOSD::OnCustomDrawBtns(NMHDR* pNMHDR, LRESULT* pResult) dc.Attach(pNMCD->hdc); CRect r; CopyRect(&r, &pNMCD->rc); - CPen penFrEnabled(PS_SOLID, 0, GetSysColor(COLOR_BTNTEXT)); - CPen penFrDisabled(PS_SOLID, 0, GetSysColor(COLOR_BTNSHADOW)); + const bool bDark = DarkTheme::IsActive(); + CPen penFrEnabled(PS_SOLID, 0, bDark ? DarkTheme::CtrlBorderColor() : GetSysColor(COLOR_BTNTEXT)); + CPen penFrDisabled(PS_SOLID, 0, bDark ? DarkTheme::CtrlBorderColor() : GetSysColor(COLOR_BTNSHADOW)); CPen* penOld = dc.SelectObject(&penFrEnabled); + CBrush brBack(bDark ? DarkTheme::FaceColor() : GetSysColor(COLOR_3DFACE)); + CBrush* pOldBrush = dc.SelectObject(&brBack); + if (bDark) { + dc.FillSolidRect(&r, DarkTheme::FaceColor()); // avoid a light ring around the rounded swatch + } if (CDIS_HOT == pNMCD->uItemState || CDIS_HOT + CDIS_FOCUS == pNMCD->uItemState || CDIS_DISABLED == pNMCD->uItemState) { dc.SelectObject(&penFrDisabled); @@ -359,6 +366,7 @@ void CPPageOSD::OnCustomDrawBtns(NMHDR* pNMHDR, LRESULT* pResult) } dc.SelectObject(&penOld); + dc.SelectObject(pOldBrush); dc.Detach(); *pResult = CDRF_SKIPDEFAULT; diff --git a/src/apps/mplayerc/PPageSheet.cpp b/src/apps/mplayerc/PPageSheet.cpp index 0729372646..3072c433e6 100644 --- a/src/apps/mplayerc/PPageSheet.cpp +++ b/src/apps/mplayerc/PPageSheet.cpp @@ -21,6 +21,8 @@ #include "stdafx.h" #include "PPageSheet.h" +#include "controls/DarkTheme.h" +#include #include // CPPageSheet @@ -100,10 +102,20 @@ CTreeCtrl* CPPageSheet::CreatePageTreeObject() BEGIN_MESSAGE_MAP(CPPageSheet, CTreePropSheet) ON_WM_CONTEXTMENU() + ON_WM_CTLCOLOR() + ON_WM_ERASEBKGND() END_MESSAGE_MAP() BOOL CPPageSheet::OnInitDialog() { + // Tell the TreePropSheet page frame which palette to use before it paints. + TreePropSheet::CPropPageFrameDefault::s_bDarkMode = DarkTheme::IsActive(); + TreePropSheet::CPropPageFrameDefault::s_clrFace = DarkTheme::FaceColor(); + TreePropSheet::CPropPageFrameDefault::s_clrText = DarkTheme::TextColor(); + + // Enable per-process dark mode before the standard controls are created. + DarkTheme::AllowDarkModeForApp(); + BOOL bResult = __super::OnInitDialog(); if (CTreeCtrl* pTree = GetPageTreeControl()) { @@ -116,9 +128,42 @@ BOOL CPPageSheet::OnInitDialog() GetPageTreeControl()->EnableWindow (FALSE); } + if (DarkTheme::IsActive()) { + DarkTheme::EnableForWindow(GetSafeHwnd()); + DarkTheme::ApplyThemeToChildren(GetSafeHwnd()); + + if (CTreeCtrl* pTree = GetPageTreeControl()) { + pTree->SetBkColor(DarkTheme::FaceColor()); + pTree->SetTextColor(DarkTheme::TextColor()); + } + } + return bResult; } +HBRUSH CPPageSheet::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) +{ + HBRUSH hbr = __super::OnCtlColor(pDC, pWnd, nCtlColor); + + if (HBRUSH hbrDark = DarkTheme::OnCtlColor(pDC, nCtlColor)) { + return hbrDark; + } + + return hbr; +} + +BOOL CPPageSheet::OnEraseBkgnd(CDC* pDC) +{ + if (DarkTheme::IsActive()) { + CRect rc; + GetClientRect(rc); + pDC->FillSolidRect(rc, DarkTheme::FaceColor()); + return TRUE; + } + + return __super::OnEraseBkgnd(pDC); +} + void CPPageSheet::OnContextMenu(CWnd* /*pWnd*/, CPoint /*point*/) { } diff --git a/src/apps/mplayerc/PPageSheet.h b/src/apps/mplayerc/PPageSheet.h index f23c5d72b9..eec9dbd239 100644 --- a/src/apps/mplayerc/PPageSheet.h +++ b/src/apps/mplayerc/PPageSheet.h @@ -112,4 +112,6 @@ class CPPageSheet : public TreePropSheet::CTreePropSheet DECLARE_MESSAGE_MAP() public: virtual BOOL OnInitDialog(); + afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); }; diff --git a/src/apps/mplayerc/PPageSubStyle.cpp b/src/apps/mplayerc/PPageSubStyle.cpp index de3a29cbe1..a123341b9b 100644 --- a/src/apps/mplayerc/PPageSubStyle.cpp +++ b/src/apps/mplayerc/PPageSubStyle.cpp @@ -22,6 +22,7 @@ #include "stdafx.h" #include "MainFrm.h" #include "PPageSubStyle.h" +#include "controls/DarkTheme.h" const struct { BYTE charSet; @@ -168,6 +169,9 @@ BOOL CPPageSubStyle::OnInitDialog() m_marginbottomspin.SetRange(-10000, 10000); for (auto& slider : m_alphasliders) { slider.SetRange(0, 255); + // These sliders lose their dark NM_CUSTOMDRAW channel after the Reset button, so + // owner-draw them fully (deterministic dark groove + celeste thumb). + DarkTheme::MakeTrackbarOwnerDrawn(slider.GetSafeHwnd()); } Init(); @@ -332,9 +336,15 @@ void CPPageSubStyle::OnCustomDrawBtns(NMHDR *pNMHDR, LRESULT *pResult) dc.Attach(pNMCD->hdc); CRect r; CopyRect(&r,&pNMCD->rc); - CPen penFrEnabled (PS_SOLID, 0, GetSysColor(COLOR_BTNTEXT)); - CPen penFrDisabled (PS_SOLID, 0, GetSysColor(COLOR_BTNSHADOW)); + const bool bDark = DarkTheme::IsActive(); + CPen penFrEnabled (PS_SOLID, 0, bDark ? DarkTheme::CtrlBorderColor() : GetSysColor(COLOR_BTNTEXT)); + CPen penFrDisabled (PS_SOLID, 0, bDark ? DarkTheme::CtrlBorderColor() : GetSysColor(COLOR_BTNSHADOW)); CPen *penOld = dc.SelectObject(&penFrEnabled); + CBrush brBack(bDark ? DarkTheme::FaceColor() : GetSysColor(COLOR_3DFACE)); + CBrush* pOldBrush = dc.SelectObject(&brBack); + if (bDark) { + dc.FillSolidRect(&r, DarkTheme::FaceColor()); // avoid a light ring around the rounded swatch + } if (CDIS_HOT == pNMCD->uItemState || CDIS_HOT + CDIS_FOCUS == pNMCD->uItemState || CDIS_DISABLED == pNMCD->uItemState) { dc.SelectObject(&penFrDisabled); @@ -358,6 +368,7 @@ void CPPageSubStyle::OnCustomDrawBtns(NMHDR *pNMHDR, LRESULT *pResult) } dc.SelectObject(&penOld); + dc.SelectObject(pOldBrush); dc.Detach(); *pResult = CDRF_SKIPDEFAULT; diff --git a/src/apps/mplayerc/PlayerListCtrl.cpp b/src/apps/mplayerc/PlayerListCtrl.cpp index 7a2c915187..af51da50f0 100644 --- a/src/apps/mplayerc/PlayerListCtrl.cpp +++ b/src/apps/mplayerc/PlayerListCtrl.cpp @@ -22,6 +22,7 @@ #include "stdafx.h" #include "PlayerListCtrl.h" #include "DSUtil/SysVersion.h" +#include "controls/DarkTheme.h" // CInPlaceHotKey @@ -290,10 +291,21 @@ BEGIN_MESSAGE_MAP(CInPlaceComboBox, CComboBox) ON_WM_KILLFOCUS() ON_WM_CHAR() ON_WM_NCDESTROY() + ON_WM_CTLCOLOR() ON_CONTROL_REFLECT(CBN_CLOSEUP, OnCloseup) //}}AFX_MSG_MAP END_MESSAGE_MAP() +// The drop-down list sends WM_CTLCOLORLISTBOX to the combo (its parent); paint it with +// the dark palette so the list items match the rest of the dark Options dialog. +HBRUSH CInPlaceComboBox::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) +{ + if (HBRUSH hbr = DarkTheme::OnCtlColor(pDC, nCtlColor)) { + return hbr; + } + return CComboBox::OnCtlColor(pDC, pWnd, nCtlColor); +} + ///////////////////////////////////////////////////////////////////////////// // CInPlaceComboBox message handlers @@ -307,6 +319,10 @@ int CInPlaceComboBox::OnCreate(LPCREATESTRUCT lpCreateStruct) CFont* font = GetParent()->GetFont(); SetFont(font); + // Match the rest of the dark Options dialog: theme the in-place combo (and its + // drop-down list) instead of leaving it the default light control. + DarkTheme::ApplyThemeToControl(GetSafeHwnd()); + for (const auto& lstItem : m_lstItems) { AddString(lstItem); } @@ -850,8 +866,40 @@ BEGIN_MESSAGE_MAP(CPlayerListCtrl, CListCtrl) ON_LBN_SELCHANGE(IDC_LIST1, OnLbnSelChangeList1) ON_NOTIFY_EX(HDN_ITEMCHANGINGW, 0, OnHdnItemchanging) ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipNotify) + ON_WM_CTLCOLOR() END_MESSAGE_MAP() +// The in-place editors (edit / hotkey / float edit) are children of this list control, +// so their WM_CTLCOLOR* messages arrive here. Paint them with the dark palette so the +// edit field matches the rest of the dark Options dialog instead of showing up white. +HBRUSH CPlayerListCtrl::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) +{ + if (HBRUSH hbr = DarkTheme::OnCtlColor(pDC, nCtlColor)) { + return hbr; + } + return CListCtrl::OnCtlColor(pDC, pWnd, nCtlColor); +} + +BOOL CPlayerListCtrl::PreTranslateMessage(MSG* pMsg) +{ + // When "locked" the control stays enabled (so it keeps its dark appearance) but must + // behave as if disabled: swallow all mouse/keyboard input so nothing can be clicked, + // toggled, hovered or edited. + if (m_bLocked && pMsg->hwnd == m_hWnd) { + switch (pMsg->message) { + case WM_LBUTTONDOWN: case WM_LBUTTONUP: case WM_LBUTTONDBLCLK: + case WM_RBUTTONDOWN: case WM_RBUTTONUP: case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: case WM_MBUTTONUP: + case WM_MOUSEMOVE: case WM_MOUSEWHEEL: case WM_MOUSEHOVER: + case WM_KEYDOWN: case WM_KEYUP: case WM_SYSKEYDOWN: + case WM_CHAR: case WM_SETCURSOR: + return TRUE; // eat it + } + } + + return CListCtrl::PreTranslateMessage(pMsg); +} + // CPlayerListCtrl message handlers void CPlayerListCtrl::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) diff --git a/src/apps/mplayerc/PlayerListCtrl.h b/src/apps/mplayerc/PlayerListCtrl.h index 3c3e4e253d..bc4b2c7b71 100644 --- a/src/apps/mplayerc/PlayerListCtrl.h +++ b/src/apps/mplayerc/PlayerListCtrl.h @@ -110,6 +110,7 @@ class CInPlaceComboBox : public CComboBox afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnCloseup(); + afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); }; class CInPlaceListBox : public CListBox @@ -168,7 +169,16 @@ class CPlayerListCtrl : public CListCtrl bool m_fInPlaceDirty = false; + // "Locked" keeps the control enabled (so the dark theme keeps painting it, instead of + // the ugly light native disabled rendering) while blocking all user interaction, so it + // behaves like a disabled list. Used by the dark Fullscreen page. + void SetLocked(bool bLocked) { m_bLocked = bLocked; } + bool IsLocked() const { return m_bLocked; } + protected: + bool m_bLocked = false; + + virtual BOOL PreTranslateMessage(MSG* pMsg); virtual void PreSubclassWindow(); virtual INT_PTR OnToolHitTest(CPoint point, TOOLINFO* pTI) const; virtual ULONG GetGestureStatus(CPoint) { return 0; }; @@ -192,6 +202,7 @@ class CPlayerListCtrl : public CListCtrl afx_msg void OnLbnSelChangeList1(); afx_msg BOOL OnHdnItemchanging(UINT id, NMHDR* pNMHDR, LRESULT* pResult); afx_msg BOOL OnToolTipNotify(UINT id, NMHDR* pNMHDR, LRESULT* pResult); + afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor); int InsertColumn(_In_ int nCol, _In_z_ LPCWSTR lpszColumnHeading, _In_ int nFormat = LVCFMT_LEFT, _In_ int nWidth = -1, _In_ int nSubItem = -1, _In_ int nMinWidth = 20); diff --git a/src/apps/mplayerc/RegFilterChooserDlg.cpp b/src/apps/mplayerc/RegFilterChooserDlg.cpp index 7499883869..cd93b8caed 100644 --- a/src/apps/mplayerc/RegFilterChooserDlg.cpp +++ b/src/apps/mplayerc/RegFilterChooserDlg.cpp @@ -26,6 +26,7 @@ #include "DSUtil/FileHandle.h" #include "DSUtil/std_helper.h" #include "PPageExternalFilters.h" +#include "controls/DarkTheme.h" // CRegFilterChooserDlg dialog @@ -99,6 +100,8 @@ BOOL CRegFilterChooserDlg::OnInitDialog() SetMinTrackSize(CSize(300,100)); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/SelectMediaType.cpp b/src/apps/mplayerc/SelectMediaType.cpp index 6bba906594..da62707d35 100644 --- a/src/apps/mplayerc/SelectMediaType.cpp +++ b/src/apps/mplayerc/SelectMediaType.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "SelectMediaType.h" +#include "controls/DarkTheme.h" CString GetMediaTypeName(const GUID& guid) { @@ -68,6 +69,8 @@ BOOL CSelectMediaType::OnInitDialog() m_guidsctrl.AddString(GetMediaTypeName(guid)); } + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/controls/DarkCheckListBox.cpp b/src/apps/mplayerc/controls/DarkCheckListBox.cpp new file mode 100644 index 0000000000..455228b35e --- /dev/null +++ b/src/apps/mplayerc/controls/DarkCheckListBox.cpp @@ -0,0 +1,101 @@ +/* + * (C) 2026 see Authors.txt + * + * This file is part of MPC-BE. + * + * MPC-BE 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. + * + * MPC-BE 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 "stdafx.h" +#include "DarkCheckListBox.h" +#include "DarkTheme.h" + +IMPLEMENT_DYNAMIC(CDarkCheckListBox, CCheckListBox) + +BEGIN_MESSAGE_MAP(CDarkCheckListBox, CCheckListBox) + ON_WM_ERASEBKGND() +END_MESSAGE_MAP() + +// Paint the whole client dark before the items are drawn, so any area an item row +// does not cover (e.g. behind a tab-control body) shows the dark background instead +// of white. +BOOL CDarkCheckListBox::OnEraseBkgnd(CDC* pDC) +{ + if (DarkTheme::IsActive()) { + CRect rc; + GetClientRect(&rc); + pDC->FillSolidRect(rc, DarkTheme::FaceColor()); + return TRUE; + } + + return CCheckListBox::OnEraseBkgnd(pDC); +} + +// This mirrors MFC's CCheckListBox::DrawItem (which only draws the item text; the +// check-box glyph is drawn separately by the non-virtual PreDrawItem), but replaces +// the hard-coded COLOR_WINDOW / COLOR_WINDOWTEXT with the dark theme palette. +void CDarkCheckListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) +{ + if (!DarkTheme::IsActive()) { + CCheckListBox::DrawItem(lpDrawItemStruct); + return; + } + + CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); + if (!pDC) { + return; + } + + if (static_cast(lpDrawItemStruct->itemID) >= 0 + && (lpDrawItemStruct->itemAction & (ODA_DRAWENTIRE | ODA_SELECT))) { + const int cyItem = GetItemHeight(lpDrawItemStruct->itemID); + const BOOL fDisabled = !IsWindowEnabled() || !IsEnabled(lpDrawItemStruct->itemID); + + COLORREF clrText = fDisabled ? RGB(120, 125, 130) : DarkTheme::TextColor(); + COLORREF clrBk = DarkTheme::FaceColor(); + if (!fDisabled && (lpDrawItemStruct->itemState & ODS_SELECTED)) { + clrText = RGB(255, 255, 255); + clrBk = RGB(45, 80, 120); // subtle dark-blue selection + } + pDC->SetTextColor(clrText); + pDC->SetBkColor(clrBk); + + CString strText; + GetText(lpDrawItemStruct->itemID, strText); + + int yText = lpDrawItemStruct->rcItem.top; + if (cyItem > static_cast(m_cyText)) { + yText += (cyItem - static_cast(m_cyText)) / 2; + } + + // Fill to the full client width: for some lists the item rect (and the DC's + // clip region) does not span the whole control, which would leave the right + // side unpainted (white). Clear the clip so the opaque fill reaches the edge. + CRect rcFill(lpDrawItemStruct->rcItem); + CRect rcClient; + GetClientRect(&rcClient); + if (rcClient.right > rcFill.right) { + rcFill.right = rcClient.right; + } + pDC->SelectClipRgn(nullptr); + + pDC->ExtTextOutW(lpDrawItemStruct->rcItem.left, yText, ETO_OPAQUE, + rcFill, strText, strText.GetLength(), nullptr); + } + + if (lpDrawItemStruct->itemAction & ODA_FOCUS) { + pDC->DrawFocusRect(&lpDrawItemStruct->rcItem); + } +} diff --git a/src/apps/mplayerc/controls/DarkCheckListBox.h b/src/apps/mplayerc/controls/DarkCheckListBox.h new file mode 100644 index 0000000000..d1d3648dc8 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkCheckListBox.h @@ -0,0 +1,37 @@ +/* + * (C) 2026 see Authors.txt + * + * This file is part of MPC-BE. + * + * MPC-BE 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. + * + * MPC-BE 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 + +// A CCheckListBox that paints its item text with the dark palette when the dark +// theme is active. Only the text foreground/background differ from the base class; +// the check-box glyph is still drawn by CCheckListBox (themed), so it stays in sync +// with the rest of the UI. Falls back to the stock rendering when the theme is off. +class CDarkCheckListBox : public CCheckListBox +{ + DECLARE_DYNAMIC(CDarkCheckListBox) + +public: + virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); + +protected: + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + DECLARE_MESSAGE_MAP() +}; diff --git a/src/apps/mplayerc/controls/DarkTabCtrl.cpp b/src/apps/mplayerc/controls/DarkTabCtrl.cpp new file mode 100644 index 0000000000..2aa998abcd --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTabCtrl.cpp @@ -0,0 +1,149 @@ +/* + * (C) 2026 see Authors.txt + * + * This file is part of MPC-BE. + * + * MPC-BE 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. + * + * MPC-BE 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 "stdafx.h" +#include "DarkTabCtrl.h" +#include "DarkTheme.h" +#include "../MainFrm.h" // ThemeRGB() + +IMPLEMENT_DYNAMIC(CDarkTabCtrl, CTabCtrl) + +BEGIN_MESSAGE_MAP(CDarkTabCtrl, CTabCtrl) + ON_WM_ERASEBKGND() + ON_WM_PAINT() +END_MESSAGE_MAP() + +CDarkTabCtrl::CDarkTabCtrl() +{ +} + +CDarkTabCtrl::~CDarkTabCtrl() +{ +} + +BOOL CDarkTabCtrl::OnEraseBkgnd(CDC* pDC) +{ + if (!DarkTheme::IsActive()) { + return CTabCtrl::OnEraseBkgnd(pDC); + } + // All painting is done (double-buffered) in OnPaint; just claim the erase. + return TRUE; +} + +// Draws one tab button: dark fill + a three-sided border (left/top/right) so the +// button "opens" into the content pane below, plus the caption. The selected tab +// is passed a slightly enlarged rect (see OnPaint) so it overlaps its neighbours +// and merges with the pane frame, exactly like a native themed tab. +void CDarkTabCtrl::DrawTabItem(int nItem, CRect rItem, bool selected, CDC* pDC) +{ + if (nItem < 0) { + return; + } + + wchar_t buf[256] = {}; + TCITEMW tci = {}; + tci.mask = TCIF_TEXT; + tci.pszText = buf; + tci.cchTextMax = _countof(buf) - 1; + GetItem(nItem, &tci); + + const COLORREF clrBorder = ThemeRGB(50, 56, 62); + const COLORREF clrBg = selected ? DarkTheme::FaceColor() : ThemeRGB(16, 20, 25); + + pDC->FillSolidRect(rItem, clrBg); + + // Vertical extents of the left/right border strokes: the selected tab and the + // first tab connect down onto the pane's horizontal border line. + const int leftY = (nItem == 0) ? rItem.bottom - 1 : rItem.bottom - 2; + const int rightY = selected ? rItem.bottom - 1 : rItem.bottom; + + CPen pen(PS_SOLID, 1, clrBorder); + CPen* pOldPen = pDC->SelectObject(&pen); + pDC->MoveTo(rItem.left, leftY); + pDC->LineTo(rItem.left, rItem.top); + pDC->LineTo(rItem.right, rItem.top); + pDC->LineTo(rItem.right, rightY); + pDC->SelectObject(pOldPen); + + CRect rText(rItem); + rText.left += 6; + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(selected ? DarkTheme::TextColor() : ThemeRGB(140, 145, 150)); + pDC->DrawTextW(buf, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); +} + +void CDarkTabCtrl::OnPaint() +{ + if (!DarkTheme::IsActive()) { + CTabCtrl::OnPaint(); + return; + } + + CPaintDC dc(this); + + CRect rClient; + GetClientRect(rClient); + if (rClient.IsRectEmpty()) { + return; + } + + // Double-buffer to avoid flicker between the pane frame and the tab buttons. + CDC memDC; + memDC.CreateCompatibleDC(&dc); + CBitmap bmp; + bmp.CreateCompatibleBitmap(&dc, rClient.Width(), rClient.Height()); + CBitmap* pOldBmp = memDC.SelectObject(&bmp); + + memDC.FillSolidRect(rClient, DarkTheme::FaceColor()); + + CFont* pOldFont = memDC.SelectObject(GetFont()); + + // Pane frame (the display area below the tab row). + CRect rContent = rClient; + AdjustRect(FALSE, rContent); + rContent.InflateRect(2, 2); + CBrush borderBrush(ThemeRGB(50, 56, 62)); + memDC.FrameRect(rContent, &borderBrush); + + const int nTab = GetItemCount(); + const int nSel = GetCurSel(); + + // Non-selected tabs first, then the selected one on top (enlarged) so it wins + // the overlap and blends into the pane. + for (int i = 0; i < nTab; ++i) { + if (i == nSel) { + continue; + } + CRect r; + GetItemRect(i, r); + DrawTabItem(i, r, false, &memDC); + } + if (nSel >= 0) { + CRect r; + GetItemRect(nSel, r); + r.top -= 2; + r.bottom += 2; + DrawTabItem(nSel, r, true, &memDC); + } + + memDC.SelectObject(pOldFont); + dc.BitBlt(0, 0, rClient.Width(), rClient.Height(), &memDC, 0, 0, SRCCOPY); + memDC.SelectObject(pOldBmp); +} diff --git a/src/apps/mplayerc/controls/DarkTabCtrl.h b/src/apps/mplayerc/controls/DarkTabCtrl.h new file mode 100644 index 0000000000..4a79ba8cba --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTabCtrl.h @@ -0,0 +1,45 @@ +/* + * (C) 2026 see Authors.txt + * + * This file is part of MPC-BE. + * + * MPC-BE 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. + * + * MPC-BE 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 + +#include + +// A CTabCtrl that fully owner-draws itself in the dark palette. The native tab +// control ignores SetWindowTheme("DarkMode_*") and always paints its body/pane +// and tab buttons with the light system theme, so we take over WM_PAINT / +// WM_ERASEBKGND and draw the whole control (background, pane frame, tab buttons) +// with DarkTheme colors. Falls back to the stock CTabCtrl when the dark theme is +// disabled. (Drawing approach mirrors MPC-HC's CMPCThemeTabCtrl.) +class CDarkTabCtrl : public CTabCtrl +{ + DECLARE_DYNAMIC(CDarkTabCtrl) + +public: + CDarkTabCtrl(); + virtual ~CDarkTabCtrl(); + +protected: + void DrawTabItem(int nItem, CRect rItem, bool selected, CDC* pDC); + + DECLARE_MESSAGE_MAP() + afx_msg void OnPaint(); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); +}; diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp new file mode 100644 index 0000000000..32b3cfebf3 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -0,0 +1,1388 @@ +/* + * (C) 2026 see Authors.txt + * + * This file is part of MPC-BE. + * + * MPC-BE 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. + * + * MPC-BE 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 "stdafx.h" +#include "DarkTheme.h" +#include "../MainFrm.h" // AfxGetAppSettings(), ThemeRGB() +#include "DSUtil/SysVersion.h" +#include +#include +#include // BP_CHECKBOX / BP_RADIOBUTTON / CBS_* / RBS_* +#include // SetWindowSubclass +#include // flat dark scrollbars (same as the playlist) + +#pragma comment(lib, "dwmapi.lib") +#pragma comment(lib, "uxtheme.lib") + +namespace DarkTheme +{ + namespace { + // ---- undocumented uxtheme.dll ordinals (Windows 10 1809+) ---- + enum PreferredAppMode { APPMODE_DEFAULT, APPMODE_ALLOWDARK, APPMODE_FORCEDARK, APPMODE_FORCELIGHT, APPMODE_MAX }; + + using fnAllowDarkModeForWindow = bool (WINAPI*)(HWND, bool); + using fnAllowDarkModeForApp = bool (WINAPI*)(bool); // 1809 (ord 135) + using fnSetPreferredAppMode = PreferredAppMode (WINAPI*)(PreferredAppMode); // 1903+ (ord 135) + using fnFlushMenuThemes = void (WINAPI*)(); + using fnRefreshImmersiveColorPolicyState = void (WINAPI*)(); + + fnAllowDarkModeForWindow pAllowDarkModeForWindow = nullptr; + fnAllowDarkModeForApp pAllowDarkModeForApp = nullptr; + fnSetPreferredAppMode pSetPreferredAppMode = nullptr; + fnFlushMenuThemes pFlushMenuThemes = nullptr; + fnRefreshImmersiveColorPolicyState pRefreshImmersiveColorPolicyState = nullptr; + + bool g_bApiChecked = false; + bool g_bApiOk = false; + bool g_bAppAllowed = false; + + // cached theme brushes (owned by the OS at exit; never handed to MFC to delete) + COLORREF g_clrFace = CLR_INVALID; + COLORREF g_clrCtrl = CLR_INVALID; + HBRUSH g_hbrFace = nullptr; + HBRUSH g_hbrCtrl = nullptr; + + bool Build1903orLater() { + static const bool b = IsWindowsVersionOrGreaterBuild(HIBYTE(_WIN32_WINNT_WIN10), LOBYTE(_WIN32_WINNT_WIN10), 18362); + return b; + } + + void LoadApi() { + if (g_bApiChecked) { + return; + } + g_bApiChecked = true; + + if (!SysVersion::IsWin10v1809orLater()) { + return; + } + + HMODULE hUx = GetModuleHandleW(L"uxtheme.dll"); + if (!hUx) { + hUx = LoadLibraryExW(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + } + if (!hUx) { + return; + } + + pAllowDarkModeForWindow = (fnAllowDarkModeForWindow)GetProcAddress(hUx, MAKEINTRESOURCEA(133)); + if (Build1903orLater()) { + pSetPreferredAppMode = (fnSetPreferredAppMode)GetProcAddress(hUx, MAKEINTRESOURCEA(135)); + } else { + pAllowDarkModeForApp = (fnAllowDarkModeForApp)GetProcAddress(hUx, MAKEINTRESOURCEA(135)); + } + pFlushMenuThemes = (fnFlushMenuThemes)GetProcAddress(hUx, MAKEINTRESOURCEA(136)); + pRefreshImmersiveColorPolicyState = (fnRefreshImmersiveColorPolicyState)GetProcAddress(hUx, MAKEINTRESOURCEA(104)); + + g_bApiOk = pAllowDarkModeForWindow && (pSetPreferredAppMode || pAllowDarkModeForApp); + } + + void EnsureBrushes() { + const COLORREF clrFace = ThemeRGB(22, 27, 32); + const COLORREF clrCtrl = ThemeRGB(10, 14, 18); + if (clrFace != g_clrFace || !g_hbrFace) { + if (g_hbrFace) { + ::DeleteObject(g_hbrFace); + } + g_hbrFace = ::CreateSolidBrush(clrFace); + g_clrFace = clrFace; + } + if (clrCtrl != g_clrCtrl || !g_hbrCtrl) { + if (g_hbrCtrl) { + ::DeleteObject(g_hbrCtrl); + } + g_hbrCtrl = ::CreateSolidBrush(clrCtrl); + g_clrCtrl = clrCtrl; + } + } + + // Group boxes (BS_GROUPBOX) are not repainted by Windows' native dark mode + // (their frame and caption stay drawn with the classic black-on-light system + // colors) and they do not emit NM_CUSTOMDRAW, so we subclass them and paint + // the frame + caption ourselves. + const UINT_PTR kGroupBoxSubclassId = 1; + + LRESULT CALLBACK GroupBoxSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_ERASEBKGND: { + CDC* pDC = CDC::FromHandle(reinterpret_cast(wParam)); + CRect rc; + ::GetClientRect(hWnd, &rc); + pDC->FillSolidRect(rc, FaceColor()); + return 1; + } + case WM_PAINT: { + PAINTSTRUCT ps; + CDC* pDC = CDC::FromHandle(::BeginPaint(hWnd, &ps)); + + CRect rc; + ::GetClientRect(hWnd, &rc); + pDC->FillSolidRect(rc, FaceColor()); + + CString text; + const int len = ::GetWindowTextLengthW(hWnd); + if (len > 0) { + ::GetWindowTextW(hWnd, text.GetBuffer(len + 1), len + 1); + text.ReleaseBuffer(); + } + + HFONT hFont = reinterpret_cast(::SendMessageW(hWnd, WM_GETFONT, 0, 0)); + CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; + + const int textH = pDC->GetTextExtent(L"Ag").cy; + + CRect rcFrame = rc; + rcFrame.top += textH / 2; + CBrush brFrame(ThemeRGB(70, 75, 80)); + pDC->FrameRect(rcFrame, &brFrame); + + if (!text.IsEmpty()) { + const int x = rc.left + 9; + const CSize ext = pDC->GetTextExtent(text); + CRect rcGap(x - 2, rc.top, x + ext.cx + 2, rc.top + textH); + pDC->FillSolidRect(rcGap, FaceColor()); // break the frame behind the caption + + const bool disabled = (::GetWindowLongW(hWnd, GWL_STYLE) & WS_DISABLED) != 0; + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(disabled ? ThemeRGB(110, 115, 120) : TextColor()); + CRect rcLabel(x, rc.top, rc.right - 4, rc.top + textH); + pDC->DrawTextW(text, rcLabel, DT_LEFT | DT_SINGLELINE | DT_TOP); + } + + if (pOldFont) { + pDC->SelectObject(pOldFont); + } + + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, GroupBoxSubclassProc, kGroupBoxSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // The native LVS_EX_GRIDLINES are drawn with a light system colour that dark + // mode does not change. We strip that style (see ThemeControl) and draw our own + // grid, a shade darker than the control borders, over the default paint. + void DrawListGridlines(HWND hWnd) { + if ((GetWindowLongW(hWnd, GWL_STYLE) & LVS_TYPEMASK) != LVS_REPORT) { + return; + } + const int count = static_cast(::SendMessageW(hWnd, LVM_GETITEMCOUNT, 0, 0)); + if (count <= 0) { + return; + } + + HDC hdc = ::GetDC(hWnd); + if (!hdc) { + return; + } + + CRect rcClient; + ::GetClientRect(hWnd, &rcClient); + + HWND hHeader = reinterpret_cast(::SendMessageW(hWnd, LVM_GETHEADER, 0, 0)); + int headerH = 0; + if (hHeader) { + RECT rh; + ::GetWindowRect(hHeader, &rh); + headerH = rh.bottom - rh.top; + } + + RECT rcLast{}; rcLast.left = LVIR_BOUNDS; + ::SendMessageW(hWnd, LVM_GETITEMRECT, count - 1, reinterpret_cast(&rcLast)); + const int gridBottom = (rcLast.bottom < rcClient.bottom) ? rcLast.bottom : rcClient.bottom; + + HPEN pen = ::CreatePen(PS_SOLID, 1, GridlineColor()); + HGDIOBJ oldPen = ::SelectObject(hdc, pen); + + // horizontal line under each visible row + const int top = static_cast(::SendMessageW(hWnd, LVM_GETTOPINDEX, 0, 0)); + const int per = static_cast(::SendMessageW(hWnd, LVM_GETCOUNTPERPAGE, 0, 0)); + for (int i = top; i <= top + per && i < count; ++i) { + RECT r{}; r.left = LVIR_BOUNDS; + ::SendMessageW(hWnd, LVM_GETITEMRECT, i, reinterpret_cast(&r)); + ::MoveToEx(hdc, rcClient.left, r.bottom - 1, nullptr); + ::LineTo(hdc, rcClient.right, r.bottom - 1); + } + + // vertical line at each column's right edge, through the item rows only + if (hHeader) { + const int cols = static_cast(::SendMessageW(hHeader, HDM_GETITEMCOUNT, 0, 0)); + for (int c = 0; c < cols; ++c) { + RECT hr{}; + if (Header_GetItemRect(hHeader, c, &hr)) { + ::MoveToEx(hdc, hr.right - 1, headerH, nullptr); + ::LineTo(hdc, hr.right - 1, gridBottom); + } + } + } + + ::SelectObject(hdc, oldPen); + ::DeleteObject(pen); + ::ReleaseDC(hWnd, hdc); + } + + // A list-view's column header (SysHeader32) is not darkened by SetWindowTheme + // and it sends its NM_CUSTOMDRAW to the list-view (its parent), not to the + // dialog, so we subclass the list-view and paint the header ourselves. When the + // list originally had grid lines, dwRefData is 1 and we also draw a dark grid. + const UINT_PTR kListViewSubclassId = 2; + + LRESULT CALLBACK ListViewSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR dwRefData) { + if (msg == WM_PAINT && dwRefData) { + const LRESULT res = DefSubclassProc(hWnd, msg, wParam, lParam); + DrawListGridlines(hWnd); + return res; + } + if (msg == WM_NOTIFY) { + NMHDR* pNM = reinterpret_cast(lParam); + HWND hHeader = reinterpret_cast(::SendMessageW(hWnd, LVM_GETHEADER, 0, 0)); + if (pNM && pNM->code == NM_CUSTOMDRAW && pNM->hwndFrom == hHeader) { + LPNMCUSTOMDRAW p = reinterpret_cast(lParam); + switch (p->dwDrawStage) { + case CDDS_PREPAINT: { + CDC* pDC = CDC::FromHandle(p->hdc); + CRect rcHdr; + ::GetClientRect(hHeader, &rcHdr); // p->rc is often empty for headers + pDC->FillSolidRect(rcHdr, FaceColor()); + return CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYPOSTPAINT; + } + case CDDS_ITEMPREPAINT: { + CDC* pDC = CDC::FromHandle(p->hdc); + CRect rc(p->rc); + pDC->FillSolidRect(rc, FaceColor()); + pDC->FillSolidRect(rc.right - 1, rc.top, 1, rc.Height(), ThemeRGB(70, 75, 80)); + pDC->FillSolidRect(rc.left, rc.bottom - 1, rc.Width(), 1, ThemeRGB(70, 75, 80)); + + wchar_t buf[256] = {}; + HDITEMW hdi = {}; + hdi.mask = HDI_TEXT | HDI_FORMAT; + hdi.pszText = buf; + hdi.cchTextMax = _countof(buf) - 1; + ::SendMessageW(hHeader, HDM_GETITEMW, p->dwItemSpec, reinterpret_cast(&hdi)); + + HFONT hFont = reinterpret_cast(::SendMessageW(hHeader, WM_GETFONT, 0, 0)); + CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; + + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(TextColor()); + CRect rcText = rc; + rcText.left += 6; + rcText.right -= 6; + const UINT just = hdi.fmt & HDF_JUSTIFYMASK; + const UINT fmt = DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS + | (just == HDF_CENTER ? DT_CENTER : just == HDF_RIGHT ? DT_RIGHT : DT_LEFT); + if (buf[0] && rcText.right > rcText.left) { + pDC->DrawTextW(buf, rcText, fmt); + } + + if (pOldFont) { + pDC->SelectObject(pOldFont); + } + return CDRF_SKIPDEFAULT; + } + case CDDS_POSTPAINT: { + // Windows repaints the empty area past the last column with the + // light theme background; overpaint it dark so it does not show + // up as a white "extra column". + const int count = static_cast(::SendMessageW(hHeader, HDM_GETITEMCOUNT, 0, 0)); + if (count > 0) { + RECT rcLast{}; + if (::SendMessageW(hHeader, HDM_GETITEMRECT, count - 1, reinterpret_cast(&rcLast))) { + CRect rcHdr; + ::GetClientRect(hHeader, &rcHdr); + if (rcLast.right < rcHdr.right) { + CDC* pDC = CDC::FromHandle(p->hdc); + pDC->FillSolidRect(rcLast.right, rcHdr.top, rcHdr.right - rcLast.right, rcHdr.Height(), FaceColor()); + } + } + } + return CDRF_DODEFAULT; + } + } + return CDRF_DODEFAULT; + } + } else if (msg == WM_NCDESTROY) { + RemoveWindowSubclass(hWnd, ListViewSubclassProc, kListViewSubclassId); + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // Draws a small filled triangle (spin-button arrow) centred in rc. + void DrawArrow(CDC* pDC, CRect rc, bool up, COLORREF clr) { + const int w = 7; // arrow width in px + const int h = 4; // arrow height in px + const int cx = rc.left + rc.Width() / 2; + const int cy = rc.top + rc.Height() / 2; + POINT pts[3]; + if (up) { + pts[0] = { cx, cy - h / 2 - 1 }; + pts[1] = { cx - w / 2, cy + h / 2 }; + pts[2] = { cx + w / 2 + 1, cy + h / 2 }; + } else { + pts[0] = { cx - w / 2, cy - h / 2 }; + pts[1] = { cx + w / 2 + 1, cy - h / 2 }; + pts[2] = { cx, cy + h / 2 + 1 }; + } + CBrush br(clr); + CBrush* pOldBr = pDC->SelectObject(&br); + CPen pen(PS_SOLID, 1, clr); + CPen* pOldPen = pDC->SelectObject(&pen); + pDC->SetPolyFillMode(WINDING); + pDC->Polygon(pts, 3); + pDC->SelectObject(pOldPen); + pDC->SelectObject(pOldBr); + } + + // Same as DrawArrow but pointing left/right (for horizontal up-down controls). + void DrawArrowLR(CDC* pDC, CRect rc, bool left, COLORREF clr) { + const int w = 7; // arrow length along its axis + const int h = 4; // arrow half-thickness base + const int cx = rc.left + rc.Width() / 2; + const int cy = rc.top + rc.Height() / 2; + POINT pts[3]; + if (left) { + pts[0] = { cx - h / 2 - 1, cy }; + pts[1] = { cx + h / 2, cy - w / 2 }; + pts[2] = { cx + h / 2, cy + w / 2 + 1 }; + } else { + pts[0] = { cx - h / 2, cy - w / 2 }; + pts[1] = { cx + h / 2 + 1, cy }; + pts[2] = { cx - h / 2, cy + w / 2 + 1 }; + } + CBrush br(clr); + CBrush* pOldBr = pDC->SelectObject(&br); + CPen pen(PS_SOLID, 1, clr); + CPen* pOldPen = pDC->SelectObject(&pen); + pDC->SetPolyFillMode(WINDING); + pDC->Polygon(pts, 3); + pDC->SelectObject(pOldPen); + pDC->SelectObject(pOldBr); + } + + // Spin (up-down) controls are drawn by Windows as two light 3D buttons that + // native dark mode does not darken, so we owner-draw them: a dark button face + // with a light-grey arrow in each half and a subtle border/divider. + const UINT_PTR kSpinSubclassId = 4; + + LRESULT CALLBACK SpinSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_ERASEBKGND: + return 1; + case WM_PAINT: { + PAINTSTRUCT ps; + CDC* pDC = CDC::FromHandle(::BeginPaint(hWnd, &ps)); + + CRect rc; + ::GetClientRect(hWnd, &rc); + + const COLORREF clrFace = ThemeRGB(38, 44, 50); + const COLORREF clrBorder = ThemeRGB(70, 75, 80); + const COLORREF clrArrow = ThemeRGB(170, 175, 180); + + pDC->FillSolidRect(rc, clrFace); + + if (::GetWindowLongW(hWnd, GWL_STYLE) & UDS_HORZ) { + // Two side-by-side buttons with left/right arrows. + const int mid = rc.left + rc.Width() / 2; + CRect rcL(rc.left, rc.top, mid, rc.bottom); + CRect rcR(mid, rc.top, rc.right, rc.bottom); + DrawArrowLR(pDC, rcL, true, clrArrow); + DrawArrowLR(pDC, rcR, false, clrArrow); + pDC->Draw3dRect(rc, clrBorder, clrBorder); // outer border + pDC->FillSolidRect(mid, rc.top, 1, rc.Height(), clrBorder); // divider + } else { + // Two stacked buttons with up/down arrows. + const int mid = rc.top + rc.Height() / 2; + CRect rcUp(rc.left, rc.top, rc.right, mid); + CRect rcDn(rc.left, mid, rc.right, rc.bottom); + DrawArrow(pDC, rcUp, true, clrArrow); + DrawArrow(pDC, rcDn, false, clrArrow); + pDC->Draw3dRect(rc, clrBorder, clrBorder); // outer border + pDC->FillSolidRect(rc.left, mid, rc.Width(), 1, clrBorder); // divider + } + + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, SpinSubclassProc, kSpinSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // Some trackbars stop honouring the NM_CUSTOMDRAW channel colour after a + // programmatic value change (e.g. the "Reset" button on the subtitle Default Style + // page repaints them with the light theme channel). For those we fully owner-draw + // the trackbar here instead of relying on custom draw: dark background, dark groove + // and a celeste thumb, painted deterministically in WM_PAINT. + const UINT_PTR kTrackbarSubclassId = 6; + + LRESULT CALLBACK TrackbarSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_ERASEBKGND: + return 1; // background is painted in WM_PAINT + case WM_PAINT: { + PAINTSTRUCT ps; + CDC* pDC = CDC::FromHandle(::BeginPaint(hWnd, &ps)); + + CRect rc; + ::GetClientRect(hWnd, &rc); + pDC->FillSolidRect(rc, FaceColor()); + + RECT rcCh{}; + ::SendMessageW(hWnd, TBM_GETCHANNELRECT, 0, reinterpret_cast(&rcCh)); + CRect ch(rcCh); + pDC->FillSolidRect(ch, ThemeRGB(10, 14, 18)); // dark groove + pDC->Draw3dRect(ch, ThemeRGB(60, 65, 70), ThemeRGB(60, 65, 70)); // subtle border + + RECT rcTh{}; + ::SendMessageW(hWnd, TBM_GETTHUMBRECT, 0, reinterpret_cast(&rcTh)); + CRect th(rcTh); + const bool disabled = (::GetWindowLongW(hWnd, GWL_STYLE) & WS_DISABLED) != 0; + pDC->FillSolidRect(th, disabled ? ThemeRGB(90, 95, 100) : RGB(76, 194, 255)); // celeste thumb + + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, TrackbarSubclassProc, kTrackbarSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // CoolSB draws its thumb purely from its own stored scroll position (see CalcThumbSize) + // and never updates it on its own: during a drag it only posts WM_VSCROLL(SB_THUMBTRACK) + // and expects the owner to feed the new position back. Wheel/keyboard scrolling it does + // not see at all. These controls (list-view, tree-view, list-box) scroll themselves and + // keep their real scroll info, so we subclass them and, after every scroll, copy that + // real info into CoolSB — otherwise the flat thumb snaps back to the top while the + // content scrolls underneath it. + const UINT_PTR kCoolSBSyncId = 8; + + // True when the control actually has something to scroll in this direction. Mirrors + // CoolSB's own IsScrollInfoActive so our show/hide decision matches its thumb logic. + bool IsBarScrollable(HWND hCtrl, int bar) { + SCROLLINFO si = { sizeof(si), SIF_RANGE | SIF_PAGE }; + if (!::GetScrollInfo(hCtrl, bar, &si)) { + return false; + } + return !(si.nPage > (UINT)si.nMax || si.nMax <= si.nMin || si.nMax == 0); + } + + // Tree-views (e.g. the Options navigation tree) carry WS_HSCROLL and, once CoolSB's + // vertical bar narrows their client, report a horizontal range — producing a phantom + // bottom bar that was never there natively. They don't need horizontal scrolling here, + // so we never manage or show their horizontal bar. + // Decides whether a control should show a horizontal scrollbar. In the Options dialog the + // only control that legitimately needs one is a multi-column report list-view (the Keys + // list). Tree-views, list-boxes (the Internal/External Filters check lists) and single- + // column lists (Formats, Select Filter) fit their content to the width, so any horizontal + // bar there is the spurious one from the vertical-bar reservation — never show it. + bool NeedsHorzBar(HWND hCtrl) { + wchar_t cls[32] = {}; + GetClassNameW(hCtrl, cls, _countof(cls)); + + if (_wcsicmp(cls, L"SysListView32") != 0) { + return false; // tree-views, list-boxes, ... : single-column here + } + if ((GetWindowLongW(hCtrl, GWL_STYLE) & LVS_TYPEMASK) != LVS_REPORT) { + return IsBarScrollable(hCtrl, SB_HORZ); // list/icon modes scroll horizontally by nature + } + HWND hHeader = reinterpret_cast(::SendMessageW(hCtrl, LVM_GETHEADER, 0, 0)); + const int cols = hHeader ? static_cast(::SendMessageW(hHeader, HDM_GETITEMCOUNT, 0, 0)) : 0; + if (cols <= 1) { + return false; // a single column fits the width + } + // Multi-column: show it only if the columns really overflow, ignoring the ~vertical- + // bar-width that the vertical bar reservation alone introduces. + SCROLLINFO si = { sizeof(si), SIF_RANGE | SIF_PAGE }; + if (!::GetScrollInfo(hCtrl, SB_HORZ, &si)) { + return false; + } + if (si.nPage > (UINT)si.nMax || si.nMax <= si.nMin || si.nMax == 0) { + return false; // nothing to scroll horizontally + } + const int overflow = (si.nMax - si.nMin + 1) - static_cast(si.nPage); + return overflow > ::GetSystemMetrics(SM_CXVSCROLL) + 4; + } + + // CoolSB reserves and draws a bar for every direction whose WS_*SCROLL style was set + // when it was initialised, even with nothing to scroll. The native control hides an + // unused bar; mirror that so no empty gutter shows up. + void UpdateBarVisibility(HWND hCtrl) { + CoolSB_ShowScrollBar(hCtrl, SB_VERT, IsBarScrollable(hCtrl, SB_VERT)); + CoolSB_ShowScrollBar(hCtrl, SB_HORZ, NeedsHorzBar(hCtrl)); + } + + void SyncCoolSB(HWND hCtrl) { + // While CoolSB is actively dragging its thumb it already paints it following the + // mouse; only update the stored position (no redraw) then, to avoid fighting that + // paint. For wheel/keyboard scrolling we do need the redraw to move the thumb. + const BOOL redraw = !CoolSB_IsThumbTracking(hCtrl); + SCROLLINFO si = { sizeof(si), SIF_ALL }; + if (::GetScrollInfo(hCtrl, SB_VERT, &si)) { + CoolSB_SetScrollInfo(hCtrl, SB_VERT, &si, redraw); + } + // Only feed the horizontal bar when it's genuinely wanted; otherwise CoolSB's + // SetScrollInfo would auto-re-show a phantom/spurious bottom bar on every scroll. + if (NeedsHorzBar(hCtrl)) { + SCROLLINFO sih = { sizeof(sih), SIF_ALL }; + if (::GetScrollInfo(hCtrl, SB_HORZ, &sih)) { + CoolSB_SetScrollInfo(hCtrl, SB_HORZ, &sih, redraw); + } + } + } + + // Scrolls a CoolSB-managed control to an absolute position. CoolSB posts the target in + // the SB_THUMB* message, but common controls (tree-view, list-view) read the drag + // position from the scrollbar's nTrackPos — which cannot be set through the API — so a + // posted SB_THUMBTRACK moves nothing on them (only CPlayerListCtrl overrides OnVScroll + // to use the message value). Instead we step the control with line scrolls, which every + // control honours (relative, no position needed), reading the real position back until + // it reaches the target. Intermediate steps are not painted (all inside one message), + // so there is no visible stepping. + void ScrollToPos(HWND hCtrl, UINT bar, int target) { + wchar_t cls[32] = {}; + GetClassNameW(hCtrl, cls, _countof(cls)); + + // List-views can scroll to an absolute position in a single shot (LVM_SCROLL takes a + // pixel delta), so avoid the per-line loop which freezes on large lists (e.g. Keys). + // Mirrors CPlayerListCtrl::OnVScroll: pixel delta = span * (target - pos) / page. + if (_wcsicmp(cls, L"SysListView32") == 0) { + SCROLLINFO si = { sizeof(si), SIF_RANGE | SIF_PAGE | SIF_POS }; + if (!::GetScrollInfo(hCtrl, bar, &si)) { + return; + } + RECT rc; ::GetClientRect(hCtrl, &rc); + const int span = (bar == SB_VERT) ? rc.bottom : rc.right; + const int denom = (si.nPage != 0) ? (int)si.nPage : (si.nMax + 1); + if (denom <= 0) { + return; + } + const int delta = (int)((LONGLONG)span * (target - si.nPos) / denom); + if (delta != 0) { + if (bar == SB_VERT) ::SendMessageW(hCtrl, LVM_SCROLL, 0, delta); + else ::SendMessageW(hCtrl, LVM_SCROLL, delta, 0); + } + return; + } + + // Generic controls (tree-view, list-box): step there with line scrolls, which every + // control honours. These have few scroll units, so the loop stays cheap. + const UINT msg = (bar == SB_VERT) ? WM_VSCROLL : WM_HSCROLL; + SCROLLINFO si = { sizeof(si), SIF_POS }; + if (!::GetScrollInfo(hCtrl, bar, &si)) { + return; + } + int cur = si.nPos; + for (int guard = 0; cur != target && guard < 20000; ++guard) { + const bool down = target > cur; // SB_LINEDOWN == SB_LINERIGHT, SB_LINEUP == SB_LINELEFT + DefSubclassProc(hCtrl, msg, MAKEWPARAM(down ? SB_LINEDOWN : SB_LINEUP, 0), 0); + SCROLLINFO now = { sizeof(now), SIF_POS }; + ::GetScrollInfo(hCtrl, bar, &now); + if (now.nPos == cur) { + break; // clamped at an end, cannot move further + } + const bool crossed = (now.nPos < target) != (cur < target); + cur = now.nPos; + if (crossed) { + break; // one line stepped past the target (control's line > 1 unit); close enough + } + } + } + + LRESULT CALLBACK CoolSBSyncProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_VSCROLL: + case WM_HSCROLL: { + const UINT bar = (msg == WM_VSCROLL) ? SB_VERT : SB_HORZ; + const int code = LOWORD(wParam); + LRESULT r = 0; + if (code == SB_THUMBTRACK || code == SB_THUMBPOSITION) { + ScrollToPos(hWnd, bar, HIWORD(wParam)); // drive the scroll ourselves + } else { + r = DefSubclassProc(hWnd, msg, wParam, lParam); + } + // A horizontal scroll bitblts the content and leaves our hand-drawn vertical + // column grid lines behind as ghosts (only the newly exposed strip repaints). + // Force a full repaint so they are redrawn cleanly. + if (msg == WM_HSCROLL) { + ::InvalidateRect(hWnd, nullptr, TRUE); + } + SyncCoolSB(hWnd); + return r; + } + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + case WM_KEYDOWN: + case WM_KEYUP: { + const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); + SyncCoolSB(hWnd); + return r; + } + case WM_PAINT: { + const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); + // When the control's scroll range changes (e.g. the Keys list is filtered as + // the user types) CoolSB doesn't hear about it. Detect the mismatch cheaply + // and recompute the flat scrollbar; only acts when the range actually changed. + SCROLLINFO real = { sizeof(real), SIF_RANGE | SIF_PAGE }; + SCROLLINFO cool = { sizeof(cool), SIF_RANGE | SIF_PAGE }; + if (::GetScrollInfo(hWnd, SB_VERT, &real) && CoolSB_GetScrollInfo(hWnd, SB_VERT, &cool) + && (real.nMin != cool.nMin || real.nMax != cool.nMax || real.nPage != cool.nPage)) { + SyncCoolSB(hWnd); + UpdateBarVisibility(hWnd); + } + return r; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, CoolSBSyncProc, kCoolSBSyncId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // Scrollbars are replaced with CoolSB -- the same flat dark scrollbar the playlist uses + // (InitializeCoolSB is handed our ThemeRGB palette). CoolSB actually replaces the native + // scrollbar (it removes the WS_*SCROLL styles and owns the non-client area), so unlike + // painting over the native bar nothing of the original ever shows through while scrolling. + void ApplyScrollBars(HWND hCtrl) { + if (CoolSB_IsCoolScrollEnabled(hCtrl)) { + return; // already initialised (ApplyThemeToChildren may run more than once) + } + // Install the sync subclass first so it sits behind CoolSB's window proc and sees the + // scroll messages after the control has processed (and scrolled) them. + SetWindowSubclass(hCtrl, CoolSBSyncProc, kCoolSBSyncId, 0); + if (!InitializeCoolSB(hCtrl, ThemeRGB)) { + RemoveWindowSubclass(hCtrl, CoolSBSyncProc, kCoolSBSyncId); + return; + } + CoolSB_SetStyle(hCtrl, SB_VERT, CSBS_HOTTRACKED); + CoolSB_SetStyle(hCtrl, SB_HORZ, CSBS_HOTTRACKED); + if (SysVersion::IsWin8orLater()) { + CoolSB_SetSize(hCtrl, SB_VERT, ::GetSystemMetrics(SM_CYVSCROLL), ::GetSystemMetrics(SM_CXVSCROLL)); + CoolSB_SetSize(hCtrl, SB_HORZ, ::GetSystemMetrics(SM_CXHSCROLL), ::GetSystemMetrics(SM_CYHSCROLL)); + } + // Seed CoolSB with the control's current scroll range/position. + SyncCoolSB(hCtrl); + // Hide the bars that have nothing to scroll (e.g. the tree's horizontal bar). + UpdateBarVisibility(hCtrl); + } + + // Subclass for auxiliary top-level dialogs (opened from the Options pages, e.g. the + // "Add filter" choosers): paints the dialog background and control colours dark via + // WM_CTLCOLOR* / WM_ERASEBKGND, mirroring what CPPageBase does for the pages. + const UINT_PTR kDialogSubclassId = 7; + + LRESULT CALLBACK DialogSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_CTLCOLORMSGBOX: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORLISTBOX: + case WM_CTLCOLORBTN: + case WM_CTLCOLORDLG: + case WM_CTLCOLORSCROLLBAR: + case WM_CTLCOLORSTATIC: { + CDC* pDC = CDC::FromHandle(reinterpret_cast(wParam)); + // WM_CTLCOLOR* run consecutively starting at WM_CTLCOLORMSGBOX, matching + // the CTLCOLOR_* constants in the same order. + if (HBRUSH hbr = OnCtlColor(pDC, msg - WM_CTLCOLORMSGBOX)) { + return reinterpret_cast(hbr); + } + break; + } + case WM_ERASEBKGND: { + CDC* pDC = CDC::FromHandle(reinterpret_cast(wParam)); + CRect rc; + ::GetClientRect(hWnd, &rc); + pDC->FillSolidRect(rc, FaceColor()); + return 1; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, DialogSubclassProc, kDialogSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // Edits, tree-views and list-views keep a light sunken border under native dark + // mode. We repaint just the outer border frame with the shared dark border + // colour (not the whole non-client ring, so a control's scrollbars are left + // alone), matching the spin buttons, colour wells and group boxes. + const UINT_PTR kBorderSubclassId = 5; + + LRESULT CALLBACK BorderSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_NCPAINT: { + const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); // draws scrollbars first + HDC hdc = ::GetWindowDC(hWnd); + if (hdc) { + RECT wr; + ::GetWindowRect(hWnd, &wr); + RECT rc = { 0, 0, wr.right - wr.left, wr.bottom - wr.top }; + POINT org = { 0, 0 }; + ::ClientToScreen(hWnd, &org); + int edge = org.y - wr.top; // border thickness: 1 (WS_BORDER) or 2 (client edge) + if (edge < 1) { + edge = 1; + } + HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); + for (int k = 0; k < edge; ++k) { + ::FrameRect(hdc, &rc, br); + ::InflateRect(&rc, -1, -1); + } + ::DeleteObject(br); + ::ReleaseDC(hWnd, hdc); + } + return r; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, BorderSubclassProc, kBorderSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + void ApplyDarkBorder(HWND hCtrl) { + SetWindowSubclass(hCtrl, BorderSubclassProc, kBorderSubclassId, 0); + ::SetWindowPos(hCtrl, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); // force NC repaint + } + + void ThemeControl(HWND hCtrl) { + if (pAllowDarkModeForWindow) { + pAllowDarkModeForWindow(hCtrl, true); + } + + wchar_t cls[64] = {}; + GetClassNameW(hCtrl, cls, _countof(cls)); + + if (_wcsicmp(cls, L"Button") == 0) { + if ((GetWindowLongW(hCtrl, GWL_STYLE) & BS_TYPEMASK) == BS_GROUPBOX) { + SetWindowSubclass(hCtrl, GroupBoxSubclassProc, kGroupBoxSubclassId, 0); + InvalidateRect(hCtrl, nullptr, TRUE); + } else { + // checkboxes, radios, push buttons + SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); + } + } else if (_wcsicmp(cls, L"ComboBox") == 0) { + SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); + } else if (_wcsicmp(cls, L"Edit") == 0) { + SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); + ApplyDarkBorder(hCtrl); + if (GetWindowLongW(hCtrl, GWL_STYLE) & ES_MULTILINE) { + ApplyScrollBars(hCtrl); // multiline edits can show scrollbars + } + } else if (_wcsicmp(cls, L"SysListView32") == 0) { + // List-view controls ignore WM_CTLCOLOR: their background (the area + // not covered by columns/rows) must be set explicitly, otherwise it + // stays the default white. + SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); + const COLORREF clrBk = FaceColor(); + ::SendMessageW(hCtrl, LVM_SETBKCOLOR, 0, static_cast(clrBk)); + ::SendMessageW(hCtrl, LVM_SETTEXTBKCOLOR, 0, static_cast(clrBk)); + ::SendMessageW(hCtrl, LVM_SETTEXTCOLOR, 0, static_cast(TextColor())); + // Double-buffer so the full repaint we force on horizontal scroll (to clear the + // hand-drawn grid-line ghosts) doesn't flicker. + ::SendMessageW(hCtrl, LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_DOUBLEBUFFER, LVS_EX_DOUBLEBUFFER); + // Replace the light native grid lines with our own dark ones: strip the + // style and remember (via the subclass ref-data) that this list wants a grid. + // Only do this the first time — on re-application the style is already gone, + // so preserve the remembered flag instead of clearing it. + DWORD_PTR gridFlag = 0; + if (!GetWindowSubclass(hCtrl, ListViewSubclassProc, kListViewSubclassId, &gridFlag)) { + gridFlag = (::SendMessageW(hCtrl, LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0) & LVS_EX_GRIDLINES) ? 1 : 0; + if (gridFlag) { + ::SendMessageW(hCtrl, LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_GRIDLINES, 0); + } + } + // Dark-paint the column header (and grid) via a subclass (see ListViewSubclassProc). + SetWindowSubclass(hCtrl, ListViewSubclassProc, kListViewSubclassId, gridFlag); + if (HWND hHeader = reinterpret_cast(::SendMessageW(hCtrl, LVM_GETHEADER, 0, 0))) { + InvalidateRect(hHeader, nullptr, TRUE); + } + ApplyDarkBorder(hCtrl); // dark outer border to match everything else + ApplyScrollBars(hCtrl); // flat MPC-HC-style scrollbars + } else if (_wcsicmp(cls, L"SysTreeView32") == 0) { + // Tree-views, like list-views, need their background/text colors set + // explicitly (SetWindowTheme only handles the glyphs and scrollbar). + SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); + ::SendMessageW(hCtrl, TVM_SETBKCOLOR, 0, static_cast(FaceColor())); + ::SendMessageW(hCtrl, TVM_SETTEXTCOLOR, 0, static_cast(TextColor())); + ApplyDarkBorder(hCtrl); // dark outer border to match everything else + ApplyScrollBars(hCtrl); // flat MPC-HC-style scrollbars + } else if (_wcsicmp(cls, UPDOWN_CLASSW) == 0) { + // Spin buttons: fully owner-drawn (native dark mode leaves them light). + SetWindowSubclass(hCtrl, SpinSubclassProc, kSpinSubclassId, 0); + InvalidateRect(hCtrl, nullptr, TRUE); + } else if (_wcsicmp(cls, L"ListBox") == 0) { + // Plain list boxes (e.g. DVD preferred-language) get their interior from + // WM_CTLCOLORLISTBOX (handled by the page), but their sunken border stays + // light — repaint it with the shared dark border like edits/lists/trees. + SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); + ApplyDarkBorder(hCtrl); + ApplyScrollBars(hCtrl); // flat MPC-HC-style scrollbars + } else if (_wcsicmp(cls, L"Static") == 0) { + // Sunken value boxes (e.g. Brightness/Contrast/Hue/Saturation on the + // Color correction page are SS_SUNKEN RTEXT statics) keep a light 3D edge + // under native dark mode. Their interior comes from WM_CTLCOLORSTATIC + // (handled by the page); repaint just the edge with the dark border. + const LONG st = GetWindowLongW(hCtrl, GWL_STYLE); + const LONG ex = GetWindowLongW(hCtrl, GWL_EXSTYLE); + if ((st & SS_SUNKEN) || (ex & (WS_EX_CLIENTEDGE | WS_EX_STATICEDGE))) { + ApplyDarkBorder(hCtrl); + } + } else { + // Note: SysTabControl32 is handled by CDarkTabCtrl (a CTabCtrl-derived + // owner-drawn control), not here — installing a comctl subclass would sit + // in front of MFC's WndProc and steal WM_PAINT from that class. + // up-down, scrollbar, trackbar, ... + SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); + } + } + + BOOL CALLBACK EnumChildProc(HWND hChild, LPARAM) { + ThemeControl(hChild); + return TRUE; + } + + // Reverses everything ThemeControl may have installed on a control, so it returns to its + // default (light) look when the dark theme is switched off while the Options dialog is + // open. Removing a subclass that isn't present is a safe no-op. + BOOL CALLBACK StripThemeChildProc(HWND hChild, LPARAM) { + RemoveWindowSubclass(hChild, GroupBoxSubclassProc, kGroupBoxSubclassId); + RemoveWindowSubclass(hChild, SpinSubclassProc, kSpinSubclassId); + RemoveWindowSubclass(hChild, BorderSubclassProc, kBorderSubclassId); + RemoveWindowSubclass(hChild, TrackbarSubclassProc, kTrackbarSubclassId); + RemoveWindowSubclass(hChild, CoolSBSyncProc, kCoolSBSyncId); + + DWORD_PTR gridFlag = 0; + const bool hadGrid = GetWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId, &gridFlag) && gridFlag; + RemoveWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId); + + if (CoolSB_IsCoolScrollEnabled(hChild)) { + UninitializeCoolSB(hChild); // restores the native scrollbars + window proc + } + + wchar_t cls[64] = {}; + GetClassNameW(hChild, cls, _countof(cls)); + if (_wcsicmp(cls, L"SysListView32") == 0) { + ::SendMessageW(hChild, LVM_SETBKCOLOR, 0, static_cast(::GetSysColor(COLOR_WINDOW))); + ::SendMessageW(hChild, LVM_SETTEXTBKCOLOR, 0, static_cast(::GetSysColor(COLOR_WINDOW))); + ::SendMessageW(hChild, LVM_SETTEXTCOLOR, 0, static_cast(::GetSysColor(COLOR_WINDOWTEXT))); + if (hadGrid) { // we had stripped the native grid lines; put them back + ::SendMessageW(hChild, LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_GRIDLINES, LVS_EX_GRIDLINES); + } + } else if (_wcsicmp(cls, L"SysTreeView32") == 0) { + ::SendMessageW(hChild, TVM_SETBKCOLOR, 0, static_cast(-1)); // -1 = default + ::SendMessageW(hChild, TVM_SETTEXTCOLOR, 0, static_cast(-1)); + } + + SetWindowTheme(hChild, nullptr, nullptr); // drop the DarkMode_* visual style override + if (pAllowDarkModeForWindow) { + pAllowDarkModeForWindow(hChild, false); + } + ::SetWindowPos(hChild, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + InvalidateRect(hChild, nullptr, TRUE); + return TRUE; + } + } // anonymous namespace + + bool IsActive() { + return AfxGetAppSettings().bUseDarkTheme && SysVersion::IsWin10v1809orLater(); + } + + COLORREF FaceColor() { return ThemeRGB(22, 27, 32); } + COLORREF TextColor() { return ThemeRGB(165, 170, 175); } + COLORREF CtrlBackColor() { return ThemeRGB(10, 14, 18); } + COLORREF CtrlBorderColor() { return ThemeRGB(70, 75, 80); } + COLORREF GridlineColor() { return ThemeRGB(40, 45, 50); } + + void AllowDarkModeForApp() { + if (!IsActive()) { + return; + } + LoadApi(); + if (!g_bApiOk || g_bAppAllowed) { + return; + } + if (pSetPreferredAppMode) { + pSetPreferredAppMode(APPMODE_FORCEDARK); + } else if (pAllowDarkModeForApp) { + pAllowDarkModeForApp(true); + } + if (pRefreshImmersiveColorPolicyState) { + pRefreshImmersiveColorPolicyState(); + } + if (pFlushMenuThemes) { + pFlushMenuThemes(); + } + g_bAppAllowed = true; + } + + void EnableForWindow(HWND hWnd) { + if (!IsActive() || !hWnd) { + return; + } + LoadApi(); + if (g_bApiOk && pAllowDarkModeForWindow) { + pAllowDarkModeForWindow(hWnd, true); + if (pRefreshImmersiveColorPolicyState) { + pRefreshImmersiveColorPolicyState(); + } + } + + // Dark title bar: attribute 20 (DWMWA_USE_IMMERSIVE_DARK_MODE) on 2004+, + // attribute 19 on 1809/1903. + BOOL bDark = TRUE; + if (FAILED(DwmSetWindowAttribute(hWnd, 20, &bDark, sizeof(bDark)))) { + DwmSetWindowAttribute(hWnd, 19, &bDark, sizeof(bDark)); + } + } + + void ApplyThemeToChildren(HWND hWndParent) { + if (!IsActive() || !hWndParent) { + return; + } + LoadApi(); + if (!g_bApiOk) { + return; + } + EnumChildWindows(hWndParent, EnumChildProc, 0); + } + + void FixGroupBoxes(HWND hWndParent) { + if (!IsActive() || !hWndParent) { + return; + } + // Collect the group boxes first — moving a window's z-order while walking the sibling + // list would disturb the walk. Only the direct children of the page can overlap here. + HWND boxes[32]; + int n = 0; + for (HWND c = ::GetWindow(hWndParent, GW_CHILD); c && n < _countof(boxes); c = ::GetWindow(c, GW_HWNDNEXT)) { + wchar_t cls[16] = {}; + GetClassNameW(c, cls, _countof(cls)); + if (_wcsicmp(cls, L"Button") == 0 && (GetWindowLongW(c, GWL_STYLE) & BS_TYPEMASK) == BS_GROUPBOX) { + boxes[n++] = c; + } + } + for (int i = 0; i < n; ++i) { + SetWindowLongW(boxes[i], GWL_STYLE, GetWindowLongW(boxes[i], GWL_STYLE) | WS_CLIPSIBLINGS); + ::SetWindowPos(boxes[i], HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + } + + void ApplyThemeToControl(HWND hCtrl) { + if (!IsActive() || !hCtrl) { + return; + } + LoadApi(); + if (!g_bApiOk) { + return; + } + ThemeControl(hCtrl); + } + + void MakeTrackbarOwnerDrawn(HWND hTrackbar) { + if (!IsActive() || !hTrackbar) { + return; + } + SetWindowSubclass(hTrackbar, TrackbarSubclassProc, kTrackbarSubclassId, 0); + ::InvalidateRect(hTrackbar, nullptr, TRUE); + } + + void ThemeScrollBars(HWND hCtrl) { + if (!IsActive() || !hCtrl) { + return; + } + ApplyScrollBars(hCtrl); + } + + void ThemeDialog(HWND hDlg) { + if (!IsActive() || !hDlg) { + return; + } + LoadApi(); + EnableForWindow(hDlg); // dark title bar + allow dark mode + SetWindowSubclass(hDlg, DialogSubclassProc, kDialogSubclassId, 0); // dark bg / ctl colours + ApplyThemeToChildren(hDlg); // theme the child controls + ::InvalidateRect(hDlg, nullptr, TRUE); + } + + void RefreshTheme(HWND hRoot) { + if (!hRoot) { + return; + } + LoadApi(); + if (IsActive()) { + // Turned on at runtime: (re)apply to the sheet and every already-created page. + EnableForWindow(hRoot); // dark title bar (checks IsActive internally) + ApplyThemeToChildren(hRoot); // recurses into all descendant controls + } else { + // Turned off at runtime: strip every subclass/override so everything goes light. + if (g_bApiOk && pAllowDarkModeForWindow) { + pAllowDarkModeForWindow(hRoot, false); + } + BOOL bDark = FALSE; // restore the light title bar + if (FAILED(DwmSetWindowAttribute(hRoot, 20, &bDark, sizeof(bDark)))) { + DwmSetWindowAttribute(hRoot, 19, &bDark, sizeof(bDark)); + } + EnumChildWindows(hRoot, StripThemeChildProc, 0); + } + ::RedrawWindow(hRoot, nullptr, nullptr, + RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW); + } + + bool MakeCheckStateImageList(CImageList& il, int size, HWND hRef, bool bDisabled) { + if (!IsActive() || size <= 0) { + return false; + } + + if (bDisabled) { + // Hand-drawn grey checkboxes for the inactive (locked) list. The theme's + // disabled glyph renders almost fully transparent through buffered paint, + // which made the checkboxes vanish; a solid grey box + grey tick reads clearly + // as an inactive checkbox and never disappears. + const COLORREF mask = RGB(255, 0, 255); + const COLORREF fill = ThemeRGB(34, 39, 44); + const COLORREF border = ThemeRGB(90, 95, 100); + const COLORREF mark = ThemeRGB(120, 125, 130); + + CClientDC screen(nullptr); + CDC dc; + dc.CreateCompatibleDC(&screen); + CBitmap bmp; + bmp.CreateCompatibleBitmap(&screen, size * 3, size); + CBitmap* pOld = dc.SelectObject(&bmp); + dc.FillSolidRect(0, 0, size * 3, size, mask); + + for (int i = 1; i <= 2; ++i) { // index 0 = blank, 1 = unchecked, 2 = checked + CRect box(i * size, 0, i * size + size, size); + box.DeflateRect(2, 2); + dc.FillSolidRect(box, fill); + dc.Draw3dRect(box, border, border); + if (i == 2) { + CPen pen(PS_SOLID, 2, mark); + CPen* pp = dc.SelectObject(&pen); + const int l = box.left, t = box.top, w = box.Width(), h = box.Height(); + dc.MoveTo(l + w * 25 / 100, t + h * 50 / 100); + dc.LineTo(l + w * 45 / 100, t + h * 70 / 100); + dc.LineTo(l + w * 75 / 100, t + h * 28 / 100); + dc.SelectObject(pp); + } + } + + dc.SelectObject(pOld); + il.DeleteImageList(); + il.Create(size, size, ILC_COLOR24 | ILC_MASK, 3, 0); + il.Add(&bmp, mask); + return il.GetImageCount() == 3; + } + + AllowDarkModeForApp(); // ensure force-dark so the BUTTON theme is the dark one + + HTHEME hTheme = ::OpenThemeData(hRef, L"BUTTON"); + if (!hTheme) { + return false; + } + + il.DeleteImageList(); + if (!il.Create(size, size, ILC_COLOR32, 3, 0)) { + ::CloseThemeData(hTheme); + return false; + } + + ::BufferedPaintInit(); + + // State index 0 = no image, 1 = unchecked, 2 = checked (LVS_EX_CHECKBOXES layout). + const int states[3] = { 0, CBS_UNCHECKEDNORMAL, CBS_CHECKEDNORMAL }; + HDC hdcScreen = ::GetDC(nullptr); + HDC hdcMem = ::CreateCompatibleDC(hdcScreen); + bool ok = true; + + for (int i = 0; i < 3 && ok; ++i) { + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = size; + bmi.bmiHeader.biHeight = -size; // top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + void* pBits = nullptr; + HBITMAP hDib = ::CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0); + if (!hDib) { + ok = false; + break; + } + HBITMAP hOldBmp = (HBITMAP)::SelectObject(hdcMem, hDib); + + RECT rcFull = { 0, 0, size, size }; + BP_PAINTPARAMS pp = { sizeof(pp) }; + pp.dwFlags = BPPF_ERASE; + HDC hdcPaint = nullptr; + HPAINTBUFFER hbp = ::BeginBufferedPaint(hdcMem, &rcFull, BPBF_TOPDOWNDIB, &pp, &hdcPaint); + if (hbp) { + if (states[i] != 0) { // index 0 stays fully transparent (no checkbox) + SIZE gsz = { size, size }; + ::GetThemePartSize(hTheme, hdcPaint, BP_CHECKBOX, states[i], nullptr, TS_DRAW, &gsz); + RECT rg; + rg.left = (size - gsz.cx) / 2; if (rg.left < 0) { rg.left = 0; } + rg.top = (size - gsz.cy) / 2; if (rg.top < 0) { rg.top = 0; } + rg.right = rg.left + gsz.cx; + rg.bottom = rg.top + gsz.cy; + ::DrawThemeBackground(hTheme, hdcPaint, BP_CHECKBOX, states[i], &rg, nullptr); + } + ::EndBufferedPaint(hbp, TRUE); + } else { + ok = false; + } + + ::SelectObject(hdcMem, hOldBmp); + if (ok) { + il.Add(CBitmap::FromHandle(hDib), (CBitmap*)nullptr); + } + ::DeleteObject(hDib); + } + + ::DeleteDC(hdcMem); + ::ReleaseDC(nullptr, hdcScreen); + ::BufferedPaintUnInit(); + ::CloseThemeData(hTheme); + + if (ok && il.GetImageCount() == 3) { + return true; + } + il.DeleteImageList(); + return false; + } + + HBRUSH OnCtlColor(CDC* pDC, UINT nCtlColor) { + if (!IsActive() || !pDC) { + return nullptr; + } + EnsureBrushes(); + + switch (nCtlColor) { + case CTLCOLOR_EDIT: + case CTLCOLOR_LISTBOX: + pDC->SetTextColor(TextColor()); + pDC->SetBkColor(g_clrCtrl); + return g_hbrCtrl; + default: // CTLCOLOR_DLG, CTLCOLOR_STATIC, CTLCOLOR_BTN, CTLCOLOR_MSGBOX, CTLCOLOR_SCROLLBAR + pDC->SetTextColor(TextColor()); + pDC->SetBkColor(g_clrFace); + return g_hbrFace; + } + } + + bool TrackbarCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { + if (!IsActive() || !pNMHDR || !pResult) { + return false; + } + + wchar_t cls[64] = {}; + GetClassNameW(pNMHDR->hwndFrom, cls, _countof(cls)); + if (_wcsicmp(cls, L"msctls_trackbar32") != 0) { + return false; + } + + LPNMCUSTOMDRAW p = reinterpret_cast(pNMHDR); + switch (p->dwDrawStage) { + case CDDS_PREPAINT: { + // Fill the whole trackbar dark up front. Otherwise the background relies on + // WM_CTLCOLOR, which is not honoured on some repaints (e.g. after the + // "Reset" button on the subtitle Default Style page), leaving it white. + CDC* pDC = CDC::FromHandle(p->hdc); + pDC->FillSolidRect(&p->rc, FaceColor()); + *pResult = CDRF_NOTIFYITEMDRAW; + return true; + } + case CDDS_ITEMPREPAINT: + if (p->dwItemSpec == TBCD_CHANNEL) { + CDC* pDC = CDC::FromHandle(p->hdc); + CRect rc(p->rc); + pDC->FillSolidRect(rc, ThemeRGB(10, 14, 18)); // dark groove + pDC->Draw3dRect(rc, ThemeRGB(60, 65, 70), ThemeRGB(60, 65, 70)); // subtle border + *pResult = CDRF_SKIPDEFAULT; + } else { + *pResult = CDRF_DODEFAULT; // keep the default thumb and tick marks + } + return true; + } + + *pResult = CDRF_DODEFAULT; + return true; + } + + bool ButtonCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { + if (!IsActive() || !pNMHDR || !pResult) { + return false; + } + + HWND hCtrl = pNMHDR->hwndFrom; + wchar_t cls[64] = {}; + GetClassNameW(hCtrl, cls, _countof(cls)); + if (_wcsicmp(cls, L"Button") != 0) { + return false; + } + + const LONG style = GetWindowLongW(hCtrl, GWL_STYLE); + const LONG btype = style & BS_TYPEMASK; + const bool isCheck = (btype == BS_CHECKBOX || btype == BS_AUTOCHECKBOX || btype == BS_3STATE || btype == BS_AUTO3STATE); + const bool isRadio = (btype == BS_RADIOBUTTON || btype == BS_AUTORADIOBUTTON); + const bool isPush = (btype == BS_PUSHBUTTON || btype == BS_DEFPUSHBUTTON); + if (!isCheck && !isRadio && !isPush) { + return false; // group boxes and owner-draw color pickers are handled elsewhere + } + + LPNMCUSTOMDRAW p = reinterpret_cast(pNMHDR); + if (p->dwDrawStage != CDDS_PREPAINT) { + *pResult = CDRF_DODEFAULT; + return true; + } + + CDC* pDC = CDC::FromHandle(p->hdc); + CRect rc(p->rc); + pDC->FillSolidRect(rc, FaceColor()); + + const bool disabled = (p->uItemState & CDIS_DISABLED) != 0; + const bool pressed = (p->uItemState & CDIS_SELECTED) != 0; + const bool hot = (p->uItemState & CDIS_HOT) != 0; + + if (isPush) { + // Flat dark push button (face darkens when pressed, lightens on hover). + const bool focus = (p->uItemState & CDIS_FOCUS) != 0; + const COLORREF face = disabled ? ThemeRGB(30, 34, 38) + : pressed ? ThemeRGB(28, 33, 38) + : hot ? ThemeRGB(52, 59, 66) + : ThemeRGB(44, 50, 56); + pDC->FillSolidRect(rc, face); + pDC->Draw3dRect(rc, ThemeRGB(80, 86, 92), ThemeRGB(80, 86, 92)); + + CString btext; + const int blen = ::GetWindowTextLengthW(hCtrl); + if (blen > 0) { + ::GetWindowTextW(hCtrl, btext.GetBuffer(blen + 1), blen + 1); + btext.ReleaseBuffer(); + } + + HFONT hbf = reinterpret_cast(::SendMessageW(hCtrl, WM_GETFONT, 0, 0)); + CFont* pOldBf = hbf ? pDC->SelectObject(CFont::FromHandle(hbf)) : nullptr; + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(disabled ? ThemeRGB(110, 115, 120) : TextColor()); + + // Centre the caption vertically even when it wraps to two lines. + CRect rcCalc = rc; + pDC->DrawTextW(btext, rcCalc, DT_CENTER | DT_WORDBREAK | DT_CALCRECT); + CRect rcText = rc; + if (rcCalc.Height() < rc.Height()) { + rcText.top += (rc.Height() - rcCalc.Height()) / 2; + } + pDC->DrawTextW(btext, rcText, DT_CENTER | DT_WORDBREAK); + + if (pOldBf) { + pDC->SelectObject(pOldBf); + } + if (focus) { + CRect rf = rc; + rf.DeflateRect(3, 3); + pDC->DrawFocusRect(rf); + } + *pResult = CDRF_SKIPDEFAULT; + return true; + } + + const LRESULT check = ::SendMessageW(hCtrl, BM_GETCHECK, 0, 0); + + const int partId = isRadio ? BP_RADIOBUTTON : BP_CHECKBOX; + int stateBase; + if (isRadio) { + stateBase = (check == BST_CHECKED) ? RBS_CHECKEDNORMAL : RBS_UNCHECKEDNORMAL; + } else if (check == BST_INDETERMINATE) { + stateBase = CBS_MIXEDNORMAL; + } else if (check == BST_CHECKED) { + stateBase = CBS_CHECKEDNORMAL; + } else { + stateBase = CBS_UNCHECKEDNORMAL; + } + // each group is ordered Normal, Hot, Pressed, Disabled + const int stateId = stateBase + (disabled ? 3 : pressed ? 2 : hot ? 1 : 0); + + SIZE glyph = { 13, 13 }; + HTHEME hTheme = OpenThemeData(hCtrl, L"Button"); + if (hTheme) { + GetThemePartSize(hTheme, pDC->GetSafeHdc(), partId, stateId, nullptr, TS_DRAW, &glyph); + } + + CRect rcGlyph; + rcGlyph.left = rc.left; + rcGlyph.top = (style & BS_MULTILINE) ? rc.top + 1 : rc.top + (rc.Height() - glyph.cy) / 2; + rcGlyph.right = rcGlyph.left + glyph.cx; + rcGlyph.bottom = rcGlyph.top + glyph.cy; + + if (hTheme) { + DrawThemeBackground(hTheme, pDC->GetSafeHdc(), partId, stateId, &rcGlyph, nullptr); + CloseThemeData(hTheme); + } else { + UINT dfcState = (isRadio ? DFCS_BUTTONRADIO : DFCS_BUTTONCHECK) + | (check == BST_CHECKED ? DFCS_CHECKED : 0u) + | (disabled ? DFCS_INACTIVE : 0u); + pDC->DrawFrameControl(rcGlyph, DFC_BUTTON, dfcState); + } + + CString text; + const int len = ::GetWindowTextLengthW(hCtrl); + if (len > 0) { + ::GetWindowTextW(hCtrl, text.GetBuffer(len + 1), len + 1); + text.ReleaseBuffer(); + } + + CRect rcText = rc; + rcText.left = rcGlyph.right + 4; + + HFONT hFont = reinterpret_cast(::SendMessageW(hCtrl, WM_GETFONT, 0, 0)); + CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; + + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(disabled ? ThemeRGB(110, 115, 120) : TextColor()); + + UINT fmt = DT_LEFT; + if (style & BS_MULTILINE) { + fmt |= DT_WORDBREAK; + } else { + fmt |= DT_SINGLELINE | DT_VCENTER; + } + pDC->DrawTextW(text, rcText, fmt); + + if (pOldFont) { + pDC->SelectObject(pOldFont); + } + + *pResult = CDRF_SKIPDEFAULT; + return true; + } +} diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h new file mode 100644 index 0000000000..d56192d771 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -0,0 +1,110 @@ +/* + * (C) 2026 see Authors.txt + * + * This file is part of MPC-BE. + * + * MPC-BE 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. + * + * MPC-BE 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 + +// Helpers to apply a dark visual theme to standard Win32/MFC dialogs (the +// Options property sheet and its pages). Uses documented UXTHEME/DWM APIs plus +// the undocumented immersive dark-mode ordinals available on Windows 10 1809+. +// +// Every entry point is a no-op when the dark theme is disabled +// (AfxGetAppSettings().bUseDarkTheme) or unsupported by the OS, so the classic +// light appearance is preserved in those cases. + +namespace DarkTheme +{ + // True when the dark theme should be applied (bUseDarkTheme && OS supports it). + bool IsActive(); + + // Enables per-process immersive dark mode for common controls. Idempotent. + void AllowDarkModeForApp(); + + // Enables dark mode and a dark title bar for a top-level window. + void EnableForWindow(HWND hWnd); + + // Applies the matching dark visual style to every child control of hWndParent + // (edits/combos -> DarkMode_CFD, buttons/tree/list/updown -> DarkMode_Explorer). + void ApplyThemeToChildren(HWND hWndParent); + + // Our owner-drawn group boxes fill their whole rect with the dark face colour. When a group + // box is defined after the controls it frames (so it sits above them in the z-order), that + // fill paints over those controls and they vanish until individually invalidated (on hover, + // an enable toggle, or Apply). Push every group box to the bottom of the sibling z-order and + // give it WS_CLIPSIBLINGS so its fill is clipped out of the controls it contains. Call AFTER + // ApplyThemeToChildren (never during it) so the child enumeration isn't disturbed. + void FixGroupBoxes(HWND hWndParent); + + // Applies the same dark visual style to a single control. Use for controls created + // on the fly (e.g. the in-place combo/edit of a list control) that never pass + // through ApplyThemeToChildren. + void ApplyThemeToControl(HWND hCtrl); + + // Fully owner-draws a trackbar (dark background, dark groove, celeste thumb) via a + // subclass. Use for sliders whose NM_CUSTOMDRAW channel colour is not reliably applied + // (e.g. the subtitle Default Style alpha sliders after the Reset button). + void MakeTrackbarOwnerDrawn(HWND hTrackbar); + + // Replaces a control's native scrollbars with the flat MPC-HC-style ones (dark gutter, + // solid grey thumb, visible arrow buttons). Applied automatically to lists/trees/list- + // boxes/multiline-edits by ApplyThemeToChildren; call it directly for scrolling controls + // that do not pass through the Options theming (e.g. the playlist). + void ThemeScrollBars(HWND hCtrl); + + // Applies the dark theme to a whole auxiliary top-level dialog opened from the Options + // pages (dark title bar, dark background + control colours, themed child controls). + // Call once from the dialog's OnInitDialog. + void ThemeDialog(HWND hDlg); + + // Re-applies or removes the dark theme across a whole window tree in response to the + // "Use dark theme" toggle being changed at runtime (Interface page + Apply). Handles both + // directions — applying when now active, stripping every subclass/override when now + // inactive — and forces a full redraw. Pass the Options property sheet HWND. + void RefreshTheme(HWND hRoot); + + // Builds a dark themed checkbox STATE image list for a list-view using + // LVS_EX_CHECKBOXES (index 0 = none, 1 = unchecked, 2 = checked). Assigning this + // via LVSIL_STATE keeps the checkboxes dark even when the list is disabled (the + // native auto-created checkboxes render with an ugly light border when disabled). + // When bDisabled is true the greyed-out (disabled) checkbox glyph is used, to show + // an inactive list. Returns false when theming is unavailable (keep the default). + bool MakeCheckStateImageList(CImageList& il, int size, HWND hRef, bool bDisabled = false); + + // WM_CTLCOLOR* helper: sets dark text/background on pDC and returns a cached + // dark brush, or nullptr when the dark theme is inactive (use default handling). + HBRUSH OnCtlColor(CDC* pDC, UINT nCtlColor); + + // NM_CUSTOMDRAW helper for trackbars (sliders): paints the channel dark, which + // Windows' native dark mode does not do. Returns true (with *pResult set) when + // the notification came from a trackbar and was handled; false otherwise. + bool TrackbarCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); + + // NM_CUSTOMDRAW helper for checkboxes and radio buttons: draws the native theme + // glyph plus a light caption. Native dark mode gives checkboxes light text but + // leaves radio-button text black, so both are drawn here for consistency. + // Returns true (with *pResult set) only for check/radio buttons; false otherwise. + bool ButtonCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); + + // Theme palette (respects nThemeBrightness / nThemeRGB via ThemeRGB()). + COLORREF FaceColor(); // dialog / page / static background + COLORREF TextColor(); // text + COLORREF CtrlBackColor(); // sunken control interior (edit / listbox) + COLORREF CtrlBorderColor(); // shared 1px border for every control (edits, spins, color wells, group boxes, tabs) + COLORREF GridlineColor(); // list-view grid lines (darker than the control borders) +} diff --git a/src/apps/mplayerc/mpc-be.vcxproj b/src/apps/mplayerc/mpc-be.vcxproj index 12a73ad48c..54560677ef 100644 --- a/src/apps/mplayerc/mpc-be.vcxproj +++ b/src/apps/mplayerc/mpc-be.vcxproj @@ -98,6 +98,9 @@ + + + @@ -231,6 +234,9 @@ + + + diff --git a/src/apps/mplayerc/mpc-be.vcxproj.filters b/src/apps/mplayerc/mpc-be.vcxproj.filters index 78fab48ba8..62867e68d2 100644 --- a/src/apps/mplayerc/mpc-be.vcxproj.filters +++ b/src/apps/mplayerc/mpc-be.vcxproj.filters @@ -66,6 +66,15 @@ Controls + + Controls + + + Controls + + + Controls + Controls @@ -530,6 +539,15 @@ Controls + + Controls + + + Controls + + + Controls + Controls diff --git a/src/apps/mplayerc/mplayerc.rc b/src/apps/mplayerc/mplayerc.rc index 98b52b054e..e1dba7c076 100644 --- a/src/apps/mplayerc/mplayerc.rc +++ b/src/apps/mplayerc/mplayerc.rc @@ -9,7 +9,7 @@ // // Generated from the TEXTINCLUDE 2 resource. // -#include "afxres.h" +#include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS @@ -92,17 +92,17 @@ BEGIN COMBOBOX IDC_COMBO7,10,136,107,62,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_COMBO9,10,151,40,89,CBS_DROPDOWNLIST | CBS_SORT | WS_DISABLED | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_COMBO10,52,151,65,116,CBS_DROPDOWNLIST | CBS_SORT | WS_DISABLED | WS_VSCROLL | WS_TABSTOP - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,167,110,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,166,110,3 CONTROL "Record Audio",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,10,169,107,9 CONTROL "Preview",IDC_CHECK4,"Button",BS_AUTO3STATE | WS_TABSTOP,10,179,107,9 COMBOBOX IDC_COMBO8,10,189,107,50,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_COMBO12,10,204,40,50,CBS_DROPDOWNLIST | CBS_SORT | WS_DISABLED | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_COMBO11,52,204,65,50,CBS_DROPDOWNLIST | CBS_SORT | WS_DISABLED | WS_VSCROLL | WS_TABSTOP - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,219,110,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,218,110,3 LTEXT "V/A Buffers:",IDC_STATIC,10,223,45,8 EDITTEXT IDC_EDIT5,59,221,28,13,ES_CENTER | ES_AUTOHSCROLL | ES_NUMBER EDITTEXT IDC_EDIT6,89,221,28,13,ES_CENTER | ES_AUTOHSCROLL | ES_NUMBER - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,236,110,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,235,110,3 EDITTEXT IDC_EDIT4,10,239,91,13,ES_AUTOHSCROLL | ES_READONLY PUSHBUTTON "...",IDC_BUTTON3,103,239,14,13 CONTROL "Audio to wav",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,10,272,65,9 @@ -285,7 +285,7 @@ BEGIN CONTROL "Remember File position",IDC_FILE_POS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,129,72,158,9 CONTROL "Remember last Pan-n-Scan Zoom",IDC_CHECK11,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,129,84,156,9 CONTROL "Remember main playlist",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,129,96,156,9 - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,129,108,158,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,129,107,158,3 CONTROL "Display title for URL in recent files",IDC_CHECK4, "Button",BS_AUTOCHECKBOX | BS_TOP | BS_MULTILINE | WS_TABSTOP,129,112,156,17 GROUPBOX "Other",IDC_STATIC,124,135,167,55,WS_GROUP @@ -533,7 +533,7 @@ BEGIN EDITTEXT IDC_EDIT1,221,75,70,13,ES_RIGHT | ES_AUTOHSCROLL | WS_GROUP PUSHBUTTON "Up",IDC_BUTTON3,221,92,33,14 PUSHBUTTON "Down",IDC_BUTTON4,258,92,33,14 - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,5,137,283,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,5,136,283,3 CONTROL "",IDC_TREE2,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_SHOWSELALWAYS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,5,141,208,95 PUSHBUTTON "Add Media Type...",IDC_BUTTON5,221,141,70,14 PUSHBUTTON "Add Sub Type...",IDC_BUTTON6,221,157,70,14 @@ -547,7 +547,7 @@ FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN ICON "",IDC_DEFAULTICON,5,5,21,20,SS_REALSIZEIMAGE EDITTEXT IDC_EDIT1,32,13,186,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,30,214,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,29,214,3 LTEXT "Type:",IDC_STATIC,10,36,60,8 EDITTEXT IDC_EDIT4,72,36,151,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER LTEXT "Size:",IDC_STATIC,10,51,60,8 @@ -558,7 +558,7 @@ BEGIN EDITTEXT IDC_EDIT5,72,81,151,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER LTEXT "Created:",IDC_STATIC,10,96,60,8 EDITTEXT IDC_EDIT6,72,96,151,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,111,214,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,110,214,3 EDITTEXT IDC_EDIT7,10,115,213,77,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY | WS_VSCROLL | WS_HSCROLL END @@ -568,7 +568,7 @@ FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN ICON "",IDC_DEFAULTICON,5,5,21,20,SS_REALSIZEIMAGE EDITTEXT IDC_EDIT1,32,13,186,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,30,214,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,29,214,3 LTEXT "Clip:",IDC_STATIC,10,36,50,8 EDITTEXT IDC_EDIT4,62,36,162,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER LTEXT "Author:",IDC_STATIC,10,51,50,8 @@ -579,10 +579,10 @@ BEGIN EDITTEXT IDC_EDIT2,62,81,162,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER LTEXT "Rating:",IDC_STATIC,10,96,50,8 EDITTEXT IDC_EDIT5,62,96,162,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,111,214,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,110,214,3 LTEXT "Location:",IDC_STATIC,10,117,50,8 EDITTEXT IDC_EDIT6,62,117,162,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,132,214,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,131,214,3 LTEXT "Description:",IDC_STATIC,10,136,50,8 EDITTEXT IDC_EDIT7,62,136,162,56,ES_MULTILINE | ES_READONLY | WS_VSCROLL END @@ -638,7 +638,7 @@ BEGIN PUSHBUTTON "Up",IDC_BUTTON4,88,76,35,12 PUSHBUTTON "Down",IDC_BUTTON5,125,76,35,12 PUSHBUTTON "&Set",IDC_BUTTON1,165,76,35,12 - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,93,188,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,92,188,3 PUSHBUTTON "&Cancel",IDCANCEL,49,100,50,14 PUSHBUTTON "&Save",IDOK,105,100,50,14 LTEXT "Pos: 0.0 -> 1.0",IDC_STATIC,134,5,70,8 @@ -848,7 +848,7 @@ BEGIN GROUPBOX "Source Filters",IDC_STATIC,5,5,285,154 RTEXT "HTTP:",IDC_STATIC,10,18,50,8 RTEXT "UDP:",IDC_STATIC,10,33,50,8 - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,9,48,277,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,9,47,277,3 RTEXT "AVI:",IDC_STATIC,10,55,50,8 RTEXT "MKV:",IDC_STATIC,10,69,50,8 RTEXT "MPEG-TS:",IDC_STATIC,10,84,50,8 @@ -946,7 +946,7 @@ BEGIN EDITTEXT IDC_EDIT1,91,154,195,13,ES_AUTOHSCROLL LTEXT "MatriX (TorrServer) address:",IDC_STATIC,10,173,103,8 EDITTEXT IDC_EDIT2,10,185,276,13,ES_AUTOHSCROLL - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,5,205,283,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,5,204,283,3 LTEXT "User agent:",IDC_STATIC,10,214,77,8 EDITTEXT IDC_EDIT3,91,212,195,13,ES_AUTOHSCROLL PUSHBUTTON "Default",IDC_BUTTON3,224,246,66,14 @@ -977,13 +977,13 @@ BEGIN LTEXT "Image quality %:",IDC_STATIC,150,7,95,8 EDITTEXT IDC_EDIT4,250,5,30,13,ES_AUTOHSCROLL | ES_NUMBER CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,280,5,12,13 - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,5,22,284,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,5,21,284,3 CONTROL "Enable compression",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,26,138,9 CONTROL "Allow access from localhost only",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,38,285,9 CONTROL "Enable preview",IDC_CHECK6,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,50,138,9 CONTROL "Print debug information",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,61,285,9 LTEXT "Launch in web browser...",IDC_STATIC1,150,26,120,8 - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,5,73,284,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,5,72,284,3 CONTROL "Serve pages from:",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,77,285,9 EDITTEXT IDC_EDIT2,15,89,167,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BUTTON1,186,89,50,14 @@ -1010,7 +1010,7 @@ FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN ICON "",IDC_DEFAULTICON,5,5,20,20,SS_REALSIZEIMAGE EDITTEXT IDC_EDIT1,32,13,197,13,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,5,29,224,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,5,28,224,3 CONTROL "",IDC_LIST1,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,5,33,224,96 PUSHBUTTON "Save As...",IDC_BUTTON1,84,183,64,14 CONTROL "",IDC_STATIC1,"Static",SS_BITMAP | SS_CENTERIMAGE | WS_BORDER,5,131,76,66 @@ -1024,7 +1024,7 @@ BEGIN COMBOBOX IDC_COMBO2,5,4,32,120,CBS_DROPDOWNLIST | WS_TABSTOP COMBOBOX IDC_COMBO1,41,4,114,120,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP PUSHBUTTON "&Add",IDC_BUTTON2,162,3,50,14 - CONTROL "",IDC_STATIC1,"Static",SS_ETCHEDHORZ,4,21,208,1 + CONTROL "",IDC_STATIC1,"Static",SS_OWNERDRAW,4,20,208,3 CONTROL "Enable pre-resize pixel shaders",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,26,150,10 LISTBOX IDC_LIST1,5,39,150,65,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP CONTROL "Enable post-resize pixel shaders",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,109,150,10 @@ -1188,7 +1188,7 @@ BEGIN CONTROL "Present at nearest VSync",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,10,109,274,8 LTEXT "Frequency adjustment:",IDC_STATIC5,20,93,85,8 EDITTEXT IDC_CYCLEDELTA,109,91,40,13,ES_RIGHT | ES_AUTOHSCROLL,WS_EX_RIGHT - CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,10,123,278,1 + CONTROL "",IDC_STATIC,"Static",SS_OWNERDRAW,10,122,278,3 LTEXT "Target sync offset:",IDC_STATIC3,10,129,100,8 EDITTEXT IDC_TARGETSYNCOFFSET,114,127,40,13,ES_RIGHT | ES_AUTOHSCROLL,WS_EX_RIGHT LTEXT "ms",IDC_STATIC4,158,129,24,8 From 31da57cb3a104c4d932abde1befe7cf971c826d9 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sat, 4 Jul 2026 22:13:47 -0300 Subject: [PATCH 02/24] Dark Options: native scrollbars, fixed palette, dark filter-config dialogs - Scrollbars: drop the CoolSB custom scrollbar for the native dark one (SetWindowTheme "DarkMode_Explorer"). The custom bar fought the OS repaint and flickered/froze while dragging; the native bar is solid and consistent. - Palette is now fixed and independent of the R/G/B/Brightness sliders (those tint the player only), so the Options dialog never half-repaints or blackens its text. - Theme each page once on first activation instead of every activation, so switching pages is instant. - Owner-drawn push buttons: keep the Win11 rounding + icon, stay dark when enabled (Apply no longer flashes white), and honour BS_MULTILINE captions. - Dark-theme the internal/external filter configuration sheets (CComPropertySheet): frame, owner-drawn dark tab, page background/controls, and light radio captions. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/ComPropertySheet.cpp | 26 ++ src/apps/mplayerc/ComPropertySheet.h | 2 + src/apps/mplayerc/PPageBase.cpp | 15 +- src/apps/mplayerc/PPageInterface.cpp | 27 +- src/apps/mplayerc/controls/DarkTheme.cpp | 462 ++++++++++------------- src/apps/mplayerc/controls/DarkTheme.h | 9 +- 6 files changed, 243 insertions(+), 298 deletions(-) diff --git a/src/apps/mplayerc/ComPropertySheet.cpp b/src/apps/mplayerc/ComPropertySheet.cpp index cb07058ba8..be7c73be5d 100644 --- a/src/apps/mplayerc/ComPropertySheet.cpp +++ b/src/apps/mplayerc/ComPropertySheet.cpp @@ -22,6 +22,11 @@ #include "stdafx.h" #include "ComPropertySheet.h" #include "filters/filters/InternalPropertyPage.h" +#include "controls/DarkTheme.h" + +// windowsx.h defines a function-like SubclassWindow(hwnd, lpfn) macro that collides with +// CWnd::SubclassWindow(HWND); undef it so the MFC method call below parses correctly. +#undef SubclassWindow // CComPropertyPageSite @@ -228,6 +233,14 @@ void CComPropertySheet::OnActivated(CPropertyPage* pPage) MoveWindow(CRect(wr.TopLeft(), ws + diff)); + // Dark-theme this page: its host dialog (pChild) gets its own WM_CTLCOLOR*, so the sheet's + // subclass doesn't reach it — theme it directly (dark background/text + themed controls). + if (DarkTheme::IsActive()) { + if (CWnd* pChild = pPage->GetWindow(GW_CHILD)) { + DarkTheme::ThemeDialog(pChild->GetSafeHwnd()); + } + } + Invalidate(); } @@ -244,5 +257,18 @@ BOOL CComPropertySheet::OnInitDialog() CenterWindow(); } + // The stock tab control ignores SetWindowTheme and keeps a light body with a ~3px light frame; + // attach the owner-drawn dark tab so it matches (it falls back to native when the theme is off). + if (DarkTheme::IsActive()) { + if (CWnd* pTab = GetDlgItem(AFX_IDC_TAB_CONTROL)) { + m_dark_tab.SubclassWindow(pTab->GetSafeHwnd()); + } + } + + // Dark-theme the whole filter-config sheet (frame, tab, OK/Cancel/Apply) so it matches the + // Options dialog it was opened from. Each page's own background/controls are themed as the + // page is activated (OnActivated), since a property page dialog gets its WM_CTLCOLOR* itself. + DarkTheme::ThemeDialog(GetSafeHwnd()); + return bResult; } diff --git a/src/apps/mplayerc/ComPropertySheet.h b/src/apps/mplayerc/ComPropertySheet.h index 47a960a8b1..bd823354d7 100644 --- a/src/apps/mplayerc/ComPropertySheet.h +++ b/src/apps/mplayerc/ComPropertySheet.h @@ -22,6 +22,7 @@ #pragma once #include "ComPropertyPage.h" +#include "controls/DarkTabCtrl.h" interface IComPropertyPageDirty { @@ -38,6 +39,7 @@ class CComPropertySheet : public CPropertySheet, public IComPropertyPageDirty std::list> m_spp; std::list m_pages; CSize m_size; + CDarkTabCtrl m_dark_tab; // owner-drawn dark tab, attached in OnInitDialog when the dark theme is on public: CComPropertySheet(UINT nIDCaption, CWnd* pParentWnd = nullptr, UINT iSelectPage = 0); diff --git a/src/apps/mplayerc/PPageBase.cpp b/src/apps/mplayerc/PPageBase.cpp index bbfdaf2786..9e9a7e6743 100644 --- a/src/apps/mplayerc/PPageBase.cpp +++ b/src/apps/mplayerc/PPageBase.cpp @@ -93,12 +93,15 @@ BOOL CPPageBase::OnSetActive() BOOL bRet = __super::OnSetActive(); - // Re-apply the dark visual style on every activation. It is idempotent (controls - // already themed are skipped), and doing it each time avoids a race on the page - // shown first, whose controls may not have been ready the very first time. - DarkTheme::ApplyThemeToChildren(GetSafeHwnd()); - // Keep group boxes behind the controls they frame so their dark fill never paints over - // them (which made labels/combos vanish until invalidated, e.g. after Apply). + // Apply the dark visual style once, on first activation. Re-theming on every activation + // (SetWindowTheme per control + repaint) made switching pages sluggish; the controls stay + // themed for the page's lifetime, so once is enough. + if (!m_bDarkThemeApplied) { + DarkTheme::ApplyThemeToChildren(GetSafeHwnd()); + m_bDarkThemeApplied = true; + } + // Keep group boxes behind the controls they frame so their dark fill never paints over them. + // Cheap (no SetWindowTheme), runs every activation so it self-heals after a runtime toggle. DarkTheme::FixGroupBoxes(GetSafeHwnd()); return bRet; diff --git a/src/apps/mplayerc/PPageInterface.cpp b/src/apps/mplayerc/PPageInterface.cpp index 4c45cbbcba..2327ae46ab 100644 --- a/src/apps/mplayerc/PPageInterface.cpp +++ b/src/apps/mplayerc/PPageInterface.cpp @@ -175,21 +175,15 @@ BOOL CPPageInterface::OnApply() //s.bDarkMenuBlurBehind = !!m_chkDarkMenuBlurBehind.GetCheck(); s.bDarkTitle = !!m_chkDarkTitle.GetCheck(); - // If the dark-theme toggle (or the theme colours) changed while the Options dialog is - // still open, re-theme the whole property sheet so it doesn't end up a mix of light and - // dark controls. GA_ROOT gives the sheet's top-level window (the pages live under it). - { - const bool bDarkToggled = (!!s.bUseDarkTheme != !!bUseDarkTheme); - const bool bColorsChanged = s.nThemeBrightness != m_nThemeBrightness_Old - || s.nThemeRed != m_nThemeRed_Old - || s.nThemeGreen != m_nThemeGreen_Old - || s.nThemeBlue != m_nThemeBlue_Old; - if (bDarkToggled || (s.bUseDarkTheme && bColorsChanged)) { - TreePropSheet::CPropPageFrameDefault::s_bDarkMode = DarkTheme::IsActive(); - TreePropSheet::CPropPageFrameDefault::s_clrFace = DarkTheme::FaceColor(); - TreePropSheet::CPropPageFrameDefault::s_clrText = DarkTheme::TextColor(); - DarkTheme::RefreshTheme(::GetAncestor(GetSafeHwnd(), GA_ROOT)); - } + // If the dark-theme toggle changed while the Options dialog is still open, re-theme the + // whole property sheet so it doesn't end up a mix of light and dark controls. (The theme + // colours no longer matter here — the Options palette is fixed — so only the toggle does.) + // GA_ROOT gives the sheet's top-level window (the pages live under it). + if (!!s.bUseDarkTheme != !!bUseDarkTheme) { + TreePropSheet::CPropPageFrameDefault::s_bDarkMode = DarkTheme::IsActive(); + TreePropSheet::CPropPageFrameDefault::s_clrFace = DarkTheme::FaceColor(); + TreePropSheet::CPropPageFrameDefault::s_clrText = DarkTheme::TextColor(); + DarkTheme::RefreshTheme(::GetAncestor(GetSafeHwnd(), GA_ROOT)); } s.fUseWin7TaskBar = !!m_fUseWin7TaskBar; @@ -305,6 +299,9 @@ void CPPageInterface::OnThemeChange() pFrame->Invalidate(); pFrame->m_wndPlaylistBar.Invalidate(); + + // The Options dialog is deliberately not repainted here: its palette is fixed, so the + // R/G/B/Brightness sliders only affect the player, never the open Options sheet. } BEGIN_MESSAGE_MAP(CPPageInterface, CPPageBase) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 32b3cfebf3..8cd2736711 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -20,13 +20,12 @@ #include "stdafx.h" #include "DarkTheme.h" -#include "../MainFrm.h" // AfxGetAppSettings(), ThemeRGB() +#include "../MainFrm.h" // AfxGetAppSettings() #include "DSUtil/SysVersion.h" #include #include #include // BP_CHECKBOX / BP_RADIOBUTTON / CBS_* / RBS_* #include // SetWindowSubclass -#include // flat dark scrollbars (same as the playlist) #pragma comment(lib, "dwmapi.lib") #pragma comment(lib, "uxtheme.lib") @@ -34,6 +33,19 @@ namespace DarkTheme { namespace { + // Fixed dark palette for the Options dialog. The R/G/B/Brightness sliders on the + // Interface page tint the *player* only; the Options window stays a constant dark + // (tying it to the sliders caused half-repaints/flicker and drove text to black). + // These equal the ThemeRGB result at the shipped defaults (brightness 15, colour + // 255,255,255), so the shade matches the default look but never changes. + inline COLORREF DRGB(int r, int g, int b) { + auto f = [](int c) -> int { + int v = (15 + c) * 255 / 256; + return v < 0 ? 0 : (v > 255 ? 255 : v); + }; + return RGB(f(r), f(g), f(b)); + } + // ---- undocumented uxtheme.dll ordinals (Windows 10 1809+) ---- enum PreferredAppMode { APPMODE_DEFAULT, APPMODE_ALLOWDARK, APPMODE_FORCEDARK, APPMODE_FORCELIGHT, APPMODE_MAX }; @@ -95,8 +107,8 @@ namespace DarkTheme } void EnsureBrushes() { - const COLORREF clrFace = ThemeRGB(22, 27, 32); - const COLORREF clrCtrl = ThemeRGB(10, 14, 18); + const COLORREF clrFace = DRGB(22, 27, 32); + const COLORREF clrCtrl = DRGB(10, 14, 18); if (clrFace != g_clrFace || !g_hbrFace) { if (g_hbrFace) { ::DeleteObject(g_hbrFace); @@ -150,7 +162,7 @@ namespace DarkTheme CRect rcFrame = rc; rcFrame.top += textH / 2; - CBrush brFrame(ThemeRGB(70, 75, 80)); + CBrush brFrame(DRGB(70, 75, 80)); pDC->FrameRect(rcFrame, &brFrame); if (!text.IsEmpty()) { @@ -161,7 +173,7 @@ namespace DarkTheme const bool disabled = (::GetWindowLongW(hWnd, GWL_STYLE) & WS_DISABLED) != 0; pDC->SetBkMode(TRANSPARENT); - pDC->SetTextColor(disabled ? ThemeRGB(110, 115, 120) : TextColor()); + pDC->SetTextColor(disabled ? RGB(110, 115, 120) : TextColor()); CRect rcLabel(x, rc.top, rc.right - 4, rc.top + textH); pDC->DrawTextW(text, rcLabel, DT_LEFT | DT_SINGLELINE | DT_TOP); } @@ -180,6 +192,128 @@ namespace DarkTheme return DefSubclassProc(hWnd, msg, wParam, lParam); } + // Push buttons: Windows 11 draws them rounded, optionally with an icon, but the dark + // BUTTON visual style ("DarkMode_Explorer") is flat and drops the icon, while leaving + // them un-themed keeps them light. So we owner-draw them: a rounded dark face (with + // hover/pressed shades from the fixed dark palette), the shared border (an accent for + // the default button), the native icon + text, and a + // focus rectangle. The default window proc still runs all the click/keyboard logic; we + // only take over painting. dwRefData carries the hot (hover) state. + const UINT_PTR kButtonSubclassId = 9; + + LRESULT CALLBACK ButtonSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR dwHot) { + switch (msg) { + case WM_MOUSEMOVE: + if (!dwHot) { + TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hWnd, 0 }; + ::TrackMouseEvent(&tme); + SetWindowSubclass(hWnd, ButtonSubclassProc, kButtonSubclassId, 1); + ::InvalidateRect(hWnd, nullptr, FALSE); + } + break; + case WM_MOUSELEAVE: + SetWindowSubclass(hWnd, ButtonSubclassProc, kButtonSubclassId, 0); + ::InvalidateRect(hWnd, nullptr, FALSE); + break; + case WM_ENABLE: + // The property sheet enables/disables Apply as pages are modified. On that + // transition the default button proc repaints itself light, bypassing our + // owner-draw; swallow it and force our own dark repaint instead. + ::InvalidateRect(hWnd, nullptr, FALSE); + return 0; + case WM_ERASEBKGND: + return 1; + case WM_PAINT: { + PAINTSTRUCT ps; + HDC hdc = ::BeginPaint(hWnd, &ps); + CDC* pDC = CDC::FromHandle(hdc); + + CRect rc; + ::GetClientRect(hWnd, &rc); + + const LONG style = ::GetWindowLongW(hWnd, GWL_STYLE); + const LRESULT bst = ::SendMessageW(hWnd, BM_GETSTATE, 0, 0); + const bool disabled = (style & WS_DISABLED) != 0; + const bool pressed = (bst & BST_PUSHED) != 0; + const bool focused = (bst & BST_FOCUS) != 0; + const bool hot = dwHot != 0 && !disabled; + const bool isDef = (style & BS_TYPEMASK) == BS_DEFPUSHBUTTON; + + pDC->FillSolidRect(rc, FaceColor()); // dialog bg behind the rounded corners + + const COLORREF face = disabled ? DRGB(38, 43, 48) + : pressed ? DRGB(36, 41, 46) + : hot ? DRGB(62, 69, 76) + : DRGB(50, 56, 62); + const COLORREF border = disabled ? DRGB(60, 65, 70) + : isDef ? RGB(76, 194, 255) + : DRGB(84, 90, 96); + + CBrush brFace(face); + CPen penBd(PS_SOLID, 1, border); + HGDIOBJ ob = pDC->SelectObject(brFace); + HGDIOBJ op = pDC->SelectObject(penBd); + pDC->RoundRect(rc.left, rc.top, rc.right, rc.bottom, 8, 8); + pDC->SelectObject(ob); + pDC->SelectObject(op); + + CString text; + const int len = ::GetWindowTextLengthW(hWnd); + if (len > 0) { + ::GetWindowTextW(hWnd, text.GetBuffer(len + 1), len + 1); + text.ReleaseBuffer(); + } + HICON hIcon = reinterpret_cast(::SendMessageW(hWnd, BM_GETIMAGE, IMAGE_ICON, 0)); + + HFONT hFont = reinterpret_cast(::SendMessageW(hWnd, WM_GETFONT, 0, 0)); + CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(disabled ? RGB(120, 125, 130) : TextColor()); + + const int icon = hIcon ? 16 : 0; + if (!text.IsEmpty() && (style & BS_MULTILINE)) { + // Multi-line captions (e.g. the "AVI Splitter\nconfiguration" filter buttons) + // must wrap: a single-line draw collapses the line break and overflows the + // button. Word-wrap and centre the text block vertically. + CRect rt(rc.left + 4, rc.top, rc.right - 4, rc.bottom); + CRect calc = rt; + pDC->DrawTextW(text, calc, DT_CENTER | DT_WORDBREAK | DT_CALCRECT); + const int oy = (rc.Height() - calc.Height()) / 2; + rt.top = rc.top + (oy > 0 ? oy : 0); + pDC->DrawTextW(text, rt, DT_CENTER | DT_WORDBREAK); + } else { + const CSize ext = text.IsEmpty() ? CSize(0, 0) : pDC->GetTextExtent(text); + const int gap = (hIcon && !text.IsEmpty()) ? 4 : 0; + int x = rc.left + (rc.Width() - (icon + gap + ext.cx)) / 2; + const int cy = rc.top + rc.Height() / 2; + if (hIcon) { + ::DrawIconEx(hdc, x, cy - icon / 2, hIcon, icon, icon, 0, nullptr, DI_NORMAL); + x += icon + gap; + } + if (!text.IsEmpty()) { + CRect rt(x, rc.top, rc.right, rc.bottom); + pDC->DrawTextW(text, rt, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + } + } + if (pOldFont) { + pDC->SelectObject(pOldFont); + } + + if (focused && !disabled) { + CRect fr = rc; + fr.DeflateRect(3, 3); + pDC->DrawFocusRect(fr); + } + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, ButtonSubclassProc, kButtonSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + // The native LVS_EX_GRIDLINES are drawn with a light system colour that dark // mode does not change. We strip that style (see ThemeControl) and draw our own // grid, a shade darker than the control borders, over the default paint. @@ -271,8 +405,8 @@ namespace DarkTheme CDC* pDC = CDC::FromHandle(p->hdc); CRect rc(p->rc); pDC->FillSolidRect(rc, FaceColor()); - pDC->FillSolidRect(rc.right - 1, rc.top, 1, rc.Height(), ThemeRGB(70, 75, 80)); - pDC->FillSolidRect(rc.left, rc.bottom - 1, rc.Width(), 1, ThemeRGB(70, 75, 80)); + pDC->FillSolidRect(rc.right - 1, rc.top, 1, rc.Height(), DRGB(70, 75, 80)); + pDC->FillSolidRect(rc.left, rc.bottom - 1, rc.Width(), 1, DRGB(70, 75, 80)); wchar_t buf[256] = {}; HDITEMW hdi = {}; @@ -396,9 +530,9 @@ namespace DarkTheme CRect rc; ::GetClientRect(hWnd, &rc); - const COLORREF clrFace = ThemeRGB(38, 44, 50); - const COLORREF clrBorder = ThemeRGB(70, 75, 80); - const COLORREF clrArrow = ThemeRGB(170, 175, 180); + const COLORREF clrFace = DRGB(38, 44, 50); + const COLORREF clrBorder = DRGB(70, 75, 80); + const COLORREF clrArrow = DRGB(170, 175, 180); pDC->FillSolidRect(rc, clrFace); @@ -454,14 +588,14 @@ namespace DarkTheme RECT rcCh{}; ::SendMessageW(hWnd, TBM_GETCHANNELRECT, 0, reinterpret_cast(&rcCh)); CRect ch(rcCh); - pDC->FillSolidRect(ch, ThemeRGB(10, 14, 18)); // dark groove - pDC->Draw3dRect(ch, ThemeRGB(60, 65, 70), ThemeRGB(60, 65, 70)); // subtle border + pDC->FillSolidRect(ch, DRGB(10, 14, 18)); // dark groove + pDC->Draw3dRect(ch, DRGB(60, 65, 70), DRGB(60, 65, 70)); // subtle border RECT rcTh{}; ::SendMessageW(hWnd, TBM_GETTHUMBRECT, 0, reinterpret_cast(&rcTh)); CRect th(rcTh); const bool disabled = (::GetWindowLongW(hWnd, GWL_STYLE) & WS_DISABLED) != 0; - pDC->FillSolidRect(th, disabled ? ThemeRGB(90, 95, 100) : RGB(76, 194, 255)); // celeste thumb + pDC->FillSolidRect(th, disabled ? DRGB(90, 95, 100) : RGB(76, 194, 255)); // celeste thumb ::EndPaint(hWnd, &ps); return 0; @@ -473,224 +607,6 @@ namespace DarkTheme return DefSubclassProc(hWnd, msg, wParam, lParam); } - // CoolSB draws its thumb purely from its own stored scroll position (see CalcThumbSize) - // and never updates it on its own: during a drag it only posts WM_VSCROLL(SB_THUMBTRACK) - // and expects the owner to feed the new position back. Wheel/keyboard scrolling it does - // not see at all. These controls (list-view, tree-view, list-box) scroll themselves and - // keep their real scroll info, so we subclass them and, after every scroll, copy that - // real info into CoolSB — otherwise the flat thumb snaps back to the top while the - // content scrolls underneath it. - const UINT_PTR kCoolSBSyncId = 8; - - // True when the control actually has something to scroll in this direction. Mirrors - // CoolSB's own IsScrollInfoActive so our show/hide decision matches its thumb logic. - bool IsBarScrollable(HWND hCtrl, int bar) { - SCROLLINFO si = { sizeof(si), SIF_RANGE | SIF_PAGE }; - if (!::GetScrollInfo(hCtrl, bar, &si)) { - return false; - } - return !(si.nPage > (UINT)si.nMax || si.nMax <= si.nMin || si.nMax == 0); - } - - // Tree-views (e.g. the Options navigation tree) carry WS_HSCROLL and, once CoolSB's - // vertical bar narrows their client, report a horizontal range — producing a phantom - // bottom bar that was never there natively. They don't need horizontal scrolling here, - // so we never manage or show their horizontal bar. - // Decides whether a control should show a horizontal scrollbar. In the Options dialog the - // only control that legitimately needs one is a multi-column report list-view (the Keys - // list). Tree-views, list-boxes (the Internal/External Filters check lists) and single- - // column lists (Formats, Select Filter) fit their content to the width, so any horizontal - // bar there is the spurious one from the vertical-bar reservation — never show it. - bool NeedsHorzBar(HWND hCtrl) { - wchar_t cls[32] = {}; - GetClassNameW(hCtrl, cls, _countof(cls)); - - if (_wcsicmp(cls, L"SysListView32") != 0) { - return false; // tree-views, list-boxes, ... : single-column here - } - if ((GetWindowLongW(hCtrl, GWL_STYLE) & LVS_TYPEMASK) != LVS_REPORT) { - return IsBarScrollable(hCtrl, SB_HORZ); // list/icon modes scroll horizontally by nature - } - HWND hHeader = reinterpret_cast(::SendMessageW(hCtrl, LVM_GETHEADER, 0, 0)); - const int cols = hHeader ? static_cast(::SendMessageW(hHeader, HDM_GETITEMCOUNT, 0, 0)) : 0; - if (cols <= 1) { - return false; // a single column fits the width - } - // Multi-column: show it only if the columns really overflow, ignoring the ~vertical- - // bar-width that the vertical bar reservation alone introduces. - SCROLLINFO si = { sizeof(si), SIF_RANGE | SIF_PAGE }; - if (!::GetScrollInfo(hCtrl, SB_HORZ, &si)) { - return false; - } - if (si.nPage > (UINT)si.nMax || si.nMax <= si.nMin || si.nMax == 0) { - return false; // nothing to scroll horizontally - } - const int overflow = (si.nMax - si.nMin + 1) - static_cast(si.nPage); - return overflow > ::GetSystemMetrics(SM_CXVSCROLL) + 4; - } - - // CoolSB reserves and draws a bar for every direction whose WS_*SCROLL style was set - // when it was initialised, even with nothing to scroll. The native control hides an - // unused bar; mirror that so no empty gutter shows up. - void UpdateBarVisibility(HWND hCtrl) { - CoolSB_ShowScrollBar(hCtrl, SB_VERT, IsBarScrollable(hCtrl, SB_VERT)); - CoolSB_ShowScrollBar(hCtrl, SB_HORZ, NeedsHorzBar(hCtrl)); - } - - void SyncCoolSB(HWND hCtrl) { - // While CoolSB is actively dragging its thumb it already paints it following the - // mouse; only update the stored position (no redraw) then, to avoid fighting that - // paint. For wheel/keyboard scrolling we do need the redraw to move the thumb. - const BOOL redraw = !CoolSB_IsThumbTracking(hCtrl); - SCROLLINFO si = { sizeof(si), SIF_ALL }; - if (::GetScrollInfo(hCtrl, SB_VERT, &si)) { - CoolSB_SetScrollInfo(hCtrl, SB_VERT, &si, redraw); - } - // Only feed the horizontal bar when it's genuinely wanted; otherwise CoolSB's - // SetScrollInfo would auto-re-show a phantom/spurious bottom bar on every scroll. - if (NeedsHorzBar(hCtrl)) { - SCROLLINFO sih = { sizeof(sih), SIF_ALL }; - if (::GetScrollInfo(hCtrl, SB_HORZ, &sih)) { - CoolSB_SetScrollInfo(hCtrl, SB_HORZ, &sih, redraw); - } - } - } - - // Scrolls a CoolSB-managed control to an absolute position. CoolSB posts the target in - // the SB_THUMB* message, but common controls (tree-view, list-view) read the drag - // position from the scrollbar's nTrackPos — which cannot be set through the API — so a - // posted SB_THUMBTRACK moves nothing on them (only CPlayerListCtrl overrides OnVScroll - // to use the message value). Instead we step the control with line scrolls, which every - // control honours (relative, no position needed), reading the real position back until - // it reaches the target. Intermediate steps are not painted (all inside one message), - // so there is no visible stepping. - void ScrollToPos(HWND hCtrl, UINT bar, int target) { - wchar_t cls[32] = {}; - GetClassNameW(hCtrl, cls, _countof(cls)); - - // List-views can scroll to an absolute position in a single shot (LVM_SCROLL takes a - // pixel delta), so avoid the per-line loop which freezes on large lists (e.g. Keys). - // Mirrors CPlayerListCtrl::OnVScroll: pixel delta = span * (target - pos) / page. - if (_wcsicmp(cls, L"SysListView32") == 0) { - SCROLLINFO si = { sizeof(si), SIF_RANGE | SIF_PAGE | SIF_POS }; - if (!::GetScrollInfo(hCtrl, bar, &si)) { - return; - } - RECT rc; ::GetClientRect(hCtrl, &rc); - const int span = (bar == SB_VERT) ? rc.bottom : rc.right; - const int denom = (si.nPage != 0) ? (int)si.nPage : (si.nMax + 1); - if (denom <= 0) { - return; - } - const int delta = (int)((LONGLONG)span * (target - si.nPos) / denom); - if (delta != 0) { - if (bar == SB_VERT) ::SendMessageW(hCtrl, LVM_SCROLL, 0, delta); - else ::SendMessageW(hCtrl, LVM_SCROLL, delta, 0); - } - return; - } - - // Generic controls (tree-view, list-box): step there with line scrolls, which every - // control honours. These have few scroll units, so the loop stays cheap. - const UINT msg = (bar == SB_VERT) ? WM_VSCROLL : WM_HSCROLL; - SCROLLINFO si = { sizeof(si), SIF_POS }; - if (!::GetScrollInfo(hCtrl, bar, &si)) { - return; - } - int cur = si.nPos; - for (int guard = 0; cur != target && guard < 20000; ++guard) { - const bool down = target > cur; // SB_LINEDOWN == SB_LINERIGHT, SB_LINEUP == SB_LINELEFT - DefSubclassProc(hCtrl, msg, MAKEWPARAM(down ? SB_LINEDOWN : SB_LINEUP, 0), 0); - SCROLLINFO now = { sizeof(now), SIF_POS }; - ::GetScrollInfo(hCtrl, bar, &now); - if (now.nPos == cur) { - break; // clamped at an end, cannot move further - } - const bool crossed = (now.nPos < target) != (cur < target); - cur = now.nPos; - if (crossed) { - break; // one line stepped past the target (control's line > 1 unit); close enough - } - } - } - - LRESULT CALLBACK CoolSBSyncProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { - switch (msg) { - case WM_VSCROLL: - case WM_HSCROLL: { - const UINT bar = (msg == WM_VSCROLL) ? SB_VERT : SB_HORZ; - const int code = LOWORD(wParam); - LRESULT r = 0; - if (code == SB_THUMBTRACK || code == SB_THUMBPOSITION) { - ScrollToPos(hWnd, bar, HIWORD(wParam)); // drive the scroll ourselves - } else { - r = DefSubclassProc(hWnd, msg, wParam, lParam); - } - // A horizontal scroll bitblts the content and leaves our hand-drawn vertical - // column grid lines behind as ghosts (only the newly exposed strip repaints). - // Force a full repaint so they are redrawn cleanly. - if (msg == WM_HSCROLL) { - ::InvalidateRect(hWnd, nullptr, TRUE); - } - SyncCoolSB(hWnd); - return r; - } - case WM_MOUSEWHEEL: - case WM_MOUSEHWHEEL: - case WM_KEYDOWN: - case WM_KEYUP: { - const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); - SyncCoolSB(hWnd); - return r; - } - case WM_PAINT: { - const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); - // When the control's scroll range changes (e.g. the Keys list is filtered as - // the user types) CoolSB doesn't hear about it. Detect the mismatch cheaply - // and recompute the flat scrollbar; only acts when the range actually changed. - SCROLLINFO real = { sizeof(real), SIF_RANGE | SIF_PAGE }; - SCROLLINFO cool = { sizeof(cool), SIF_RANGE | SIF_PAGE }; - if (::GetScrollInfo(hWnd, SB_VERT, &real) && CoolSB_GetScrollInfo(hWnd, SB_VERT, &cool) - && (real.nMin != cool.nMin || real.nMax != cool.nMax || real.nPage != cool.nPage)) { - SyncCoolSB(hWnd); - UpdateBarVisibility(hWnd); - } - return r; - } - case WM_NCDESTROY: - RemoveWindowSubclass(hWnd, CoolSBSyncProc, kCoolSBSyncId); - break; - } - return DefSubclassProc(hWnd, msg, wParam, lParam); - } - - // Scrollbars are replaced with CoolSB -- the same flat dark scrollbar the playlist uses - // (InitializeCoolSB is handed our ThemeRGB palette). CoolSB actually replaces the native - // scrollbar (it removes the WS_*SCROLL styles and owns the non-client area), so unlike - // painting over the native bar nothing of the original ever shows through while scrolling. - void ApplyScrollBars(HWND hCtrl) { - if (CoolSB_IsCoolScrollEnabled(hCtrl)) { - return; // already initialised (ApplyThemeToChildren may run more than once) - } - // Install the sync subclass first so it sits behind CoolSB's window proc and sees the - // scroll messages after the control has processed (and scrolled) them. - SetWindowSubclass(hCtrl, CoolSBSyncProc, kCoolSBSyncId, 0); - if (!InitializeCoolSB(hCtrl, ThemeRGB)) { - RemoveWindowSubclass(hCtrl, CoolSBSyncProc, kCoolSBSyncId); - return; - } - CoolSB_SetStyle(hCtrl, SB_VERT, CSBS_HOTTRACKED); - CoolSB_SetStyle(hCtrl, SB_HORZ, CSBS_HOTTRACKED); - if (SysVersion::IsWin8orLater()) { - CoolSB_SetSize(hCtrl, SB_VERT, ::GetSystemMetrics(SM_CYVSCROLL), ::GetSystemMetrics(SM_CXVSCROLL)); - CoolSB_SetSize(hCtrl, SB_HORZ, ::GetSystemMetrics(SM_CXHSCROLL), ::GetSystemMetrics(SM_CYHSCROLL)); - } - // Seed CoolSB with the control's current scroll range/position. - SyncCoolSB(hCtrl); - // Hide the bars that have nothing to scroll (e.g. the tree's horizontal bar). - UpdateBarVisibility(hCtrl); - } - // Subclass for auxiliary top-level dialogs (opened from the Options pages, e.g. the // "Add filter" choosers): paints the dialog background and control colours dark via // WM_CTLCOLOR* / WM_ERASEBKGND, mirroring what CPPageBase does for the pages. @@ -713,6 +629,19 @@ namespace DarkTheme } break; } + case WM_NOTIFY: { + // Radio buttons keep black text under native dark mode (checkboxes go light); + // draw radios/checkboxes (and any sliders) ourselves via NM_CUSTOMDRAW, like the + // Options pages do, so their captions match. + NMHDR* pNMHDR = reinterpret_cast(lParam); + if (pNMHDR && pNMHDR->code == NM_CUSTOMDRAW) { + LRESULT result = 0; + if (TrackbarCustomDraw(pNMHDR, &result) || ButtonCustomDraw(pNMHDR, &result)) { + return result; + } + } + break; + } case WM_ERASEBKGND: { CDC* pDC = CDC::FromHandle(reinterpret_cast(wParam)); CRect rc; @@ -780,11 +709,16 @@ namespace DarkTheme GetClassNameW(hCtrl, cls, _countof(cls)); if (_wcsicmp(cls, L"Button") == 0) { - if ((GetWindowLongW(hCtrl, GWL_STYLE) & BS_TYPEMASK) == BS_GROUPBOX) { + const LONG bt = GetWindowLongW(hCtrl, GWL_STYLE) & BS_TYPEMASK; + if (bt == BS_GROUPBOX) { SetWindowSubclass(hCtrl, GroupBoxSubclassProc, kGroupBoxSubclassId, 0); InvalidateRect(hCtrl, nullptr, TRUE); + } else if (bt == BS_PUSHBUTTON || bt == BS_DEFPUSHBUTTON) { + // Owner-draw so they stay dark AND keep the Win11 rounding + icon. + SetWindowSubclass(hCtrl, ButtonSubclassProc, kButtonSubclassId, 0); + InvalidateRect(hCtrl, nullptr, TRUE); } else { - // checkboxes, radios, push buttons + // checkboxes, radios SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); } } else if (_wcsicmp(cls, L"ComboBox") == 0) { @@ -792,9 +726,6 @@ namespace DarkTheme } else if (_wcsicmp(cls, L"Edit") == 0) { SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); ApplyDarkBorder(hCtrl); - if (GetWindowLongW(hCtrl, GWL_STYLE) & ES_MULTILINE) { - ApplyScrollBars(hCtrl); // multiline edits can show scrollbars - } } else if (_wcsicmp(cls, L"SysListView32") == 0) { // List-view controls ignore WM_CTLCOLOR: their background (the area // not covered by columns/rows) must be set explicitly, otherwise it @@ -824,7 +755,6 @@ namespace DarkTheme InvalidateRect(hHeader, nullptr, TRUE); } ApplyDarkBorder(hCtrl); // dark outer border to match everything else - ApplyScrollBars(hCtrl); // flat MPC-HC-style scrollbars } else if (_wcsicmp(cls, L"SysTreeView32") == 0) { // Tree-views, like list-views, need their background/text colors set // explicitly (SetWindowTheme only handles the glyphs and scrollbar). @@ -832,7 +762,6 @@ namespace DarkTheme ::SendMessageW(hCtrl, TVM_SETBKCOLOR, 0, static_cast(FaceColor())); ::SendMessageW(hCtrl, TVM_SETTEXTCOLOR, 0, static_cast(TextColor())); ApplyDarkBorder(hCtrl); // dark outer border to match everything else - ApplyScrollBars(hCtrl); // flat MPC-HC-style scrollbars } else if (_wcsicmp(cls, UPDOWN_CLASSW) == 0) { // Spin buttons: fully owner-drawn (native dark mode leaves them light). SetWindowSubclass(hCtrl, SpinSubclassProc, kSpinSubclassId, 0); @@ -843,7 +772,6 @@ namespace DarkTheme // light — repaint it with the shared dark border like edits/lists/trees. SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); ApplyDarkBorder(hCtrl); - ApplyScrollBars(hCtrl); // flat MPC-HC-style scrollbars } else if (_wcsicmp(cls, L"Static") == 0) { // Sunken value boxes (e.g. Brightness/Contrast/Hue/Saturation on the // Color correction page are SS_SUNKEN RTEXT statics) keep a light 3D edge @@ -873,19 +801,15 @@ namespace DarkTheme // open. Removing a subclass that isn't present is a safe no-op. BOOL CALLBACK StripThemeChildProc(HWND hChild, LPARAM) { RemoveWindowSubclass(hChild, GroupBoxSubclassProc, kGroupBoxSubclassId); + RemoveWindowSubclass(hChild, ButtonSubclassProc, kButtonSubclassId); RemoveWindowSubclass(hChild, SpinSubclassProc, kSpinSubclassId); RemoveWindowSubclass(hChild, BorderSubclassProc, kBorderSubclassId); RemoveWindowSubclass(hChild, TrackbarSubclassProc, kTrackbarSubclassId); - RemoveWindowSubclass(hChild, CoolSBSyncProc, kCoolSBSyncId); DWORD_PTR gridFlag = 0; const bool hadGrid = GetWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId, &gridFlag) && gridFlag; RemoveWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId); - if (CoolSB_IsCoolScrollEnabled(hChild)) { - UninitializeCoolSB(hChild); // restores the native scrollbars + window proc - } - wchar_t cls[64] = {}; GetClassNameW(hChild, cls, _countof(cls)); if (_wcsicmp(cls, L"SysListView32") == 0) { @@ -915,11 +839,14 @@ namespace DarkTheme return AfxGetAppSettings().bUseDarkTheme && SysVersion::IsWin10v1809orLater(); } - COLORREF FaceColor() { return ThemeRGB(22, 27, 32); } - COLORREF TextColor() { return ThemeRGB(165, 170, 175); } - COLORREF CtrlBackColor() { return ThemeRGB(10, 14, 18); } - COLORREF CtrlBorderColor() { return ThemeRGB(70, 75, 80); } - COLORREF GridlineColor() { return ThemeRGB(40, 45, 50); } + // The whole Options palette is FIXED (DRGB = the ThemeRGB result at the shipped defaults): + // the R/G/B/Brightness sliders tint the player interface only, never the Options dialog, so + // it never half-repaints or drives its text to black when the sliders move. + COLORREF FaceColor() { return DRGB(22, 27, 32); } + COLORREF TextColor() { return RGB(165, 170, 175); } + COLORREF CtrlBackColor() { return DRGB(10, 14, 18); } + COLORREF CtrlBorderColor() { return DRGB(70, 75, 80); } + COLORREF GridlineColor() { return DRGB(40, 45, 50); } void AllowDarkModeForApp() { if (!IsActive()) { @@ -1014,13 +941,6 @@ namespace DarkTheme ::InvalidateRect(hTrackbar, nullptr, TRUE); } - void ThemeScrollBars(HWND hCtrl) { - if (!IsActive() || !hCtrl) { - return; - } - ApplyScrollBars(hCtrl); - } - void ThemeDialog(HWND hDlg) { if (!IsActive() || !hDlg) { return; @@ -1029,7 +949,9 @@ namespace DarkTheme EnableForWindow(hDlg); // dark title bar + allow dark mode SetWindowSubclass(hDlg, DialogSubclassProc, kDialogSubclassId, 0); // dark bg / ctl colours ApplyThemeToChildren(hDlg); // theme the child controls - ::InvalidateRect(hDlg, nullptr, TRUE); + // Repaint the dialog AND its child controls: InvalidateRect alone doesn't reach the + // child windows, so freshly-themed checkboxes/statics would keep their stale light paint. + ::RedrawWindow(hDlg, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN); } void RefreshTheme(HWND hRoot) { @@ -1067,9 +989,9 @@ namespace DarkTheme // which made the checkboxes vanish; a solid grey box + grey tick reads clearly // as an inactive checkbox and never disappears. const COLORREF mask = RGB(255, 0, 255); - const COLORREF fill = ThemeRGB(34, 39, 44); - const COLORREF border = ThemeRGB(90, 95, 100); - const COLORREF mark = ThemeRGB(120, 125, 130); + const COLORREF fill = DRGB(34, 39, 44); + const COLORREF border = DRGB(90, 95, 100); + const COLORREF mark = DRGB(120, 125, 130); CClientDC screen(nullptr); CDC dc; @@ -1225,8 +1147,8 @@ namespace DarkTheme if (p->dwItemSpec == TBCD_CHANNEL) { CDC* pDC = CDC::FromHandle(p->hdc); CRect rc(p->rc); - pDC->FillSolidRect(rc, ThemeRGB(10, 14, 18)); // dark groove - pDC->Draw3dRect(rc, ThemeRGB(60, 65, 70), ThemeRGB(60, 65, 70)); // subtle border + pDC->FillSolidRect(rc, DRGB(10, 14, 18)); // dark groove + pDC->Draw3dRect(rc, DRGB(60, 65, 70), DRGB(60, 65, 70)); // subtle border *pResult = CDRF_SKIPDEFAULT; } else { *pResult = CDRF_DODEFAULT; // keep the default thumb and tick marks @@ -1276,12 +1198,12 @@ namespace DarkTheme if (isPush) { // Flat dark push button (face darkens when pressed, lightens on hover). const bool focus = (p->uItemState & CDIS_FOCUS) != 0; - const COLORREF face = disabled ? ThemeRGB(30, 34, 38) - : pressed ? ThemeRGB(28, 33, 38) - : hot ? ThemeRGB(52, 59, 66) - : ThemeRGB(44, 50, 56); + const COLORREF face = disabled ? DRGB(30, 34, 38) + : pressed ? DRGB(28, 33, 38) + : hot ? DRGB(52, 59, 66) + : DRGB(44, 50, 56); pDC->FillSolidRect(rc, face); - pDC->Draw3dRect(rc, ThemeRGB(80, 86, 92), ThemeRGB(80, 86, 92)); + pDC->Draw3dRect(rc, DRGB(80, 86, 92), DRGB(80, 86, 92)); CString btext; const int blen = ::GetWindowTextLengthW(hCtrl); @@ -1293,7 +1215,7 @@ namespace DarkTheme HFONT hbf = reinterpret_cast(::SendMessageW(hCtrl, WM_GETFONT, 0, 0)); CFont* pOldBf = hbf ? pDC->SelectObject(CFont::FromHandle(hbf)) : nullptr; pDC->SetBkMode(TRANSPARENT); - pDC->SetTextColor(disabled ? ThemeRGB(110, 115, 120) : TextColor()); + pDC->SetTextColor(disabled ? RGB(110, 115, 120) : TextColor()); // Centre the caption vertically even when it wraps to two lines. CRect rcCalc = rc; @@ -1368,7 +1290,7 @@ namespace DarkTheme CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; pDC->SetBkMode(TRANSPARENT); - pDC->SetTextColor(disabled ? ThemeRGB(110, 115, 120) : TextColor()); + pDC->SetTextColor(disabled ? RGB(110, 115, 120) : TextColor()); UINT fmt = DT_LEFT; if (style & BS_MULTILINE) { diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h index d56192d771..49d56bb5ad 100644 --- a/src/apps/mplayerc/controls/DarkTheme.h +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -61,12 +61,6 @@ namespace DarkTheme // (e.g. the subtitle Default Style alpha sliders after the Reset button). void MakeTrackbarOwnerDrawn(HWND hTrackbar); - // Replaces a control's native scrollbars with the flat MPC-HC-style ones (dark gutter, - // solid grey thumb, visible arrow buttons). Applied automatically to lists/trees/list- - // boxes/multiline-edits by ApplyThemeToChildren; call it directly for scrolling controls - // that do not pass through the Options theming (e.g. the playlist). - void ThemeScrollBars(HWND hCtrl); - // Applies the dark theme to a whole auxiliary top-level dialog opened from the Options // pages (dark title bar, dark background + control colours, themed child controls). // Call once from the dialog's OnInitDialog. @@ -101,7 +95,8 @@ namespace DarkTheme // Returns true (with *pResult set) only for check/radio buttons; false otherwise. bool ButtonCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); - // Theme palette (respects nThemeBrightness / nThemeRGB via ThemeRGB()). + // Fixed dark palette for the Options dialog: independent of the R/G/B/Brightness sliders + // (those tint the player interface only), so the Options window never changes shade. COLORREF FaceColor(); // dialog / page / static background COLORREF TextColor(); // text COLORREF CtrlBackColor(); // sunken control interior (edit / listbox) From e3abafe549b65ea978aab0be3a9d64ba84599a32 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 01:45:28 -0300 Subject: [PATCH 03/24] Dark Options: fix colour-well buttons and Formats checkboxes - Colour-well buttons (Interface / OSD / Subtitle Default Style) are push buttons that their page fills with the selected colour via NM_CUSTOMDRAW; skip owner-drawing them so they show the colour swatch again instead of their "B"/"O" caption. - Formats checkboxes: render them with the native visual style (unchecked / checked / mixed for the partial state) in both light and dark, and move them from the item icon (LVSIL_SMALL) to the state image (LVSIL_STATE) so the row-selection highlight no longer paints the checkbox. Rebuilt when the dark theme is toggled at runtime. This also changes the light-mode Formats checkbox from the custom SVG glyphs to the native ones, to match the other checklists. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/PPageFormats.cpp | 94 +++++++++++++++--------- src/apps/mplayerc/PPageFormats.h | 4 + src/apps/mplayerc/controls/DarkTheme.cpp | 32 +++++++- 3 files changed, 91 insertions(+), 39 deletions(-) diff --git a/src/apps/mplayerc/PPageFormats.cpp b/src/apps/mplayerc/PPageFormats.cpp index 89a30dff84..244d2a4946 100644 --- a/src/apps/mplayerc/PPageFormats.cpp +++ b/src/apps/mplayerc/PPageFormats.cpp @@ -44,14 +44,17 @@ static constexpr auto registeredKey = L"Software\\Clients\\Media\\MPC-BE\ // premultiplied alpha instead darkened the celeste and left the unchecked box white. // Because the app runs in force-dark mode, OpenThemeData(..., "BUTTON") resolves to the // dark (celeste) checkbox. -static bool MakeThemedCheckImageList(CImageList& il, int h, HWND hRef) +static bool MakeThemedCheckImageList(CImageList& il, int h, HWND hRef, bool bDark) { - DarkTheme::AllowDarkModeForApp(); // force-dark app mode - DarkTheme::ApplyThemeToControl(hRef); // allow dark mode on the list *now* — this runs at - // OnInitDialog time, before the page's OnSetActive - // themes it, so without this OpenThemeData(hRef, - // "BUTTON") would resolve to the light checkbox - // (a darker blue with a white unchecked box). + if (bDark) { + DarkTheme::AllowDarkModeForApp(); // force-dark app mode + DarkTheme::ApplyThemeToControl(hRef); // allow dark mode on the list *now* — this runs at + // OnInitDialog time, before the page's OnSetActive + // themes it, so OpenThemeData(hRef, "BUTTON") resolves + // to the dark checkbox (celeste check), not the light one. + } + // In light mode the control keeps its default theme, so OpenThemeData resolves to the native + // light checkbox glyph — the same one the other checklists show when the dark theme is off. HTHEME hTheme = ::OpenThemeData(hRef, L"BUTTON"); if (!hTheme) { @@ -136,7 +139,7 @@ static bool MakeThemedCheckImageList(CImageList& il, int h, HWND hRef) // with a celeste checkmark, matching the palette of the rest of the dark dialog. static void MakeDarkCheckImageList(CImageList& il, int h, HWND hRef) { - if (MakeThemedCheckImageList(il, h, hRef)) { + if (MakeThemedCheckImageList(il, h, hRef, true)) { return; } @@ -229,22 +232,16 @@ void CPPageFormats::DoDataExchange(CDataExchange* pDX) int CPPageFormats::GetChecked(int iItem) { - LVITEM lvi; - lvi.iItem = iItem; - lvi.iSubItem = 0; - lvi.mask = LVIF_IMAGE; - m_list.GetItem(&lvi); - return(lvi.iImage); + // The check state lives in the item's state-image index (1-based). Using the state image + // instead of the small icon keeps the checkbox out of the row's selection highlight, like a + // native list-view checkbox (the small icon shares the item cell and gets the highlight). + const UINT st = m_list.GetItemState(iItem, LVIS_STATEIMAGEMASK); + return (int)(st >> 12) - 1; // 0 = none, 1 = all, 2 = partial } void CPPageFormats::SetChecked(int iItem, int iChecked) { - LVITEM lvi; - lvi.iItem = iItem; - lvi.iSubItem = 0; - lvi.mask = LVIF_IMAGE; - lvi.iImage = iChecked; - m_list.SetItem(&lvi); + m_list.SetItemState(iItem, INDEXTOSTATEIMAGEMASK(iChecked + 1), LVIS_STATEIMAGEMASK); } CString CPPageFormats::GetEnqueueCommand() @@ -816,18 +813,8 @@ END_MESSAGE_MAP() // CPPageFormats message handlers -BOOL CPPageFormats::OnInitDialog() +void CPPageFormats::BuildCheckImageList() { - __super::OnInitDialog(); - - SetCursor(m_hWnd, IDC_BUTTON1, IDC_HAND); - - m_bFileExtChanged = false; - - m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_FULLROWSELECT); - - m_list.InsertColumn(COL_CATEGORY, L"Category", LVCFMT_LEFT); - int chkH = 0; if (CDPI* pDpi = dynamic_cast(AfxGetMainWnd())) { chkH = pDpi->ScaleY(12); @@ -837,9 +824,13 @@ BOOL CPPageFormats::OnInitDialog() chkH = dpi.ScaleY(12); } + m_onoff.DeleteImageList(); // rebuildable: re-run when the dark theme is toggled at runtime + if (DarkTheme::IsActive()) { MakeDarkCheckImageList(m_onoff, chkH, m_list.GetSafeHwnd()); - } else { + } else if (!MakeThemedCheckImageList(m_onoff, chkH, m_list.GetSafeHwnd(), false)) { + // Native (themed) light checkboxes matching the other checklists; fall back to upstream's + // SVG glyphs only if the visual style is unavailable. CSvgImage svgImage; if (svgImage.Load(IDF_SVG_ONOFF)) { int w = 0; @@ -854,7 +845,37 @@ BOOL CPPageFormats::OnInitDialog() } } - m_list.SetImageList(&m_onoff, LVSIL_SMALL); + m_list.SetImageList(&m_onoff, LVSIL_STATE); + m_onoffDark = DarkTheme::IsActive(); +} + +BOOL CPPageFormats::OnSetActive() +{ + const BOOL bRet = __super::OnSetActive(); + + // The checkbox glyphs are baked into an image list at init time; if the dark theme was + // toggled since then, rebuild them so unchecked boxes don't stay dark on a light dialog. + if (m_onoffDark != DarkTheme::IsActive()) { + BuildCheckImageList(); + m_list.Invalidate(); + } + + return bRet; +} + +BOOL CPPageFormats::OnInitDialog() +{ + __super::OnInitDialog(); + + SetCursor(m_hWnd, IDC_BUTTON1, IDC_HAND); + + m_bFileExtChanged = false; + + m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_FULLROWSELECT); + + m_list.InsertColumn(COL_CATEGORY, L"Category", LVCFMT_LEFT); + + BuildCheckImageList(); CMediaFormats& mf = AfxGetAppSettings().m_Formats; mf.UpdateData(false); @@ -1233,9 +1254,10 @@ void CPPageFormats::OnNMClickList1(NMHDR* pNMHDR, LRESULT* pResult) LPNMLISTVIEW lpnmlv = (LPNMLISTVIEW)pNMHDR; if (lpnmlv->iItem >= 0 && lpnmlv->iSubItem == COL_CATEGORY) { - CRect r; - m_list.GetItemRect(lpnmlv->iItem, r, LVIR_ICON); - if (r.PtInRect(lpnmlv->ptAction)) { + LVHITTESTINFO hti = {}; + hti.pt = lpnmlv->ptAction; + m_list.HitTest(&hti); + if (hti.flags & LVHT_ONITEMSTATEICON) { if (m_bInsufficientPrivileges) { MessageBoxW(ResStr (IDS_CANNOT_CHANGE_FORMAT)); } else { diff --git a/src/apps/mplayerc/PPageFormats.h b/src/apps/mplayerc/PPageFormats.h index 90a908db43..8050261f63 100644 --- a/src/apps/mplayerc/PPageFormats.h +++ b/src/apps/mplayerc/PPageFormats.h @@ -38,9 +38,12 @@ class CPPageFormats : public CPPageBase private: CImageList m_onoff; + bool m_onoffDark = false; // the dark-theme state the m_onoff checkbox glyphs were built for bool m_bInsufficientPrivileges; bool m_bFileExtChanged; + void BuildCheckImageList(); // (re)builds the checkbox glyph image list for the current theme + int GetChecked(int iItem); void SetChecked(int iItem, int fChecked); @@ -96,6 +99,7 @@ class CPPageFormats : public CPPageBase protected: virtual void DoDataExchange(CDataExchange* pDX); virtual BOOL OnInitDialog(); + virtual BOOL OnSetActive(); virtual BOOL OnApply(); DECLARE_MESSAGE_MAP() diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 8cd2736711..68840ba6fa 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -700,6 +700,27 @@ namespace DarkTheme SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); // force NC repaint } + // The colour-well buttons on the Interface / OSD / Subtitle-style pages are push buttons + // that each page paints with the selected colour via NM_CUSTOMDRAW (its OnCustomDrawBtns). + // Owner-drawing them here would steal WM_PAINT and show the button caption ("B", "O", ...) + // instead of the colour swatch, so ThemeControl skips them and lets the page draw them. + bool IsOwnerColorButton(HWND hCtrl) { + switch (::GetDlgCtrlID(hCtrl)) { + case IDC_BUTTON_CLRFACE: + case IDC_BUTTON_CLROUTLINE: + case IDC_BUTTON_CLRFONT: + case IDC_BUTTON_CLRGRAD1: + case IDC_BUTTON_CLRGRAD2: + case IDC_COLORPRI: + case IDC_COLORSEC: + case IDC_COLOROUTL: + case IDC_COLORSHAD: + return true; + default: + return false; + } + } + void ThemeControl(HWND hCtrl) { if (pAllowDarkModeForWindow) { pAllowDarkModeForWindow(hCtrl, true); @@ -714,9 +735,14 @@ namespace DarkTheme SetWindowSubclass(hCtrl, GroupBoxSubclassProc, kGroupBoxSubclassId, 0); InvalidateRect(hCtrl, nullptr, TRUE); } else if (bt == BS_PUSHBUTTON || bt == BS_DEFPUSHBUTTON) { - // Owner-draw so they stay dark AND keep the Win11 rounding + icon. - SetWindowSubclass(hCtrl, ButtonSubclassProc, kButtonSubclassId, 0); - InvalidateRect(hCtrl, nullptr, TRUE); + if (IsOwnerColorButton(hCtrl)) { + // Colour-well button: leave it for its page's NM_CUSTOMDRAW handler, which + // fills it with the selected colour (owner-drawing would show its caption). + } else { + // Owner-draw so they stay dark AND keep the Win11 rounding + icon. + SetWindowSubclass(hCtrl, ButtonSubclassProc, kButtonSubclassId, 0); + InvalidateRect(hCtrl, nullptr, TRUE); + } } else { // checkboxes, radios SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); From bd99f92b9b99a5b7d75e78933476d6c721912535 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 04:35:19 -0300 Subject: [PATCH 04/24] Dark Options: theme-coloured background/title, flat disabled text, addressing review - The Options background now follows the R/G/B/Brightness sliders (ThemeRGB), so it matches the player, and the title bar is tinted with DWMWA_CAPTION_COLOR like the player's caption. Text stays a fixed readable colour (never driven to black). The sheet re-tints when a slider drag ends (repainting standard controls on every tick flickers, and WS_EX_COMPOSITED breaks the list controls), so it snaps to the final colour on release while the player follows live. - Theme sliders are owner-drawn and paint from a committed colour snapshot, so the one being dragged doesn't recolour under the cursor; all four move together on release. - Disabled text labels are owner-drawn flat instead of the embossed grey Windows draws on dark (addresses maintainer feedback). All Options text/glyphs are fixed, never tinted. - Elevated "Modify" Formats dialog now loads bUseDarkTheme, so it matches the theme instead of always appearing dark. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/PPageFullscreen.cpp | 4 +- src/apps/mplayerc/PPageInterface.cpp | 46 ++++- src/apps/mplayerc/PPageSheet.cpp | 2 + src/apps/mplayerc/controls/DarkTabCtrl.cpp | 2 +- src/apps/mplayerc/controls/DarkTheme.cpp | 209 ++++++++++++++++----- src/apps/mplayerc/controls/DarkTheme.h | 14 +- src/apps/mplayerc/mplayerc.cpp | 4 + 7 files changed, 221 insertions(+), 60 deletions(-) diff --git a/src/apps/mplayerc/PPageFullscreen.cpp b/src/apps/mplayerc/PPageFullscreen.cpp index c26fc67f5a..673c94b580 100644 --- a/src/apps/mplayerc/PPageFullscreen.cpp +++ b/src/apps/mplayerc/PPageFullscreen.cpp @@ -291,9 +291,9 @@ void CPPageFullscreen::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) if (DarkTheme::IsActive()) { crBkgnd = DarkTheme::FaceColor(); - crText = (m_fullScreenModes.bEnabled == FALSE) ? ThemeRGB(110, 115, 120) : DarkTheme::TextColor(); + crText = (m_fullScreenModes.bEnabled == FALSE) ? RGB(110, 115, 120) : DarkTheme::TextColor(); if (m_list.GetCheck(pLVCD->nmcd.dwItemSpec) == false) { - crText = ThemeRGB(110, 115, 120); + crText = RGB(110, 115, 120); } } else { if (m_fullScreenModes.bEnabled == FALSE) { diff --git a/src/apps/mplayerc/PPageInterface.cpp b/src/apps/mplayerc/PPageInterface.cpp index 2327ae46ab..2769751fc0 100644 --- a/src/apps/mplayerc/PPageInterface.cpp +++ b/src/apps/mplayerc/PPageInterface.cpp @@ -92,6 +92,15 @@ BOOL CPPageInterface::OnInitDialog() m_ThemeGreenCtrl.SetRange (0, 255, TRUE); m_ThemeBlueCtrl.SetRange (0, 255, TRUE); + // Owner-draw the theme sliders deterministically. Relying on NM_CUSTOMDRAW left the active + // slider's background unpainted (white) on some repaints and didn't refresh the others; the + // subclass always fills the themed background, so all four stay consistent. + DarkTheme::MakeTrackbarOwnerDrawn(m_ThemeBrightnessCtrl.GetSafeHwnd(), true); + DarkTheme::MakeTrackbarOwnerDrawn(m_ThemeRedCtrl.GetSafeHwnd(), true); + DarkTheme::MakeTrackbarOwnerDrawn(m_ThemeGreenCtrl.GetSafeHwnd(), true); + DarkTheme::MakeTrackbarOwnerDrawn(m_ThemeBlueCtrl.GetSafeHwnd(), true); + DarkTheme::CommitThemeColors(); // initial snapshot for the sliders + m_clrFaceABGR = m_clrFaceABGR_Old = s.clrFaceABGR; m_clrOutlineABGR = m_clrOutlineABGR_Old = s.clrOutlineABGR; m_fUseWin7TaskBar = s.fUseWin7TaskBar; @@ -300,8 +309,9 @@ void CPPageInterface::OnThemeChange() pFrame->Invalidate(); pFrame->m_wndPlaylistBar.Invalidate(); - // The Options dialog is deliberately not repainted here: its palette is fixed, so the - // R/G/B/Brightness sliders only affect the player, never the open Options sheet. + // OnThemeChange runs on every slider tick to keep the player live. The Options sheet is + // re-tinted separately, when the drag ends (see OnHScroll), because repainting its standard + // controls on every tick flickers (they aren't double-buffered like the player's). } BEGIN_MESSAGE_MAP(CPPageInterface, CPPageBase) @@ -373,6 +383,19 @@ void CPPageInterface::OnClickClrDefault() m_clrOutlineABGR_Old = s.clrOutlineABGR; UpdateData(FALSE); + + // Reset is a one-shot (not a drag), so re-tint the Options sheet to the default colours here. + if (DarkTheme::IsActive()) { + DarkTheme::CommitThemeColors(); + if (HWND hSheet = ::GetAncestor(GetSafeHwnd(), GA_ROOT)) { + TreePropSheet::CPropPageFrameDefault::s_clrFace = DarkTheme::FaceColor(); + DarkTheme::RefreshColors(hSheet); + } + m_ThemeBrightnessCtrl.Invalidate(); + m_ThemeRedCtrl.Invalidate(); + m_ThemeGreenCtrl.Invalidate(); + m_ThemeBlueCtrl.Invalidate(); + } } void CPPageInterface::OnClickClrFace() @@ -501,6 +524,25 @@ void CPPageInterface::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) OnThemeChange(); } + // Re-tint the open Options sheet to match the new colours when the drag ends (SB_ENDSCROLL). + // It is done on release, not on every tick: continuously repainting the sheet's standard + // controls flickers (they are not owner-drawn / double-buffered like the player's). The player + // follows live via OnThemeChange above. + const bool bThemeSlider = (*pScrollBar == m_ThemeBrightnessCtrl || *pScrollBar == m_ThemeRedCtrl + || *pScrollBar == m_ThemeGreenCtrl || *pScrollBar == m_ThemeBlueCtrl); + if (bThemeSlider && nSBCode == SB_ENDSCROLL && DarkTheme::IsActive()) { + DarkTheme::CommitThemeColors(); // snapshot the final colour so all four sliders move together + if (HWND hSheet = ::GetAncestor(GetSafeHwnd(), GA_ROOT)) { + TreePropSheet::CPropPageFrameDefault::s_clrFace = DarkTheme::FaceColor(); + DarkTheme::RefreshColors(hSheet); + } + // Repaint the four sliders so they all move to the final (committed) colour on release. + m_ThemeBrightnessCtrl.Invalidate(); + m_ThemeRedCtrl.Invalidate(); + m_ThemeGreenCtrl.Invalidate(); + m_ThemeBlueCtrl.Invalidate(); + } + SetModified(); __super::OnHScroll(nSBCode, nPos, pScrollBar); diff --git a/src/apps/mplayerc/PPageSheet.cpp b/src/apps/mplayerc/PPageSheet.cpp index 3072c433e6..1265a4cd1c 100644 --- a/src/apps/mplayerc/PPageSheet.cpp +++ b/src/apps/mplayerc/PPageSheet.cpp @@ -135,6 +135,8 @@ BOOL CPPageSheet::OnInitDialog() if (CTreeCtrl* pTree = GetPageTreeControl()) { pTree->SetBkColor(DarkTheme::FaceColor()); pTree->SetTextColor(DarkTheme::TextColor()); + // Double-buffer the nav tree so it doesn't flicker during the live slider re-tint. + pTree->SendMessage(TVM_SETEXTENDEDSTYLE, TVS_EX_DOUBLEBUFFER, TVS_EX_DOUBLEBUFFER); } } diff --git a/src/apps/mplayerc/controls/DarkTabCtrl.cpp b/src/apps/mplayerc/controls/DarkTabCtrl.cpp index 2aa998abcd..945d2b1d17 100644 --- a/src/apps/mplayerc/controls/DarkTabCtrl.cpp +++ b/src/apps/mplayerc/controls/DarkTabCtrl.cpp @@ -85,7 +85,7 @@ void CDarkTabCtrl::DrawTabItem(int nItem, CRect rItem, bool selected, CDC* pDC) CRect rText(rItem); rText.left += 6; pDC->SetBkMode(TRANSPARENT); - pDC->SetTextColor(selected ? DarkTheme::TextColor() : ThemeRGB(140, 145, 150)); + pDC->SetTextColor(selected ? DarkTheme::TextColor() : RGB(140, 145, 150)); // fixed: text never tints pDC->DrawTextW(buf, rText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); } diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 68840ba6fa..3c026efafe 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -33,19 +33,6 @@ namespace DarkTheme { namespace { - // Fixed dark palette for the Options dialog. The R/G/B/Brightness sliders on the - // Interface page tint the *player* only; the Options window stays a constant dark - // (tying it to the sliders caused half-repaints/flicker and drove text to black). - // These equal the ThemeRGB result at the shipped defaults (brightness 15, colour - // 255,255,255), so the shade matches the default look but never changes. - inline COLORREF DRGB(int r, int g, int b) { - auto f = [](int c) -> int { - int v = (15 + c) * 255 / 256; - return v < 0 ? 0 : (v > 255 ? 255 : v); - }; - return RGB(f(r), f(g), f(b)); - } - // ---- undocumented uxtheme.dll ordinals (Windows 10 1809+) ---- enum PreferredAppMode { APPMODE_DEFAULT, APPMODE_ALLOWDARK, APPMODE_FORCEDARK, APPMODE_FORCELIGHT, APPMODE_MAX }; @@ -71,6 +58,14 @@ namespace DarkTheme HBRUSH g_hbrFace = nullptr; HBRUSH g_hbrCtrl = nullptr; + // Snapshot of the colours used by the R/G/B/Brightness sliders. Those sliders control the + // theme, so painting them with the *live* colour makes the one being dragged change colour + // under the cursor while the others wait for release. Instead they paint from this snapshot, + // updated only when a drag ends (CommitThemeColors), so all four move together on release. + COLORREF g_committedFace = CLR_INVALID; + COLORREF g_committedGroove = CLR_INVALID; + COLORREF g_committedBorder = CLR_INVALID; + bool Build1903orLater() { static const bool b = IsWindowsVersionOrGreaterBuild(HIBYTE(_WIN32_WINNT_WIN10), LOBYTE(_WIN32_WINNT_WIN10), 18362); return b; @@ -107,8 +102,8 @@ namespace DarkTheme } void EnsureBrushes() { - const COLORREF clrFace = DRGB(22, 27, 32); - const COLORREF clrCtrl = DRGB(10, 14, 18); + const COLORREF clrFace = ThemeRGB(22, 27, 32); + const COLORREF clrCtrl = ThemeRGB(10, 14, 18); if (clrFace != g_clrFace || !g_hbrFace) { if (g_hbrFace) { ::DeleteObject(g_hbrFace); @@ -162,7 +157,7 @@ namespace DarkTheme CRect rcFrame = rc; rcFrame.top += textH / 2; - CBrush brFrame(DRGB(70, 75, 80)); + CBrush brFrame(ThemeRGB(70, 75, 80)); pDC->FrameRect(rcFrame, &brFrame); if (!text.IsEmpty()) { @@ -195,7 +190,7 @@ namespace DarkTheme // Push buttons: Windows 11 draws them rounded, optionally with an icon, but the dark // BUTTON visual style ("DarkMode_Explorer") is flat and drops the icon, while leaving // them un-themed keeps them light. So we owner-draw them: a rounded dark face (with - // hover/pressed shades from the fixed dark palette), the shared border (an accent for + // hover/pressed shades from the themed palette), the shared border (an accent for // the default button), the native icon + text, and a // focus rectangle. The default window proc still runs all the click/keyboard logic; we // only take over painting. dwRefData carries the hot (hover) state. @@ -241,13 +236,13 @@ namespace DarkTheme pDC->FillSolidRect(rc, FaceColor()); // dialog bg behind the rounded corners - const COLORREF face = disabled ? DRGB(38, 43, 48) - : pressed ? DRGB(36, 41, 46) - : hot ? DRGB(62, 69, 76) - : DRGB(50, 56, 62); - const COLORREF border = disabled ? DRGB(60, 65, 70) + const COLORREF face = disabled ? ThemeRGB(38, 43, 48) + : pressed ? ThemeRGB(36, 41, 46) + : hot ? ThemeRGB(62, 69, 76) + : ThemeRGB(50, 56, 62); + const COLORREF border = disabled ? ThemeRGB(60, 65, 70) : isDef ? RGB(76, 194, 255) - : DRGB(84, 90, 96); + : ThemeRGB(84, 90, 96); CBrush brFace(face); CPen penBd(PS_SOLID, 1, border); @@ -405,8 +400,8 @@ namespace DarkTheme CDC* pDC = CDC::FromHandle(p->hdc); CRect rc(p->rc); pDC->FillSolidRect(rc, FaceColor()); - pDC->FillSolidRect(rc.right - 1, rc.top, 1, rc.Height(), DRGB(70, 75, 80)); - pDC->FillSolidRect(rc.left, rc.bottom - 1, rc.Width(), 1, DRGB(70, 75, 80)); + pDC->FillSolidRect(rc.right - 1, rc.top, 1, rc.Height(), ThemeRGB(70, 75, 80)); + pDC->FillSolidRect(rc.left, rc.bottom - 1, rc.Width(), 1, ThemeRGB(70, 75, 80)); wchar_t buf[256] = {}; HDITEMW hdi = {}; @@ -530,9 +525,9 @@ namespace DarkTheme CRect rc; ::GetClientRect(hWnd, &rc); - const COLORREF clrFace = DRGB(38, 44, 50); - const COLORREF clrBorder = DRGB(70, 75, 80); - const COLORREF clrArrow = DRGB(170, 175, 180); + const COLORREF clrFace = ThemeRGB(38, 44, 50); + const COLORREF clrBorder = ThemeRGB(70, 75, 80); + const COLORREF clrArrow = RGB(170, 175, 180); // fixed: foreground glyph never tints pDC->FillSolidRect(rc, clrFace); @@ -573,7 +568,7 @@ namespace DarkTheme // and a celeste thumb, painted deterministically in WM_PAINT. const UINT_PTR kTrackbarSubclassId = 6; - LRESULT CALLBACK TrackbarSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + LRESULT CALLBACK TrackbarSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR dwData) { switch (msg) { case WM_ERASEBKGND: return 1; // background is painted in WM_PAINT @@ -583,19 +578,26 @@ namespace DarkTheme CRect rc; ::GetClientRect(hWnd, &rc); - pDC->FillSolidRect(rc, FaceColor()); + + // The R/G/B/Brightness sliders (dwData != 0) paint from the committed snapshot so + // the one being dragged doesn't recolour live; other sliders use the live colour. + const bool frozen = (dwData != 0 && g_committedFace != CLR_INVALID); + const COLORREF face = frozen ? g_committedFace : FaceColor(); + const COLORREF groove = frozen ? g_committedGroove : ThemeRGB(10, 14, 18); + const COLORREF border = frozen ? g_committedBorder : ThemeRGB(60, 65, 70); + pDC->FillSolidRect(rc, face); RECT rcCh{}; ::SendMessageW(hWnd, TBM_GETCHANNELRECT, 0, reinterpret_cast(&rcCh)); CRect ch(rcCh); - pDC->FillSolidRect(ch, DRGB(10, 14, 18)); // dark groove - pDC->Draw3dRect(ch, DRGB(60, 65, 70), DRGB(60, 65, 70)); // subtle border + pDC->FillSolidRect(ch, groove); // dark groove + pDC->Draw3dRect(ch, border, border); // subtle border RECT rcTh{}; ::SendMessageW(hWnd, TBM_GETTHUMBRECT, 0, reinterpret_cast(&rcTh)); CRect th(rcTh); const bool disabled = (::GetWindowLongW(hWnd, GWL_STYLE) & WS_DISABLED) != 0; - pDC->FillSolidRect(th, disabled ? DRGB(90, 95, 100) : RGB(76, 194, 255)); // celeste thumb + pDC->FillSolidRect(th, disabled ? ThemeRGB(90, 95, 100) : RGB(76, 194, 255)); // celeste thumb ::EndPaint(hWnd, &ps); return 0; @@ -721,6 +723,58 @@ namespace DarkTheme } } + // Disabled text statics are drawn by Windows with an embossed grey (a light 1px shadow), + // which looks bad on a dark background. Owner-draw disabled text labels flat instead — a + // plain grey caption, no emboss. Enabled statics fall through to the default painting. + const UINT_PTR kStaticSubclassId = 10; + + LRESULT CALLBACK StaticSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_ENABLE: + ::InvalidateRect(hWnd, nullptr, TRUE); // repaint when the enable state changes + break; + case WM_PAINT: + if (::GetWindowLongW(hWnd, GWL_STYLE) & WS_DISABLED) { + PAINTSTRUCT ps; + CDC* pDC = CDC::FromHandle(::BeginPaint(hWnd, &ps)); + CRect rc; + ::GetClientRect(hWnd, &rc); + pDC->FillSolidRect(rc, FaceColor()); + + HFONT hFont = reinterpret_cast(::SendMessageW(hWnd, WM_GETFONT, 0, 0)); + CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(RGB(110, 115, 120)); // flat disabled grey, no emboss + + CString text; + const int len = ::GetWindowTextLengthW(hWnd); + if (len > 0) { + ::GetWindowTextW(hWnd, text.GetBuffer(len + 1), len + 1); + text.ReleaseBuffer(); + } + + const LONG st = ::GetWindowLongW(hWnd, GWL_STYLE) & SS_TYPEMASK; + UINT fmt = DT_NOPREFIX; + if (st == SS_CENTER) fmt |= DT_CENTER; + else if (st == SS_RIGHT) fmt |= DT_RIGHT; + if (st == SS_LEFTNOWORDWRAP || st == SS_SIMPLE) fmt |= DT_SINGLELINE | DT_VCENTER; + else fmt |= DT_WORDBREAK; + pDC->DrawTextW(text, rc, fmt); + + if (pOldFont) { + pDC->SelectObject(pOldFont); + } + ::EndPaint(hWnd, &ps); + return 0; + } + break; // enabled: let the default painting run (colour via WM_CTLCOLORSTATIC) + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, StaticSubclassProc, kStaticSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + void ThemeControl(HWND hCtrl) { if (pAllowDarkModeForWindow) { pAllowDarkModeForWindow(hCtrl, true); @@ -805,8 +859,14 @@ namespace DarkTheme // (handled by the page); repaint just the edge with the dark border. const LONG st = GetWindowLongW(hCtrl, GWL_STYLE); const LONG ex = GetWindowLongW(hCtrl, GWL_EXSTYLE); + const LONG sType = st & SS_TYPEMASK; if ((st & SS_SUNKEN) || (ex & (WS_EX_CLIENTEDGE | WS_EX_STATICEDGE))) { ApplyDarkBorder(hCtrl); + } else if (sType == SS_LEFT || sType == SS_CENTER || sType == SS_RIGHT + || sType == SS_LEFTNOWORDWRAP || sType == SS_SIMPLE) { + // Plain text labels: owner-draw disabled ones flat (Windows would emboss them, + // which looks bad on dark). Enabled ones keep the default painting. + SetWindowSubclass(hCtrl, StaticSubclassProc, kStaticSubclassId, 0); } } else { // Note: SysTabControl32 is handled by CDarkTabCtrl (a CTabCtrl-derived @@ -831,6 +891,7 @@ namespace DarkTheme RemoveWindowSubclass(hChild, SpinSubclassProc, kSpinSubclassId); RemoveWindowSubclass(hChild, BorderSubclassProc, kBorderSubclassId); RemoveWindowSubclass(hChild, TrackbarSubclassProc, kTrackbarSubclassId); + RemoveWindowSubclass(hChild, StaticSubclassProc, kStaticSubclassId); DWORD_PTR gridFlag = 0; const bool hadGrid = GetWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId, &gridFlag) && gridFlag; @@ -865,14 +926,15 @@ namespace DarkTheme return AfxGetAppSettings().bUseDarkTheme && SysVersion::IsWin10v1809orLater(); } - // The whole Options palette is FIXED (DRGB = the ThemeRGB result at the shipped defaults): - // the R/G/B/Brightness sliders tint the player interface only, never the Options dialog, so - // it never half-repaints or drives its text to black when the sliders move. - COLORREF FaceColor() { return DRGB(22, 27, 32); } + // The background palette follows the R/G/B/Brightness sliders (ThemeRGB), so the Options + // dialog tints in real time to match the player. The TEXT colour is deliberately FIXED (not + // run through ThemeRGB) so it stays readable wherever the sliders are, instead of being + // driven to black when a channel is lowered. + COLORREF FaceColor() { return ThemeRGB(22, 27, 32); } COLORREF TextColor() { return RGB(165, 170, 175); } - COLORREF CtrlBackColor() { return DRGB(10, 14, 18); } - COLORREF CtrlBorderColor() { return DRGB(70, 75, 80); } - COLORREF GridlineColor() { return DRGB(40, 45, 50); } + COLORREF CtrlBackColor() { return ThemeRGB(10, 14, 18); } + COLORREF CtrlBorderColor() { return ThemeRGB(70, 75, 80); } + COLORREF GridlineColor() { return ThemeRGB(40, 45, 50); } void AllowDarkModeForApp() { if (!IsActive()) { @@ -914,6 +976,12 @@ namespace DarkTheme if (FAILED(DwmSetWindowAttribute(hWnd, 20, &bDark, sizeof(bDark)))) { DwmSetWindowAttribute(hWnd, 19, &bDark, sizeof(bDark)); } + // Win11: tint the title-bar background with the same colour as the player's caption + // (ThemeRGB(45,50,55)), so the Options title matches the player and follows the sliders. + if (SysVersion::IsWin11orLater()) { + COLORREF cap = ThemeRGB(45, 50, 55); + DwmSetWindowAttribute(hWnd, 35 /*DWMWA_CAPTION_COLOR*/, &cap, sizeof(cap)); + } } void ApplyThemeToChildren(HWND hWndParent) { @@ -959,11 +1027,19 @@ namespace DarkTheme ThemeControl(hCtrl); } - void MakeTrackbarOwnerDrawn(HWND hTrackbar) { + void CommitThemeColors() { + // Snapshot the current theme colours for the R/G/B/Brightness sliders. Call this when a + // slider drag ends so all four repaint together to the final colour (see TrackbarSubclassProc). + g_committedFace = FaceColor(); + g_committedGroove = ThemeRGB(10, 14, 18); + g_committedBorder = ThemeRGB(60, 65, 70); + } + + void MakeTrackbarOwnerDrawn(HWND hTrackbar, bool bThemeControl) { if (!IsActive() || !hTrackbar) { return; } - SetWindowSubclass(hTrackbar, TrackbarSubclassProc, kTrackbarSubclassId, 0); + SetWindowSubclass(hTrackbar, TrackbarSubclassProc, kTrackbarSubclassId, bThemeControl ? 1 : 0); ::InvalidateRect(hTrackbar, nullptr, TRUE); } @@ -1004,6 +1080,35 @@ namespace DarkTheme RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW); } + void RefreshColors(HWND hRoot) { + if (!IsActive() || !hRoot) { + return; + } + // Re-apply the themed background colours the sliders just changed (tree/list backgrounds + // are stored in the control, so they don't re-tint on their own), then invalidate the + // sheet. WM_CTLCOLOR* refreshes the cached brushes (EnsureBrushes) for everything else. + // The text colour is fixed, so nothing there needs updating. Called when a slider drag + // ends (not on every tick), so this one repaint doesn't produce visible flicker. + EnumChildWindows(hRoot, [](HWND h, LPARAM) -> BOOL { + wchar_t cls[32] = {}; + GetClassNameW(h, cls, _countof(cls)); + if (_wcsicmp(cls, L"SysTreeView32") == 0) { + ::SendMessageW(h, TVM_SETBKCOLOR, 0, static_cast(FaceColor())); + } else if (_wcsicmp(cls, L"SysListView32") == 0) { + const COLORREF bk = FaceColor(); + ::SendMessageW(h, LVM_SETBKCOLOR, 0, static_cast(bk)); + ::SendMessageW(h, LVM_SETTEXTBKCOLOR, 0, static_cast(bk)); + } + return TRUE; + }, 0); + // Also re-tint the title bar (Win11) so the caption follows the sliders, like the player's. + if (SysVersion::IsWin11orLater()) { + COLORREF cap = ThemeRGB(45, 50, 55); + DwmSetWindowAttribute(hRoot, 35 /*DWMWA_CAPTION_COLOR*/, &cap, sizeof(cap)); + } + ::RedrawWindow(hRoot, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN); + } + bool MakeCheckStateImageList(CImageList& il, int size, HWND hRef, bool bDisabled) { if (!IsActive() || size <= 0) { return false; @@ -1015,9 +1120,9 @@ namespace DarkTheme // which made the checkboxes vanish; a solid grey box + grey tick reads clearly // as an inactive checkbox and never disappears. const COLORREF mask = RGB(255, 0, 255); - const COLORREF fill = DRGB(34, 39, 44); - const COLORREF border = DRGB(90, 95, 100); - const COLORREF mark = DRGB(120, 125, 130); + const COLORREF fill = ThemeRGB(34, 39, 44); + const COLORREF border = ThemeRGB(90, 95, 100); + const COLORREF mark = ThemeRGB(120, 125, 130); CClientDC screen(nullptr); CDC dc; @@ -1173,8 +1278,8 @@ namespace DarkTheme if (p->dwItemSpec == TBCD_CHANNEL) { CDC* pDC = CDC::FromHandle(p->hdc); CRect rc(p->rc); - pDC->FillSolidRect(rc, DRGB(10, 14, 18)); // dark groove - pDC->Draw3dRect(rc, DRGB(60, 65, 70), DRGB(60, 65, 70)); // subtle border + pDC->FillSolidRect(rc, ThemeRGB(10, 14, 18)); // dark groove + pDC->Draw3dRect(rc, ThemeRGB(60, 65, 70), ThemeRGB(60, 65, 70)); // subtle border *pResult = CDRF_SKIPDEFAULT; } else { *pResult = CDRF_DODEFAULT; // keep the default thumb and tick marks @@ -1224,12 +1329,12 @@ namespace DarkTheme if (isPush) { // Flat dark push button (face darkens when pressed, lightens on hover). const bool focus = (p->uItemState & CDIS_FOCUS) != 0; - const COLORREF face = disabled ? DRGB(30, 34, 38) - : pressed ? DRGB(28, 33, 38) - : hot ? DRGB(52, 59, 66) - : DRGB(44, 50, 56); + const COLORREF face = disabled ? ThemeRGB(30, 34, 38) + : pressed ? ThemeRGB(28, 33, 38) + : hot ? ThemeRGB(52, 59, 66) + : ThemeRGB(44, 50, 56); pDC->FillSolidRect(rc, face); - pDC->Draw3dRect(rc, DRGB(80, 86, 92), DRGB(80, 86, 92)); + pDC->Draw3dRect(rc, ThemeRGB(80, 86, 92), ThemeRGB(80, 86, 92)); CString btext; const int blen = ::GetWindowTextLengthW(hCtrl); diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h index 49d56bb5ad..3a29315689 100644 --- a/src/apps/mplayerc/controls/DarkTheme.h +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -59,7 +59,11 @@ namespace DarkTheme // Fully owner-draws a trackbar (dark background, dark groove, celeste thumb) via a // subclass. Use for sliders whose NM_CUSTOMDRAW channel colour is not reliably applied // (e.g. the subtitle Default Style alpha sliders after the Reset button). - void MakeTrackbarOwnerDrawn(HWND hTrackbar); + // bThemeControl marks the R/G/B/Brightness sliders (which control the theme colour): they + // paint from a committed snapshot instead of the live colour, so the one being dragged doesn't + // recolour under the cursor. Call CommitThemeColors() when a drag ends to update the snapshot. + void MakeTrackbarOwnerDrawn(HWND hTrackbar, bool bThemeControl = false); + void CommitThemeColors(); // Applies the dark theme to a whole auxiliary top-level dialog opened from the Options // pages (dark title bar, dark background + control colours, themed child controls). @@ -72,6 +76,10 @@ namespace DarkTheme // inactive — and forces a full redraw. Pass the Options property sheet HWND. void RefreshTheme(HWND hRoot); + // Re-tints the themed backgrounds live as the R/G/B/Brightness sliders move, so the Options + // dialog tracks the player colour in real time (the text colour stays fixed). Pass the sheet. + void RefreshColors(HWND hRoot); + // Builds a dark themed checkbox STATE image list for a list-view using // LVS_EX_CHECKBOXES (index 0 = none, 1 = unchecked, 2 = checked). Assigning this // via LVSIL_STATE keeps the checkboxes dark even when the list is disabled (the @@ -95,8 +103,8 @@ namespace DarkTheme // Returns true (with *pResult set) only for check/radio buttons; false otherwise. bool ButtonCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); - // Fixed dark palette for the Options dialog: independent of the R/G/B/Brightness sliders - // (those tint the player interface only), so the Options window never changes shade. + // Background palette follows the R/G/B/Brightness sliders (tints in real time to match the + // player); TextColor() is fixed so text stays readable regardless of the slider values. COLORREF FaceColor(); // dialog / page / static background COLORREF TextColor(); // text COLORREF CtrlBackColor(); // sunken control interior (edit / listbox) diff --git a/src/apps/mplayerc/mplayerc.cpp b/src/apps/mplayerc/mplayerc.cpp index 1d1a6c6b18..e2c863bb96 100644 --- a/src/apps/mplayerc/mplayerc.cpp +++ b/src/apps/mplayerc/mplayerc.cpp @@ -921,6 +921,10 @@ BOOL CMPlayerCApp::InitInstance() if (m_s.nCLSwitches & CLSW_ADMINOPTION) { m_bRunAdmin = true; m_s.LoadFormats(true); + // This elevated instance only loads the Formats settings, so bUseDarkTheme would stay at + // its default (dark) and the dialog would appear dark even when the user turned the dark + // theme off. Load the flag so the elevated Formats dialog matches the chosen theme. + AfxGetProfile().ReadBool(IDS_R_THEME, IDS_RS_USEDARKTHEME, m_s.bUseDarkTheme); switch (m_s.iAdminOption) { case CPPageFormats::IDD : { From 56e26586a85c353216c8592e04208cd800275426 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 05:37:02 -0300 Subject: [PATCH 05/24] Dark Options: draw the UAC shield on the owner-drawn "Modify" button The elevation shield set via BCM_SETSHIELD is drawn internally by the button and is not returned by BM_GETIMAGE, so owner-drawing the button dropped it. Flag the button (MarkUacShield) and paint IDI_SHIELD ourselves when flagged, so the non-admin Formats "Modify" button shows its shield again in the dark theme. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/PPageFormats.cpp | 1 + src/apps/mplayerc/controls/DarkTheme.cpp | 17 +++++++++++++++++ src/apps/mplayerc/controls/DarkTheme.h | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/src/apps/mplayerc/PPageFormats.cpp b/src/apps/mplayerc/PPageFormats.cpp index 244d2a4946..762f9711b8 100644 --- a/src/apps/mplayerc/PPageFormats.cpp +++ b/src/apps/mplayerc/PPageFormats.cpp @@ -939,6 +939,7 @@ BOOL CPPageFormats::OnInitDialog() GetDlgItem(IDC_BUTTON5)->ShowWindow(SW_SHOW); GetDlgItem(IDC_BUTTON5)->SendMessageW(BCM_SETSHIELD, 0, 1); + DarkTheme::MarkUacShield(GetDlgItem(IDC_BUTTON5)->GetSafeHwnd()); m_bInsufficientPrivileges = true; } else { diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 3c026efafe..e46df20c05 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -259,6 +259,12 @@ namespace DarkTheme text.ReleaseBuffer(); } HICON hIcon = reinterpret_cast(::SendMessageW(hWnd, BM_GETIMAGE, IMAGE_ICON, 0)); + // The UAC elevation shield (BCM_SETSHIELD) is drawn internally by the button and + // is NOT returned by BM_GETIMAGE, so owner-drawing dropped it. Draw it ourselves + // when the button is flagged (MarkUacShield). IDI_SHIELD is a shared system icon. + if (!hIcon && ::GetPropW(hWnd, L"MPC_UAC_SHIELD")) { + hIcon = ::LoadIconW(nullptr, IDI_SHIELD); + } HFONT hFont = reinterpret_cast(::SendMessageW(hWnd, WM_GETFONT, 0, 0)); CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; @@ -303,6 +309,7 @@ namespace DarkTheme return 0; } case WM_NCDESTROY: + ::RemovePropW(hWnd, L"MPC_UAC_SHIELD"); RemoveWindowSubclass(hWnd, ButtonSubclassProc, kButtonSubclassId); break; } @@ -1043,6 +1050,16 @@ namespace DarkTheme ::InvalidateRect(hTrackbar, nullptr, TRUE); } + void MarkUacShield(HWND hButton) { + if (!hButton) { + return; + } + // Flag a push button so its owner-draw (ButtonSubclassProc) paints the UAC elevation shield. + // Needed because BCM_SETSHIELD's glyph is drawn internally and lost when we owner-draw. + ::SetPropW(hButton, L"MPC_UAC_SHIELD", reinterpret_cast(1)); + ::InvalidateRect(hButton, nullptr, FALSE); + } + void ThemeDialog(HWND hDlg) { if (!IsActive() || !hDlg) { return; diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h index 3a29315689..a7c5281140 100644 --- a/src/apps/mplayerc/controls/DarkTheme.h +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -65,6 +65,10 @@ namespace DarkTheme void MakeTrackbarOwnerDrawn(HWND hTrackbar, bool bThemeControl = false); void CommitThemeColors(); + // Flags a push button so its owner-draw paints the UAC elevation shield. Call it alongside + // BCM_SETSHIELD: the shield glyph is drawn internally by the button and lost when we owner-draw. + void MarkUacShield(HWND hButton); + // Applies the dark theme to a whole auxiliary top-level dialog opened from the Options // pages (dark title bar, dark background + control colours, themed child controls). // Call once from the dialog's OnInitDialog. From 29e33d1faf8fcc9b0927cf6997aa8f33219a5b98 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 06:09:49 -0300 Subject: [PATCH 06/24] Dark Options: revert app dark-mode on theme-off; robust UAC shield icon - When the dark theme is turned off at runtime, reset the process-wide preferred app mode (FORCELIGHT + FlushMenuThemes) so the main window's immersive-dark menus revert to light immediately instead of staying dark until the app is restarted. Re-arm it when turned on. - Load the UAC shield via SHGetStockIconInfo(SIID_SHIELD) (cached) instead of LoadIcon(IDI_SHIELD), which could return null, so the Formats "Modify" button reliably shows its shield in the dark theme. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/controls/DarkTheme.cpp | 48 +++++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index e46df20c05..ba5345604d 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -26,6 +26,7 @@ #include #include // BP_CHECKBOX / BP_RADIOBUTTON / CBS_* / RBS_* #include // SetWindowSubclass +#include // SHGetStockIconInfo (UAC shield) #pragma comment(lib, "dwmapi.lib") #pragma comment(lib, "uxtheme.lib") @@ -260,10 +261,21 @@ namespace DarkTheme } HICON hIcon = reinterpret_cast(::SendMessageW(hWnd, BM_GETIMAGE, IMAGE_ICON, 0)); // The UAC elevation shield (BCM_SETSHIELD) is drawn internally by the button and - // is NOT returned by BM_GETIMAGE, so owner-drawing dropped it. Draw it ourselves - // when the button is flagged (MarkUacShield). IDI_SHIELD is a shared system icon. - if (!hIcon && ::GetPropW(hWnd, L"MPC_UAC_SHIELD")) { - hIcon = ::LoadIconW(nullptr, IDI_SHIELD); + // is NOT returned by BM_GETIMAGE, so owner-drawing dropped it. When the button is + // flagged (MarkUacShield), draw the system shield ourselves. + if (::GetPropW(hWnd, L"MPC_UAC_SHIELD")) { + static HICON s_hShield = nullptr; // cached once for the process lifetime + if (!s_hShield) { + SHSTOCKICONINFO sii = { sizeof(sii) }; + if (SUCCEEDED(::SHGetStockIconInfo(SIID_SHIELD, SHGSI_ICON | SHGSI_SMALLICON, &sii))) { + s_hShield = sii.hIcon; + } else { + s_hShield = ::LoadIconW(nullptr, IDI_SHIELD); + } + } + if (s_hShield) { + hIcon = s_hShield; + } } HFONT hFont = reinterpret_cast(::SendMessageW(hWnd, WM_GETFONT, 0, 0)); @@ -965,6 +977,28 @@ namespace DarkTheme g_bAppAllowed = true; } + void DisallowDarkModeForApp() { + LoadApi(); + if (!g_bApiOk) { + return; + } + // Undo the process-wide force-dark set by AllowDarkModeForApp, so turning the dark theme off + // at runtime reverts the main window's menus: they use the immersive dark menu theme, which + // would otherwise stay dark until the app restarts. FORCELIGHT keeps the app light here. + if (pSetPreferredAppMode) { + pSetPreferredAppMode(APPMODE_FORCELIGHT); + } else if (pAllowDarkModeForApp) { + pAllowDarkModeForApp(false); + } + if (pRefreshImmersiveColorPolicyState) { + pRefreshImmersiveColorPolicyState(); + } + if (pFlushMenuThemes) { + pFlushMenuThemes(); + } + g_bAppAllowed = false; + } + void EnableForWindow(HWND hWnd) { if (!IsActive() || !hWnd) { return; @@ -1080,10 +1114,14 @@ namespace DarkTheme LoadApi(); if (IsActive()) { // Turned on at runtime: (re)apply to the sheet and every already-created page. + AllowDarkModeForApp(); // re-arm the process-wide force-dark (menus etc.) EnableForWindow(hRoot); // dark title bar (checks IsActive internally) ApplyThemeToChildren(hRoot); // recurses into all descendant controls } else { - // Turned off at runtime: strip every subclass/override so everything goes light. + // Turned off at runtime: strip every subclass/override so everything goes light, and + // undo the process-wide force-dark so the main window's menus revert to light (they use + // the immersive dark menu theme, which otherwise stays dark until the app restarts). + DisallowDarkModeForApp(); if (g_bApiOk && pAllowDarkModeForWindow) { pAllowDarkModeForWindow(hRoot, false); } From ddff644f9511727c45f70e59920d7b5e85c53119 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 06:25:24 -0300 Subject: [PATCH 07/24] Dark Options: centre owner-drawn button captions with accelerators Measure the caption width with DT_CALCRECT (which drops the "&" accelerator prefix) instead of GetTextExtent (which counts it), so single-line owner-drawn push buttons whose caption has an accelerator (e.g. the Formats "&All" / "A&udio" association buttons) are centred instead of shifted to the left. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/controls/DarkTheme.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index ba5345604d..855115d261 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -295,9 +295,16 @@ namespace DarkTheme rt.top = rc.top + (oy > 0 ? oy : 0); pDC->DrawTextW(text, rt, DT_CENTER | DT_WORDBREAK); } else { - const CSize ext = text.IsEmpty() ? CSize(0, 0) : pDC->GetTextExtent(text); + // Measure the *rendered* width with DT_CALCRECT so the "&" accelerator prefix + // isn't counted (GetTextExtent includes it, which shifted the caption left). + int textW = 0; + if (!text.IsEmpty()) { + CRect calc(0, 0, 0, 0); + pDC->DrawTextW(text, calc, DT_SINGLELINE | DT_CALCRECT); + textW = calc.Width(); + } const int gap = (hIcon && !text.IsEmpty()) ? 4 : 0; - int x = rc.left + (rc.Width() - (icon + gap + ext.cx)) / 2; + int x = rc.left + (rc.Width() - (icon + gap + textW)) / 2; const int cy = rc.top + rc.Height() / 2; if (hIcon) { ::DrawIconEx(hdc, x, cy - icon / 2, hIcon, icon, icon, 0, nullptr, DI_NORMAL); From 45db3b5822be90e393dca006b0e2cc4882892cde Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 17:35:50 -0300 Subject: [PATCH 08/24] Dark Options: extend the dark theme to the auxiliary dialogs, docking bars and message boxes Reviewers asked for the rest of the app's dialogs to follow the dark theme too. This covers them centrally, reusing the existing helpers. Auxiliary dialogs (one DarkTheme::ThemeDialog call in each OnInitDialog): About, Command Line Switches, History, Shader Editor, Go To, Favorite Add / Organize, Media Types, Tuner Scan, Add Command, Authentication, Item Properties, Open, Capture, Playlist Name, Pan&Scan Presets, Shader Combine / New / AutoComplete. Docking bars (Shader Editor / Capture / Navigation / Subresync): - ColorThemeRGB() was only overridden by the playlist bar, so every other bar painted its frame / gripper / close button pure black; move the override up to CPlayerBar so all bars get the real themed colours. - Set m_bUseDarkTheme on all docking bars (creation + theme toggle), not just the playlist bar, so their sizing frame is dark instead of the light system colour. - Draw the sizing-bar gripper in the dark theme (it was only drawn in the light branch, so the drag lines vanished on the dark frame). - Apply the dark title bar to the floating mini-frame so a detached bar matches the theme. Message boxes (Check for Updates, and reusable for the rest): - CDarkMessageBoxHook installs a thread-local WH_CBT hook that themes the standard "#32770" message box as it activates; a dedicated subclass repaints the button band dark (the OS paints it a light system colour in its own WM_PAINT, over the erase). Other fixes surfaced while testing: - Dark scrollbars on multiline edits: use DarkMode_Explorer (dark scrollbar) instead of DarkMode_CFD, which left the scrollbar light. - ThemeDialog forces the process-wide immersive dark mode and a synchronous full repaint, so a dialog opened before Options gets dark scrollbars/borders and third-party filter pages stop needing a hover. - Scale the owner-drawn UAC shield by DPI (was a fixed 16px, too small at 150-200% scaling) and load the large stock icon for a crisp downscale. Co-Authored-By: Claude Opus 4.8 --- src/ExtLib/ui/sizecbar/scbarg.cpp | 56 ++++++------ src/apps/mplayerc/AboutDlg.cpp | 3 + src/apps/mplayerc/AddCommandDlg.cpp | 3 + src/apps/mplayerc/AuthDlg.cpp | 3 + src/apps/mplayerc/CmdLineHelpDlg.cpp | 3 + src/apps/mplayerc/FavoriteAddDlg.cpp | 3 + src/apps/mplayerc/FavoriteOrganizeDlg.cpp | 3 + src/apps/mplayerc/GoToDlg.cpp | 3 + src/apps/mplayerc/HistoryDlg.cpp | 3 + src/apps/mplayerc/ItemPropertiesDlg.cpp | 3 + src/apps/mplayerc/MainFrm.cpp | 6 +- src/apps/mplayerc/MediaTypesDlg.cpp | 3 + src/apps/mplayerc/OpenDlg.cpp | 3 + src/apps/mplayerc/PPageInterface.cpp | 16 ++-- src/apps/mplayerc/PlayerBar.cpp | 16 ++++ src/apps/mplayerc/PlayerBar.h | 5 ++ src/apps/mplayerc/PlayerCaptureDialog.cpp | 3 + src/apps/mplayerc/PlaylistNameDlg.cpp | 3 + src/apps/mplayerc/PnSPresetsDlg.cpp | 3 + src/apps/mplayerc/ShaderAutoCompleteDlg.cpp | 3 + src/apps/mplayerc/ShaderCombineDlg.cpp | 3 + src/apps/mplayerc/ShaderEditorDlg.cpp | 9 ++ src/apps/mplayerc/ShaderNewDlg.cpp | 3 + src/apps/mplayerc/TunerScanDlg.cpp | 3 + src/apps/mplayerc/UpdateChecker.cpp | 2 + src/apps/mplayerc/controls/DarkTheme.cpp | 98 ++++++++++++++++++++- src/apps/mplayerc/controls/DarkTheme.h | 16 ++++ 27 files changed, 239 insertions(+), 39 deletions(-) diff --git a/src/ExtLib/ui/sizecbar/scbarg.cpp b/src/ExtLib/ui/sizecbar/scbarg.cpp index 7e8668cfc0..5cbf04e9fe 100644 --- a/src/ExtLib/ui/sizecbar/scbarg.cpp +++ b/src/ExtLib/ui/sizecbar/scbarg.cpp @@ -116,36 +116,36 @@ void CSizingControlBarG::NcPaintGripper(CDC* pDC, CRect rcClient) if (!HasGripper()) return; - if (!m_bUseDarkTheme) { - // paints a simple "two raised lines" gripper - // override this if you want a more sophisticated gripper - CRect gripper = rcClient; - CRect rcbtn = m_biHide.GetRect(CSize(ScaleX(m_cyGripper), ScaleY(m_cyGripper))); - BOOL bHorz = IsHorzDocked(); - - gripper.DeflateRect(1, 1); - const auto sizeX = ScaleX(m_cyGripper) / 4; - const auto sizeY = ScaleY(m_cyGripper) / 4; - if (bHorz) - { // gripper at left - gripper.left -= (ScaleX(m_cyGripper) / 2 + sizeX * 2); - gripper.right = gripper.left + sizeX; - gripper.top = rcbtn.bottom + 3; - } - else - { // gripper at top - gripper.top -= (ScaleY(m_cyGripper) / 2 + sizeY * 2); - gripper.bottom = gripper.top + sizeY; - gripper.right = rcbtn.left - 3; - } - pDC->Draw3dRect(gripper, ::GetSysColor(COLOR_BTNHIGHLIGHT), - ::GetSysColor(COLOR_BTNSHADOW)); + // paints a simple "two raised lines" gripper + // override this if you want a more sophisticated gripper + CRect gripper = rcClient; + CRect rcbtn = m_biHide.GetRect(CSize(ScaleX(m_cyGripper), ScaleY(m_cyGripper))); + BOOL bHorz = IsHorzDocked(); + + gripper.DeflateRect(1, 1); + const auto sizeX = ScaleX(m_cyGripper) / 4; + const auto sizeY = ScaleY(m_cyGripper) / 4; + if (bHorz) + { // gripper at left + gripper.left -= (ScaleX(m_cyGripper) / 2 + sizeX * 2); + gripper.right = gripper.left + sizeX; + gripper.top = rcbtn.bottom + 3; + } + else + { // gripper at top + gripper.top -= (ScaleY(m_cyGripper) / 2 + sizeY * 2); + gripper.bottom = gripper.top + sizeY; + gripper.right = rcbtn.left - 3; + } - gripper.OffsetRect(bHorz ? sizeX : 0, bHorz ? 0 : sizeY); + // In the dark theme the system 3D colours are near-black on the dark frame (the gripper lines + // vanished); use themed light/dark greys so the two raised lines stay visible. + const COLORREF clrHi = m_bUseDarkTheme ? ColorThemeRGB(95, 100, 105) : ::GetSysColor(COLOR_BTNHIGHLIGHT); + const COLORREF clrLo = m_bUseDarkTheme ? ColorThemeRGB(20, 25, 30) : ::GetSysColor(COLOR_BTNSHADOW); - pDC->Draw3dRect(gripper, ::GetSysColor(COLOR_BTNHIGHLIGHT), - ::GetSysColor(COLOR_BTNSHADOW)); - } + pDC->Draw3dRect(gripper, clrHi, clrLo); + gripper.OffsetRect(bHorz ? sizeX : 0, bHorz ? 0 : sizeY); + pDC->Draw3dRect(gripper, clrHi, clrLo); m_biHide.Paint(pDC, this, CSize(ScaleX(m_cyGripper), ScaleY(m_cyGripper))); } diff --git a/src/apps/mplayerc/AboutDlg.cpp b/src/apps/mplayerc/AboutDlg.cpp index 06988f4c31..ec4f5d065a 100644 --- a/src/apps/mplayerc/AboutDlg.cpp +++ b/src/apps/mplayerc/AboutDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "AboutDlg.h" #include "DSUtil/FileHandle.h" +#include "controls/DarkTheme.h" #include "Version.h" @@ -94,6 +95,8 @@ BOOL CAboutDlg::OnInitDialog() GetDlgItem(IDOK)->SetFocus(); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return FALSE; } diff --git a/src/apps/mplayerc/AddCommandDlg.cpp b/src/apps/mplayerc/AddCommandDlg.cpp index bd4f6ca711..7a3c4b15a3 100644 --- a/src/apps/mplayerc/AddCommandDlg.cpp +++ b/src/apps/mplayerc/AddCommandDlg.cpp @@ -20,6 +20,7 @@ #include "stdafx.h" #include "AddCommandDlg.h" +#include "controls/DarkTheme.h" // CAddCommandDlg dialog @@ -135,6 +136,8 @@ BOOL CAddCommandDlg::OnInitDialog() m_okButton.EnableWindow(FALSE); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/AuthDlg.cpp b/src/apps/mplayerc/AuthDlg.cpp index 7c5220e2b4..0b25290a22 100644 --- a/src/apps/mplayerc/AuthDlg.cpp +++ b/src/apps/mplayerc/AuthDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "AuthDlg.h" +#include "controls/DarkTheme.h" // CAuthDlg dialog @@ -82,6 +83,8 @@ BOOL CAuthDlg::OnInitDialog() m_usernamectrl.SetFocus(); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/CmdLineHelpDlg.cpp b/src/apps/mplayerc/CmdLineHelpDlg.cpp index b44047df88..2cbff018e0 100644 --- a/src/apps/mplayerc/CmdLineHelpDlg.cpp +++ b/src/apps/mplayerc/CmdLineHelpDlg.cpp @@ -22,6 +22,7 @@ #include "stdafx.h" #include "CmdLineHelpDlg.h" #include "Misc.h" +#include "controls/DarkTheme.h" CmdLineHelpDlg::CmdLineHelpDlg(const CStringW& cmdLine) : CResizableDialog(CmdLineHelpDlg::IDD) @@ -117,5 +118,7 @@ BOOL CmdLineHelpDlg::OnInitDialog() EnableSaveRestore(IDS_R_DLG_CMD_LINE_HELP); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return FALSE; } diff --git a/src/apps/mplayerc/FavoriteAddDlg.cpp b/src/apps/mplayerc/FavoriteAddDlg.cpp index c519300fb4..826dd90b3c 100644 --- a/src/apps/mplayerc/FavoriteAddDlg.cpp +++ b/src/apps/mplayerc/FavoriteAddDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "FavoriteAddDlg.h" +#include "controls/DarkTheme.h" // CFavoriteAddDlg dialog @@ -74,6 +75,8 @@ BOOL CFavoriteAddDlg::OnInitDialog() m_namectrl.SetCurSel(0); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/FavoriteOrganizeDlg.cpp b/src/apps/mplayerc/FavoriteOrganizeDlg.cpp index 8819728b59..304bfbeef3 100644 --- a/src/apps/mplayerc/FavoriteOrganizeDlg.cpp +++ b/src/apps/mplayerc/FavoriteOrganizeDlg.cpp @@ -24,6 +24,7 @@ #include "ItemPropertiesDlg.h" #include "FavoriteOrganizeDlg.h" #include "DSUtil/std_helper.h" +#include "controls/DarkTheme.h" // CFavoriteOrganizeDlg dialog @@ -178,6 +179,8 @@ BOOL CFavoriteOrganizeDlg::OnInitDialog() EnableSaveRestore(IDS_R_DLG_ORGANIZE_FAV); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/GoToDlg.cpp b/src/apps/mplayerc/GoToDlg.cpp index 51b3544ffd..0735edabb0 100644 --- a/src/apps/mplayerc/GoToDlg.cpp +++ b/src/apps/mplayerc/GoToDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "GoToDlg.h" +#include "controls/DarkTheme.h" // CGoToDlg dialog @@ -101,6 +102,8 @@ BOOL CGoToDlg::OnInitDialog() } } + DarkTheme::ThemeDialog(GetSafeHwnd()); + return FALSE; } diff --git a/src/apps/mplayerc/HistoryDlg.cpp b/src/apps/mplayerc/HistoryDlg.cpp index 32ffd81866..14a9b862b2 100644 --- a/src/apps/mplayerc/HistoryDlg.cpp +++ b/src/apps/mplayerc/HistoryDlg.cpp @@ -23,6 +23,7 @@ #include "ItemPropertiesDlg.h" #include "HistoryDlg.h" #include "DSUtil/std_helper.h" +#include "controls/DarkTheme.h" // CHistoryDlg dialog @@ -285,6 +286,8 @@ BOOL CHistoryDlg::OnInitDialog() EnableSaveRestore(IDS_R_DLG_HISTORY); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/ItemPropertiesDlg.cpp b/src/apps/mplayerc/ItemPropertiesDlg.cpp index 6e48686c46..26d3139e31 100644 --- a/src/apps/mplayerc/ItemPropertiesDlg.cpp +++ b/src/apps/mplayerc/ItemPropertiesDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include #include "ItemPropertiesDlg.h" +#include "controls/DarkTheme.h" // CItemPropertiesDlg @@ -69,6 +70,8 @@ BOOL CItemPropertiesDlg::OnInitDialog() SetMinTrackSize(r.Size()); SetMaxTrackSize({ 1000, r.Height() }); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/MainFrm.cpp b/src/apps/mplayerc/MainFrm.cpp index 15d65ab9f7..9914e2ed30 100644 --- a/src/apps/mplayerc/MainFrm.cpp +++ b/src/apps/mplayerc/MainFrm.cpp @@ -817,6 +817,10 @@ int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) // Hide all dockable bars by default for (const auto& pDockingBar : m_dockingbars) { + // Draw the bar frame / gripper / floating-frame border dark, like the playlist bar. Without + // this the Shader Editor, Capture, Navigation and Subresync bars render their sizing frame + // with light system colours (a bright "white frame", docked or floating). + pDockingBar->m_bUseDarkTheme = s.bUseDarkTheme; pDockingBar->ShowWindow(SW_HIDE); } @@ -7890,7 +7894,7 @@ void CMainFrame::OnViewRotate(UINT nID) } CString info; - info.Format(L"Rotation: %d�", rotation); + info.Format(L"Rotation: %d�", rotation); SendStatusMessage(info, 3000); } } diff --git a/src/apps/mplayerc/MediaTypesDlg.cpp b/src/apps/mplayerc/MediaTypesDlg.cpp index 5dd7719416..f702128df6 100644 --- a/src/apps/mplayerc/MediaTypesDlg.cpp +++ b/src/apps/mplayerc/MediaTypesDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "MediaTypesDlg.h" +#include "controls/DarkTheme.h" #include @@ -77,6 +78,8 @@ BOOL CMediaTypesDlg::OnInitDialog() SetMinTrackSize(CSize(300, 200)); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/OpenDlg.cpp b/src/apps/mplayerc/OpenDlg.cpp index f6e1058cdf..c29754029e 100644 --- a/src/apps/mplayerc/OpenDlg.cpp +++ b/src/apps/mplayerc/OpenDlg.cpp @@ -25,6 +25,7 @@ #include "OpenDlg.h" #include "FileDialogs.h" #include "MainFrm.h" +#include "controls/DarkTheme.h" // // COpenDlg dialog @@ -142,6 +143,8 @@ BOOL COpenDlg::OnInitDialog() pStat->SetIcon(m_hIcon); } + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/PPageInterface.cpp b/src/apps/mplayerc/PPageInterface.cpp index 2769751fc0..fbf65e6146 100644 --- a/src/apps/mplayerc/PPageInterface.cpp +++ b/src/apps/mplayerc/PPageInterface.cpp @@ -236,14 +236,18 @@ BOOL CPPageInterface::OnApply() pFrame->m_wndPreView.SetRelativeSize(s.iSmartSeekSize); - pFrame->m_wndPlaylistBar.m_bUseDarkTheme = s.bUseDarkTheme; + // Update every docking bar's dark-frame flag (not just the playlist bar) so the Shader Editor, + // Capture, Navigation and Subresync bar frames follow the theme toggle too, then repaint them. + for (const auto& pDockingBar : pFrame->m_dockingbars) { + pDockingBar->m_bUseDarkTheme = s.bUseDarkTheme; + if (pDockingBar->IsWindowVisible()) { + pDockingBar->SendMessageW(WM_NCPAINT, 1, NULL); + pDockingBar->RedrawWindow(nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE); + pDockingBar->Invalidate(); + } + } pFrame->SetColor(); pFrame->SetColorTitle(); - if (pFrame->m_wndPlaylistBar.IsWindowVisible()) { - pFrame->m_wndPlaylistBar.SendMessageW(WM_NCPAINT, 1, NULL); - pFrame->m_wndPlaylistBar.RedrawWindow(nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE); - pFrame->m_wndPlaylistBar.Invalidate(); - } pFrame->ResetMenu(); pFrame->m_wndStatusBar.SetMenu(); diff --git a/src/apps/mplayerc/PlayerBar.cpp b/src/apps/mplayerc/PlayerBar.cpp index 4625b226c6..b169156a28 100644 --- a/src/apps/mplayerc/PlayerBar.cpp +++ b/src/apps/mplayerc/PlayerBar.cpp @@ -20,9 +20,16 @@ #include "stdafx.h" #include "PlayerBar.h" +#include "Misc.h" // ThemeRGB +#include "controls/DarkTheme.h" IMPLEMENT_DYNAMIC(CPlayerBar, CSizingControlBarG) +COLORREF CPlayerBar::ColorThemeRGB(const int iR, const int iG, const int iB) const +{ + return ThemeRGB(iR, iG, iB); +} + CPlayerBar::CPlayerBar(void) : m_defDockBarID(0) { @@ -114,6 +121,15 @@ void CPlayerBar::OnWindowPosChanged(WINDOWPOS* lpwndpos) if (lpwndpos->flags & SWP_HIDEWINDOW) { GetParentFrame()->SetFocus(); } + + // When floated, the bar lives in an MFC mini-frame that is an independent top-level window whose + // caption Windows paints light. Apply the dark title bar (immersive dark mode + themed caption + // colour, like the main window and the Options dialogs) so the floating window matches the theme. + if (IsFloating() && AfxGetAppSettings().bDarkTitle) { + if (CFrameWnd* pMiniFrame = GetParentFrame()) { + DarkTheme::EnableForWindow(pMiniFrame->GetSafeHwnd()); + } + } } CSize CPlayerBar::CalcFixedLayout(BOOL bStretch, BOOL bHorz) diff --git a/src/apps/mplayerc/PlayerBar.h b/src/apps/mplayerc/PlayerBar.h index e12b1384fc..8b976f8b64 100644 --- a/src/apps/mplayerc/PlayerBar.h +++ b/src/apps/mplayerc/PlayerBar.h @@ -46,4 +46,9 @@ protected : afx_msg void OnWindowPosChanged(WINDOWPOS* lpwndpos); virtual CSize CalcFixedLayout(BOOL bStretch, BOOL bHorz) override; + + // The base returns 0 (black) — only the playlist bar used to override this, so every other + // docking bar (Shader Editor, Capture, Navigation, Subresync) painted its dark frame / gripper / + // close button pure black. Provide the themed colour here so all CPlayerBar-derived bars match. + COLORREF ColorThemeRGB(const int iR, const int iG, const int iB) const override; }; diff --git a/src/apps/mplayerc/PlayerCaptureDialog.cpp b/src/apps/mplayerc/PlayerCaptureDialog.cpp index 061a720b91..d6db966e56 100644 --- a/src/apps/mplayerc/PlayerCaptureDialog.cpp +++ b/src/apps/mplayerc/PlayerCaptureDialog.cpp @@ -23,6 +23,7 @@ #include "MainFrm.h" #include "PlayerCaptureDialog.h" #include "FileDialogs.h" +#include "controls/DarkTheme.h" #include #include "filters/muxer/WavDest/WavDest.h" #include "filters/muxer/MatroskaMuxer/MatroskaMuxer.h" @@ -1345,6 +1346,8 @@ BOOL CPlayerCaptureDialog::OnInitDialog() { __super::OnInitDialog(); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return FALSE; // return FALSE so that the dialog does not steal focus // EXCEPTION: OCX Property Pages should return FALSE } diff --git a/src/apps/mplayerc/PlaylistNameDlg.cpp b/src/apps/mplayerc/PlaylistNameDlg.cpp index 42ddc736b1..94e06258a5 100644 --- a/src/apps/mplayerc/PlaylistNameDlg.cpp +++ b/src/apps/mplayerc/PlaylistNameDlg.cpp @@ -20,6 +20,7 @@ #include "stdafx.h" #include "PlaylistNameDlg.h" +#include "controls/DarkTheme.h" IMPLEMENT_DYNAMIC(CPlaylistNameDlg, CCmdUIDialog) CPlaylistNameDlg::CPlaylistNameDlg(const CString& str, CWnd* pParent/* = nullptr*/) @@ -45,6 +46,8 @@ BOOL CPlaylistNameDlg::OnInitDialog() m_namectrl.SetWindowText(m_name); m_namectrl.SetSel(0, -1); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return FALSE; } diff --git a/src/apps/mplayerc/PnSPresetsDlg.cpp b/src/apps/mplayerc/PnSPresetsDlg.cpp index 0a84dd6559..5d9d8fb0bd 100644 --- a/src/apps/mplayerc/PnSPresetsDlg.cpp +++ b/src/apps/mplayerc/PnSPresetsDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "PnSPresetsDlg.h" +#include "controls/DarkTheme.h" // CPnSPresetsDlg dialog @@ -64,6 +65,8 @@ BOOL CPnSPresetsDlg::OnInitDialog() } } + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/ShaderAutoCompleteDlg.cpp b/src/apps/mplayerc/ShaderAutoCompleteDlg.cpp index 87cbb5b259..8199ae580e 100644 --- a/src/apps/mplayerc/ShaderAutoCompleteDlg.cpp +++ b/src/apps/mplayerc/ShaderAutoCompleteDlg.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "ShaderAutoCompleteDlg.h" +#include "controls/DarkTheme.h" // CShaderAutoCompleteDlg dialog @@ -163,6 +164,8 @@ BOOL CShaderAutoCompleteDlg::OnInitDialog() ::SendMessageW(m_hToolTipWnd, TTM_ADDTOOLW, 0, (LPARAM)&m_ti); ::SendMessageW(m_hToolTipWnd, TTM_SETMAXTIPWIDTH, 0, (LPARAM)400); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/ShaderCombineDlg.cpp b/src/apps/mplayerc/ShaderCombineDlg.cpp index d79d183d88..7e33a7ec57 100644 --- a/src/apps/mplayerc/ShaderCombineDlg.cpp +++ b/src/apps/mplayerc/ShaderCombineDlg.cpp @@ -22,6 +22,7 @@ #include "stdafx.h" #include "MainFrm.h" #include "ShaderCombineDlg.h" +#include "controls/DarkTheme.h" // CShaderCombineDlg dialog @@ -103,6 +104,8 @@ BOOL CShaderCombineDlg::OnInitDialog() UpdateData(FALSE); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/ShaderEditorDlg.cpp b/src/apps/mplayerc/ShaderEditorDlg.cpp index db37f0ec03..278c507d7a 100644 --- a/src/apps/mplayerc/ShaderEditorDlg.cpp +++ b/src/apps/mplayerc/ShaderEditorDlg.cpp @@ -23,6 +23,7 @@ #include "MainFrm.h" #include "ShaderNewDlg.h" #include "ShaderEditorDlg.h" +#include "controls/DarkTheme.h" // CShaderEdit @@ -221,6 +222,14 @@ BOOL CShaderEditorDlg::Create(CWnd* pParent) m_bD3D11 = (AfxGetAppSettings().m_VRSettings.iVideoRenderer == VIDRNDT_MPCVR && IsWindows8OrGreater()); + DarkTheme::ThemeDialog(GetSafeHwnd()); + if (DarkTheme::IsActive()) { + // The source editor's line-number margin is a self-drawn CStatic that paints its own light + // background (RGB(200,200,200)); recolour it so the left gutter isn't a bright strip. + m_edSrcdata.SetMarginBackgroundColor(DarkTheme::FaceColor(), TRUE); + m_edSrcdata.SetMarginForegroundColor(RGB(120, 125, 130), TRUE); + } + return TRUE; } diff --git a/src/apps/mplayerc/ShaderNewDlg.cpp b/src/apps/mplayerc/ShaderNewDlg.cpp index d620d171fb..6a29f3be34 100644 --- a/src/apps/mplayerc/ShaderNewDlg.cpp +++ b/src/apps/mplayerc/ShaderNewDlg.cpp @@ -20,6 +20,7 @@ #include "stdafx.h" #include "ShaderNewDlg.h" +#include "controls/DarkTheme.h" // CShaderNewDlg dialog @@ -46,6 +47,8 @@ BOOL CShaderNewDlg::OnInitDialog() UpdateData(FALSE); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/TunerScanDlg.cpp b/src/apps/mplayerc/TunerScanDlg.cpp index eb5aa404d9..d0e1ff29ff 100644 --- a/src/apps/mplayerc/TunerScanDlg.cpp +++ b/src/apps/mplayerc/TunerScanDlg.cpp @@ -22,6 +22,7 @@ #include "MainFrm.h" #include "TunerScanDlg.h" #include "DVBChannel.h" +#include "controls/DarkTheme.h" enum TSC_COLUMN { TSCC_NUMBER, @@ -69,6 +70,8 @@ BOOL CTunerScanDlg::OnInitDialog() m_Quality.SetRange(0, 100); m_btnSave.EnableWindow(FALSE); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/UpdateChecker.cpp b/src/apps/mplayerc/UpdateChecker.cpp index ac344ec604..b799637218 100644 --- a/src/apps/mplayerc/UpdateChecker.cpp +++ b/src/apps/mplayerc/UpdateChecker.cpp @@ -25,6 +25,7 @@ #include "DSUtil/HTTPAsync.h" #include "rapidjsonHelper.h" #include "UpdateChecker.h" +#include "controls/DarkTheme.h" #include "Version.h" @@ -128,6 +129,7 @@ UINT UpdateChecker::RunCheckForUpdateThread(LPVOID pParam) ASSERT(0); } + DarkTheme::CDarkMessageBoxHook mbHook; // dark-theme the result message box when the theme is on if (updateStatus == UPDATER_NEW_VERSION_IS_AVAILABLE) { if (IDYES == AfxMessageBox(text, nType)) { ShellExecuteW(nullptr, L"open", m_UpdateURL, nullptr, nullptr, SW_SHOWDEFAULT); diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 855115d261..caa34f7bb6 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -266,8 +266,11 @@ namespace DarkTheme if (::GetPropW(hWnd, L"MPC_UAC_SHIELD")) { static HICON s_hShield = nullptr; // cached once for the process lifetime if (!s_hShield) { + // Load the large (32px) shield rather than the small (16px) one: we scale + // it to the DPI-adjusted size below, and downscaling 32->size stays crisp + // where upscaling a 16px source would blur (visible at 150-200% DPI). SHSTOCKICONINFO sii = { sizeof(sii) }; - if (SUCCEEDED(::SHGetStockIconInfo(SIID_SHIELD, SHGSI_ICON | SHGSI_SMALLICON, &sii))) { + if (SUCCEEDED(::SHGetStockIconInfo(SIID_SHIELD, SHGSI_ICON, &sii))) { s_hShield = sii.hIcon; } else { s_hShield = ::LoadIconW(nullptr, IDI_SHIELD); @@ -283,7 +286,9 @@ namespace DarkTheme pDC->SetBkMode(TRANSPARENT); pDC->SetTextColor(disabled ? RGB(120, 125, 130) : TextColor()); - const int icon = hIcon ? 16 : 0; + // Scale the icon (UAC shield) by the DC's DPI so it isn't a tiny 16px glyph on a + // button that Windows has scaled up at 150-200% display scaling. + const int icon = hIcon ? ::MulDiv(16, pDC->GetDeviceCaps(LOGPIXELSY), 96) : 0; if (!text.IsEmpty() && (style & BS_MULTILINE)) { // Multi-line captions (e.g. the "AVI Splitter\nconfiguration" filter buttons) // must wrap: a single-line draw collapses the line break and overflows the @@ -830,7 +835,21 @@ namespace DarkTheme } else if (_wcsicmp(cls, L"ComboBox") == 0) { SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); } else if (_wcsicmp(cls, L"Edit") == 0) { - SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); + // DarkMode_CFD darkens the interior/border but leaves the control's own scrollbars + // light. For multiline edits that actually have scrollbars (the Command Line Switches + // help box, the Shader Editor source/output), use DarkMode_Explorer instead so the + // scrollbar is dark like the tree/list controls; the border is redrawn by + // ApplyDarkBorder either way, so we don't lose the CFD border styling that matters. + // DarkMode_CFD darkens the interior/border but leaves the control's own scrollbars + // light. For multiline edits that actually have scrollbars (the Command Line Switches + // help box, the Shader Editor source/output), use DarkMode_Explorer instead so the + // scrollbar is dark like the tree/list controls. + const LONG est = GetWindowLongW(hCtrl, GWL_STYLE); + if (est & (WS_VSCROLL | WS_HSCROLL)) { + SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); + } else { + SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); + } ApplyDarkBorder(hCtrl); } else if (_wcsicmp(cls, L"SysListView32") == 0) { // List-view controls ignore WM_CTLCOLOR: their background (the area @@ -1106,12 +1125,83 @@ namespace DarkTheme return; } LoadApi(); + // Ensure the process-wide immersive dark mode is on. Without it the native scrollbars of + // child edits/lists render light and the DarkMode_CFD border stays a light "frame" — visible + // when an auxiliary dialog (Command Line Switches, Shader Editor, ...) is opened directly, + // before the Options sheet (which used to be the only caller of this) has ever been shown. + AllowDarkModeForApp(); EnableForWindow(hDlg); // dark title bar + allow dark mode SetWindowSubclass(hDlg, DialogSubclassProc, kDialogSubclassId, 0); // dark bg / ctl colours ApplyThemeToChildren(hDlg); // theme the child controls // Repaint the dialog AND its child controls: InvalidateRect alone doesn't reach the // child windows, so freshly-themed checkboxes/statics would keep their stale light paint. - ::RedrawWindow(hDlg, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN); + // RDW_UPDATENOW forces a synchronous repaint so controls that otherwise only redraw on the + // next interaction (some third-party filter property-page widgets stayed light until hovered) + // pick up the theme immediately; RDW_FRAME also refreshes their non-client border. + ::RedrawWindow(hDlg, nullptr, nullptr, + RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW); + } + + // --- Dark message boxes (MessageBox / AfxMessageBox) via a thread-local CBT hook --------------- + // A message box is a standard "#32770" dialog the OS creates and paints with system colours; the + // only way to theme it is to catch its creation and re-theme it. CDarkMessageBoxHook installs a + // WH_CBT hook for the lifetime of the guard; when the message box activates we run ThemeDialog on + // it. Scope the guard tightly around the AfxMessageBox call so nothing else is affected. + namespace { + HHOOK g_msgBoxHook = nullptr; + const UINT_PTR kMsgBoxSubclassId = 11; + + // The message box repaints its lower button "band" with a light system colour in its own + // WM_PAINT, over the dark erase ThemeDialog installs. Paint the whole client dark ourselves and + // skip the default paint so the band stays dark; the icon / text / buttons are child windows and + // repaint on top. Installed on top of ThemeDialog's subclass, so WM_CTLCOLOR* still go dark. + LRESULT CALLBACK MsgBoxSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_PAINT: { + PAINTSTRUCT ps; + HDC hdc = ::BeginPaint(hWnd, &ps); + CRect rc; + ::GetClientRect(hWnd, &rc); + CDC::FromHandle(hdc)->FillSolidRect(rc, FaceColor()); + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, MsgBoxSubclassProc, kMsgBoxSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + LRESULT CALLBACK MsgBoxCBTProc(int code, WPARAM wParam, LPARAM lParam) { + if (code == HCBT_ACTIVATE) { + HWND hWnd = reinterpret_cast(wParam); + wchar_t cls[16] = {}; + ::GetClassNameW(hWnd, cls, _countof(cls)); + if (wcscmp(cls, L"#32770") == 0) { // the dialog class MessageBox uses + ThemeDialog(hWnd); + SetWindowSubclass(hWnd, MsgBoxSubclassProc, kMsgBoxSubclassId, 0); + ::RedrawWindow(hWnd, nullptr, nullptr, + RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW); + } + } + return ::CallNextHookEx(g_msgBoxHook, code, wParam, lParam); + } + } + + CDarkMessageBoxHook::CDarkMessageBoxHook() : m_hooked(false) { + // One hook per thread at a time; nested message boxes reuse the outer guard's hook. + if (IsActive() && !g_msgBoxHook) { + g_msgBoxHook = ::SetWindowsHookExW(WH_CBT, MsgBoxCBTProc, nullptr, ::GetCurrentThreadId()); + m_hooked = (g_msgBoxHook != nullptr); + } + } + + CDarkMessageBoxHook::~CDarkMessageBoxHook() { + if (m_hooked && g_msgBoxHook) { + ::UnhookWindowsHookEx(g_msgBoxHook); + g_msgBoxHook = nullptr; + } } void RefreshTheme(HWND hRoot) { diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h index a7c5281140..2f84a224c9 100644 --- a/src/apps/mplayerc/controls/DarkTheme.h +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -74,6 +74,22 @@ namespace DarkTheme // Call once from the dialog's OnInitDialog. void ThemeDialog(HWND hDlg); + // RAII guard that dark-themes standard Win32 message boxes (MessageBox / AfxMessageBox) shown on + // the current thread while it is alive: a thread-local WH_CBT hook catches the message box window + // (class "#32770") as it activates and runs ThemeDialog on it (dark title, dark background, light + // text, themed buttons). No-op when the dark theme is inactive. Message boxes are drawn by the OS + // with system colours and can't be themed any other way. Construct one on the stack immediately + // before the AfxMessageBox/MessageBox call, e.g. the "Check for Updates" result dialog. + class CDarkMessageBoxHook { + public: + CDarkMessageBoxHook(); + ~CDarkMessageBoxHook(); + CDarkMessageBoxHook(const CDarkMessageBoxHook&) = delete; + CDarkMessageBoxHook& operator=(const CDarkMessageBoxHook&) = delete; + private: + bool m_hooked; + }; + // Re-applies or removes the dark theme across a whole window tree in response to the // "Use dark theme" toggle being changed at runtime (Interface page + Apply). Handles both // directions — applying when now active, stripping every subclass/override when now From 08642e4268eebce39236e79f98fefd4ecede8c5c Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 17:58:50 -0300 Subject: [PATCH 09/24] Dark Options: theme every message box via a UI-thread CBT hook The updater message box was themed via a scoped guard, but the app shows ~70 other MessageBox/AfxMessageBox dialogs (Reset settings, Export settings, error prompts, ...) that stayed light. Wrapping each call site is impractical; install one persistent WH_CBT hook on the UI thread that themes every standard message box as it activates. It only touches genuine message boxes: class "#32770", not already carrying our DialogSubclassProc (so the Options sheet and the auxiliary dialogs, which theme themselves, are skipped) and containing solely static labels + push buttons (so real dialogs with a tree / tabs / edits are left alone). The hook checks the theme state each time, so it follows the "Use dark theme" toggle. Worker-thread message boxes (the updater) keep the scoped CDarkMessageBoxHook, since a thread hook only sees its own thread. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/MainFrm.cpp | 5 ++ src/apps/mplayerc/controls/DarkTheme.cpp | 77 +++++++++++++++++++----- src/apps/mplayerc/controls/DarkTheme.h | 9 +++ 3 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/apps/mplayerc/MainFrm.cpp b/src/apps/mplayerc/MainFrm.cpp index 9914e2ed30..7a6813ffaf 100644 --- a/src/apps/mplayerc/MainFrm.cpp +++ b/src/apps/mplayerc/MainFrm.cpp @@ -71,6 +71,7 @@ #include #include "filters/ffmpeg_link_fix.h" #include "ComPropertySheet.h" +#include "controls/DarkTheme.h" #include #include @@ -824,6 +825,10 @@ int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) pDockingBar->ShowWindow(SW_HIDE); } + // Dark-theme every standard message box shown on the UI thread (Reset/Export settings, error + // prompts, ...) without wrapping each call site. Follows the theme toggle; no-op when it's off. + DarkTheme::InstallMessageBoxHook(); + m_fileDropTarget.Register(this); GetDesktopWindow()->GetWindowRect(&m_rcDesktop); diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index caa34f7bb6..61eb77b34f 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -1142,13 +1142,14 @@ namespace DarkTheme RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN | RDW_UPDATENOW); } - // --- Dark message boxes (MessageBox / AfxMessageBox) via a thread-local CBT hook --------------- + // --- Dark message boxes (MessageBox / AfxMessageBox) via a WH_CBT hook ------------------------- // A message box is a standard "#32770" dialog the OS creates and paints with system colours; the - // only way to theme it is to catch its creation and re-theme it. CDarkMessageBoxHook installs a - // WH_CBT hook for the lifetime of the guard; when the message box activates we run ThemeDialog on - // it. Scope the guard tightly around the AfxMessageBox call so nothing else is affected. + // only way to theme it is to catch it as it activates and re-theme it. A persistent hook on the UI + // thread (InstallMessageBoxHook) covers all main-thread message boxes; CDarkMessageBoxHook is a + // scoped guard for the few shown from a worker thread (a thread hook only sees its own thread). namespace { - HHOOK g_msgBoxHook = nullptr; + HHOOK g_msgBoxHook = nullptr; // scoped (CDarkMessageBoxHook) + HHOOK g_persistentMsgBoxHook = nullptr; // process-lifetime (InstallMessageBoxHook) const UINT_PTR kMsgBoxSubclassId = 11; // The message box repaints its lower button "band" with a light system colour in its own @@ -1173,24 +1174,68 @@ namespace DarkTheme return DefSubclassProc(hWnd, msg, wParam, lParam); } + // A standard message box holds only static labels / icon and push buttons. Any other control + // class (edit, combo, tree, tab, list, or a nested dialog / property page) means it's a real + // dialog that themes itself — the hook must not blanket-theme those (that would re-theme the + // Options sheet and every aux dialog on activation and risk breaking them). + BOOL CALLBACK MsgBoxChildProc(HWND hChild, LPARAM lParam) { + auto* flags = reinterpret_cast(lParam); // [0] = only statics/buttons, [1] = has a button + wchar_t cls[32] = {}; + GetClassNameW(hChild, cls, _countof(cls)); + if (_wcsicmp(cls, L"Button") == 0) { + flags[1] = true; + } else if (_wcsicmp(cls, L"Static") != 0) { + flags[0] = false; + return FALSE; // found a non-message-box control, stop + } + return TRUE; + } + + bool LooksLikeMessageBox(HWND hWnd) { + bool flags[2] = { true, false }; + EnumChildWindows(hWnd, MsgBoxChildProc, reinterpret_cast(flags)); + return flags[0] && flags[1]; + } + + void ThemeMessageBoxWindow(HWND hWnd) { + if (!IsActive() || !hWnd) { + return; + } + wchar_t cls[16] = {}; + ::GetClassNameW(hWnd, cls, _countof(cls)); + if (wcscmp(cls, L"#32770") != 0) { + return; // not the dialog class MessageBox uses + } + DWORD_PTR ref = 0; + if (GetWindowSubclass(hWnd, DialogSubclassProc, kDialogSubclassId, &ref)) { + return; // one of our own dialogs (it themes itself) — leave it alone + } + if (!LooksLikeMessageBox(hWnd)) { + return; // a real dialog, not a message box + } + ThemeDialog(hWnd); + SetWindowSubclass(hWnd, MsgBoxSubclassProc, kMsgBoxSubclassId, 0); + ::RedrawWindow(hWnd, nullptr, nullptr, + RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW); + } + LRESULT CALLBACK MsgBoxCBTProc(int code, WPARAM wParam, LPARAM lParam) { if (code == HCBT_ACTIVATE) { - HWND hWnd = reinterpret_cast(wParam); - wchar_t cls[16] = {}; - ::GetClassNameW(hWnd, cls, _countof(cls)); - if (wcscmp(cls, L"#32770") == 0) { // the dialog class MessageBox uses - ThemeDialog(hWnd); - SetWindowSubclass(hWnd, MsgBoxSubclassProc, kMsgBoxSubclassId, 0); - ::RedrawWindow(hWnd, nullptr, nullptr, - RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW); - } + ThemeMessageBoxWindow(reinterpret_cast(wParam)); } - return ::CallNextHookEx(g_msgBoxHook, code, wParam, lParam); + return ::CallNextHookEx(nullptr, code, wParam, lParam); // hhk is ignored on modern Windows + } + } + + void InstallMessageBoxHook() { + // Persistent, checks IsActive() per message box (so it follows the theme toggle), installed once. + if (!g_persistentMsgBoxHook) { + g_persistentMsgBoxHook = ::SetWindowsHookExW(WH_CBT, MsgBoxCBTProc, nullptr, ::GetCurrentThreadId()); } } CDarkMessageBoxHook::CDarkMessageBoxHook() : m_hooked(false) { - // One hook per thread at a time; nested message boxes reuse the outer guard's hook. + // One scoped hook per thread at a time; nested message boxes reuse the outer guard's hook. if (IsActive() && !g_msgBoxHook) { g_msgBoxHook = ::SetWindowsHookExW(WH_CBT, MsgBoxCBTProc, nullptr, ::GetCurrentThreadId()); m_hooked = (g_msgBoxHook != nullptr); diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h index 2f84a224c9..f1c485b65e 100644 --- a/src/apps/mplayerc/controls/DarkTheme.h +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -90,6 +90,15 @@ namespace DarkTheme bool m_hooked; }; + // Installs a process-lifetime WH_CBT hook on the calling (main UI) thread that dark-themes every + // standard message box shown on that thread, so we don't have to wrap each of the ~70 + // AfxMessageBox/MessageBox call sites. Only windows that actually look like a message box (class + // "#32770" containing solely static labels + push buttons, not one of our own themed dialogs) are + // touched. The hook checks the theme state each time, so it follows the "Use dark theme" toggle. + // Call once, e.g. from the main frame's OnCreate. Idempotent. Worker-thread message boxes (the + // updater) still use CDarkMessageBoxHook, since a thread hook only sees its own thread. + void InstallMessageBoxHook(); + // Re-applies or removes the dark theme across a whole window tree in response to the // "Use dark theme" toggle being changed at runtime (Interface page + Apply). Handles both // directions — applying when now active, stripping every subclass/override when now From 90935ddbbf0b44fae4adedf20bc64ba025a5c775 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sun, 5 Jul 2026 23:01:23 -0300 Subject: [PATCH 10/24] Dark Options: theme the remaining tab controls and the File Properties sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner spotted the Organize Favorites dialog's Files/DVDs tab rendering with a light body/frame. Native tab controls ignore SetWindowTheme, so ThemeDialog can't darken them — they need the owner-drawn CDarkTabCtrl. - Organize Favorites: its CTabCtrl is now a CDarkTabCtrl (drop-in; falls back to native when the theme is off). - File Properties (CPPageFileInfoSheet): the whole property sheet was un-themed. Theme the frame + attach CDarkTabCtrl to its tab, and each page (Details / Clip / MediaInfo) dark-themes itself in its OnInitDialog (the Res page already does, via CPPageBase). Sweep confirmed these were the last light tabs/sheets: every SysTabControl32 in the app (Organize Favorites, Internal Filters, filter-config sheet) now uses CDarkTabCtrl, and both CPropertySheet-derived dialogs are themed. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/FavoriteOrganizeDlg.h | 3 ++- src/apps/mplayerc/PPageFileInfoClip.cpp | 3 +++ src/apps/mplayerc/PPageFileInfoDetails.cpp | 3 +++ src/apps/mplayerc/PPageFileInfoSheet.cpp | 15 +++++++++++++++ src/apps/mplayerc/PPageFileInfoSheet.h | 3 +++ src/apps/mplayerc/PPageFileMediaInfo.cpp | 3 +++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/apps/mplayerc/FavoriteOrganizeDlg.h b/src/apps/mplayerc/FavoriteOrganizeDlg.h index 7c923b31af..baa24abbeb 100644 --- a/src/apps/mplayerc/FavoriteOrganizeDlg.h +++ b/src/apps/mplayerc/FavoriteOrganizeDlg.h @@ -24,6 +24,7 @@ #include #include #include +#include "controls/DarkTabCtrl.h" // CFavoriteOrganizeDlg dialog @@ -42,7 +43,7 @@ class CFavoriteOrganizeDlg : public CResizableDialog enum { IDD = IDD_FAVORGANIZE }; - CTabCtrl m_tab; + CDarkTabCtrl m_tab; // owner-drawn dark tab (falls back to native when the dark theme is off) CListCtrl m_list; protected: diff --git a/src/apps/mplayerc/PPageFileInfoClip.cpp b/src/apps/mplayerc/PPageFileInfoClip.cpp index 054caf217f..19cf6faaac 100644 --- a/src/apps/mplayerc/PPageFileInfoClip.cpp +++ b/src/apps/mplayerc/PPageFileInfoClip.cpp @@ -22,6 +22,7 @@ #include "stdafx.h" #include "MainFrm.h" #include "PPageFileInfoClip.h" +#include "controls/DarkTheme.h" #include @@ -232,6 +233,8 @@ BOOL CPPageFileInfoClip::OnInitDialog() UpdateData(FALSE); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/PPageFileInfoDetails.cpp b/src/apps/mplayerc/PPageFileInfoDetails.cpp index 4b71282c3e..819d01571d 100644 --- a/src/apps/mplayerc/PPageFileInfoDetails.cpp +++ b/src/apps/mplayerc/PPageFileInfoDetails.cpp @@ -21,6 +21,7 @@ #include "stdafx.h" #include "PPageFileInfoDetails.h" +#include "controls/DarkTheme.h" #include #include #include @@ -229,6 +230,8 @@ BOOL CPPageFileInfoDetails::OnInitDialog() m_encoding.SetWindowTextW(m_encodingText); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/PPageFileInfoSheet.cpp b/src/apps/mplayerc/PPageFileInfoSheet.cpp index 204f154b2c..6bd1c01529 100644 --- a/src/apps/mplayerc/PPageFileInfoSheet.cpp +++ b/src/apps/mplayerc/PPageFileInfoSheet.cpp @@ -24,6 +24,11 @@ #include "PPageFileInfoSheet.h" #include "PPageFileMediaInfo.h" #include "FileDialogs.h" +#include "controls/DarkTheme.h" + +// windowsx.h defines a function-like SubclassWindow(hwnd, lpfn) macro that collides with +// CWnd::SubclassWindow(HWND); undef it so the MFC method call below parses correctly. +#undef SubclassWindow IMPLEMENT_DYNAMIC(CMPCPropertySheet, CPropertySheet) CMPCPropertySheet::CMPCPropertySheet(LPCWSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage) @@ -120,6 +125,16 @@ BOOL CPPageFileInfoSheet::OnInitDialog() ModifyStyle(0, WS_MAXIMIZEBOX); + // Dark-theme the property-sheet frame + the two dynamically-created MediaInfo buttons, and attach + // the owner-drawn dark tab (the stock tab keeps a light body/frame; it falls back to native when + // the theme is off). Each page dark-themes itself in its own OnInitDialog. + if (DarkTheme::IsActive()) { + if (CTabCtrl* pTab = GetTabControl()) { + m_dark_tab.SubclassWindow(pTab->GetSafeHwnd()); + } + } + DarkTheme::ThemeDialog(GetSafeHwnd()); + for (int i = 0; i < GetPageCount(); i++) { DWORD nID = GetResourceId(i); if (nID == s.nLastFileInfoPage) { diff --git a/src/apps/mplayerc/PPageFileInfoSheet.h b/src/apps/mplayerc/PPageFileInfoSheet.h index b587419663..75e0b5b389 100644 --- a/src/apps/mplayerc/PPageFileInfoSheet.h +++ b/src/apps/mplayerc/PPageFileInfoSheet.h @@ -26,6 +26,7 @@ #include "PPageFileInfoDetails.h" #include "PPageFileInfoRes.h" #include "PPageFileMediaInfo.h" +#include "controls/DarkTabCtrl.h" class CMainFrame; @@ -75,6 +76,8 @@ class CPPageFileInfoSheet : public CMPCPropertySheet, public CDPI CButton m_Button_MI_SaveAs; CButton m_Button_MI_Clipboard; + CDarkTabCtrl m_dark_tab; // owner-drawn dark tab attached in OnInitDialog when the dark theme is on + BOOL m_bNeedInit = TRUE; CRect m_rCrt; CRect m_rWnd; diff --git a/src/apps/mplayerc/PPageFileMediaInfo.cpp b/src/apps/mplayerc/PPageFileMediaInfo.cpp index b9fd91ecc2..9c02ff0b43 100644 --- a/src/apps/mplayerc/PPageFileMediaInfo.cpp +++ b/src/apps/mplayerc/PPageFileMediaInfo.cpp @@ -22,6 +22,7 @@ #include "MainFrm.h" #include "PPageFileMediaInfo.h" #include "DSUtil/FileHandle.h" +#include "controls/DarkTheme.h" static MediaInfoLib::String mi_get_lang_file() { @@ -117,6 +118,8 @@ BOOL CPPageFileMediaInfo::OnInitDialog() } OnComboFileChange(); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } From beaf1e2f5ee801d93954f4f10737618c915a1e14 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Mon, 6 Jul 2026 04:21:08 -0300 Subject: [PATCH 11/24] Fix crash opening the Window Size options page (upstream typo) Not a dark-theme issue: commit d4ac8ca66 (v0lt, "use global arrays for value sets", came in via the merge with master) populates the scale-level combo with str.Format(L"%d%", scale). The trailing lone "%" is a malformed format string; MFC's FormatV calls _vscwprintf, which returns -1, and the subsequent GetBuffer(-1) crashes. Reproduces with the dark theme off and is absent from the older official release, confirming it's the upstream change. Fix the format to L"%d%%" (also what was intended: the combo now shows "50%" / "100%" / "200%" instead of "50" / "100" / "200"). Should be fixed upstream in PPageWindowSize.cpp too. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/PPageWindowSize.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/mplayerc/PPageWindowSize.cpp b/src/apps/mplayerc/PPageWindowSize.cpp index 3cf8dcc1fe..e669b22b90 100644 --- a/src/apps/mplayerc/PPageWindowSize.cpp +++ b/src/apps/mplayerc/PPageWindowSize.cpp @@ -75,7 +75,7 @@ BOOL CPPageWindowSize::OnInitDialog() CStringW str; for (const auto scale : g_AutoScaleFactors) { - str.Format(L"%d%", scale); + str.Format(L"%d%%", scale); AddStringData(m_cmbScaleLevel, str, scale); } SelectByItemData(m_cmbScaleLevel, s.nAutoScaleFactor); From f2ba140166bbc6aee9a290314f4bf818b31f4175 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Mon, 6 Jul 2026 04:21:25 -0300 Subject: [PATCH 12/24] Dark Options: theme owner-drawn list rows, the Subresync grid, and fix listview artefacts Owner-drawn / custom-drawn list content isn't reached by ThemeDialog, and a couple of list-view rendering artefacts showed up on the dark background: - Organize Favorites list: it's owner-drawn (OnDrawItem) with fixed light colours (COLOR_WINDOW background, black text) -> rows stayed white. Paint the rows in the dark palette (dark background, light text, celeste-tinted selection). - Subresync bar grid: custom-drawn with fixed light colours, and the bar isn't run through ThemeDialog, so the empty list had a white background, border and column header. Remap the per-row colours to the dark palette (keeping the play-position / edited-marker / alternating-row semantics) and theme the control (dark background / border / header). - ListViewSubclassProc: fill the empty area below the last item dark, which covers the light column separator a report list draws in an empty body (the stray vertical line in the empty Organize Favorites list); and offset the custom vertical grid lines by the header's scroll origin so they follow a horizontal scroll instead of ghosting at the unscrolled column positions (the Keys list), forcing a full repaint on WM_HSCROLL. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/FavoriteOrganizeDlg.cpp | 17 ++++-- src/apps/mplayerc/PlayerSubresyncBar.cpp | 41 +++++++++++---- src/apps/mplayerc/controls/DarkTheme.cpp | 63 +++++++++++++++++++++-- 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/apps/mplayerc/FavoriteOrganizeDlg.cpp b/src/apps/mplayerc/FavoriteOrganizeDlg.cpp index 304bfbeef3..debc96df6f 100644 --- a/src/apps/mplayerc/FavoriteOrganizeDlg.cpp +++ b/src/apps/mplayerc/FavoriteOrganizeDlg.cpp @@ -214,20 +214,29 @@ void CFavoriteOrganizeDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStr CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); + // This list is owner-drawn, so ThemeDialog can't recolour it — paint the rows in the dark palette + // ourselves (the fixed light COLOR_WINDOW / black text left the items white on the dark theme). + const bool dark = DarkTheme::IsActive(); + CBrush b; if (!!m_list.GetItemState(nItem, LVIS_SELECTED)) { - b.CreateSolidBrush(0xf1dacc); + b.CreateSolidBrush(dark ? RGB(38, 79, 120) : 0xf1dacc); pDC->FillRect(rcItem, &b); b.DeleteObject(); - b.CreateSolidBrush(0xc56a31); + b.CreateSolidBrush(dark ? RGB(76, 194, 255) : 0xc56a31); pDC->FrameRect(rcItem, &b); } else { - b.CreateSysColorBrush(COLOR_WINDOW); + if (dark) { + b.CreateSolidBrush(DarkTheme::FaceColor()); + } else { + b.CreateSysColorBrush(COLOR_WINDOW); + } pDC->FillRect(rcItem, &b); } CStringW str; - pDC->SetTextColor(0); + pDC->SetBkMode(TRANSPARENT); // don't paint a light box behind the text over the dark row + pDC->SetTextColor(dark ? DarkTheme::TextColor() : 0); str = m_list.GetItemText(nItem, 0); pDC->TextOut(rcItem.left + 3, (rcItem.top + rcItem.bottom - pDC->GetTextExtent(str).cy) / 2, str); diff --git a/src/apps/mplayerc/PlayerSubresyncBar.cpp b/src/apps/mplayerc/PlayerSubresyncBar.cpp index 69e7b2dbd8..2f9fb2d32f 100644 --- a/src/apps/mplayerc/PlayerSubresyncBar.cpp +++ b/src/apps/mplayerc/PlayerSubresyncBar.cpp @@ -22,6 +22,7 @@ #include "stdafx.h" #include "MainFrm.h" #include "PlayerSubresyncBar.h" +#include "controls/DarkTheme.h" // CPlayerSubresyncBar @@ -52,6 +53,11 @@ BOOL CPlayerSubresyncBar::Create(CWnd* pParentWnd, UINT defDockBarID, CCritSec* m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER); + // This bar isn't run through ThemeDialog, so the list kept a white empty background, a light + // WS_EX_CLIENTEDGE border and a light column header. Theme the control directly (dark background / + // border / header; per-item colours are still handled by our NM_CUSTOMDRAW). No-op when off. + DarkTheme::ApplyThemeToControl(m_list.GetSafeHwnd()); + return TRUE; } @@ -1180,37 +1186,50 @@ void CPlayerSubresyncBar::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) COLORREF clrText; COLORREF clrTextBk; + // This subtitle grid is custom-drawn with fixed light colours; remap them to the dark palette + // when the dark theme is on, keeping the same semantics (editable vs read-only columns, + // alternating groups, the currently-playing line, the edited start/end markers). + const bool dark = DarkTheme::IsActive(); + if ((pLVCD->iSubItem == COL_START || pLVCD->iSubItem == COL_END || pLVCD->iSubItem == COL_TEXT || pLVCD->iSubItem == COL_STYLE || pLVCD->iSubItem == COL_LAYER || pLVCD->iSubItem == COL_ACTOR || pLVCD->iSubItem == COL_EFFECT) && m_mode == TEXTSUB) { - clrText = 0; + clrText = dark ? DarkTheme::TextColor() : (COLORREF)0; } else if ((pLVCD->iSubItem == COL_START) && m_mode == VOBSUB) { - clrText = 0; + clrText = dark ? DarkTheme::TextColor() : (COLORREF)0; } else { - clrText = 0x606060; + clrText = dark ? RGB(120, 125, 130) : (COLORREF)0x606060; } - clrTextBk = 0xffffff; - //if (s_totalGroups > 0) - clrTextBk -= ((s_itemGroups[pLVCD->nmcd.dwItemSpec] & 1) ? 0x100010 : 0x200020); + if (dark) { + // two subtly different dark shades for the alternating groups + clrTextBk = (s_itemGroups[pLVCD->nmcd.dwItemSpec] & 1) ? DarkTheme::FaceColor() : DarkTheme::CtrlBackColor(); + } else { + clrTextBk = 0xffffff; + //if (s_totalGroups > 0) + clrTextBk -= ((s_itemGroups[pLVCD->nmcd.dwItemSpec] & 1) ? 0x100010 : 0x200020); + } if (m_sts[pLVCD->nmcd.dwItemSpec].start <= m_rt / 10000 && m_rt / 10000 < m_sts[pLVCD->nmcd.dwItemSpec].end) { - clrText |= 0xFF; + clrText |= 0xFF; // tint the currently-playing line } int nCheck = (int)m_list.GetItemData((int)pLVCD->nmcd.dwItemSpec); + const COLORREF clrEditStrong = dark ? RGB(50, 70, 100) : (COLORREF)0xffddbb; // edited start/end marker + const COLORREF clrEditWeak = dark ? RGB(40, 55, 78) : (COLORREF)0xffeedd; // preview marker + if ((nCheck & 1) && (pLVCD->iSubItem == COL_START || pLVCD->iSubItem == COL_PREVSTART)) { - clrTextBk = 0xffddbb; + clrTextBk = clrEditStrong; } else if ((nCheck & 4) && (/*pLVCD->iSubItem == COL_START ||*/ pLVCD->iSubItem == COL_PREVSTART)) { - clrTextBk = 0xffeedd; + clrTextBk = clrEditWeak; } if ((nCheck & 2) && (pLVCD->iSubItem == COL_END || pLVCD->iSubItem == COL_PREVEND)) { - clrTextBk = 0xffddbb; + clrTextBk = clrEditStrong; } else if ((nCheck & 8) && (/*pLVCD->iSubItem == COL_END ||*/ pLVCD->iSubItem == COL_PREVEND)) { - clrTextBk = 0xffeedd; + clrTextBk = clrEditWeak; } pLVCD->clrText = clrText; diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 61eb77b34f..36b7bc6b5c 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -385,14 +385,21 @@ namespace DarkTheme ::LineTo(hdc, rcClient.right, r.bottom - 1); } - // vertical line at each column's right edge, through the item rows only + // vertical line at each column's right edge, through the item rows only. Header_GetItemRect + // is relative to the header window, which is scrolled left when the list is scrolled + // horizontally; map the header's client origin into the list-view's client so the lines + // follow the scroll (otherwise they stay at the unscrolled column positions). if (hHeader) { + POINT hdrOrg = { 0, 0 }; + ::ClientToScreen(hHeader, &hdrOrg); + ::ScreenToClient(hWnd, &hdrOrg); // hdrOrg.x = -horizontal scroll offset const int cols = static_cast(::SendMessageW(hHeader, HDM_GETITEMCOUNT, 0, 0)); for (int c = 0; c < cols; ++c) { RECT hr{}; if (Header_GetItemRect(hHeader, c, &hr)) { - ::MoveToEx(hdc, hr.right - 1, headerH, nullptr); - ::LineTo(hdc, hr.right - 1, gridBottom); + const int x = hdrOrg.x + hr.right - 1; + ::MoveToEx(hdc, x, headerH, nullptr); + ::LineTo(hdc, x, gridBottom); } } } @@ -402,6 +409,41 @@ namespace DarkTheme ::ReleaseDC(hWnd, hdc); } + // A report list-view draws faint column separators (and, past the last item, a light + // "empty" strip) that the light system theme provides; on the dark background they show as + // a bright vertical line (e.g. the empty Organize Favorites list) or a light band. Overpaint + // the area from the last item's bottom down to the client bottom with the dark face colour. + // For an empty list that is the whole body, which covers the stray column separator. + void FillListEmptyArea(HWND hWnd) { + if ((GetWindowLongW(hWnd, GWL_STYLE) & LVS_TYPEMASK) != LVS_REPORT) { + return; + } + CRect rcClient; + ::GetClientRect(hWnd, &rcClient); + + int top = rcClient.top; + const int count = static_cast(::SendMessageW(hWnd, LVM_GETITEMCOUNT, 0, 0)); + if (count > 0) { + RECT rcLast{}; rcLast.left = LVIR_BOUNDS; + ::SendMessageW(hWnd, LVM_GETITEMRECT, count - 1, reinterpret_cast(&rcLast)); + top = rcLast.bottom; + } else if (HWND hHeader = reinterpret_cast(::SendMessageW(hWnd, LVM_GETHEADER, 0, 0))) { + if (::IsWindowVisible(hHeader)) { + RECT rh; ::GetClientRect(hHeader, &rh); + top = rcClient.top + rh.bottom; + } + } + if (top < rcClient.bottom) { + if (HDC hdc = ::GetDC(hWnd)) { + RECT rcEmpty = { rcClient.left, top, rcClient.right, rcClient.bottom }; + HBRUSH br = ::CreateSolidBrush(FaceColor()); + ::FillRect(hdc, &rcEmpty, br); + ::DeleteObject(br); + ::ReleaseDC(hWnd, hdc); + } + } + } + // A list-view's column header (SysHeader32) is not darkened by SetWindowTheme // and it sends its NM_CUSTOMDRAW to the list-view (its parent), not to the // dialog, so we subclass the list-view and paint the header ourselves. When the @@ -409,9 +451,20 @@ namespace DarkTheme const UINT_PTR kListViewSubclassId = 2; LRESULT CALLBACK ListViewSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR dwRefData) { - if (msg == WM_PAINT && dwRefData) { + if (msg == WM_PAINT) { + const LRESULT res = DefSubclassProc(hWnd, msg, wParam, lParam); + FillListEmptyArea(hWnd); // dark over the light empty strip / stray column separator + if (dwRefData) { + DrawListGridlines(hWnd); // dark grid (only for lists that originally had grid lines) + } + return res; + } + if (msg == WM_HSCROLL && dwRefData) { + // A horizontal scroll bit-blts the client, smearing our overpainted vertical grid lines + // (they end up doubled / misaligned, e.g. the Keys list). Force a full repaint so they + // are redrawn at the new offset. Only needed for grid lists (double-buffered → no flicker). const LRESULT res = DefSubclassProc(hWnd, msg, wParam, lParam); - DrawListGridlines(hWnd); + ::InvalidateRect(hWnd, nullptr, FALSE); return res; } if (msg == WM_NOTIFY) { From cb5d2262f7c999df0c73b65d15d206d04fb0f27d Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Mon, 6 Jul 2026 04:47:09 -0300 Subject: [PATCH 13/24] Dark Options: hover/pressed states for sliders, and owner-draw all of them xLn2 noted some sliders had no hover/pressed feedback, and the ones left on the native control showed ugly black-on-hover / white-when-pressed thumbs (the native DarkMode trackbar states). - TrackbarSubclassProc now tracks the thumb state (WM_MOUSEMOVE + TrackMouse- Event for hover over the thumb, WM_LBUTTONDOWN/UP for pressed) and paints the thumb lighter on hover / brightest while dragging (disabled overrides). The mouse messages pass through to the default proc so dragging still works. - ThemeControl now owner-draws EVERY trackbar (not just the theme R/G/B and subtitle sliders that were explicitly converted), so OSD / Colour correction / Sound processing sliders get the same dark celeste thumb + states instead of the native black/white ones. Trackbars already subclassed are skipped so the theme sliders keep their frozen-colour snapshot flag (dwData = 1). Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/controls/DarkTheme.cpp | 59 +++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 36b7bc6b5c..57e9579011 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -680,13 +680,57 @@ namespace DarkTheme RECT rcTh{}; ::SendMessageW(hWnd, TBM_GETTHUMBRECT, 0, reinterpret_cast(&rcTh)); CRect th(rcTh); + // Thumb states: dragged (pressed) > hovered > normal, so the slider gives the same + // visual feedback as a native one. Disabled overrides all. const bool disabled = (::GetWindowLongW(hWnd, GWL_STYLE) & WS_DISABLED) != 0; - pDC->FillSolidRect(th, disabled ? ThemeRGB(90, 95, 100) : RGB(76, 194, 255)); // celeste thumb + const bool pressed = ::GetPropW(hWnd, L"MPC_TB_PRESSED") != nullptr; + const bool hover = ::GetPropW(hWnd, L"MPC_TB_HOVER") != nullptr; + COLORREF thumbClr; + if (disabled) { thumbClr = ThemeRGB(90, 95, 100); } + else if (pressed) { thumbClr = RGB(150, 224, 255); } // brightest while dragging + else if (hover) { thumbClr = RGB(115, 210, 255); } // lighter on hover + else { thumbClr = RGB(76, 194, 255); } // celeste + pDC->FillSolidRect(th, thumbClr); ::EndPaint(hWnd, &ps); return 0; } + case WM_MOUSEMOVE: { + // Hover only when the cursor is over the thumb itself, not the whole track. + RECT rcTh{}; + ::SendMessageW(hWnd, TBM_GETTHUMBRECT, 0, reinterpret_cast(&rcTh)); + const POINT pt = { static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam)) }; + const bool over = ::PtInRect(&rcTh, pt) != FALSE; + if (over != (::GetPropW(hWnd, L"MPC_TB_HOVER") != nullptr)) { + if (over) { ::SetPropW(hWnd, L"MPC_TB_HOVER", reinterpret_cast(1)); } + else { ::RemovePropW(hWnd, L"MPC_TB_HOVER"); } + ::InvalidateRect(hWnd, nullptr, FALSE); + } + if (over) { + TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hWnd, 0 }; + ::TrackMouseEvent(&tme); // so we get WM_MOUSELEAVE to clear the hover + } + break; // pass through so the trackbar still handles the move + } + case WM_MOUSELEAVE: + if (::GetPropW(hWnd, L"MPC_TB_HOVER")) { + ::RemovePropW(hWnd, L"MPC_TB_HOVER"); + ::InvalidateRect(hWnd, nullptr, FALSE); + } + break; + case WM_LBUTTONDOWN: + ::SetPropW(hWnd, L"MPC_TB_PRESSED", reinterpret_cast(1)); + ::InvalidateRect(hWnd, nullptr, FALSE); + break; // pass through so the trackbar drags + case WM_LBUTTONUP: + if (::GetPropW(hWnd, L"MPC_TB_PRESSED")) { + ::RemovePropW(hWnd, L"MPC_TB_PRESSED"); + ::InvalidateRect(hWnd, nullptr, FALSE); + } + break; case WM_NCDESTROY: + ::RemovePropW(hWnd, L"MPC_TB_HOVER"); + ::RemovePropW(hWnd, L"MPC_TB_PRESSED"); RemoveWindowSubclass(hWnd, TrackbarSubclassProc, kTrackbarSubclassId); break; } @@ -966,11 +1010,22 @@ namespace DarkTheme // which looks bad on dark). Enabled ones keep the default painting. SetWindowSubclass(hCtrl, StaticSubclassProc, kStaticSubclassId, 0); } + } else if (_wcsicmp(cls, TRACKBAR_CLASSW) == 0) { + // Owner-draw every slider (dark groove + celeste thumb with hover / pressed states). + // The native DarkMode trackbar thumb renders ugly black-on-hover / white-when-pressed + // states otherwise. Skip trackbars already owner-drawn explicitly: the theme + // R/G/B/Brightness sliders carry dwData = 1 for their frozen-colour snapshot, and + // re-subclassing here (dwData = 0) would clobber that. + DWORD_PTR existing = 0; + if (!GetWindowSubclass(hCtrl, TrackbarSubclassProc, kTrackbarSubclassId, &existing)) { + SetWindowSubclass(hCtrl, TrackbarSubclassProc, kTrackbarSubclassId, 0); + InvalidateRect(hCtrl, nullptr, TRUE); + } } else { // Note: SysTabControl32 is handled by CDarkTabCtrl (a CTabCtrl-derived // owner-drawn control), not here — installing a comctl subclass would sit // in front of MFC's WndProc and steal WM_PAINT from that class. - // up-down, scrollbar, trackbar, ... + // up-down, scrollbar, ... SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); } } From b46521c489f3c8437539efbcaae09e29fdb45ae6 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Mon, 6 Jul 2026 22:46:21 -0300 Subject: [PATCH 14/24] Dark Options: fix Subresync bar theming regressions + more aux dialogs - Subresync bar list: theme it reversibly (new DarkTheme::RefreshThemeForControl) so it follows the "Use dark theme" toggle instead of staying dark (dark header/ border/background over light rows) after the theme is turned off. The list is themed once in Create via control subclasses that paint unconditionally, so they must be stripped, not just left installed, when the theme goes off. - Floating docking bars: apply the dark title once per mini-frame instead of on every OnWindowPosChanged, so dragging a floated bar no longer floods the DWM (RefreshImmersiveColorPolicyState + DwmSetWindowAttribute) and smears the window. - Subresync grid: remap the hard-coded light separator/grid-line greys to the dark palette - subtle grid lines, a clearly-visible group separator. - List-views: repaint fully on a column resize (header divider drag / auto-size double-click) so the hand-drawn dark grid lines don't ghost. - List-view in-place label edit (Organize Favorites, ...) now gets the dark edit colours via WM_CTLCOLOREDIT instead of showing a white box. - Subtitle "Styles..." property sheet: new CDarkPropertySheet themes its frame (dark tab + title bar + OK/Cancel/Apply); each page already themes itself. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/MainFrm.cpp | 3 +- src/apps/mplayerc/PPageInterface.cpp | 7 ++++ src/apps/mplayerc/PlayerBar.cpp | 11 +++++- src/apps/mplayerc/PlayerBar.h | 6 ++++ src/apps/mplayerc/PlayerSubresyncBar.cpp | 29 +++++++++++++--- src/apps/mplayerc/PlayerSubresyncBar.h | 5 +++ src/apps/mplayerc/controls/DarkTabCtrl.cpp | 37 ++++++++++++++++++++ src/apps/mplayerc/controls/DarkTabCtrl.h | 23 +++++++++++++ src/apps/mplayerc/controls/DarkTheme.cpp | 39 ++++++++++++++++++++++ src/apps/mplayerc/controls/DarkTheme.h | 8 +++++ 10 files changed, 161 insertions(+), 7 deletions(-) diff --git a/src/apps/mplayerc/MainFrm.cpp b/src/apps/mplayerc/MainFrm.cpp index cea2b8129e..b1a6d08150 100644 --- a/src/apps/mplayerc/MainFrm.cpp +++ b/src/apps/mplayerc/MainFrm.cpp @@ -72,6 +72,7 @@ #include "filters/ffmpeg_link_fix.h" #include "ComPropertySheet.h" #include "controls/DarkTheme.h" +#include "controls/DarkTabCtrl.h" #include #include @@ -9056,7 +9057,7 @@ void CMainFrame::OnMenuSubtitlesStyle() CString caption = ResStr(IDS_SUBTITLES_STYLES); caption.Replace(L"&", nullptr); - CPropertySheet dlg(caption, GetModalParent()); + CDarkPropertySheet dlg(caption, GetModalParent()); for (size_t i = 0; i < pages.size(); i++) { dlg.AddPage(pages[i].get()); } diff --git a/src/apps/mplayerc/PPageInterface.cpp b/src/apps/mplayerc/PPageInterface.cpp index fbf65e6146..ccd08a10cc 100644 --- a/src/apps/mplayerc/PPageInterface.cpp +++ b/src/apps/mplayerc/PPageInterface.cpp @@ -240,6 +240,13 @@ BOOL CPPageInterface::OnApply() // Capture, Navigation and Subresync bar frames follow the theme toggle too, then repaint them. for (const auto& pDockingBar : pFrame->m_dockingbars) { pDockingBar->m_bUseDarkTheme = s.bUseDarkTheme; + // The frame flag above doesn't reach the Subresync bar's list, which is themed once at + // creation (dark header / border / background via control subclasses that paint + // unconditionally). Re-apply or strip that so the list follows the toggle instead of staying + // dark (dark chrome, light rows) when the theme is turned off. + if (auto* pSubresyncBar = dynamic_cast(pDockingBar)) { + pSubresyncBar->RefreshListDarkTheme(); + } if (pDockingBar->IsWindowVisible()) { pDockingBar->SendMessageW(WM_NCPAINT, 1, NULL); pDockingBar->RedrawWindow(nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE); diff --git a/src/apps/mplayerc/PlayerBar.cpp b/src/apps/mplayerc/PlayerBar.cpp index b169156a28..9310c0b454 100644 --- a/src/apps/mplayerc/PlayerBar.cpp +++ b/src/apps/mplayerc/PlayerBar.cpp @@ -125,10 +125,19 @@ void CPlayerBar::OnWindowPosChanged(WINDOWPOS* lpwndpos) // When floated, the bar lives in an MFC mini-frame that is an independent top-level window whose // caption Windows paints light. Apply the dark title bar (immersive dark mode + themed caption // colour, like the main window and the Options dialogs) so the floating window matches the theme. + // Do it once per mini-frame, not on every position change: dragging a floating bar fires this + // continuously, and re-running EnableForWindow each time floods the DWM (RefreshImmersiveColor + // PolicyState + DwmSetWindowAttribute) and leaves the window smearing across the screen. if (IsFloating() && AfxGetAppSettings().bDarkTitle) { if (CFrameWnd* pMiniFrame = GetParentFrame()) { - DarkTheme::EnableForWindow(pMiniFrame->GetSafeHwnd()); + HWND hMiniFrame = pMiniFrame->GetSafeHwnd(); + if (hMiniFrame != m_hThemedMiniFrame) { + DarkTheme::EnableForWindow(hMiniFrame); + m_hThemedMiniFrame = hMiniFrame; + } } + } else { + m_hThemedMiniFrame = nullptr; // redocked / hidden — re-theme next time it floats } } diff --git a/src/apps/mplayerc/PlayerBar.h b/src/apps/mplayerc/PlayerBar.h index 8b976f8b64..e391c90afa 100644 --- a/src/apps/mplayerc/PlayerBar.h +++ b/src/apps/mplayerc/PlayerBar.h @@ -33,6 +33,12 @@ protected : UINT m_defDockBarID; CString m_strSettingName; + // The floating mini-frame we last applied the dark title to. Dragging a floating bar fires + // OnWindowPosChanged continuously; re-running EnableForWindow (which calls the heavy + // RefreshImmersiveColorPolicyState + DwmSetWindowAttribute) on every move floods the DWM and + // smears the window. Track the frame so we theme it once per float, not once per move. + HWND m_hThemedMiniFrame = nullptr; + public: CPlayerBar(); virtual ~CPlayerBar(); diff --git a/src/apps/mplayerc/PlayerSubresyncBar.cpp b/src/apps/mplayerc/PlayerSubresyncBar.cpp index 2f9fb2d32f..0bd65d2176 100644 --- a/src/apps/mplayerc/PlayerSubresyncBar.cpp +++ b/src/apps/mplayerc/PlayerSubresyncBar.cpp @@ -23,6 +23,7 @@ #include "MainFrm.h" #include "PlayerSubresyncBar.h" #include "controls/DarkTheme.h" +#include "controls/DarkTabCtrl.h" // CPlayerSubresyncBar @@ -55,12 +56,20 @@ BOOL CPlayerSubresyncBar::Create(CWnd* pParentWnd, UINT defDockBarID, CCritSec* // This bar isn't run through ThemeDialog, so the list kept a white empty background, a light // WS_EX_CLIENTEDGE border and a light column header. Theme the control directly (dark background / - // border / header; per-item colours are still handled by our NM_CUSTOMDRAW). No-op when off. - DarkTheme::ApplyThemeToControl(m_list.GetSafeHwnd()); + // border / header; per-item colours are still handled by our NM_CUSTOMDRAW). Use the reversible + // refresh so the list also follows a later "Use dark theme" toggle-off. No-op / strip when off. + RefreshListDarkTheme(); return TRUE; } +void CPlayerSubresyncBar::RefreshListDarkTheme() +{ + if (::IsWindow(m_list.GetSafeHwnd())) { + DarkTheme::RefreshThemeForControl(m_list.GetSafeHwnd()); + } +} + void CPlayerSubresyncBar::ReloadTranslatableResources() { SetWindowText(ResStr(IDS_SUBRESYNC_CAPTION)); @@ -1075,7 +1084,7 @@ void CPlayerSubresyncBar::OnRclickList(NMHDR* pNMHDR, LRESULT* pResult) } } - CPropertySheet dlg(L"Styles...", this, iSelPage); + CDarkPropertySheet dlg(L"Styles...", this, iSelPage); for (const auto& page : pages) { dlg.AddPage(page.get()); } @@ -1254,9 +1263,18 @@ void CPlayerSubresyncBar::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) CRect rcItem; m_list.GetItemRect(nItem, &rcItem, LVIR_BOUNDS); + // The row separator and column grid lines were drawn with fixed light greys: 0xe0e0e0 shows + // as a bright near-white line on the dark rows ("not look good"), and the group-separator's + // 0x404040 is nearly invisible against the dark background ("badly represented"). Remap both + // to the dark palette when it's active — subtle grid lines, and a clearly lighter separator. + const bool dark = DarkTheme::IsActive(); + { bool fSeparator = nItem < m_list.GetItemCount() - 1 && (m_list.GetItemData(nItem + 1)&TSEP); - CPen p(PS_INSIDEFRAME, 1, fSeparator ? 0x404040 : 0xe0e0e0); + const COLORREF clrLine = dark + ? (fSeparator ? ThemeRGB(95, 105, 115) : DarkTheme::GridlineColor()) + : (fSeparator ? RGB(64, 64, 64) : RGB(224, 224, 224)); + CPen p(PS_INSIDEFRAME, 1, clrLine); CPen* old = pDC->SelectObject(&p); pDC->MoveTo(CPoint(rcItem.left, rcItem.bottom - 1)); pDC->LineTo(CPoint(rcItem.right, rcItem.bottom - 1)); @@ -1264,7 +1282,8 @@ void CPlayerSubresyncBar::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) } { - CPen p(PS_INSIDEFRAME, 1, 0xe0e0e0); + const COLORREF clrGrid = dark ? DarkTheme::GridlineColor() : RGB(224, 224, 224); + CPen p(PS_INSIDEFRAME, 1, clrGrid); CPen* old = pDC->SelectObject(&p); CHeaderCtrl* pHeader = (CHeaderCtrl*)m_list.GetDlgItem(0); diff --git a/src/apps/mplayerc/PlayerSubresyncBar.h b/src/apps/mplayerc/PlayerSubresyncBar.h index 8423a1f014..5140c2c5ce 100644 --- a/src/apps/mplayerc/PlayerSubresyncBar.h +++ b/src/apps/mplayerc/PlayerSubresyncBar.h @@ -126,6 +126,11 @@ class CPlayerSubresyncBar : public CPlayerBar bool ShiftSubtitle(int nItem, long lValue, REFERENCE_TIME& rtPos); bool SaveToDisk(); + // Re-applies or strips the dark theme on the list to match the current "Use dark theme" state. + // The list is themed once in Create; without this it stays dark (dark header/border/background, + // light rows) after the theme is toggled off. Called from the Interface page's toggle handler. + void RefreshListDarkTheme(); + protected: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); diff --git a/src/apps/mplayerc/controls/DarkTabCtrl.cpp b/src/apps/mplayerc/controls/DarkTabCtrl.cpp index 945d2b1d17..4d0a8acc53 100644 --- a/src/apps/mplayerc/controls/DarkTabCtrl.cpp +++ b/src/apps/mplayerc/controls/DarkTabCtrl.cpp @@ -23,6 +23,10 @@ #include "DarkTheme.h" #include "../MainFrm.h" // ThemeRGB() +// windowsx.h defines a function-like SubclassWindow(hwnd, lpfn) macro that collides with +// CWnd::SubclassWindow(HWND); undef it so the MFC method call below parses correctly. +#undef SubclassWindow + IMPLEMENT_DYNAMIC(CDarkTabCtrl, CTabCtrl) BEGIN_MESSAGE_MAP(CDarkTabCtrl, CTabCtrl) @@ -147,3 +151,36 @@ void CDarkTabCtrl::OnPaint() dc.BitBlt(0, 0, rClient.Width(), rClient.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); } + +// CDarkPropertySheet + +IMPLEMENT_DYNAMIC(CDarkPropertySheet, CPropertySheet) + +BEGIN_MESSAGE_MAP(CDarkPropertySheet, CPropertySheet) +END_MESSAGE_MAP() + +CDarkPropertySheet::CDarkPropertySheet(LPCWSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage) + : CPropertySheet(pszCaption, pParentWnd, iSelectPage) +{ +} + +CDarkPropertySheet::~CDarkPropertySheet() +{ +} + +BOOL CDarkPropertySheet::OnInitDialog() +{ + BOOL bResult = CPropertySheet::OnInitDialog(); + + // Attach the owner-drawn dark tab (the stock tab keeps a light body/frame; CDarkTabCtrl falls + // back to native when the theme is off), then dark-theme the sheet frame: title bar, background + // and the OK/Cancel/Apply buttons. Each page dark-themes its own contents in its OnInitDialog. + if (DarkTheme::IsActive()) { + if (CTabCtrl* pTab = GetTabControl()) { + m_darkTab.SubclassWindow(pTab->GetSafeHwnd()); + } + } + DarkTheme::ThemeDialog(GetSafeHwnd()); + + return bResult; +} diff --git a/src/apps/mplayerc/controls/DarkTabCtrl.h b/src/apps/mplayerc/controls/DarkTabCtrl.h index 4a79ba8cba..cda6a2d596 100644 --- a/src/apps/mplayerc/controls/DarkTabCtrl.h +++ b/src/apps/mplayerc/controls/DarkTabCtrl.h @@ -21,6 +21,7 @@ #pragma once #include +#include // CPropertySheet // A CTabCtrl that fully owner-draws itself in the dark palette. The native tab // control ignores SetWindowTheme("DarkMode_*") and always paints its body/pane @@ -43,3 +44,25 @@ class CDarkTabCtrl : public CTabCtrl afx_msg void OnPaint(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); }; + +// A CPropertySheet that dark-themes its own frame: the owner-drawn dark tab strip (the stock +// SysTabControl32 ignores SetWindowTheme and keeps a light body/frame) plus the sheet's dark title +// bar, background and OK/Cancel/Apply buttons via DarkTheme::ThemeDialog. The pages themselves are +// expected to dark-theme their own contents (CPPageBase does this). Falls back to a plain light +// property sheet when the dark theme is off. Use in place of CPropertySheet for stand-alone sheets +// (e.g. the subtitle "Styles..." dialog) that aren't the Options sheet. +class CDarkPropertySheet : public CPropertySheet +{ + DECLARE_DYNAMIC(CDarkPropertySheet) + +public: + CDarkPropertySheet(LPCWSTR pszCaption, CWnd* pParentWnd = nullptr, UINT iSelectPage = 0); + virtual ~CDarkPropertySheet(); + +protected: + CDarkTabCtrl m_darkTab; // owner-drawn dark tab, attached in OnInitDialog when the dark theme is on + + virtual BOOL OnInitDialog(); + + DECLARE_MESSAGE_MAP() +}; diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 57e9579011..1adac53a3f 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -451,6 +451,16 @@ namespace DarkTheme const UINT_PTR kListViewSubclassId = 2; LRESULT CALLBACK ListViewSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR dwRefData) { + if (msg == WM_CTLCOLOREDIT) { + // The in-place label-edit control a list-view creates for renaming an item (e.g. the + // Organize Favorites list, the Subresync grid) is a child of the list-view, so its + // WM_CTLCOLOREDIT is sent here, not to the dialog. Give it the dark edit colours so it + // isn't a bright white box hovering over the dark list. + CDC* pDC = CDC::FromHandle(reinterpret_cast(wParam)); + if (HBRUSH hbr = OnCtlColor(pDC, CTLCOLOR_EDIT)) { + return reinterpret_cast(hbr); + } + } if (msg == WM_PAINT) { const LRESULT res = DefSubclassProc(hWnd, msg, wParam, lParam); FillListEmptyArea(hWnd); // dark over the light empty strip / stray column separator @@ -470,6 +480,17 @@ namespace DarkTheme if (msg == WM_NOTIFY) { NMHDR* pNM = reinterpret_cast(lParam); HWND hHeader = reinterpret_cast(::SendMessageW(hWnd, LVM_GETHEADER, 0, 0)); + if (pNM && pNM->hwndFrom == hHeader + && (pNM->code == HDN_ITEMCHANGEDA || pNM->code == HDN_ITEMCHANGEDW + || pNM->code == HDN_ENDTRACKA || pNM->code == HDN_ENDTRACKW)) { + // A column resize — dragging a header divider, or double-clicking the divider + // "gripper" to auto-size the column — bit-blts the rows to the right of that divider, + // which smears our hand-drawn grid lines / dark header-fill (they double up or stay at + // the old offsets). Let the list process the size change, then force a clean repaint. + const LRESULT res = DefSubclassProc(hWnd, msg, wParam, lParam); + ::InvalidateRect(hWnd, nullptr, FALSE); + return res; + } if (pNM && pNM->code == NM_CUSTOMDRAW && pNM->hwndFrom == hHeader) { LPNMCUSTOMDRAW p = reinterpret_cast(lParam); switch (p->dwDrawStage) { @@ -1202,6 +1223,24 @@ namespace DarkTheme ThemeControl(hCtrl); } + void RefreshThemeForControl(HWND hCtrl) { + if (!hCtrl) { + return; + } + LoadApi(); + if (IsActive()) { + if (g_bApiOk) { + ThemeControl(hCtrl); + } + } else { + // Undo everything ThemeControl installed (subclasses that paint dark unconditionally, + // the DarkMode_* visual style, the dark list/tree background) so the control goes back to + // its native light look instead of leaving a half-dark control after a runtime toggle-off. + StripThemeChildProc(hCtrl, 0); + } + ::InvalidateRect(hCtrl, nullptr, TRUE); + } + void CommitThemeColors() { // Snapshot the current theme colours for the R/G/B/Brightness sliders. Call this when a // slider drag ends so all four repaint together to the final colour (see TrackbarSubclassProc). diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h index f1c485b65e..d0a675d049 100644 --- a/src/apps/mplayerc/controls/DarkTheme.h +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -56,6 +56,14 @@ namespace DarkTheme // through ApplyThemeToChildren. void ApplyThemeToControl(HWND hCtrl); + // Applies OR strips the dark theme on a single control to match the current state, and repaints + // it. Unlike ApplyThemeToControl (which only ever applies), this reverts the control to its light + // look when the theme is off — so a control themed once (e.g. the Subresync bar's list, themed in + // its Create) can follow the "Use dark theme" toggle instead of staying stuck dark. Its dark + // paint subclasses draw unconditionally, so they MUST be removed (not just left installed) when + // the theme is turned off, which is what the strip path does. + void RefreshThemeForControl(HWND hCtrl); + // Fully owner-draws a trackbar (dark background, dark groove, celeste thumb) via a // subclass. Use for sliders whose NM_CUSTOMDRAW channel colour is not reliably applied // (e.g. the subtitle Default Style alpha sliders after the Reset button). From a9dd597dff085eb07b1830f92f51672b2737a893 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Mon, 6 Jul 2026 23:16:27 -0300 Subject: [PATCH 15/24] Dark Options: auto-size Subresync columns once after load (cheap) The End (4 px) and Charset (20 px) columns are created far too narrow to show their values / header captions. Size every column to fit the wider of its header caption and its first data row, once, right after the whole subtitle has been inserted (not per line). Measure the header text + first row by hand instead of SetColumnWidth( LVSCW_AUTOSIZE): the latter scans every row of all 11 columns (~5 s on a large subtitle) and forces the list to repaint mid-population, which piled up ghost separator lines until the load finished. Every row in a time/number column is the same width, so the first row is representative; columns only ever grow, so the wide Text column keeps its default width. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/PlayerSubresyncBar.cpp | 36 +++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/apps/mplayerc/PlayerSubresyncBar.cpp b/src/apps/mplayerc/PlayerSubresyncBar.cpp index 0bd65d2176..ae6bd8aa4e 100644 --- a/src/apps/mplayerc/PlayerSubresyncBar.cpp +++ b/src/apps/mplayerc/PlayerSubresyncBar.cpp @@ -272,16 +272,38 @@ void CPlayerSubresyncBar::ResetSubtitle() m_list.SetItemData(i, (DWORD_PTR)TSEP); } prevstart = m_subtimes[i].orgstart; - - // Since all items in COL_START and COL_PREVSTART have the same text size, - // we can compute it for the first element only so that it's faster. - if (i == 0) { - m_list.SetColumnWidth(COL_START, LVSCW_AUTOSIZE); - m_list.SetColumnWidth(COL_PREVSTART, LVSCW_AUTOSIZE); - } } UpdateStrings(); + + // Size the narrow columns (End = 4 px, Charset = 20 px by default) to fit their header caption + // and first-row value. Measure only the header text + the first data row, NOT LVSCW_AUTOSIZE + // per column: that scans every row of all 11 columns (slow on big subs) and forces the list to + // repaint mid-population, which left ghost separator lines piling up until the load finished. + // Every row in a time/number column is the same width, so the first row is representative; + // columns only ever grow, so the wide Text column keeps its default width. + if (CHeaderCtrl* pHeader = m_list.GetHeaderCtrl()) { + CClientDC dc(&m_list); + CFont* pOldFont = dc.SelectObject(m_list.GetFont()); + for (int col = 0, nCols = pHeader->GetItemCount(); col < nCols; col++) { + wchar_t buf[256] = {}; + HDITEMW hdi = {}; + hdi.mask = HDI_TEXT; + hdi.pszText = buf; + hdi.cchTextMax = _countof(buf) - 1; + int wNeed = pHeader->GetItem(col, &hdi) ? dc.GetTextExtent(buf).cx + 12 : 0; // header + padding + if (nCount > 0) { + const int wCell = dc.GetTextExtent(m_list.GetItemText(0, col)).cx + 12; // first row + padding + if (wCell > wNeed) { + wNeed = wCell; + } + } + if (wNeed > m_list.GetColumnWidth(col)) { + m_list.SetColumnWidth(col, wNeed); // only grow, so Text keeps its default width + } + } + dc.SelectObject(pOldFont); + } } m_list.SetRedraw(TRUE); From 5e772a8f23b353974717c3f5372e52535771b0c4 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Wed, 8 Jul 2026 00:53:26 -0300 Subject: [PATCH 16/24] Dark Options: stop control borders flickering / breaking on scroll The custom dark 1px border (BorderSubclassProc) was overpainted on top of the default non-client paint. Themed edits/list boxes repaint their border on hover / mouse-move, so the light->dark overpaint strobed (About, File Properties fields, Internal Filters lists), and on a multiline edit the border got dragged into the middle of the text by ScrollWindowEx (MediaInfo). - WM_NCPAINT: for controls WITHOUT scroll bars, skip the default paint entirely so the light client-edge is never drawn (no flash); for controls WITH scroll bars, let the default paint + validate the frame (so scrolling keeps it), then overpaint. - Don't custom-draw the border on scrolling multiline edits or scrolling list boxes at all - the DarkMode_Explorer theme's own border stays put and dark enough; the overpaint only fought their scroll/hover repaints. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/controls/DarkTheme.cpp | 62 +++++++++++++++--------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 1adac53a3f..371f9334f7 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -816,18 +816,29 @@ namespace DarkTheme LRESULT CALLBACK BorderSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { switch (msg) { case WM_NCPAINT: { - const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); // draws scrollbars first - HDC hdc = ::GetWindowDC(hWnd); - if (hdc) { - RECT wr; - ::GetWindowRect(hWnd, &wr); + RECT wr; + ::GetWindowRect(hWnd, &wr); + POINT org = { 0, 0 }; + ::ClientToScreen(hWnd, &org); + int edge = org.y - wr.top; // NC border thickness: 1 (WS_BORDER) or 2 (client edge) + if (edge < 1) { + edge = 1; + } + // Controls WITH scroll bars (multiline edits like MediaInfo, list/tree views): let the + // default proc paint — and validate — the whole non-client with the real update region, + // then overpaint our dark frame. Clipping the frame out of the default's region instead + // left it perpetually invalid, so scrolling dropped the border entirely. + // Controls WITHOUT scroll bars (single-line edits, the About / File Properties fields, + // sunken statics): skip the default entirely so it never draws the light client-edge — + // that light->dark overpaint was what flickered as themed edits (DarkMode_CFD/_Explorer) + // repaint their border on hover / mouse-move. Nothing else lives in their non-client. + const LONG style = ::GetWindowLongW(hWnd, GWL_STYLE); + LRESULT r = 0; + if (style & (WS_VSCROLL | WS_HSCROLL)) { + r = DefSubclassProc(hWnd, msg, wParam, lParam); + } + if (HDC hdc = ::GetWindowDC(hWnd)) { RECT rc = { 0, 0, wr.right - wr.left, wr.bottom - wr.top }; - POINT org = { 0, 0 }; - ::ClientToScreen(hWnd, &org); - int edge = org.y - wr.top; // border thickness: 1 (WS_BORDER) or 2 (client edge) - if (edge < 1) { - edge = 1; - } HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); for (int k = 0; k < edge; ++k) { ::FrameRect(hdc, &rc, br); @@ -953,22 +964,21 @@ namespace DarkTheme } else if (_wcsicmp(cls, L"ComboBox") == 0) { SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); } else if (_wcsicmp(cls, L"Edit") == 0) { - // DarkMode_CFD darkens the interior/border but leaves the control's own scrollbars - // light. For multiline edits that actually have scrollbars (the Command Line Switches - // help box, the Shader Editor source/output), use DarkMode_Explorer instead so the - // scrollbar is dark like the tree/list controls; the border is redrawn by - // ApplyDarkBorder either way, so we don't lose the CFD border styling that matters. - // DarkMode_CFD darkens the interior/border but leaves the control's own scrollbars - // light. For multiline edits that actually have scrollbars (the Command Line Switches - // help box, the Shader Editor source/output), use DarkMode_Explorer instead so the - // scrollbar is dark like the tree/list controls. const LONG est = GetWindowLongW(hCtrl, GWL_STYLE); if (est & (WS_VSCROLL | WS_HSCROLL)) { + // Multiline scrolling edits (MediaInfo, Command Line Switches, Shader Editor): + // DarkMode_Explorer gives a dark scrollbar and a dark-enough border. Do NOT custom- + // draw the border here: a multiline edit scrolls its client with ScrollWindowEx, which + // dragged our overpainted non-client border into the middle of the text on every + // scroll. The theme border stays put, so it's the right owner for scrolling edits. SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); } else { + // Single-line / non-scrolling edits (most Options fields, the About / File Properties + // value boxes): DarkMode_CFD darkens the interior; the client-edge stays light, so we + // repaint it dark (BorderSubclassProc skips the default paint for these, no flicker). SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); + ApplyDarkBorder(hCtrl); } - ApplyDarkBorder(hCtrl); } else if (_wcsicmp(cls, L"SysListView32") == 0) { // List-view controls ignore WM_CTLCOLOR: their background (the area // not covered by columns/rows) must be set explicitly, otherwise it @@ -1011,10 +1021,14 @@ namespace DarkTheme InvalidateRect(hCtrl, nullptr, TRUE); } else if (_wcsicmp(cls, L"ListBox") == 0) { // Plain list boxes (e.g. DVD preferred-language) get their interior from - // WM_CTLCOLORLISTBOX (handled by the page), but their sunken border stays - // light — repaint it with the shared dark border like edits/lists/trees. + // WM_CTLCOLORLISTBOX (handled by the page), but their sunken border stays light. SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); - ApplyDarkBorder(hCtrl); + // Only custom-draw the border on non-scrolling list boxes. A scrolling one (the + // Internal Filters lists) repaints its border on hover, so overpainting it flickered; + // leave the theme's dark border for those, like scrolling edits. + if (!(GetWindowLongW(hCtrl, GWL_STYLE) & (WS_VSCROLL | WS_HSCROLL))) { + ApplyDarkBorder(hCtrl); + } } else if (_wcsicmp(cls, L"Static") == 0) { // Sunken value boxes (e.g. Brightness/Contrast/Hue/Saturation on the // Color correction page are SS_SUNKEN RTEXT statics) keep a light 3D edge From fb0b197a8cdb9f0e4881e9cb03f1101ff0dd79d2 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Sat, 11 Jul 2026 02:32:29 -0300 Subject: [PATCH 17/24] Dark Options: fix white borders, combo border, and list-box overflow Follow-up to the border rework (reviewers saw issues our machine didn't): - White borders (File Properties Details/Clip big field, Internal Filters lists, Add-to-Favorites dropdown): go back to overpainting a dark frame over whatever the theme drew, so the border is dark on every machine. Skipping / clipping the default paint rendered a white border on some systems. - Overpaint now self-skips when the control reserves no non-client border (edge 0, e.g. the MediaInfo NOT-WS_BORDER edit): a frame there lands in the client and gets dragged into the text by ScrollWindowEx. So the bordered multiline edits (Details/Clip) get a dark frame with no scroll drag, and MediaInfo stays clean. - Restore the dark border on scrolling list boxes, and add one to combo boxes (the CFD combo border stayed light on some machines). - CDarkCheckListBox: clip a partially-visible last item to the client bottom (ETO_CLIPPED) so its row no longer spills below the list box into the page. Co-Authored-By: Claude Opus 4.8 --- .../mplayerc/controls/DarkCheckListBox.cpp | 8 +- src/apps/mplayerc/controls/DarkTheme.cpp | 78 +++++++++---------- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkCheckListBox.cpp b/src/apps/mplayerc/controls/DarkCheckListBox.cpp index 455228b35e..e8df012505 100644 --- a/src/apps/mplayerc/controls/DarkCheckListBox.cpp +++ b/src/apps/mplayerc/controls/DarkCheckListBox.cpp @@ -89,9 +89,15 @@ void CDarkCheckListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) if (rcClient.right > rcFill.right) { rcFill.right = rcClient.right; } + // ...but bound the fill to the client bottom, and clip the text (ETO_CLIPPED): clearing the + // clip let a partially-visible last item paint its background/text *below* the list box, so the + // row spilled out of the control into the page underneath ("the list leaves its box"). + if (rcFill.bottom > rcClient.bottom) { + rcFill.bottom = rcClient.bottom; + } pDC->SelectClipRgn(nullptr); - pDC->ExtTextOutW(lpDrawItemStruct->rcItem.left, yText, ETO_OPAQUE, + pDC->ExtTextOutW(lpDrawItemStruct->rcItem.left, yText, ETO_OPAQUE | ETO_CLIPPED, rcFill, strText, strText.GetLength(), nullptr); } diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 371f9334f7..865e9d59a1 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -816,36 +816,31 @@ namespace DarkTheme LRESULT CALLBACK BorderSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { switch (msg) { case WM_NCPAINT: { + // Let the default proc paint the non-client (scroll bars + its own border), then + // overpaint our dark frame on top. Drawing over whatever the theme rendered keeps the + // border dark on every system — skipping / clipping the default looked fine locally but + // rendered a WHITE border on other machines (nothing dark was painted over the edge). + const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); RECT wr; ::GetWindowRect(hWnd, &wr); POINT org = { 0, 0 }; ::ClientToScreen(hWnd, &org); - int edge = org.y - wr.top; // NC border thickness: 1 (WS_BORDER) or 2 (client edge) - if (edge < 1) { - edge = 1; - } - // Controls WITH scroll bars (multiline edits like MediaInfo, list/tree views): let the - // default proc paint — and validate — the whole non-client with the real update region, - // then overpaint our dark frame. Clipping the frame out of the default's region instead - // left it perpetually invalid, so scrolling dropped the border entirely. - // Controls WITHOUT scroll bars (single-line edits, the About / File Properties fields, - // sunken statics): skip the default entirely so it never draws the light client-edge — - // that light->dark overpaint was what flickered as themed edits (DarkMode_CFD/_Explorer) - // repaint their border on hover / mouse-move. Nothing else lives in their non-client. - const LONG style = ::GetWindowLongW(hWnd, GWL_STYLE); - LRESULT r = 0; - if (style & (WS_VSCROLL | WS_HSCROLL)) { - r = DefSubclassProc(hWnd, msg, wParam, lParam); - } - if (HDC hdc = ::GetWindowDC(hWnd)) { - RECT rc = { 0, 0, wr.right - wr.left, wr.bottom - wr.top }; - HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); - for (int k = 0; k < edge; ++k) { - ::FrameRect(hdc, &rc, br); - ::InflateRect(&rc, -1, -1); + const int edge = org.y - wr.top; // NC border: 0 = none, 1 = WS_BORDER, 2 = client edge + // Only overpaint when the control actually reserves a non-client border. A borderless + // control (NOT WS_BORDER, e.g. the MediaInfo edit) has edge 0; a 1px frame there lands in + // the *client*, which the edit's ScrollWindowEx then drags into the middle of the text on + // scroll. Skip it — such controls stay borderless, as their template intends. + if (edge >= 1) { + if (HDC hdc = ::GetWindowDC(hWnd)) { + RECT rc = { 0, 0, wr.right - wr.left, wr.bottom - wr.top }; + HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); + for (int k = 0; k < edge; ++k) { + ::FrameRect(hdc, &rc, br); + ::InflateRect(&rc, -1, -1); + } + ::DeleteObject(br); + ::ReleaseDC(hWnd, hdc); } - ::DeleteObject(br); - ::ReleaseDC(hWnd, hdc); } return r; } @@ -963,22 +958,21 @@ namespace DarkTheme } } else if (_wcsicmp(cls, L"ComboBox") == 0) { SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); + // The CFD combo border stays light on some machines (e.g. the Add-Favorite dropdown); + // overpaint it dark like the edits. + ApplyDarkBorder(hCtrl); } else if (_wcsicmp(cls, L"Edit") == 0) { const LONG est = GetWindowLongW(hCtrl, GWL_STYLE); if (est & (WS_VSCROLL | WS_HSCROLL)) { - // Multiline scrolling edits (MediaInfo, Command Line Switches, Shader Editor): - // DarkMode_Explorer gives a dark scrollbar and a dark-enough border. Do NOT custom- - // draw the border here: a multiline edit scrolls its client with ScrollWindowEx, which - // dragged our overpainted non-client border into the middle of the text on every - // scroll. The theme border stays put, so it's the right owner for scrolling edits. - SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); + SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); // dark scrollbar } else { - // Single-line / non-scrolling edits (most Options fields, the About / File Properties - // value boxes): DarkMode_CFD darkens the interior; the client-edge stays light, so we - // repaint it dark (BorderSubclassProc skips the default paint for these, no flicker). SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); - ApplyDarkBorder(hCtrl); } + // Repaint the light client-edge dark. This self-skips on a borderless edit (NOT WS_BORDER, + // e.g. the MediaInfo dump) — where a drawn frame would land in the client and get dragged + // into the text on scroll — so only edits with a real border (the Details/Clip multiline + // field, single-line value boxes) get the dark frame. No white edge, no scroll drag. + ApplyDarkBorder(hCtrl); } else if (_wcsicmp(cls, L"SysListView32") == 0) { // List-view controls ignore WM_CTLCOLOR: their background (the area // not covered by columns/rows) must be set explicitly, otherwise it @@ -1020,15 +1014,13 @@ namespace DarkTheme SetWindowSubclass(hCtrl, SpinSubclassProc, kSpinSubclassId, 0); InvalidateRect(hCtrl, nullptr, TRUE); } else if (_wcsicmp(cls, L"ListBox") == 0) { - // Plain list boxes (e.g. DVD preferred-language) get their interior from - // WM_CTLCOLORLISTBOX (handled by the page), but their sunken border stays light. + // Plain list boxes (e.g. DVD preferred-language, the Internal Filters lists) get their + // interior from WM_CTLCOLORLISTBOX (handled by the page), but their sunken border stays + // light — repaint it dark. Unlike a multiline edit, a list box doesn't drag the overpaint + // on scroll, so keep the custom border even when it scrolls (the theme's own border + // rendered white on some machines). SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); - // Only custom-draw the border on non-scrolling list boxes. A scrolling one (the - // Internal Filters lists) repaints its border on hover, so overpainting it flickered; - // leave the theme's dark border for those, like scrolling edits. - if (!(GetWindowLongW(hCtrl, GWL_STYLE) & (WS_VSCROLL | WS_HSCROLL))) { - ApplyDarkBorder(hCtrl); - } + ApplyDarkBorder(hCtrl); } else if (_wcsicmp(cls, L"Static") == 0) { // Sunken value boxes (e.g. Brightness/Contrast/Hue/Saturation on the // Color correction page are SS_SUNKEN RTEXT statics) keep a light 3D edge From edde8ac3458544dbe8e0df0e7f069ae9bc5f3979 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Wed, 15 Jul 2026 00:44:11 -0300 Subject: [PATCH 18/24] Dark Options: fix combo focus border, edit hover flicker, Keys button, and more Round of reviewer-reported fixes on 1.9.0.76: - Edit hover flicker (About / History / Format / Keys filter / Logo fields): the cause was DarkMode_CFD's hover/hot border state, which self-invalidates the edit's non-client on every mouse-move, so our WM_NCPAINT overpaint flashed light->dark each time. Single-line edits now DISABLE the visual style (SetWindowTheme "") instead of CFD, so there's no hover border to repaint; the dark overpaint border is drawn once and the interior stays dark via WM_CTLCOLOR*. - Add-to-Favorites combo showed a WHITE border when focused: a combo paints its border in the CLIENT area and repaints it light on focus, which the NC overpaint never covered. BorderSubclassProc now also overpaints the client-edge frame on WM_PAINT and repaints on focus change - gated on the ComboBox class only. - Keys "edit hotkey" showed a white button: CEditWithButton_Base::DrawButton drew a light themed button; added a dark branch (dark face/border, light caption). - Internal Filters: the next item's checkbox peeked below the box. Our DrawItem cleared the DC clip (SelectClipRgn(nullptr)), so the base CCheckListBox glyph spilled; clip to the client rect instead. - Language-pack version-mismatch message box rendered light: it fires during early startup before the persistent message-box hook exists and before bUseDarkTheme is read. Read the dark flag before SetLanguage, and wrap the box in a CDarkMessageBoxHook guard. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/AppSettings.cpp | 5 +++ .../mplayerc/controls/DarkCheckListBox.cpp | 15 +++++--- src/apps/mplayerc/controls/DarkTheme.cpp | 37 ++++++++++++++++++- src/apps/mplayerc/controls/EditWithButton.cpp | 27 ++++++++++++++ src/apps/mplayerc/mplayerc.cpp | 4 ++ 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/apps/mplayerc/AppSettings.cpp b/src/apps/mplayerc/AppSettings.cpp index 68e65dda20..b33d4e72cd 100644 --- a/src/apps/mplayerc/AppSettings.cpp +++ b/src/apps/mplayerc/AppSettings.cpp @@ -852,6 +852,11 @@ void CAppSettings::LoadSettings(bool bForce/* = false*/) if (iLanguage < 0) { iLanguage = CMPlayerCApp::GetDefLanguage(); } + // Read the dark-theme flag before SetLanguage: SetLanguage may pop the "language pack will not + // work with this version" message box, and DarkTheme::IsActive() must already reflect the user's + // saved preference for that box to be themed (bUseDarkTheme is otherwise not read until further + // below, so at this point it would still hold the ResetSettings default). Harmless duplicate read. + profile.ReadBool(IDS_R_THEME, IDS_RS_USEDARKTHEME, bUseDarkTheme); CMPlayerCApp::SetLanguage(iLanguage, false); FiltersPriority.LoadSettings(); diff --git a/src/apps/mplayerc/controls/DarkCheckListBox.cpp b/src/apps/mplayerc/controls/DarkCheckListBox.cpp index e8df012505..0db267dff1 100644 --- a/src/apps/mplayerc/controls/DarkCheckListBox.cpp +++ b/src/apps/mplayerc/controls/DarkCheckListBox.cpp @@ -82,20 +82,25 @@ void CDarkCheckListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) // Fill to the full client width: for some lists the item rect (and the DC's // clip region) does not span the whole control, which would leave the right - // side unpainted (white). Clear the clip so the opaque fill reaches the edge. + // side unpainted (white). CRect rcFill(lpDrawItemStruct->rcItem); CRect rcClient; GetClientRect(&rcClient); if (rcClient.right > rcFill.right) { rcFill.right = rcClient.right; } - // ...but bound the fill to the client bottom, and clip the text (ETO_CLIPPED): clearing the - // clip let a partially-visible last item paint its background/text *below* the list box, so the - // row spilled out of the control into the page underneath ("the list leaves its box"). if (rcFill.bottom > rcClient.bottom) { rcFill.bottom = rcClient.bottom; } - pDC->SelectClipRgn(nullptr); + // Clip to the client rect (not "no clip"): the list box reuses one DC for every row and does + // not reset the clip per row, so clearing it let a partially-visible last row spill below the + // box — both our text (bounded above + ETO_CLIPPED) AND the checkbox glyph the base + // CCheckListBox::PreDrawItem paints (which our clamp can't reach). A client-rect clip still + // lets the opaque fill reach the right edge but keeps every row's paint — glyph included — + // inside the box, so the next item's checkbox no longer peeks out the bottom. + CRgn rgnClient; + rgnClient.CreateRectRgnIndirect(&rcClient); + pDC->SelectClipRgn(&rgnClient); pDC->ExtTextOutW(lpDrawItemStruct->rcItem.left, yText, ETO_OPAQUE | ETO_CLIPPED, rcFill, strText, strText.GetLength(), nullptr); diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 865e9d59a1..ade37ce7ba 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -844,6 +844,34 @@ namespace DarkTheme } return r; } + case WM_PAINT: + case WM_SETFOCUS: + case WM_KILLFOCUS: { + // Combo boxes draw their border in the CLIENT area (not the non-client), and repaint it + // in a light 'focused' state on focus — which the WM_NCPAINT overpaint above never + // covers, so an active combo showed a white border (Add-to-Favorites). Only for combos, + // overpaint the client-edge frame dark after each WM_PAINT, and force a repaint on focus + // change. Gated on the ComboBox class so edits / lists / trees / statics are untouched. + wchar_t cls[16] = {}; + ::GetClassNameW(hWnd, cls, _countof(cls)); + if (_wcsicmp(cls, L"ComboBox") != 0) { + break; // not a combo — fall through to the default handling + } + const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); + if (msg == WM_PAINT) { + if (HDC hdc = ::GetDC(hWnd)) { + RECT rc; + ::GetClientRect(hWnd, &rc); + HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); + ::FrameRect(hdc, &rc, br); + ::DeleteObject(br); + ::ReleaseDC(hWnd, hdc); + } + } else { + ::InvalidateRect(hWnd, nullptr, FALSE); // focus changed — repaint so the frame stays dark + } + return r; + } case WM_NCDESTROY: RemoveWindowSubclass(hWnd, BorderSubclassProc, kBorderSubclassId); break; @@ -966,7 +994,14 @@ namespace DarkTheme if (est & (WS_VSCROLL | WS_HSCROLL)) { SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); // dark scrollbar } else { - SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); + // Single-line edits: DISABLE the visual style (empty theme) rather than DarkMode_CFD. + // CFD owns a hover/hot border state and self-invalidates the edit's non-client on every + // mouse-move; our WM_NCPAINT overpaint then flashed light->dark on each hover repaint + // (About / History / Format / Keys filter / Logo fields). A style-less edit has no hover + // border, so nothing repaints the NC on hover — the dark overpaint below is drawn once + // (on show/focus/resize) and stays. Interior stays dark via WM_CTLCOLOR* (OnCtlColor), + // which a classic edit honours fully, so dropping CFD doesn't lighten the interior. + SetWindowTheme(hCtrl, L"", L""); } // Repaint the light client-edge dark. This self-skips on a borderless edit (NOT WS_BORDER, // e.g. the MediaInfo dump) — where a drawn frame would land in the client and get dragged diff --git a/src/apps/mplayerc/controls/EditWithButton.cpp b/src/apps/mplayerc/controls/EditWithButton.cpp index 036c435c23..61d096a61b 100644 --- a/src/apps/mplayerc/controls/EditWithButton.cpp +++ b/src/apps/mplayerc/controls/EditWithButton.cpp @@ -20,6 +20,8 @@ #include "stdafx.h" #include "EditWithButton.h" +#include "DarkTheme.h" +#include "../Misc.h" // ThemeRGB #define WM_EDITWITHBUTTON_RECALCNCSIZE (WM_USER + 200) @@ -75,6 +77,31 @@ void CEditWithButton_Base::DrawButton(CRect rectButton) { CWindowDC dc(this); + // Dark theme: this NC-area button is fully owner-drawn (not a real BUTTON control), so the dark + // machinery never reaches it and OpenThemeData("Button") below would render a light system button + // on the dark page (seen editing a hotkey in Options > Keys, and the search/clear edits). Draw it + // dark instead — matching the owner-drawn dialog buttons — then let DrawButtonContent paint the + // caption light (its non-themed path uses the DC's text colour). + if (DarkTheme::IsActive()) { + const int st = GetButtonThemeState(); + const bool disabled = (st == PBS_DISABLED); + const bool pressed = (st == PBS_PRESSED); + const bool hot = (st == PBS_HOT); + const COLORREF face = disabled ? ThemeRGB(38, 43, 48) + : pressed ? ThemeRGB(36, 41, 46) + : hot ? ThemeRGB(62, 69, 76) + : ThemeRGB(50, 56, 62); + dc.FillSolidRect(rectButton, face); + CBrush brBorder(DarkTheme::CtrlBorderColor()); + dc.FrameRect(rectButton, &brBorder); + if (pressed) { + rectButton.OffsetRect(1, 1); // "push" the caption slightly + } + dc.SetTextColor(disabled ? RGB(120, 125, 130) : DarkTheme::TextColor()); + DrawButtonContent(dc, rectButton, nullptr); + return; + } + HTHEME hButtonTheme = OpenThemeData(m_hWnd, L"Button"); if (hButtonTheme) diff --git a/src/apps/mplayerc/mplayerc.cpp b/src/apps/mplayerc/mplayerc.cpp index e2c863bb96..52070c2fd9 100644 --- a/src/apps/mplayerc/mplayerc.cpp +++ b/src/apps/mplayerc/mplayerc.cpp @@ -26,6 +26,7 @@ #include #include "MainFrm.h" #include "Misc.h" +#include "controls/DarkTheme.h" #include #include "Ifo.h" #include "MultiMonitor.h" @@ -1535,6 +1536,9 @@ void CMPlayerCApp::SetLanguage(int nLanguage, bool bSave/* = true*/) s.iLanguage = nLanguage; } } else { + // Dark-theme this box (it fires during early startup, before the persistent message-box + // hook is installed in CMainFrame::OnCreate, so the RAII guard installs a scoped one). + DarkTheme::CDarkMessageBoxHook mbHook; // This message should stay in English! MessageBoxW(nullptr, L"Your language pack will not work with this version. Please download a compatible one from the MPC-BE homepage.", L"MPC-BE", MB_OK); From 92a83dffa7d7d3ee34c2ca243550bd34f98bd2f1 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Fri, 17 Jul 2026 20:13:16 -0300 Subject: [PATCH 19/24] Dark Options: fix bar client white-flash, GoTo masked edit, combo twitch, grid backstop Reviewer round on 1.9.0.95: - White rectangle when repositioning a docking bar / white border when resizing the Shader editor bar: CSizingControlBar never painted its own CLIENT background dark (only the NC frame follows m_bUseDarkTheme; the client was erased by DefWindowProc with the shared light class brush). Added a dark OnEraseBkgnd so an exposed client strip on redock/resize/toggle no longer flashes white. - GoTo time field stayed light: it's a CMFCMaskedEdit (window class "MFCMaskedEdit", a superclass of WC_EDIT), so ThemeControl's class check missed it. Handle "MFCMaskedEdit" in the Edit branch (interior still darkens via the parent's WM_CTLCOLOREDIT). - Shader editor combo twitched when its dropdown opened: last round's combo-border fix invalidated the whole combo on focus (interior + button + border). Narrow the focus invalidate to just the 1px border frame. - "Grid view unnecessary rows": fill the list's empty area AFTER the grid lines so it backstops any line drawn past the last item in the empty space below a short list. Co-Authored-By: Claude Opus 4.8 --- src/ExtLib/ui/sizecbar/sizecbar.cpp | 17 ++++++++++++++++ src/ExtLib/ui/sizecbar/sizecbar.h | 1 + src/apps/mplayerc/controls/DarkTheme.cpp | 26 +++++++++++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/ExtLib/ui/sizecbar/sizecbar.cpp b/src/ExtLib/ui/sizecbar/sizecbar.cpp index 210125ca4f..97895f6649 100644 --- a/src/ExtLib/ui/sizecbar/sizecbar.cpp +++ b/src/ExtLib/ui/sizecbar/sizecbar.cpp @@ -106,6 +106,7 @@ BEGIN_MESSAGE_MAP(CSizingControlBar, baseCSizingControlBar) ON_WM_CREATE() ON_WM_PAINT() ON_WM_NCPAINT() + ON_WM_ERASEBKGND() ON_WM_NCCALCSIZE() ON_WM_WINDOWPOSCHANGING() ON_WM_CAPTURECHANGED() @@ -631,6 +632,22 @@ void CSizingControlBar::OnPaint() CPaintDC dc(this); } +BOOL CSizingControlBar::OnEraseBkgnd(CDC* pDC) +{ + // The bar's own client background is otherwise erased by DefWindowProc with the shared window-class + // brush (light COLOR_BTNFACE), which is only swapped dark as a side effect of the dark NC paint - so + // when a child doesn't fully cover the client (e.g. a 2px inset), or right after a redock/resize/theme + // toggle before the next NC paint, the exposed strip flashed WHITE. Paint it dark deterministically. + if (m_bUseDarkTheme) { + CRect rc; + GetClientRect(rc); + pDC->FillSolidRect(rc, ColorThemeRGB(45, 50, 55)); // same shade as the NC frame fill + return TRUE; + } + + return baseCSizingControlBar::OnEraseBkgnd(pDC); +} + LRESULT CSizingControlBar::OnNcHitTest(CPoint point) { CRect rcBar, rcEdge; diff --git a/src/ExtLib/ui/sizecbar/sizecbar.h b/src/ExtLib/ui/sizecbar/sizecbar.h index 34ad882f7e..9b21993ac4 100644 --- a/src/ExtLib/ui/sizecbar/sizecbar.h +++ b/src/ExtLib/ui/sizecbar/sizecbar.h @@ -195,6 +195,7 @@ class CSizingControlBar : public baseCSizingControlBar afx_msg void OnRButtonDown(UINT nFlags, CPoint point); afx_msg void OnWindowPosChanging(WINDOWPOS FAR* lpwndpos); afx_msg void OnPaint(); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg void OnClose(); afx_msg void OnSize(UINT nType, int cx, int cy); //}}AFX_MSG diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index ade37ce7ba..e059e698bb 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -463,10 +463,12 @@ namespace DarkTheme } if (msg == WM_PAINT) { const LRESULT res = DefSubclassProc(hWnd, msg, wParam, lParam); - FillListEmptyArea(hWnd); // dark over the light empty strip / stray column separator if (dwRefData) { DrawListGridlines(hWnd); // dark grid (only for lists that originally had grid lines) } + // Fill the empty area AFTER the grid so it also backstops it: any grid line drawn past the + // last item ("unnecessary rows" in the empty space below a short list) is overpainted dark. + FillListEmptyArea(hWnd); // dark over the light empty strip / stray column separator return res; } if (msg == WM_HSCROLL && dwRefData) { @@ -868,7 +870,21 @@ namespace DarkTheme ::ReleaseDC(hWnd, hdc); } } else { - ::InvalidateRect(hWnd, nullptr, FALSE); // focus changed — repaint so the frame stays dark + // Focus changed: invalidate ONLY the 1px border frame, not the whole control. A full + // InvalidateRect(nullptr) repainted the interior + dropdown button too, which made the + // combo twitch each time its dropdown opened/closed (Shader Editor). The border-only + // invalidate still re-runs the WM_PAINT overpaint so the frame stays dark. + RECT rc; + ::GetClientRect(hWnd, &rc); + const RECT edges[4] = { + { rc.left, rc.top, rc.right, rc.top + 1 }, // top + { rc.left, rc.bottom - 1, rc.right, rc.bottom }, // bottom + { rc.left, rc.top, rc.left + 1, rc.bottom }, // left + { rc.right - 1, rc.top, rc.right, rc.bottom }, // right + }; + for (const auto& e : edges) { + ::InvalidateRect(hWnd, &e, FALSE); + } } return r; } @@ -989,7 +1005,11 @@ namespace DarkTheme // The CFD combo border stays light on some machines (e.g. the Add-Favorite dropdown); // overpaint it dark like the edits. ApplyDarkBorder(hCtrl); - } else if (_wcsicmp(cls, L"Edit") == 0) { + } else if (_wcsicmp(cls, L"Edit") == 0 || _wcsicmp(cls, L"MFCMaskedEdit") == 0) { + // CMFCMaskedEdit (the GoTo dialog's time field) registers class "MFCMaskedEdit" by + // superclassing WC_EDIT, so it isn't class "Edit" and fell through un-themed (light). It + // doesn't reflect WM_CTLCOLOR, so the parent's WM_CTLCOLOREDIT still darkens the interior — + // it just needs the same single-line handling as a normal edit. const LONG est = GetWindowLongW(hCtrl, GWL_STYLE); if (est & (WS_VSCROLL | WS_HSCROLL)) { SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); // dark scrollbar From c0eb5e5ccbb7037f1651beb330f4b3877e300836 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Mon, 20 Jul 2026 22:02:15 -0300 Subject: [PATCH 20/24] =?UTF-8?q?Dark=20Options:=20owning-border=20rework?= =?UTF-8?q?=20=E2=80=94=20flicker-free=20dark=20control=20borders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the "overpaint a dark frame over the theme's light border" technique (which flashed light->dark on every non-client repaint — hover, tooltip, tab-switch, focus — and had repeatedly regressed to white borders / scroll-drag) with an owning-border subclass that removes the light source entirely. For edits / list boxes / list-views / tree-views / sunken statics (OwnerBorderSubclassProc, ApplyOwnerBorder): strip WS_EX_CLIENTEDGE/WS_BORDER so DefWindowProc draws no edge, re-reserve the identical band in WM_NCCALCSIZE (Def first, then InflateRect — so the scrollbar sizes correctly and the client metrics don't shift), and paint the band ourselves in WM_NCPAINT (fill the control's interior colour, then a 1px stroke, ExcludeClipRect over the scrollbar). The DarkMode_Explorer theme stays only for the dark scrollbar; there is no border style left for it to draw light, so nothing can flash. Hover/focus feedback is restored (frame-only RedrawWindow, dark->dark-tone, never a full invalidate). Borderless controls (MediaInfo NOT-WS_BORDER edit) get logical==0: no reserve, no paint, no subclass — stays borderless, nothing to drag on scroll. All thicknesses via GetSystemMetricsForDpi/GetDpiForWindow (reserve == stroke, no DPI seam); DPI change re-runs NCCALCSIZE. Band fill matches each control's real interior (editable edit/listbox = CtrlBackColor; read-only edit / list / tree / static = FaceColor) so there's no seam. Combos (ComboBorderSubclassProc, ApplyComboBorder): their border is client-area. CBS_DROPDOWNLIST is double-buffered (WM_PRINTCLIENT -> mem DC -> stroke -> one BitBlt); the editable CBS_DROPDOWN keeps the known-good post-Def client re-stroke with a border-ONLY focus invalidate (no dropdown twitch). StripThemeChildProc (toggle-off) restores the stripped styles and frees the heap data via FreeOwnerBorder. The old BorderSubclassProc / ApplyDarkBorder are removed. Design produced by an adversarial multi-agent pass against the regression history; ships behind the dark theme, needs a visual test pass (16-point checklist). Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/controls/DarkTheme.cpp | 346 +++++++++++++++++------ 1 file changed, 266 insertions(+), 80 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index e059e698bb..9b5a8e97ab 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -809,96 +809,280 @@ namespace DarkTheme return DefSubclassProc(hWnd, msg, wParam, lParam); } - // Edits, tree-views and list-views keep a light sunken border under native dark - // mode. We repaint just the outer border frame with the shared dark border - // colour (not the whole non-client ring, so a control's scrollbars are left - // alone), matching the spin buttons, colour wells and group boxes. - const UINT_PTR kBorderSubclassId = 5; + // ---- Owning-border subclass (replaces the overpaint BorderSubclassProc) -------------------- + // The overpaint approach let the theme draw the light client-edge, then painted dark over it = + // a light->dark FLASH on every non-client repaint (hover, tooltip, tab-switch, focus). Instead + // we REMOVE the light source: strip WS_EX_CLIENTEDGE / WS_BORDER so DefWindowProc draws no edge, + // re-reserve the identical band in WM_NCCALCSIZE (client metrics/text don't shift), and paint the + // band ourselves (fill the interior colour, then a 1px stroke). The control keeps its DarkMode_* + // theme only for the SCROLLBAR / dropdown, which Def still paints — but there is no border style + // left for it to draw light, so nothing can flash. (Mirrors Notepad++/darkmodelib.) + const UINT_PTR kOwnerBorderSubclassId = 13; + const UINT_PTR kComboBorderSubclassId = 12; + + enum { OB_CLIENTEDGE = 1, OB_STATICEDGE = 2, OB_WSBORDER = 4 }; + + struct OwnerBorderData { + DWORD stripped = 0; // OB_* bits we removed — so toggle-off can restore them exactly + int logical = 0; // 0 / 1 / 2 logical px of the owned border band + bool hot = false; + bool focus = false; + }; + + int OwnerBorderDpi(HWND h) { + const UINT dpi = ::GetDpiForWindow(h); + return dpi ? static_cast(dpi) : 96; + } + + int PhysBorder(HWND h, int logical) { // reserve width == stroke-band width, per-DPI + if (logical <= 0) { + return 0; + } + return logical * ::GetSystemMetricsForDpi(SM_CXBORDER, OwnerBorderDpi(h)); + } + + // Interior colour of the reserved band, matched to what OnCtlColor gives each control's client + // so the band blends seamlessly with the interior (edits/listbox = sunken CtrlBackColor; lists/ + // trees/statics = FaceColor). + COLORREF OwnerBorderFill(HWND h) { + wchar_t cls[32] = {}; + GetClassNameW(h, cls, _countof(cls)); + const bool isEdit = (_wcsicmp(cls, L"Edit") == 0 || _wcsicmp(cls, L"MFCMaskedEdit") == 0); + // Editable edits and list boxes get the sunken CtrlBackColor interior (WM_CTLCOLOREDIT/LISTBOX); + // read-only edits (WM_CTLCOLORSTATIC) and lists/trees/statics use FaceColor. Match the band fill + // to each control's real interior so the reserved band blends with the client (no seam). + if ((isEdit && !(GetWindowLongW(h, GWL_STYLE) & ES_READONLY)) || _wcsicmp(cls, L"ListBox") == 0) { + return CtrlBackColor(); + } + return FaceColor(); + } + + COLORREF OwnerBorderFrame(const OwnerBorderData* d, HWND h) { + if (::GetWindowLongW(h, GWL_STYLE) & WS_DISABLED) { + return ThemeRGB(50, 55, 60); + } + if (d->focus) { + return RGB(76, 194, 255); // celeste accent (matches buttons / trackbar thumb) + } + if (d->hot) { + return ThemeRGB(100, 105, 110); // subtle hover lift + } + return CtrlBorderColor(); // ThemeRGB(70, 75, 80) + } - LRESULT CALLBACK BorderSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + void OwnerBorderRefreshFrame(HWND h) { // frame ONLY — never the client (no twitch, no flash) + ::RedrawWindow(h, nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW); + } + + LRESULT CALLBACK OwnerBorderSubclassProc(HWND h, UINT msg, WPARAM w, LPARAM l, UINT_PTR /*id*/, DWORD_PTR ref) { + auto* d = reinterpret_cast(ref); switch (msg) { + case WM_NCCALCSIZE: { + // Def reserves the SCROLLBAR strip FIRST (list/tree/scrolling edit); THEN we carve our + // band out of the client so text/rows never sit under the stroke. Def-first is mandatory + // (shrinking before Def mis-sizes the scrollbar). Reserve == the thickness we removed, so + // the client rect stays pixel-identical to the light baseline (no reflow). + const LRESULT r = DefSubclassProc(h, msg, w, l); + if (w) { + const int t = PhysBorder(h, d->logical); + ::InflateRect(&reinterpret_cast(l)->rgrc[0], -t, -t); + } + return r; + } case WM_NCPAINT: { - // Let the default proc paint the non-client (scroll bars + its own border), then - // overpaint our dark frame on top. Drawing over whatever the theme rendered keeps the - // border dark on every system — skipping / clipping the default looked fine locally but - // rendered a WHITE border on other machines (nothing dark was painted over the edge). - const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); - RECT wr; - ::GetWindowRect(hWnd, &wr); - POINT org = { 0, 0 }; - ::ClientToScreen(hWnd, &org); - const int edge = org.y - wr.top; // NC border: 0 = none, 1 = WS_BORDER, 2 = client edge - // Only overpaint when the control actually reserves a non-client border. A borderless - // control (NOT WS_BORDER, e.g. the MediaInfo edit) has edge 0; a 1px frame there lands in - // the *client*, which the edit's ScrollWindowEx then drags into the middle of the text on - // scroll. Skip it — such controls stay borderless, as their template intends. - if (edge >= 1) { - if (HDC hdc = ::GetWindowDC(hWnd)) { - RECT rc = { 0, 0, wr.right - wr.left, wr.bottom - wr.top }; - HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); - for (int k = 0; k < edge; ++k) { - ::FrameRect(hdc, &rc, br); - ::InflateRect(&rc, -1, -1); - } - ::DeleteObject(br); - ::ReleaseDC(hWnd, hdc); + // Def paints ONLY the dark scrollbar — it CANNOT paint a light edge (we stripped the + // border styles), so no light state ever exists to flash from. Then we fill the reserved + // band with the interior colour (no sub-pixel gap can leak light on any DPI) and stroke + // a 1px outer border, keeping our paint OFF the scrollbar so we never touch it. + const LRESULT r = DefSubclassProc(h, msg, w, l); + const int t = PhysBorder(h, d->logical); + if (t <= 0) { + return r; + } + if (HDC hdc = ::GetWindowDC(h)) { + RECT wr; + ::GetWindowRect(h, &wr); + RECT band = { 0, 0, wr.right - wr.left, wr.bottom - wr.top }; + const LONG st = ::GetWindowLongW(h, GWL_STYLE); + const int dpi = OwnerBorderDpi(h); + if (st & WS_VSCROLL) { + const int sw = ::GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); + ::ExcludeClipRect(hdc, band.right - sw, band.top, band.right, band.bottom); } + if (st & WS_HSCROLL) { + const int sh = ::GetSystemMetricsForDpi(SM_CYHSCROLL, dpi); + ::ExcludeClipRect(hdc, band.left, band.bottom - sh, band.right, band.bottom); + } + HBRUSH fill = ::CreateSolidBrush(OwnerBorderFill(h)); + ::FillRect(hdc, &band, fill); + ::DeleteObject(fill); + HBRUSH edge = ::CreateSolidBrush(OwnerBorderFrame(d, h)); + ::FrameRect(hdc, &band, edge); + ::DeleteObject(edge); + ::ReleaseDC(h, hdc); } return r; } - case WM_PAINT: - case WM_SETFOCUS: - case WM_KILLFOCUS: { - // Combo boxes draw their border in the CLIENT area (not the non-client), and repaint it - // in a light 'focused' state on focus — which the WM_NCPAINT overpaint above never - // covers, so an active combo showed a white border (Add-to-Favorites). Only for combos, - // overpaint the client-edge frame dark after each WM_PAINT, and force a repaint on focus - // change. Gated on the ComboBox class so edits / lists / trees / statics are untouched. - wchar_t cls[16] = {}; - ::GetClassNameW(hWnd, cls, _countof(cls)); - if (_wcsicmp(cls, L"ComboBox") != 0) { - break; // not a combo — fall through to the default handling + case WM_MOUSEMOVE: + if (!d->hot) { + d->hot = true; + TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, h, 0 }; + ::TrackMouseEvent(&tme); + OwnerBorderRefreshFrame(h); } - const LRESULT r = DefSubclassProc(hWnd, msg, wParam, lParam); - if (msg == WM_PAINT) { - if (HDC hdc = ::GetDC(hWnd)) { + break; + case WM_MOUSELEAVE: + if (d->hot) { + d->hot = false; + OwnerBorderRefreshFrame(h); + } + break; + case WM_SETFOCUS: + d->focus = true; + OwnerBorderRefreshFrame(h); + break; + case WM_KILLFOCUS: + d->focus = false; + OwnerBorderRefreshFrame(h); + break; + case WM_ENABLE: + OwnerBorderRefreshFrame(h); + break; + case WM_DPICHANGED_AFTERPARENT: + ::SetWindowPos(h, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + OwnerBorderRefreshFrame(h); + break; + case WM_NCDESTROY: + delete d; + RemoveWindowSubclass(h, OwnerBorderSubclassProc, kOwnerBorderSubclassId); + break; + } + return DefSubclassProc(h, msg, w, l); + } + + // Frees the heap OwnerBorderData and removes the subclass on a runtime toggle-OFF (where + // RemoveWindowSubclass does NOT send WM_NCDESTROY, so the proc's delete never runs). Restores + // the exact border styles we stripped, so DefWindowProc redraws the native edge and the client + // metrics return to the light baseline. + void FreeOwnerBorder(HWND h) { + OwnerBorderData* d = nullptr; + if (::GetWindowSubclass(h, OwnerBorderSubclassProc, kOwnerBorderSubclassId, reinterpret_cast(&d))) { + if (d) { + LONG ex = ::GetWindowLongW(h, GWL_EXSTYLE); + LONG st = ::GetWindowLongW(h, GWL_STYLE); + if (d->stripped & OB_CLIENTEDGE) ex |= WS_EX_CLIENTEDGE; + if (d->stripped & OB_STATICEDGE) ex |= WS_EX_STATICEDGE; + if (d->stripped & OB_WSBORDER) st |= WS_BORDER; + ::SetWindowLongW(h, GWL_EXSTYLE, ex); + ::SetWindowLongW(h, GWL_STYLE, st); + delete d; + } + ::RemoveWindowSubclass(h, OwnerBorderSubclassProc, kOwnerBorderSubclassId); + } + } + + void ApplyOwnerBorder(HWND h) { + OwnerBorderData* existing = nullptr; + if (::GetWindowSubclass(h, OwnerBorderSubclassProc, kOwnerBorderSubclassId, reinterpret_cast(&existing))) { + return; // idempotent: never re-strip / re-capture (would lose the restore flags) + } + + const LONG ex = ::GetWindowLongW(h, GWL_EXSTYLE); + const LONG st = ::GetWindowLongW(h, GWL_STYLE); + + auto* d = new OwnerBorderData; + if (ex & WS_EX_CLIENTEDGE) { d->stripped |= OB_CLIENTEDGE; d->logical = 2; } + else if (st & WS_BORDER) { d->stripped |= OB_WSBORDER; d->logical = 1; } + if (ex & WS_EX_STATICEDGE) d->stripped |= OB_STATICEDGE; + + // Borderless control (e.g. the MediaInfo IDC_MIEDIT: NOT WS_BORDER, no client edge) → logical 0: + // reserve nothing, paint nothing, no subclass. It stays genuinely borderless, so its + // ScrollWindowEx has no client-drawn frame to smear into the text. (Decided from the REAL styles, + // never the old org.y heuristic that produced the white-border regression.) + if (d->logical == 0) { + delete d; + return; + } + + ::SetWindowLongW(h, GWL_EXSTYLE, ex & ~(WS_EX_CLIENTEDGE | WS_EX_STATICEDGE)); + if (st & WS_BORDER) { + ::SetWindowLongW(h, GWL_STYLE, st & ~WS_BORDER); + } + ::SetWindowSubclass(h, OwnerBorderSubclassProc, kOwnerBorderSubclassId, reinterpret_cast(d)); + ::SetWindowPos(h, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); // re-run NCCALCSIZE + NCPAINT + } + + // Combos draw their border in the CLIENT area (not the non-client), so they get their own + // subclass. Style-split: CBS_DROPDOWNLIST (no child edit) is double-buffered so its border can't + // flash; the editable CBS_DROPDOWN (has a self-painting child edit — WM_PRINTCLIENT would be + // unfaithful) keeps a post-Def client re-stroke with a border-ONLY invalidate on focus (a full + // InvalidateRect(nullptr) twitched the interior + dropdown button). + LRESULT CALLBACK ComboBorderSubclassProc(HWND h, UINT msg, WPARAM w, LPARAM l, UINT_PTR /*id*/, DWORD_PTR /*ref*/) { + const bool listType = (::GetWindowLongW(h, GWL_STYLE) & CBS_DROPDOWNLIST) == CBS_DROPDOWNLIST; + switch (msg) { + case WM_PAINT: + if (listType) { + RECT rc; + ::GetClientRect(h, &rc); + PAINTSTRUCT ps; + HDC hdc = ::BeginPaint(h, &ps); + HDC mem = ::CreateCompatibleDC(hdc); + HBITMAP bmp = ::CreateCompatibleBitmap(hdc, rc.right, rc.bottom); + HBITMAP old = static_cast(::SelectObject(mem, bmp)); + ::SendMessageW(h, WM_PRINTCLIENT, reinterpret_cast(mem), PRF_CLIENT | PRF_ERASEBKGND); + HBRUSH br = ::CreateSolidBrush((::GetFocus() == h) ? RGB(76, 194, 255) : CtrlBorderColor()); + ::FrameRect(mem, &rc, br); + ::DeleteObject(br); + ::BitBlt(hdc, 0, 0, rc.right, rc.bottom, mem, 0, 0, SRCCOPY); + ::SelectObject(mem, old); + ::DeleteObject(bmp); + ::DeleteDC(mem); + ::EndPaint(h, &ps); + return 0; + } else { + const LRESULT r = DefSubclassProc(h, msg, w, l); + if (HDC hdc = ::GetDC(h)) { RECT rc; - ::GetClientRect(hWnd, &rc); - HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); + ::GetClientRect(h, &rc); + HBRUSH br = ::CreateSolidBrush((::GetFocus() == h) ? RGB(76, 194, 255) : CtrlBorderColor()); ::FrameRect(hdc, &rc, br); ::DeleteObject(br); - ::ReleaseDC(hWnd, hdc); + ::ReleaseDC(h, hdc); } + return r; + } + case WM_SETFOCUS: + case WM_KILLFOCUS: { + const LRESULT r = DefSubclassProc(h, msg, w, l); + if (listType) { + ::InvalidateRect(h, nullptr, FALSE); // safe: its WM_PAINT is fully buffered } else { - // Focus changed: invalidate ONLY the 1px border frame, not the whole control. A full - // InvalidateRect(nullptr) repainted the interior + dropdown button too, which made the - // combo twitch each time its dropdown opened/closed (Shader Editor). The border-only - // invalidate still re-runs the WM_PAINT overpaint so the frame stays dark. + // border-ONLY invalidate; never InvalidateRect(nullptr) (that twitched the dropdown). RECT rc; - ::GetClientRect(hWnd, &rc); - const RECT edges[4] = { - { rc.left, rc.top, rc.right, rc.top + 1 }, // top - { rc.left, rc.bottom - 1, rc.right, rc.bottom }, // bottom - { rc.left, rc.top, rc.left + 1, rc.bottom }, // left - { rc.right - 1, rc.top, rc.right, rc.bottom }, // right + ::GetClientRect(h, &rc); + const RECT e[4] = { + { rc.left, rc.top, rc.right, rc.top + 1 }, + { rc.left, rc.bottom - 1, rc.right, rc.bottom }, + { rc.left, rc.top, rc.left + 1, rc.bottom }, + { rc.right - 1, rc.top, rc.right, rc.bottom }, }; - for (const auto& e : edges) { - ::InvalidateRect(hWnd, &e, FALSE); + for (const auto& x : e) { + ::InvalidateRect(h, &x, FALSE); } } return r; } case WM_NCDESTROY: - RemoveWindowSubclass(hWnd, BorderSubclassProc, kBorderSubclassId); + RemoveWindowSubclass(h, ComboBorderSubclassProc, kComboBorderSubclassId); break; } - return DefSubclassProc(hWnd, msg, wParam, lParam); + return DefSubclassProc(h, msg, w, l); } - void ApplyDarkBorder(HWND hCtrl) { - SetWindowSubclass(hCtrl, BorderSubclassProc, kBorderSubclassId, 0); - ::SetWindowPos(hCtrl, nullptr, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); // force NC repaint + void ApplyComboBorder(HWND h) { + SetWindowSubclass(h, ComboBorderSubclassProc, kComboBorderSubclassId, 0); + ::InvalidateRect(h, nullptr, FALSE); } // The colour-well buttons on the Interface / OSD / Subtitle-style pages are push buttons @@ -1001,10 +1185,11 @@ namespace DarkTheme SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); } } else if (_wcsicmp(cls, L"ComboBox") == 0) { - SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); - // The CFD combo border stays light on some machines (e.g. the Add-Favorite dropdown); - // overpaint it dark like the edits. - ApplyDarkBorder(hCtrl); + SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); // dark dropdown list + interior + // A combo paints its border in the CLIENT area (not the non-client), so it gets its own + // subclass that re-strokes the border dark flash-free (buffered for CBS_DROPDOWNLIST, + // border-only invalidate for the editable CBS_DROPDOWN). + ApplyComboBorder(hCtrl); } else if (_wcsicmp(cls, L"Edit") == 0 || _wcsicmp(cls, L"MFCMaskedEdit") == 0) { // CMFCMaskedEdit (the GoTo dialog's time field) registers class "MFCMaskedEdit" by // superclassing WC_EDIT, so it isn't class "Edit" and fell through un-themed (light). It @@ -1023,11 +1208,11 @@ namespace DarkTheme // which a classic edit honours fully, so dropping CFD doesn't lighten the interior. SetWindowTheme(hCtrl, L"", L""); } - // Repaint the light client-edge dark. This self-skips on a borderless edit (NOT WS_BORDER, - // e.g. the MediaInfo dump) — where a drawn frame would land in the client and get dragged - // into the text on scroll — so only edits with a real border (the Details/Clip multiline - // field, single-line value boxes) get the dark frame. No white edge, no scroll drag. - ApplyDarkBorder(hCtrl); + // Own the border dark: ApplyOwnerBorder strips the light client-edge/border, reserves the + // same band in WM_NCCALCSIZE, and paints it itself — there is no light source, so no + // hover/tab-switch flash. Self-skips a borderless edit (the MediaInfo dump), which stays + // borderless so nothing gets dragged into the text on scroll. + ApplyOwnerBorder(hCtrl); } else if (_wcsicmp(cls, L"SysListView32") == 0) { // List-view controls ignore WM_CTLCOLOR: their background (the area // not covered by columns/rows) must be set explicitly, otherwise it @@ -1056,14 +1241,14 @@ namespace DarkTheme if (HWND hHeader = reinterpret_cast(::SendMessageW(hCtrl, LVM_GETHEADER, 0, 0))) { InvalidateRect(hHeader, nullptr, TRUE); } - ApplyDarkBorder(hCtrl); // dark outer border to match everything else + ApplyOwnerBorder(hCtrl); // owned dark border (scrollbar stays DarkMode_Explorer) } else if (_wcsicmp(cls, L"SysTreeView32") == 0) { // Tree-views, like list-views, need their background/text colors set // explicitly (SetWindowTheme only handles the glyphs and scrollbar). SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); ::SendMessageW(hCtrl, TVM_SETBKCOLOR, 0, static_cast(FaceColor())); ::SendMessageW(hCtrl, TVM_SETTEXTCOLOR, 0, static_cast(TextColor())); - ApplyDarkBorder(hCtrl); // dark outer border to match everything else + ApplyOwnerBorder(hCtrl); // owned dark border (scrollbar stays DarkMode_Explorer) } else if (_wcsicmp(cls, UPDOWN_CLASSW) == 0) { // Spin buttons: fully owner-drawn (native dark mode leaves them light). SetWindowSubclass(hCtrl, SpinSubclassProc, kSpinSubclassId, 0); @@ -1075,7 +1260,7 @@ namespace DarkTheme // on scroll, so keep the custom border even when it scrolls (the theme's own border // rendered white on some machines). SetWindowTheme(hCtrl, L"DarkMode_Explorer", nullptr); - ApplyDarkBorder(hCtrl); + ApplyOwnerBorder(hCtrl); // owned dark border (scrollbar stays DarkMode_Explorer) } else if (_wcsicmp(cls, L"Static") == 0) { // Sunken value boxes (e.g. Brightness/Contrast/Hue/Saturation on the // Color correction page are SS_SUNKEN RTEXT statics) keep a light 3D edge @@ -1085,7 +1270,7 @@ namespace DarkTheme const LONG ex = GetWindowLongW(hCtrl, GWL_EXSTYLE); const LONG sType = st & SS_TYPEMASK; if ((st & SS_SUNKEN) || (ex & (WS_EX_CLIENTEDGE | WS_EX_STATICEDGE))) { - ApplyDarkBorder(hCtrl); + ApplyOwnerBorder(hCtrl); } else if (sType == SS_LEFT || sType == SS_CENTER || sType == SS_RIGHT || sType == SS_LEFTNOWORDWRAP || sType == SS_SIMPLE) { // Plain text labels: owner-draw disabled ones flat (Windows would emboss them, @@ -1124,7 +1309,8 @@ namespace DarkTheme RemoveWindowSubclass(hChild, GroupBoxSubclassProc, kGroupBoxSubclassId); RemoveWindowSubclass(hChild, ButtonSubclassProc, kButtonSubclassId); RemoveWindowSubclass(hChild, SpinSubclassProc, kSpinSubclassId); - RemoveWindowSubclass(hChild, BorderSubclassProc, kBorderSubclassId); + FreeOwnerBorder(hChild); // owning border: restores the stripped WS_EX_CLIENTEDGE/WS_BORDER + frees the heap data + RemoveWindowSubclass(hChild, ComboBorderSubclassProc, kComboBorderSubclassId); RemoveWindowSubclass(hChild, TrackbarSubclassProc, kTrackbarSubclassId); RemoveWindowSubclass(hChild, StaticSubclassProc, kStaticSubclassId); From 1d5d7c88db066269e19315e10d3034ebb92e0b5f Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Mon, 20 Jul 2026 23:36:20 -0300 Subject: [PATCH 21/24] Dark Options: dark scrollbar edge, dark border colour, drop focus accent Follow-ups to the owning-border rework, from visual testing: - CtrlBorderColor: ThemeRGB(35,40,45). The old (70,75,80) rendered as a light/near-white line once the user's theme brightness/colour sliders are up (ThemeRGB tints by (brightness+value)*tint/256) - the actual source of every "white control border" report. - OwnerBorder WM_NCPAINT: cover the scrollbar's light inner edge using the scrollbar's REAL rect (GetScrollBarInfo); the earlier band.right-sw guess landed wrong after the custom WM_NCCALCSIZE and never covered it. Also exclude the client rect so the band fill can't blank list content. - Drop the celeste focus / grey hover accent from the owning border and the combo border: a bright frame around the focused Options nav tree read as garish and followed the focus around. Consistent dark border in every state (disabled stays dimmer). - OwnerBorderRefreshFrame uses SWP_FRAMECHANGED (frame-only repaint). Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/controls/DarkTheme.cpp | 87 ++++++++++++------------ 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 9b5a8e97ab..efbf2b7757 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -857,21 +857,18 @@ namespace DarkTheme return FaceColor(); } - COLORREF OwnerBorderFrame(const OwnerBorderData* d, HWND h) { - if (::GetWindowLongW(h, GWL_STYLE) & WS_DISABLED) { - return ThemeRGB(50, 55, 60); - } - if (d->focus) { - return RGB(76, 194, 255); // celeste accent (matches buttons / trackbar thumb) - } - if (d->hot) { - return ThemeRGB(100, 105, 110); // subtle hover lift - } - return CtrlBorderColor(); // ThemeRGB(70, 75, 80) + COLORREF OwnerBorderFrame(const OwnerBorderData* /*d*/, HWND h) { + // A consistent dark border in every state. (A hover/focus accent was tried to restore the + // hover/click feedback, but a bright celeste frame around a big focused panel — the Options + // nav tree — read as garish and "followed" the focus around, so it's dropped.) + return (::GetWindowLongW(h, GWL_STYLE) & WS_DISABLED) ? ThemeRGB(50, 55, 60) : CtrlBorderColor(); } - void OwnerBorderRefreshFrame(HWND h) { // frame ONLY — never the client (no twitch, no flash) - ::RedrawWindow(h, nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW); + void OwnerBorderRefreshFrame(HWND h) { + // Frame ONLY — SWP_FRAMECHANGED re-runs WM_NCCALCSIZE + WM_NCPAINT WITHOUT invalidating the + // CLIENT, so a hover/focus border repaint never touches (nor flickers) the list/edit content. + ::SetWindowPos(h, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); } LRESULT CALLBACK OwnerBorderSubclassProc(HWND h, UINT msg, WPARAM w, LPARAM l, UINT_PTR /*id*/, DWORD_PTR ref) { @@ -903,15 +900,37 @@ namespace DarkTheme RECT wr; ::GetWindowRect(h, &wr); RECT band = { 0, 0, wr.right - wr.left, wr.bottom - wr.top }; + // CRITICAL: GetWindowDC is the WHOLE window (client included). Exclude the CLIENT rect + // so our fill/stroke only touch the reserved border band — otherwise FillRect(band) + // paints the interior colour over the whole control (blanking the list content on every + // hover repaint). Map the client rect into window coordinates first. + RECT client; + ::GetClientRect(h, &client); + ::MapWindowPoints(h, nullptr, reinterpret_cast(&client), 2); // client -> screen + ::OffsetRect(&client, -wr.left, -wr.top); // screen -> window + ::ExcludeClipRect(hdc, client.left, client.top, client.right, client.bottom); const LONG st = ::GetWindowLongW(h, GWL_STYLE); const int dpi = OwnerBorderDpi(h); + // Exclude the scrollbar so we don't overpaint it, but leave its INNER 2px unexcluded so + // our dark fill covers the light edge DarkMode_Explorer draws between the list and the + // scrollbar. Use the scrollbar's REAL rect from GetScrollBarInfo — the earlier + // band.right - sw guess was wrong after our custom WM_NCCALCSIZE, so it never covered it. + const int inner = 2 * ::GetSystemMetricsForDpi(SM_CXBORDER, dpi); if (st & WS_VSCROLL) { - const int sw = ::GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); - ::ExcludeClipRect(hdc, band.right - sw, band.top, band.right, band.bottom); + SCROLLBARINFO sbi = { sizeof(sbi) }; + if (::GetScrollBarInfo(h, OBJID_VSCROLL, &sbi) && !(sbi.rgstate[0] & STATE_SYSTEM_INVISIBLE)) { + RECT sb = sbi.rcScrollBar; + ::OffsetRect(&sb, -wr.left, -wr.top); // screen -> window + ::ExcludeClipRect(hdc, sb.left + inner, sb.top, sb.right, sb.bottom); + } } if (st & WS_HSCROLL) { - const int sh = ::GetSystemMetricsForDpi(SM_CYHSCROLL, dpi); - ::ExcludeClipRect(hdc, band.left, band.bottom - sh, band.right, band.bottom); + SCROLLBARINFO sbi = { sizeof(sbi) }; + if (::GetScrollBarInfo(h, OBJID_HSCROLL, &sbi) && !(sbi.rgstate[0] & STATE_SYSTEM_INVISIBLE)) { + RECT sb = sbi.rcScrollBar; + ::OffsetRect(&sb, -wr.left, -wr.top); + ::ExcludeClipRect(hdc, sb.left, sb.top + inner, sb.right, sb.bottom); + } } HBRUSH fill = ::CreateSolidBrush(OwnerBorderFill(h)); ::FillRect(hdc, &band, fill); @@ -923,34 +942,11 @@ namespace DarkTheme } return r; } - case WM_MOUSEMOVE: - if (!d->hot) { - d->hot = true; - TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, h, 0 }; - ::TrackMouseEvent(&tme); - OwnerBorderRefreshFrame(h); - } - break; - case WM_MOUSELEAVE: - if (d->hot) { - d->hot = false; - OwnerBorderRefreshFrame(h); - } - break; - case WM_SETFOCUS: - d->focus = true; - OwnerBorderRefreshFrame(h); - break; - case WM_KILLFOCUS: - d->focus = false; - OwnerBorderRefreshFrame(h); - break; case WM_ENABLE: OwnerBorderRefreshFrame(h); break; case WM_DPICHANGED_AFTERPARENT: - ::SetWindowPos(h, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); - OwnerBorderRefreshFrame(h); + OwnerBorderRefreshFrame(h); // SWP_FRAMECHANGED re-runs NCCALCSIZE (new-DPI reserve) + NCPAINT break; case WM_NCDESTROY: delete d; @@ -1031,7 +1027,7 @@ namespace DarkTheme HBITMAP bmp = ::CreateCompatibleBitmap(hdc, rc.right, rc.bottom); HBITMAP old = static_cast(::SelectObject(mem, bmp)); ::SendMessageW(h, WM_PRINTCLIENT, reinterpret_cast(mem), PRF_CLIENT | PRF_ERASEBKGND); - HBRUSH br = ::CreateSolidBrush((::GetFocus() == h) ? RGB(76, 194, 255) : CtrlBorderColor()); + HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); // consistent dark border in every state (incl. focused) ::FrameRect(mem, &rc, br); ::DeleteObject(br); ::BitBlt(hdc, 0, 0, rc.right, rc.bottom, mem, 0, 0, SRCCOPY); @@ -1045,7 +1041,7 @@ namespace DarkTheme if (HDC hdc = ::GetDC(h)) { RECT rc; ::GetClientRect(h, &rc); - HBRUSH br = ::CreateSolidBrush((::GetFocus() == h) ? RGB(76, 194, 255) : CtrlBorderColor()); + HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); // consistent dark border in every state (incl. focused) ::FrameRect(hdc, &rc, br); ::DeleteObject(br); ::ReleaseDC(h, hdc); @@ -1354,7 +1350,10 @@ namespace DarkTheme COLORREF FaceColor() { return ThemeRGB(22, 27, 32); } COLORREF TextColor() { return RGB(165, 170, 175); } COLORREF CtrlBackColor() { return ThemeRGB(10, 14, 18); } - COLORREF CtrlBorderColor() { return ThemeRGB(70, 75, 80); } + // Kept close to FaceColor(22,27,32) on purpose: ThemeRGB is (brightness + value) * tint / 256, so a + // high base like 70/75/80 renders as a LIGHT (near-white, tinted) line once the user's theme + // brightness/colour sliders are up — which is what made every control border read as white. + COLORREF CtrlBorderColor() { return ThemeRGB(35, 40, 45); } COLORREF GridlineColor() { return ThemeRGB(40, 45, 50); } void AllowDarkModeForApp() { From dc9c56517840edaa1f8a25eda133a3ff630dba11 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Fri, 24 Jul 2026 03:31:35 -0300 Subject: [PATCH 22/24] Dark Options: fix localized white bars, sunken statics, combo shades, thin borders Localized UIs showed light "white bars" on many pages (Player/History, Web Interface, Online services, Frame sync, External Filters, Priority, File Properties). The base .rc draws the page dividers as SS_OWNERDRAW statics that CPPageBase::OnDrawItem paints dark, but the per-language resources still carry the older SS_ETCHEDHORZ dividers, which draw their own light 3D line and never reach OnDrawItem. Own their paint with a subclass that draws the same flat dark line, self-contained so it also covers dialogs that don't handle WM_DRAWITEM. The Color Correction value boxes (Brightness/Contrast/Hue/Saturation) are RTEXT SS_SUNKEN statics. ApplyOwnerBorder only recognised WS_EX_CLIENTEDGE/WS_BORDER, so it left them at logical 0 and painted nothing - their native sunken edge stayed light ("white-gray borders"). Recognise SS_SUNKEN (gated to the Static class, since 0x1000 aliases ES_WANTRETURN on edits) and WS_EX_STATICEDGE as a 1px owned border, stripping SS_SUNKEN because its edge is client-drawn. A combo's dropdown list (ComboLBox) is a popup, not a child, so the theme pass never reached it; it only looked dark because comctl32 propagated the combo's theme. The disable/enable cycle behind Reset (Color Correction) and Default (Sound processing) rebuilt the list and dropped that, so it reopened in another shade. Theme it directly and re-assert on WM_ENABLE. An editable combo's child edit sends WM_CTLCOLOREDIT to the combo, not to the page, so nothing darkened its interior and it read as highlighted on open; add the handler. DarkMode_CFD also repaints its border hot on hover (the white line) - re-stroke it dark once per hover-enter, gated by a flag so it can't churn. FrameRect is always 1px, so the 2px band reserved for client-edge lists/trees showed a 1px border - half what it replaced. Stroke the full band with concentric frames; edits and value-box statics stay 1px. Dropping the list-type combo's full InvalidateRect on focus removes the twitch when its dropdown opens: Windows already repaints it on focus, and the border is state-independent, so the extra full repaint only fought the drop. Sizing bars erased their client with the lighter NC/caption shade while hosting dark content, so a strip exposed during a redock/resize flashed pale (near-white at high brightness). Give the client its own brush at the content shade and keep the NC frame as it was. Also free each bar brush exactly once - the destructor released m_hBrush twice and leaked m_hBrushFrame. Co-Authored-By: Claude Opus 4.8 --- src/ExtLib/ui/sizecbar/sizecbar.cpp | 37 ++++- src/ExtLib/ui/sizecbar/sizecbar.h | 3 + src/apps/mplayerc/controls/DarkTheme.cpp | 187 +++++++++++++++++++---- 3 files changed, 193 insertions(+), 34 deletions(-) diff --git a/src/ExtLib/ui/sizecbar/sizecbar.cpp b/src/ExtLib/ui/sizecbar/sizecbar.cpp index 97895f6649..12212b5cb8 100644 --- a/src/ExtLib/ui/sizecbar/sizecbar.cpp +++ b/src/ExtLib/ui/sizecbar/sizecbar.cpp @@ -82,6 +82,7 @@ CSizingControlBar::CSizingControlBar() m_hBrush_orig = nullptr; m_hBrush = nullptr; + m_hBrushClient = nullptr; m_dwBrushColor = 0; m_bUseDarkTheme = false; @@ -90,13 +91,15 @@ CSizingControlBar::CSizingControlBar() CSizingControlBar::~CSizingControlBar() { + // (m_hBrush was deleted twice here and m_hBrushFrame was guarded by m_hBrush_orig, so the frame brush + // leaked while a freed handle was passed to DeleteObject a second time. Each brush is freed once now.) if (m_hBrush) { ::DeleteObject(m_hBrush); } - if (m_hBrush_orig) { - ::DeleteObject(m_hBrush); + if (m_hBrushClient) { + ::DeleteObject(m_hBrushClient); } - if (m_hBrush_orig) { + if (m_hBrushFrame) { ::DeleteObject(m_hBrushFrame); } } @@ -574,15 +577,30 @@ void CSizingControlBar::OnNcPaint() //MPC-BE custom code start if (m_bUseDarkTheme) { const auto dwBrushColor = ColorThemeRGB(45, 50, 55); - if (m_dwBrushColor != dwBrushColor && m_hBrush) { - ::DeleteObject(m_hBrush); - m_hBrush = nullptr; + if (m_dwBrushColor != dwBrushColor) { + // the theme sliders moved - drop both cached brushes so they re-tint + if (m_hBrush) { + ::DeleteObject(m_hBrush); + m_hBrush = nullptr; + } + if (m_hBrushClient) { + ::DeleteObject(m_hBrushClient); + m_hBrushClient = nullptr; + } } m_dwBrushColor = dwBrushColor; if (!m_hBrush) { m_hBrush = ::CreateSolidBrush(dwBrushColor); } - ::SetClassLongPtrW(m_hWnd, GCLP_HBRBACKGROUND, (LONG_PTR)m_hBrush); //MPC-BE patch + // The CLASS background brush is what DefWindowProc uses to erase a client strip that gets exposed + // during a redock/resize (before the child dialog catches up). It must be the DARK CONTENT shade, + // not the lighter NC/caption shade above: ColorThemeRGB scales with the brightness slider, so + // (45,50,55) reads as a pale/near-white rectangle at high brightness while the hosted content + // (shader editor dialog, lists) is (22,27,32). Keep the NC frame fill below on m_hBrush. + if (!m_hBrushClient) { + m_hBrushClient = ::CreateSolidBrush(ColorThemeRGB(22, 27, 32)); + } + ::SetClassLongPtrW(m_hWnd, GCLP_HBRBACKGROUND, (LONG_PTR)m_hBrushClient); //MPC-BE patch mdc.FrameRect(rcDraw, CBrush::FromHandle(m_hBrushFrame)); // Draw Black Frame @@ -641,7 +659,10 @@ BOOL CSizingControlBar::OnEraseBkgnd(CDC* pDC) if (m_bUseDarkTheme) { CRect rc; GetClientRect(rc); - pDC->FillSolidRect(rc, ColorThemeRGB(45, 50, 55)); // same shade as the NC frame fill + // Match the DARK content the bar hosts (22,27,32), NOT the lighter NC/caption shade: ColorThemeRGB + // scales with the brightness slider, so the old (45,50,55) fill showed up as a pale/near-white + // rectangle whenever a strip was briefly exposed on redock/resize. + pDC->FillSolidRect(rc, ColorThemeRGB(22, 27, 32)); return TRUE; } diff --git a/src/ExtLib/ui/sizecbar/sizecbar.h b/src/ExtLib/ui/sizecbar/sizecbar.h index 9b21993ac4..74a1c3753a 100644 --- a/src/ExtLib/ui/sizecbar/sizecbar.h +++ b/src/ExtLib/ui/sizecbar/sizecbar.h @@ -172,6 +172,9 @@ class CSizingControlBar : public baseCSizingControlBar CSize m_szFixedFloat; HBRUSH m_hBrush, m_hBrush_orig, m_hBrushFrame; + // Separate CLIENT background brush. m_hBrush is the lighter NC/caption shade; the client must match the + // DARK content the bar hosts (shader editor dialog / lists), or an exposed strip reads as a pale rectangle. + HBRUSH m_hBrushClient; COLORREF m_dwBrushColor; //MPC-BE custom code end diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index efbf2b7757..abae90b197 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -820,7 +820,7 @@ namespace DarkTheme const UINT_PTR kOwnerBorderSubclassId = 13; const UINT_PTR kComboBorderSubclassId = 12; - enum { OB_CLIENTEDGE = 1, OB_STATICEDGE = 2, OB_WSBORDER = 4 }; + enum { OB_CLIENTEDGE = 1, OB_STATICEDGE = 2, OB_WSBORDER = 4, OB_SSSUNKEN = 8 }; struct OwnerBorderData { DWORD stripped = 0; // OB_* bits we removed — so toggle-off can restore them exactly @@ -935,8 +935,25 @@ namespace DarkTheme HBRUSH fill = ::CreateSolidBrush(OwnerBorderFill(h)); ::FillRect(hdc, &band, fill); ::DeleteObject(fill); + // Stroke the border. GDI FrameRect is always 1px, so a 2px reserved band (WS_EX_CLIENTEDGE + // lists/trees/listboxes, logical=2) showed only a 1px line with 1px of interior fill — half + // the native client-edge thickness ("borders too thin"). Stroke the FULL band with + // concentric 1px frames so the visible border matches what it replaced. Keep single-line + // edits and the small value-box statics at 1px (a 2px frame reads heavy on a one-line field); + // their inner band keeps the interior fill. The inner frames stay strictly inside the + // reserved band (client is ExcludeClipRect'd) and the scrollbar strip is still excluded, so + // no list content or scrollbar is touched — SWP_FRAMECHANGED keeps it flicker-free. + wchar_t obcls[32] = {}; + GetClassNameW(h, obcls, _countof(obcls)); + const bool thinBorder = (_wcsicmp(obcls, L"Edit") == 0 || _wcsicmp(obcls, L"MFCMaskedEdit") == 0 + || _wcsicmp(obcls, L"Static") == 0); + const int stroke = thinBorder ? ::GetSystemMetricsForDpi(SM_CXBORDER, dpi) : t; HBRUSH edge = ::CreateSolidBrush(OwnerBorderFrame(d, h)); - ::FrameRect(hdc, &band, edge); + RECT fr = band; + for (int i = 0; i < stroke; ++i) { + ::FrameRect(hdc, &fr, edge); + ::InflateRect(&fr, -1, -1); + } ::DeleteObject(edge); ::ReleaseDC(h, hdc); } @@ -969,6 +986,7 @@ namespace DarkTheme if (d->stripped & OB_CLIENTEDGE) ex |= WS_EX_CLIENTEDGE; if (d->stripped & OB_STATICEDGE) ex |= WS_EX_STATICEDGE; if (d->stripped & OB_WSBORDER) st |= WS_BORDER; + if (d->stripped & OB_SSSUNKEN) st |= SS_SUNKEN; ::SetWindowLongW(h, GWL_EXSTYLE, ex); ::SetWindowLongW(h, GWL_STYLE, st); delete d; @@ -989,7 +1007,18 @@ namespace DarkTheme auto* d = new OwnerBorderData; if (ex & WS_EX_CLIENTEDGE) { d->stripped |= OB_CLIENTEDGE; d->logical = 2; } else if (st & WS_BORDER) { d->stripped |= OB_WSBORDER; d->logical = 1; } - if (ex & WS_EX_STATICEDGE) d->stripped |= OB_STATICEDGE; + if (ex & WS_EX_STATICEDGE) { d->stripped |= OB_STATICEDGE; if (d->logical == 0) { d->logical = 1; } } + + // A light 1px edge that is NOT WS_EX_CLIENTEDGE/WS_BORDER still needs owning, or it stays light + // under dark mode: the Color Correction value boxes (Brightness/Contrast/Hue/Saturation, IDC_STATIC1..4) + // are RTEXT SS_SUNKEN statics. Their sunken edge is drawn in the CLIENT (not the non-client), so we + // STRIP SS_SUNKEN (below) — else it keeps painting itself light under our band — and own a 1px band. + // SS_SUNKEN (0x1000) aliases ES_WANTRETURN on edits, so gate it to the Static class. + if (d->logical == 0 && (st & SS_SUNKEN)) { + wchar_t cls[32] = {}; + GetClassNameW(h, cls, _countof(cls)); + if (_wcsicmp(cls, L"Static") == 0) { d->stripped |= OB_SSSUNKEN; d->logical = 1; } + } // Borderless control (e.g. the MediaInfo IDC_MIEDIT: NOT WS_BORDER, no client edge) → logical 0: // reserve nothing, paint nothing, no subclass. It stays genuinely borderless, so its @@ -1001,22 +1030,65 @@ namespace DarkTheme } ::SetWindowLongW(h, GWL_EXSTYLE, ex & ~(WS_EX_CLIENTEDGE | WS_EX_STATICEDGE)); - if (st & WS_BORDER) { - ::SetWindowLongW(h, GWL_STYLE, st & ~WS_BORDER); + LONG newSt = st; + if (st & WS_BORDER) { newSt &= ~WS_BORDER; } + if (d->stripped & OB_SSSUNKEN) { newSt &= ~SS_SUNKEN; } // its edge is client-drawn; remove it so only our band shows + if (newSt != st) { + ::SetWindowLongW(h, GWL_STYLE, newSt); } ::SetWindowSubclass(h, OwnerBorderSubclassProc, kOwnerBorderSubclassId, reinterpret_cast(d)); ::SetWindowPos(h, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); // re-run NCCALCSIZE + NCPAINT } - // Combos draw their border in the CLIENT area (not the non-client), so they get their own - // subclass. Style-split: CBS_DROPDOWNLIST (no child edit) is double-buffered so its border can't - // flash; the editable CBS_DROPDOWN (has a self-painting child edit — WM_PRINTCLIENT would be - // unfaithful) keeps a post-Def client re-stroke with a border-ONLY invalidate on focus (a full - // InvalidateRect(nullptr) twitched the interior + dropdown button). + // Dark-theme a combo's dropdown LIST popup (class ComboLBox) directly. It is a WS_POPUP owned by the + // combo, NOT a child of the page, so ApplyThemeToChildren's EnumChildWindows never reaches it — it + // only looked dark because comctl32 propagated the combo's DarkMode_CFD theme when it built the list. + // A disable->enable cycle (Color-Correction Reset, Audio Sound-processing Default, which toggle the + // combo's WS_DISABLED) makes comctl32 rebuild the list and drop that theme, so it repaints in a second + // shade. Assert the theme on the list ourselves so its shade is the same in every state. + void ThemeComboDropList(HWND hCombo) { + COMBOBOXINFO cbi = { sizeof(cbi) }; + if (::GetComboBoxInfo(hCombo, &cbi) && cbi.hwndList) { + if (pAllowDarkModeForWindow) { pAllowDarkModeForWindow(cbi.hwndList, true); } + ::SetWindowTheme(cbi.hwndList, L"DarkMode_CFD", nullptr); + } + } + + // Invalidate ONLY the 1px border ring of a combo (never InvalidateRect(nullptr) — that twitched the + // interior + dropdown button). The WM_PAINT re-stroke then repaints the dark border over it. + void InvalidateComboBorderEdges(HWND h) { + RECT rc; + ::GetClientRect(h, &rc); + const RECT e[4] = { + { rc.left, rc.top, rc.right, rc.top + 1 }, + { rc.left, rc.bottom - 1, rc.right, rc.bottom }, + { rc.left, rc.top, rc.left + 1, rc.bottom }, + { rc.right - 1, rc.top, rc.right, rc.bottom }, + }; + for (const auto& x : e) { + ::InvalidateRect(h, &x, FALSE); + } + } + + // Combos draw their border in the CLIENT area (not the non-client), so they get their own subclass. + // Style-split: CBS_DROPDOWNLIST (no child edit) is double-buffered so its border can't flash; the + // editable CBS_DROPDOWN (has a self-painting child edit — WM_PRINTCLIENT would be unfaithful) keeps a + // post-Def client re-stroke with a border-ONLY invalidate on focus/hover. LRESULT CALLBACK ComboBorderSubclassProc(HWND h, UINT msg, WPARAM w, LPARAM l, UINT_PTR /*id*/, DWORD_PTR /*ref*/) { const bool listType = (::GetWindowLongW(h, GWL_STYLE) & CBS_DROPDOWNLIST) == CBS_DROPDOWNLIST; switch (msg) { + case WM_CTLCOLOREDIT: { + // The editable combo's child Edit sends WM_CTLCOLOREDIT to the combo (its parent), not to + // the page, so the page's dark handler never sees it and the edit interior renders light + // ("highlighted" the moment the sheet opens). Give it the same dark edit brush a standalone + // edit gets. Mirrors the list-view in-place edit handler (WM_CTLCOLOREDIT above). + CDC* pDC = CDC::FromHandle(reinterpret_cast(w)); + if (HBRUSH hbr = OnCtlColor(pDC, CTLCOLOR_EDIT)) { + return reinterpret_cast(hbr); + } + break; + } case WM_PAINT: if (listType) { RECT rc; @@ -1051,25 +1123,42 @@ namespace DarkTheme case WM_SETFOCUS: case WM_KILLFOCUS: { const LRESULT r = DefSubclassProc(h, msg, w, l); - if (listType) { - ::InvalidateRect(h, nullptr, FALSE); // safe: its WM_PAINT is fully buffered - } else { - // border-ONLY invalidate; never InvalidateRect(nullptr) (that twitched the dropdown). - RECT rc; - ::GetClientRect(h, &rc); - const RECT e[4] = { - { rc.left, rc.top, rc.right, rc.top + 1 }, - { rc.left, rc.bottom - 1, rc.right, rc.bottom }, - { rc.left, rc.top, rc.left + 1, rc.bottom }, - { rc.right - 1, rc.top, rc.right, rc.bottom }, - }; - for (const auto& x : e) { - ::InvalidateRect(h, &x, FALSE); - } + if (!listType) { + // editable: border-only re-stroke on focus change. + InvalidateComboBorderEdges(h); } + // listType: NO explicit invalidate. Windows already repaints the combo on focus (to show + // its focus indicator), which re-runs our buffered WM_PAINT and redraws the dark border. + // An extra full InvalidateRect here double-painted and TWITCHED the dropdown as it opened + // (the Shader-editor combos). The border is state-independent, so none is needed. + return r; + } + case WM_MOUSEMOVE: + // DarkMode_CFD repaints the editable combo's border in a light "hot" colour on hover (the + // white line xLn2 saw). Overpaint it dark by re-stroking the border once per hover-enter. + // One-shot prop so we don't invalidate on every move (that would churn/flicker). + if (!listType && !::GetPropW(h, L"MPC_CB_HOT")) { + ::SetPropW(h, L"MPC_CB_HOT", reinterpret_cast(1)); + TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, h, 0 }; + ::TrackMouseEvent(&tme); // so we get WM_MOUSELEAVE to clear the flag + InvalidateComboBorderEdges(h); + } + break; // pass through so the combo still handles the move + case WM_MOUSELEAVE: + if (::GetPropW(h, L"MPC_CB_HOT")) { + ::RemovePropW(h, L"MPC_CB_HOT"); + InvalidateComboBorderEdges(h); // re-stroke dark once the CFD hot border clears + } + break; + case WM_ENABLE: { + // Reset/Default toggle the combo's WS_DISABLED, which makes comctl32 rebuild the drop list + // and lose its theme -> it reopens in a different shade. Re-assert the dark theme on it. + const LRESULT r = DefSubclassProc(h, msg, w, l); + ThemeComboDropList(h); return r; } case WM_NCDESTROY: + ::RemovePropW(h, L"MPC_CB_HOT"); RemoveWindowSubclass(h, ComboBorderSubclassProc, kComboBorderSubclassId); break; } @@ -1154,6 +1243,46 @@ namespace DarkTheme return DefSubclassProc(hWnd, msg, wParam, lParam); } + // Separator statics (SS_ETCHEDHORZ / SS_ETCHEDVERT / SS_ETCHEDFRAME). The base (English) .rc draws + // the Options-page dividers as SS_OWNERDRAW statics that CPPageBase::OnDrawItem paints dark. But the + // per-language resources (mpcresources..dll) still carry the OLD SS_ETCHEDHORZ dividers, which + // draw their own light 3D etched line from the system colours — a "white bar" on the dark background + // in every non-English UI (Player/History, Web Interface, Online services, Frame sync, External + // Filters, Priority, File Properties...). Those never reach OnDrawItem (they aren't owner-draw), so we + // own their paint here and draw the same flat dark line OnDrawItem uses. Self-contained, so it also + // covers aux dialogs / File Properties whose parent doesn't handle WM_DRAWITEM. + const UINT_PTR kEtchedStaticSubclassId = 14; + + LRESULT CALLBACK EtchedStaticSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { + switch (msg) { + case WM_ERASEBKGND: + return 1; // painted whole in WM_PAINT + case WM_PAINT: { + PAINTSTRUCT ps; + CDC* pDC = CDC::FromHandle(::BeginPaint(hWnd, &ps)); + CRect rc; + ::GetClientRect(hWnd, &rc); + pDC->FillSolidRect(rc, FaceColor()); + const LONG t = ::GetWindowLongW(hWnd, GWL_STYLE) & SS_TYPEMASK; + const COLORREF line = CtrlBorderColor(); + if (t == SS_ETCHEDFRAME) { + CBrush br(line); + pDC->FrameRect(rc, &br); + } else if (t == SS_ETCHEDVERT) { + pDC->FillSolidRect(rc.left + rc.Width() / 2, rc.top, 1, rc.Height(), line); + } else { // SS_ETCHEDHORZ + pDC->FillSolidRect(rc.left, rc.top + rc.Height() / 2, rc.Width(), 1, line); + } + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, EtchedStaticSubclassProc, kEtchedStaticSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + void ThemeControl(HWND hCtrl) { if (pAllowDarkModeForWindow) { pAllowDarkModeForWindow(hCtrl, true); @@ -1182,6 +1311,7 @@ namespace DarkTheme } } else if (_wcsicmp(cls, L"ComboBox") == 0) { SetWindowTheme(hCtrl, L"DarkMode_CFD", nullptr); // dark dropdown list + interior + ThemeComboDropList(hCtrl); // theme the ComboLBox popup directly (EnumChildWindows never reaches it) // A combo paints its border in the CLIENT area (not the non-client), so it gets its own // subclass that re-strokes the border dark flash-free (buffered for CBS_DROPDOWNLIST, // border-only invalidate for the editable CBS_DROPDOWN). @@ -1265,7 +1395,11 @@ namespace DarkTheme const LONG st = GetWindowLongW(hCtrl, GWL_STYLE); const LONG ex = GetWindowLongW(hCtrl, GWL_EXSTYLE); const LONG sType = st & SS_TYPEMASK; - if ((st & SS_SUNKEN) || (ex & (WS_EX_CLIENTEDGE | WS_EX_STATICEDGE))) { + if (sType == SS_ETCHEDHORZ || sType == SS_ETCHEDVERT || sType == SS_ETCHEDFRAME) { + // Localized-resource dividers (SS_ETCHEDHORZ) draw a light 3D line; owner-draw it dark. + SetWindowSubclass(hCtrl, EtchedStaticSubclassProc, kEtchedStaticSubclassId, 0); + InvalidateRect(hCtrl, nullptr, TRUE); + } else if ((st & SS_SUNKEN) || (ex & (WS_EX_CLIENTEDGE | WS_EX_STATICEDGE))) { ApplyOwnerBorder(hCtrl); } else if (sType == SS_LEFT || sType == SS_CENTER || sType == SS_RIGHT || sType == SS_LEFTNOWORDWRAP || sType == SS_SIMPLE) { @@ -1309,6 +1443,7 @@ namespace DarkTheme RemoveWindowSubclass(hChild, ComboBorderSubclassProc, kComboBorderSubclassId); RemoveWindowSubclass(hChild, TrackbarSubclassProc, kTrackbarSubclassId); RemoveWindowSubclass(hChild, StaticSubclassProc, kStaticSubclassId); + RemoveWindowSubclass(hChild, EtchedStaticSubclassProc, kEtchedStaticSubclassId); DWORD_PTR gridFlag = 0; const bool hadGrid = GetWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId, &gridFlag) && gridFlag; From eae846511ea578153eae63f50dc1462e69974fef Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Wed, 29 Jul 2026 01:57:04 -0300 Subject: [PATCH 23/24] Dark Options: convert localized etched separators to owner-draw, fix disabled labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The localized "white bar" fix from the previous build only half-worked: a WM_PAINT subclass drew a dark line over the SS_ETCHEDHORZ separators, but the control's native etched line still bled through (two bars — the themed line plus the native grey one, with white end caps). Instead of fighting the native paint, convert the SS_ETCHEDHORZ/VERT/FRAME separators to SS_OWNERDRAW at theme time: an owner-draw static has no native rendering at all, and the page's existing OnDrawItem paints the same flat dark line it already draws for the English separators. CPPageBase pages get it via their OnDrawItem; pages/dialogs themed through ThemeDialog (File Properties Details/Clip, aux dialogs) get it via a WM_DRAWITEM case added to the dialog subclass, guarded on a conversion marker prop so it only ever touches the separators we converted and never a dialog's own owner-drawn controls. The original style is remembered and restored on a runtime theme toggle-off. Also: labels that were already disabled before the page was themed (Sound Processing disables its Level/Release labels in OnInitDialog, before OnSetActive themes the page) kept Windows' light disabled text — near-white under force-dark — because installing the static owner-draw subclass didn't repaint them. Force a repaint on install so the disabled owner-draw runs immediately. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/controls/DarkTheme.cpp | 88 +++++++++++++----------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index abae90b197..3afe477bed 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -802,6 +802,21 @@ namespace DarkTheme pDC->FillSolidRect(rc, FaceColor()); return 1; } + case WM_DRAWITEM: { + // Draw the separators ThemeControl converted from SS_ETCHED* to SS_OWNERDRAW. CPPageBase + // pages draw these via their own OnDrawItem, but pages/dialogs themed through ThemeDialog + // (File Properties Details/Clip, aux dialogs) reach US instead. Guard on the conversion prop + // so we ONLY touch our own separators and never clobber a dialog's real owner-drawn controls. + const DRAWITEMSTRUCT* dis = reinterpret_cast(lParam); + if (dis && dis->CtlType == ODT_STATIC && dis->hwndItem && ::GetPropW(dis->hwndItem, L"MPC_ETCHED_ORIG")) { + CDC* pDC = CDC::FromHandle(dis->hDC); + CRect rc(dis->rcItem); + pDC->FillSolidRect(rc, FaceColor()); + pDC->FillSolidRect(rc.left, rc.top + rc.Height() / 2, rc.Width(), 1, CtrlBorderColor()); + return TRUE; + } + break; + } case WM_NCDESTROY: RemoveWindowSubclass(hWnd, DialogSubclassProc, kDialogSubclassId); break; @@ -1243,45 +1258,11 @@ namespace DarkTheme return DefSubclassProc(hWnd, msg, wParam, lParam); } - // Separator statics (SS_ETCHEDHORZ / SS_ETCHEDVERT / SS_ETCHEDFRAME). The base (English) .rc draws - // the Options-page dividers as SS_OWNERDRAW statics that CPPageBase::OnDrawItem paints dark. But the - // per-language resources (mpcresources..dll) still carry the OLD SS_ETCHEDHORZ dividers, which - // draw their own light 3D etched line from the system colours — a "white bar" on the dark background - // in every non-English UI (Player/History, Web Interface, Online services, Frame sync, External - // Filters, Priority, File Properties...). Those never reach OnDrawItem (they aren't owner-draw), so we - // own their paint here and draw the same flat dark line OnDrawItem uses. Self-contained, so it also - // covers aux dialogs / File Properties whose parent doesn't handle WM_DRAWITEM. - const UINT_PTR kEtchedStaticSubclassId = 14; - - LRESULT CALLBACK EtchedStaticSubclassProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uId*/, DWORD_PTR /*dw*/) { - switch (msg) { - case WM_ERASEBKGND: - return 1; // painted whole in WM_PAINT - case WM_PAINT: { - PAINTSTRUCT ps; - CDC* pDC = CDC::FromHandle(::BeginPaint(hWnd, &ps)); - CRect rc; - ::GetClientRect(hWnd, &rc); - pDC->FillSolidRect(rc, FaceColor()); - const LONG t = ::GetWindowLongW(hWnd, GWL_STYLE) & SS_TYPEMASK; - const COLORREF line = CtrlBorderColor(); - if (t == SS_ETCHEDFRAME) { - CBrush br(line); - pDC->FrameRect(rc, &br); - } else if (t == SS_ETCHEDVERT) { - pDC->FillSolidRect(rc.left + rc.Width() / 2, rc.top, 1, rc.Height(), line); - } else { // SS_ETCHEDHORZ - pDC->FillSolidRect(rc.left, rc.top + rc.Height() / 2, rc.Width(), 1, line); - } - ::EndPaint(hWnd, &ps); - return 0; - } - case WM_NCDESTROY: - RemoveWindowSubclass(hWnd, EtchedStaticSubclassProc, kEtchedStaticSubclassId); - break; - } - return DefSubclassProc(hWnd, msg, wParam, lParam); - } + // Separator statics: see the SS_ETCHED* handling in ThemeControl. The base (English) .rc draws the + // Options-page dividers as SS_OWNERDRAW statics that CPPageBase::OnDrawItem paints dark; the + // per-language resources still carry the OLD SS_ETCHEDHORZ dividers. We convert those to SS_OWNERDRAW + // at theme time so the SAME OnDrawItem path draws them and there is NO native etched line to leak + // (a WM_PAINT-only subclass couldn't suppress the native etched — it bled through at the ends). void ThemeControl(HWND hCtrl) { if (pAllowDarkModeForWindow) { @@ -1396,8 +1377,20 @@ namespace DarkTheme const LONG ex = GetWindowLongW(hCtrl, GWL_EXSTYLE); const LONG sType = st & SS_TYPEMASK; if (sType == SS_ETCHEDHORZ || sType == SS_ETCHEDVERT || sType == SS_ETCHEDFRAME) { - // Localized-resource dividers (SS_ETCHEDHORZ) draw a light 3D line; owner-draw it dark. - SetWindowSubclass(hCtrl, EtchedStaticSubclassProc, kEtchedStaticSubclassId, 0); + // Localized-resource dividers use SS_ETCHEDHORZ (a light 3D line native dark mode never + // darkens) where the base .rc uses SS_OWNERDRAW. Convert to SS_OWNERDRAW so there is NO + // native rendering to leak and the page's CPPageBase::OnDrawItem paints the same flat dark + // line it draws for the English separators. Remember the original type so a toggle-off + // restores the native etched look. (A WM_PAINT subclass was tried first but the native + // etched still bled through at the ends — the owner-draw conversion removes it entirely.) + if (!::GetPropW(hCtrl, L"MPC_ETCHED_ORIG")) { + ::SetPropW(hCtrl, L"MPC_ETCHED_ORIG", reinterpret_cast(static_cast(sType) + 1)); + } + ::SetWindowLongW(hCtrl, GWL_STYLE, (st & ~SS_TYPEMASK) | SS_OWNERDRAW); + // SWP_FRAMECHANGED so the static re-evaluates its (now owner-draw) style and starts sending + // WM_DRAWITEM instead of self-drawing the etched line. + ::SetWindowPos(hCtrl, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); InvalidateRect(hCtrl, nullptr, TRUE); } else if ((st & SS_SUNKEN) || (ex & (WS_EX_CLIENTEDGE | WS_EX_STATICEDGE))) { ApplyOwnerBorder(hCtrl); @@ -1406,6 +1399,11 @@ namespace DarkTheme // Plain text labels: owner-draw disabled ones flat (Windows would emboss them, // which looks bad on dark). Enabled ones keep the default painting. SetWindowSubclass(hCtrl, StaticSubclassProc, kStaticSubclassId, 0); + // Force a repaint so a label that is ALREADY disabled when we theme it (e.g. Sound + // Processing disables its Level/Release labels in OnInitDialog, before OnSetActive themes + // the page) gets owner-drawn now. Without this it keeps Windows' light disabled text — which + // under force-dark reads as near-white — until some later invalidate that may never come. + InvalidateRect(hCtrl, nullptr, TRUE); } } else if (_wcsicmp(cls, TRACKBAR_CLASSW) == 0) { // Owner-draw every slider (dark groove + celeste thumb with hover / pressed states). @@ -1443,7 +1441,13 @@ namespace DarkTheme RemoveWindowSubclass(hChild, ComboBorderSubclassProc, kComboBorderSubclassId); RemoveWindowSubclass(hChild, TrackbarSubclassProc, kTrackbarSubclassId); RemoveWindowSubclass(hChild, StaticSubclassProc, kStaticSubclassId); - RemoveWindowSubclass(hChild, EtchedStaticSubclassProc, kEtchedStaticSubclassId); + // Restore a separator we converted from SS_ETCHED* to SS_OWNERDRAW, so it draws its native + // (light) etched line again in the light theme. + if (HANDLE p = ::GetPropW(hChild, L"MPC_ETCHED_ORIG")) { + const LONG origType = static_cast(reinterpret_cast(p) - 1); + ::SetWindowLongW(hChild, GWL_STYLE, (::GetWindowLongW(hChild, GWL_STYLE) & ~SS_TYPEMASK) | origType); + ::RemovePropW(hChild, L"MPC_ETCHED_ORIG"); + } DWORD_PTR gridFlag = 0; const bool hadGrid = GetWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId, &gridFlag) && gridFlag; From 3ebfcfae685c09b9dc93efd262e68eb9a2b06d09 Mon Sep 17 00:00:00 2001 From: kcinickgx Date: Wed, 19 Aug 2026 09:10:53 -0300 Subject: [PATCH 24/24] Dark Options: hide the localized etched separators and draw them ourselves The localized resources draw the page dividers with SS_ETCHED* statics, which paint a light 3D line that dark mode never darkens, so every non-English UI showed a white separator. Two earlier attempts did not hold: - Overpainting the line from a WM_PAINT subclass. The native etched line still reached the screen on some repaint paths, leaving a doubled line with light end caps. - Converting the style to SS_OWNERDRAW at theme time, so the existing OnDrawItem would draw it. A static picks its paint routine when it is created, so changing the style type afterwards does nothing: the conversion silently had no effect and the native etched line was drawn in full. This is also why it looked intermittent - it was never being suppressed at all. So stop competing with the control for the pixels. Hide it, and draw the divider from the parent's background paint instead: a hidden window is never painted by anyone, so no repaint path or timing can leak the native line. The divider is drawn right after the dark background fill, in the control's own rect, so it lands exactly where the resource put it and matches the line the English SS_OWNERDRAW separators already get. Both painting paths are covered: pages via CPPageBase::OnEraseBkgnd, and dialogs themed through ThemeDialog (File Properties Details/Clip/Res, Capture, Shader combine, Pan&Scan Edit) via the dialog subclass - together those account for every dialog that carries an etched separator. A runtime theme toggle-off shows the control again, so it draws its own etched line for the light theme. Co-Authored-By: Claude Opus 4.8 --- src/apps/mplayerc/PPageBase.cpp | 3 + src/apps/mplayerc/controls/DarkTheme.cpp | 95 ++++++++++++++++-------- src/apps/mplayerc/controls/DarkTheme.h | 7 ++ 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/src/apps/mplayerc/PPageBase.cpp b/src/apps/mplayerc/PPageBase.cpp index 9e9a7e6743..4edc14f472 100644 --- a/src/apps/mplayerc/PPageBase.cpp +++ b/src/apps/mplayerc/PPageBase.cpp @@ -124,6 +124,9 @@ BOOL CPPageBase::OnEraseBkgnd(CDC* pDC) CRect rc; GetClientRect(rc); pDC->FillSolidRect(rc, DarkTheme::FaceColor()); + // Localized resources use SS_ETCHED* dividers, which paint a light 3D line dark mode never darkens. + // They are hidden while theming, so draw them here, on top of the background we just filled. + DarkTheme::DrawHiddenSeparators(GetSafeHwnd(), pDC); return TRUE; } diff --git a/src/apps/mplayerc/controls/DarkTheme.cpp b/src/apps/mplayerc/controls/DarkTheme.cpp index 3afe477bed..00687157b6 100644 --- a/src/apps/mplayerc/controls/DarkTheme.cpp +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -800,23 +800,12 @@ namespace DarkTheme CRect rc; ::GetClientRect(hWnd, &rc); pDC->FillSolidRect(rc, FaceColor()); + // Draw the dividers we hid while theming (localized SS_ETCHED* separators). CPPageBase pages + // do this in their own OnEraseBkgnd; dialogs themed through ThemeDialog (File Properties + // Details/Clip, aux dialogs like Pan&Scan Edit) reach US instead. + DrawHiddenSeparators(hWnd, pDC); return 1; } - case WM_DRAWITEM: { - // Draw the separators ThemeControl converted from SS_ETCHED* to SS_OWNERDRAW. CPPageBase - // pages draw these via their own OnDrawItem, but pages/dialogs themed through ThemeDialog - // (File Properties Details/Clip, aux dialogs) reach US instead. Guard on the conversion prop - // so we ONLY touch our own separators and never clobber a dialog's real owner-drawn controls. - const DRAWITEMSTRUCT* dis = reinterpret_cast(lParam); - if (dis && dis->CtlType == ODT_STATIC && dis->hwndItem && ::GetPropW(dis->hwndItem, L"MPC_ETCHED_ORIG")) { - CDC* pDC = CDC::FromHandle(dis->hDC); - CRect rc(dis->rcItem); - pDC->FillSolidRect(rc, FaceColor()); - pDC->FillSolidRect(rc.left, rc.top + rc.Height() / 2, rc.Width(), 1, CtrlBorderColor()); - return TRUE; - } - break; - } case WM_NCDESTROY: RemoveWindowSubclass(hWnd, DialogSubclassProc, kDialogSubclassId); break; @@ -1377,21 +1366,25 @@ namespace DarkTheme const LONG ex = GetWindowLongW(hCtrl, GWL_EXSTYLE); const LONG sType = st & SS_TYPEMASK; if (sType == SS_ETCHEDHORZ || sType == SS_ETCHEDVERT || sType == SS_ETCHEDFRAME) { - // Localized-resource dividers use SS_ETCHEDHORZ (a light 3D line native dark mode never - // darkens) where the base .rc uses SS_OWNERDRAW. Convert to SS_OWNERDRAW so there is NO - // native rendering to leak and the page's CPPageBase::OnDrawItem paints the same flat dark - // line it draws for the English separators. Remember the original type so a toggle-off - // restores the native etched look. (A WM_PAINT subclass was tried first but the native - // etched still bled through at the ends — the owner-draw conversion removes it entirely.) + // Localized-resource dividers use SS_ETCHED* (a light 3D line native dark mode never + // darkens) where the base .rc uses SS_OWNERDRAW, so every non-English UI showed a white + // separator. Two earlier attempts failed: overpainting in a WM_PAINT subclass (the native + // etched still bled through) and converting the style to SS_OWNERDRAW at runtime (a static + // picks its paint routine when it is CREATED, so the conversion silently did nothing and + // the native etched line was drawn in full). + // + // So stop trying to out-paint the control: HIDE it, and draw the divider ourselves from the + // parent's background paint (DrawHiddenSeparators, called after the dark FillSolidRect in + // CPPageBase::OnEraseBkgnd and in DialogSubclassProc). A hidden window is never painted by + // anyone, so there is no native rendering left to leak on any repaint path or timing. + // The marker prop records the original type so a runtime toggle-off can restore it. if (!::GetPropW(hCtrl, L"MPC_ETCHED_ORIG")) { ::SetPropW(hCtrl, L"MPC_ETCHED_ORIG", reinterpret_cast(static_cast(sType) + 1)); } - ::SetWindowLongW(hCtrl, GWL_STYLE, (st & ~SS_TYPEMASK) | SS_OWNERDRAW); - // SWP_FRAMECHANGED so the static re-evaluates its (now owner-draw) style and starts sending - // WM_DRAWITEM instead of self-drawing the etched line. - ::SetWindowPos(hCtrl, nullptr, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); - InvalidateRect(hCtrl, nullptr, TRUE); + ::ShowWindow(hCtrl, SW_HIDE); + if (HWND hParent = ::GetParent(hCtrl)) { + ::InvalidateRect(hParent, nullptr, TRUE); // repaint the strip the control used to cover + } } else if ((st & SS_SUNKEN) || (ex & (WS_EX_CLIENTEDGE | WS_EX_STATICEDGE))) { ApplyOwnerBorder(hCtrl); } else if (sType == SS_LEFT || sType == SS_CENTER || sType == SS_RIGHT @@ -1441,12 +1434,11 @@ namespace DarkTheme RemoveWindowSubclass(hChild, ComboBorderSubclassProc, kComboBorderSubclassId); RemoveWindowSubclass(hChild, TrackbarSubclassProc, kTrackbarSubclassId); RemoveWindowSubclass(hChild, StaticSubclassProc, kStaticSubclassId); - // Restore a separator we converted from SS_ETCHED* to SS_OWNERDRAW, so it draws its native - // (light) etched line again in the light theme. - if (HANDLE p = ::GetPropW(hChild, L"MPC_ETCHED_ORIG")) { - const LONG origType = static_cast(reinterpret_cast(p) - 1); - ::SetWindowLongW(hChild, GWL_STYLE, (::GetWindowLongW(hChild, GWL_STYLE) & ~SS_TYPEMASK) | origType); + // Un-hide a separator we hid while dark (see the SS_ETCHED* branch in ThemeControl) so it draws + // its own native etched line again, which is the correct look in the light theme. + if (::GetPropW(hChild, L"MPC_ETCHED_ORIG")) { ::RemovePropW(hChild, L"MPC_ETCHED_ORIG"); + ::ShowWindow(hChild, SW_SHOW); } DWORD_PTR gridFlag = 0; @@ -1962,6 +1954,45 @@ namespace DarkTheme return false; } + void DrawHiddenSeparators(HWND hWndParent, CDC* pDC) { + if (!IsActive() || !hWndParent || !pDC) { + return; + } + // Draw a divider for every child ThemeControl hid (marker prop MPC_ETCHED_ORIG). Their window rects + // are still valid while hidden, so the line lands exactly where the resource put it. Direct children + // only: a hidden divider always belongs to the page/dialog whose background is being painted. + struct Ctx { HWND parent; CDC* dc; }; + Ctx ctx = { hWndParent, pDC }; + ::EnumChildWindows(hWndParent, [](HWND hChild, LPARAM lp) -> BOOL { + auto* c = reinterpret_cast(lp); + if (::GetParent(hChild) != c->parent || !::GetPropW(hChild, L"MPC_ETCHED_ORIG")) { + return TRUE; + } + RECT rc; + ::GetWindowRect(hChild, &rc); + ::MapWindowPoints(nullptr, c->parent, reinterpret_cast(&rc), 2); // screen -> parent client + const int w = rc.right - rc.left; + const int h = rc.bottom - rc.top; + if (w <= 0 || h <= 0) { + return TRUE; + } + // The localized dividers are 1 dialog unit tall (~2px) where the English SS_OWNERDRAW ones are 3, + // so centre a single 1px line in the control's own rect exactly like CPPageBase::OnDrawItem does. + const COLORREF line = CtrlBorderColor(); + const LONG type = static_cast(reinterpret_cast(::GetPropW(hChild, L"MPC_ETCHED_ORIG")) - 1); + if (type == SS_ETCHEDVERT) { + c->dc->FillSolidRect(rc.left + w / 2, rc.top, 1, h, line); + } else if (type == SS_ETCHEDFRAME) { + CBrush br(line); + CRect r(rc); + c->dc->FrameRect(r, &br); + } else { // SS_ETCHEDHORZ + c->dc->FillSolidRect(rc.left, rc.top + h / 2, w, 1, line); + } + return TRUE; + }, reinterpret_cast(&ctx)); + } + HBRUSH OnCtlColor(CDC* pDC, UINT nCtlColor) { if (!IsActive() || !pDC) { return nullptr; diff --git a/src/apps/mplayerc/controls/DarkTheme.h b/src/apps/mplayerc/controls/DarkTheme.h index d0a675d049..01a6121cc3 100644 --- a/src/apps/mplayerc/controls/DarkTheme.h +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -125,6 +125,13 @@ namespace DarkTheme // an inactive list. Returns false when theming is unavailable (keep the default). bool MakeCheckStateImageList(CImageList& il, int size, HWND hRef, bool bDisabled = false); + // Draws the separator lines for dividers we hid while theming hWndParent. The localized resources + // use SS_ETCHED* statics, which paint a light 3D line that dark mode never darkens and that cannot be + // reliably overpainted or restyled at runtime, so ThemeControl HIDES them and we draw the divider here + // instead. Call from the parent's background paint, right AFTER filling it with FaceColor (see + // CPPageBase::OnEraseBkgnd); no-op when the theme is off or the parent has no hidden dividers. + void DrawHiddenSeparators(HWND hWndParent, CDC* pDC); + // WM_CTLCOLOR* helper: sets dark text/background on pDC and returns a cached // dark brush, or nullptr when the dark theme is inactive (use default handling). HBRUSH OnCtlColor(CDC* pDC, UINT nCtlColor);