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/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/ExtLib/ui/sizecbar/sizecbar.cpp b/src/ExtLib/ui/sizecbar/sizecbar.cpp index 210125ca4f..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); } } @@ -106,6 +109,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() @@ -573,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 @@ -631,6 +650,25 @@ 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); + // 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; + } + + 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..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 @@ -195,6 +198,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/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 f62184feed..0aeb3cdb95 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 @@ -137,6 +138,8 @@ BOOL CAddCommandDlg::OnInitDialog() SHAutoComplete(m_FilterEdit, SHACF_AUTOAPPEND_FORCE_OFF | SHACF_AUTOSUGGEST_FORCE_OFF); + DarkTheme::ThemeDialog(GetSafeHwnd()); + return TRUE; } diff --git a/src/apps/mplayerc/AppSettings.cpp b/src/apps/mplayerc/AppSettings.cpp index 3d9fccd25c..43a5608304 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/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/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/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..debc96df6f 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; } @@ -211,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/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/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 86d0e1cc6e..275f5607fe 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 @@ -287,6 +288,8 @@ BOOL CHistoryDlg::OnInitDialog() SHAutoComplete(m_FilterEdit, SHACF_AUTOAPPEND_FORCE_OFF | SHACF_AUTOSUGGEST_FORCE_OFF); + 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 1199060bb7..3d6e4ee369 100644 --- a/src/apps/mplayerc/MainFrm.cpp +++ b/src/apps/mplayerc/MainFrm.cpp @@ -71,6 +71,8 @@ #include #include "filters/ffmpeg_link_fix.h" #include "ComPropertySheet.h" +#include "controls/DarkTheme.h" +#include "controls/DarkTabCtrl.h" #include #include @@ -817,9 +819,17 @@ 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); } + // 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); @@ -9079,7 +9089,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/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/PPageAccelTbl.cpp b/src/apps/mplayerc/PPageAccelTbl.cpp index 0e64c34828..833b4748b8 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" #include "Misc.h" //#define MASK_NUMBER 0xFFF @@ -842,11 +843,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..4edc14f472 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,93 @@ BOOL CPPageBase::OnSetActive() { AfxGetAppSettings().nLastUsedPage = (UINT)(ULONG_PTR)m_pPSP->pszTemplate; - return __super::OnSetActive(); + BOOL bRet = __super::OnSetActive(); + + // 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; +} + +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()); + // 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; + } + + 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/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; } diff --git a/src/apps/mplayerc/PPageFormats.cpp b/src/apps/mplayerc/PPageFormats.cpp index fb64da19f1..762f9711b8 100644 --- a/src/apps/mplayerc/PPageFormats.cpp +++ b/src/apps/mplayerc/PPageFormats.cpp @@ -27,11 +27,165 @@ #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, bool bDark) +{ + 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) { + 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, true)) { + 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; @@ -78,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() @@ -665,6 +813,56 @@ END_MESSAGE_MAP() // CPPageFormats message handlers +void CPPageFormats::BuildCheckImageList() +{ + 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); + } + + 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 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; + 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); + } + } + } + + 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(); @@ -677,27 +875,7 @@ 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); - } - DeleteObject(hBitmap); - } - } - - m_list.SetImageList(&m_onoff, LVSIL_SMALL); + BuildCheckImageList(); CMediaFormats& mf = AfxGetAppSettings().m_Formats; mf.UpdateData(false); @@ -713,6 +891,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); @@ -752,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 { @@ -1067,9 +1255,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/PPageFullscreen.cpp b/src/apps/mplayerc/PPageFullscreen.cpp index 060231a3f3..673c94b580 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) ? RGB(110, 115, 120) : DarkTheme::TextColor(); + if (m_list.GetCheck(pLVCD->nmcd.dwItemSpec) == false) { + crText = RGB(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..ccd08a10cc 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 @@ -90,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; @@ -173,6 +184,17 @@ BOOL CPPageInterface::OnApply() //s.bDarkMenuBlurBehind = !!m_chkDarkMenuBlurBehind.GetCheck(); s.bDarkTitle = !!m_chkDarkTitle.GetCheck(); + // 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; s.fUseTimeTooltip = !!m_fUseTimeTooltip; s.nTimeTooltipPosition = m_TimeTooltipPosition.GetCurSel(); @@ -214,14 +236,25 @@ 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; + // 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); + 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(); @@ -286,6 +319,10 @@ void CPPageInterface::OnThemeChange() pFrame->Invalidate(); pFrame->m_wndPlaylistBar.Invalidate(); + + // 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) @@ -357,6 +394,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() @@ -400,9 +450,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 +474,7 @@ void CPPageInterface::OnCustomDrawBtns(NMHDR *pNMHDR, LRESULT *pResult) } dc.SelectObject(&penOld); + dc.SelectObject(pOldBrush); dc.Detach(); *pResult = CDRF_SKIPDEFAULT; @@ -478,6 +535,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/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..1265a4cd1c 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,44 @@ 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()); + // 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); + } + } + 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/PlayerBar.cpp b/src/apps/mplayerc/PlayerBar.cpp index 4625b226c6..9310c0b454 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,24 @@ 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. + // 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()) { + 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 + } } CSize CPlayerBar::CalcFixedLayout(BOOL bStretch, BOOL bHorz) diff --git a/src/apps/mplayerc/PlayerBar.h b/src/apps/mplayerc/PlayerBar.h index e12b1384fc..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(); @@ -46,4 +52,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/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/PlayerSubresyncBar.cpp b/src/apps/mplayerc/PlayerSubresyncBar.cpp index 69e7b2dbd8..ae6bd8aa4e 100644 --- a/src/apps/mplayerc/PlayerSubresyncBar.cpp +++ b/src/apps/mplayerc/PlayerSubresyncBar.cpp @@ -22,6 +22,8 @@ #include "stdafx.h" #include "MainFrm.h" #include "PlayerSubresyncBar.h" +#include "controls/DarkTheme.h" +#include "controls/DarkTabCtrl.h" // CPlayerSubresyncBar @@ -52,9 +54,22 @@ 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). 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)); @@ -257,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); @@ -1069,7 +1106,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()); } @@ -1180,37 +1217,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; @@ -1235,9 +1285,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)); @@ -1245,7 +1304,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/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/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/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/DarkCheckListBox.cpp b/src/apps/mplayerc/controls/DarkCheckListBox.cpp new file mode 100644 index 0000000000..0db267dff1 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkCheckListBox.cpp @@ -0,0 +1,112 @@ +/* + * (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). + CRect rcFill(lpDrawItemStruct->rcItem); + CRect rcClient; + GetClientRect(&rcClient); + if (rcClient.right > rcFill.right) { + rcFill.right = rcClient.right; + } + if (rcFill.bottom > rcClient.bottom) { + rcFill.bottom = rcClient.bottom; + } + // 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); + } + + 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..4d0a8acc53 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTabCtrl.cpp @@ -0,0 +1,186 @@ +/* + * (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() + +// 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) + 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() : RGB(140, 145, 150)); // fixed: text never tints + 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); +} + +// 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 new file mode 100644 index 0000000000..cda6a2d596 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTabCtrl.h @@ -0,0 +1,68 @@ +/* + * (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 +#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 +// 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); +}; + +// 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 new file mode 100644 index 0000000000..00687157b6 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTheme.cpp @@ -0,0 +1,2201 @@ +/* + * (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() +#include "DSUtil/SysVersion.h" +#include +#include +#include // BP_CHECKBOX / BP_RADIOBUTTON / CBS_* / RBS_* +#include // SetWindowSubclass +#include // SHGetStockIconInfo (UAC shield) + +#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; + + // 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; + } + + 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 ? 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); + } + + if (pOldFont) { + pDC->SelectObject(pOldFont); + } + + ::EndPaint(hWnd, &ps); + return 0; + } + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, GroupBoxSubclassProc, kGroupBoxSubclassId); + break; + } + 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 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. + 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 ? 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) + : ThemeRGB(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)); + // The UAC elevation shield (BCM_SETSHIELD) is drawn internally by the button and + // 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) { + // 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, &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)); + CFont* pOldFont = hFont ? pDC->SelectObject(CFont::FromHandle(hFont)) : nullptr; + pDC->SetBkMode(TRANSPARENT); + pDC->SetTextColor(disabled ? RGB(120, 125, 130) : TextColor()); + + // 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 + // 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 { + // 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 + 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); + 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: + ::RemovePropW(hWnd, L"MPC_UAC_SHIELD"); + 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. + 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. 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)) { + const int x = hdrOrg.x + hr.right - 1; + ::MoveToEx(hdc, x, headerH, nullptr); + ::LineTo(hdc, x, gridBottom); + } + } + } + + ::SelectObject(hdc, oldPen); + ::DeleteObject(pen); + ::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 + // 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_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); + 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) { + // 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); + ::InvalidateRect(hWnd, nullptr, FALSE); + return res; + } + 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) { + 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 = RGB(170, 175, 180); // fixed: foreground glyph never tints + + 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 dwData) { + 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); + + // 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, groove); // dark groove + pDC->Draw3dRect(ch, border, border); // subtle border + + 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; + 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; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // 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_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; + ::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_NCDESTROY: + RemoveWindowSubclass(hWnd, DialogSubclassProc, kDialogSubclassId); + break; + } + return DefSubclassProc(hWnd, msg, wParam, lParam); + } + + // ---- 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, OB_SSSUNKEN = 8 }; + + 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) { + // 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 — 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) { + 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: { + // 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 }; + // 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) { + 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) { + 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); + ::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)); + RECT fr = band; + for (int i = 0; i < stroke; ++i) { + ::FrameRect(hdc, &fr, edge); + ::InflateRect(&fr, -1, -1); + } + ::DeleteObject(edge); + ::ReleaseDC(h, hdc); + } + return r; + } + case WM_ENABLE: + OwnerBorderRefreshFrame(h); + break; + case WM_DPICHANGED_AFTERPARENT: + OwnerBorderRefreshFrame(h); // SWP_FRAMECHANGED re-runs NCCALCSIZE (new-DPI reserve) + NCPAINT + 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; + if (d->stripped & OB_SSSUNKEN) st |= SS_SUNKEN; + ::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; 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 + // 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)); + 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 + } + + // 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; + ::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(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); + ::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(h, &rc); + HBRUSH br = ::CreateSolidBrush(CtrlBorderColor()); // consistent dark border in every state (incl. focused) + ::FrameRect(hdc, &rc, br); + ::DeleteObject(br); + ::ReleaseDC(h, hdc); + } + return r; + } + case WM_SETFOCUS: + case WM_KILLFOCUS: { + const LRESULT r = DefSubclassProc(h, msg, w, l); + 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; + } + return DefSubclassProc(h, msg, w, l); + } + + 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 + // 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; + } + } + + // 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); + } + + // 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) { + pAllowDarkModeForWindow(hCtrl, true); + } + + wchar_t cls[64] = {}; + GetClassNameW(hCtrl, cls, _countof(cls)); + + if (_wcsicmp(cls, L"Button") == 0) { + 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) { + 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); + } + } 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). + 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 + // 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 + } else { + // 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""); + } + // 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 + // 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); + } + 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())); + 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); + InvalidateRect(hCtrl, nullptr, TRUE); + } else if (_wcsicmp(cls, L"ListBox") == 0) { + // 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); + 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 + // 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); + const LONG sType = st & SS_TYPEMASK; + if (sType == SS_ETCHEDHORZ || sType == SS_ETCHEDVERT || sType == SS_ETCHEDFRAME) { + // 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)); + } + ::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 + || 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); + // 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). + // 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, ... + 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, ButtonSubclassProc, kButtonSubclassId); + RemoveWindowSubclass(hChild, SpinSubclassProc, kSpinSubclassId); + 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); + // 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; + const bool hadGrid = GetWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId, &gridFlag) && gridFlag; + RemoveWindowSubclass(hChild, ListViewSubclassProc, kListViewSubclassId); + + 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(); + } + + // 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 ThemeRGB(10, 14, 18); } + // 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() { + 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 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; + } + 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)); + } + // 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) { + 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 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). + 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, bThemeControl ? 1 : 0); + ::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; + } + 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. + // 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 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 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; // 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 + // 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); + } + + // 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) { + ThemeMessageBoxWindow(reinterpret_cast(wParam)); + } + 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 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); + } + } + + CDarkMessageBoxHook::~CDarkMessageBoxHook() { + if (m_hooked && g_msgBoxHook) { + ::UnhookWindowsHookEx(g_msgBoxHook); + g_msgBoxHook = nullptr; + } + } + + void RefreshTheme(HWND hRoot) { + if (!hRoot) { + return; + } + 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, 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); + } + 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); + } + + 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; + } + + 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; + } + + 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; + } + 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 ? RGB(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 ? RGB(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..01a6121cc3 --- /dev/null +++ b/src/apps/mplayerc/controls/DarkTheme.h @@ -0,0 +1,157 @@ +/* + * (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); + + // 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). + // 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(); + + // 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. + 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; + }; + + // 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 + // 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 + // 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); + + // 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); + + // 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); + + // 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) + 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/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/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.cpp b/src/apps/mplayerc/mplayerc.cpp index 1d1a6c6b18..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" @@ -921,6 +922,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 : { @@ -1531,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); diff --git a/src/apps/mplayerc/mplayerc.rc b/src/apps/mplayerc/mplayerc.rc index 1d1b8053f8..045074f2ed 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 @@ -853,7 +853,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 @@ -951,7 +951,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 @@ -982,13 +982,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 @@ -1015,7 +1015,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 @@ -1029,7 +1029,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 @@ -1193,7 +1193,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