From a5c33fc13a195bbc184304e5a6a97b070035d0b1 Mon Sep 17 00:00:00 2001 From: YellowNest <59575587+YellowNest@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:07:56 +0200 Subject: [PATCH] Build YellowNest 4.4.214 integration preview Bundle current master with pending PRs #2547, #2548, #2551, #2552, and #2553 for local integration testing. This branch is test packaging only and is not intended for upstream merge. --- .github/workflows/build.yml | 8 +- Src/ClassicExplorer/ExplorerBHO.cpp | 125 +++ Src/ClassicExplorer/ExplorerBand.cpp | 203 ++++ Src/ClassicExplorer/ExplorerBand.h | 6 + Src/Lib/ResourceHelper.cpp | 37 + Src/Lib/ResourceHelper.h | 9 + Src/Setup/Setup.cpp | 74 ++ Src/StartMenu/StartMenuDLL/MenuContainer.cpp | 7 +- Src/StartMenu/StartMenuDLL/MenuPaint.cpp | 22 +- Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp | 5 + .../StartMenuDLL/StartMenuDLL.vcxproj | 2 + .../StartMenuDLL/StartMenuDLL.vcxproj.filters | 6 + .../StartMenuDLL/Win11StartButton.cpp | 976 ++++++++++++++++++ Src/StartMenu/StartMenuDLL/Win11StartButton.h | 8 + 14 files changed, 1477 insertions(+), 11 deletions(-) create mode 100644 Src/StartMenu/StartMenuDLL/Win11StartButton.cpp create mode 100644 Src/StartMenu/StartMenuDLL/Win11StartButton.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a60ba1569..5238aec8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,8 +41,12 @@ jobs: $version = [version]$latestTag.Substring(1) $baseVersion = "$($version.Major).$($version.Minor).$($version.Build + 1)" - # Handle PR Suffix - if ("${{ github.event_name }}" -eq "pull_request") { + # Private YellowNest integration preview. Keep this branch-specific + # override out of all upstream PR branches so the generated setup can + # upgrade the locally installed 4.4.213 test build. + if ("${{ github.event_name }}" -eq "pull_request" -and "${{ github.head_ref }}" -eq "integration/yellownest-preview-4.4.214") { + $finalVersion = "4.4.214" + } elseif ("${{ github.event_name }}" -eq "pull_request") { $shortSha = "${{ github.event.pull_request.head.sha }}".Substring(0, 7) $finalVersion = "$baseVersion-pr-$shortSha" } else { diff --git a/Src/ClassicExplorer/ExplorerBHO.cpp b/Src/ClassicExplorer/ExplorerBHO.cpp index cfd456364..36162fc0a 100644 --- a/Src/ClassicExplorer/ExplorerBHO.cpp +++ b/Src/ClassicExplorer/ExplorerBHO.cpp @@ -19,6 +19,7 @@ #include #include #include +#include // CExplorerBHO - a browser helper object that implements Alt+Enter for the folder tree @@ -593,6 +594,128 @@ LRESULT CALLBACK CExplorerBHO::SubclassStatusProc8( HWND hWnd, UINT uMsg, WPARAM return 0; } } + + if (uMsg==WM_SETTINGCHANGE && IsColorSchemeChangeMessage(lParam)) + { + SetWindowTheme(hWnd,ShouldAppsUseDarkMode()?L"DarkMode_Explorer":NULL,NULL); + ::RedrawWindow(hWnd,NULL,NULL,RDW_INVALIDATE|RDW_ERASE|RDW_FRAME); + } + + if (ShouldAppsUseDarkMode()) + { + if (uMsg==WM_NCPAINT) + { + LRESULT result=DefSubclassProc(hWnd,uMsg,wParam,lParam); + HDC hdc=GetWindowDC(hWnd); + if (hdc) + { + RECT rc; + GetWindowRect(hWnd,&rc); + OffsetRect(&rc,-rc.left,-rc.top); + rc.bottom=rc.top+1; + SetDCBrushColor(hdc,RGB(0x3A,0x3A,0x3A)); + FillRect(hdc,&rc,(HBRUSH)GetStockObject(DC_BRUSH)); + ReleaseDC(hWnd,hdc); + } + return result; + } + + if (uMsg==WM_ERASEBKGND) + { + HDC hdc=(HDC)wParam; + RECT rc; + if (GetClipBox(hdc,&rc)!=ERROR) + { + SetDCBrushColor(hdc,RGB(0x1C,0x1C,0x1C)); + FillRect(hdc,&rc,(HBRUSH)GetStockObject(DC_BRUSH)); + } + return 1; + } + + if (uMsg==WM_PAINT) + { + PAINTSTRUCT ps; + HDC hdc=BeginPaint(hWnd,&ps); + if (!hdc) + return 0; + + int saved=SaveDC(hdc); + RECT client; + GetClientRect(hWnd,&client); + SetDCBrushColor(hdc,RGB(0x1C,0x1C,0x1C)); + FillRect(hdc,&ps.rcPaint,(HBRUSH)GetStockObject(DC_BRUSH)); + + HFONT font=(HFONT)DefSubclassProc(hWnd,WM_GETFONT,0,0); + if (font) + SelectObject(hdc,font); + SetBkMode(hdc,TRANSPARENT); + SetTextColor(hdc,RGB(0xFF,0xFF,0xFF)); + + SIZE textSize={}; + GetTextExtentPoint32(hdc,L"T",1,&textSize); + int smallIcon=GetSystemMetrics(SM_CXSMICON); + + // Let the status theme draw a DPI-aware resize grip. + HTHEME theme=OpenThemeData(hWnd,L"STATUS"); + if (theme) + { + RECT grip; + grip.left=client.right-GetSystemMetrics(SM_CXHSCROLL)+(GetDpi(hWnd)0 && textLength<(int)_countof(text) && !(flags&SBT_OWNERDRAW)) + { + DefSubclassProc(hWnd,SB_GETTEXT,i,(LPARAM)text); + rc.left+=2; + rc.top+=(rc.bottom-rc.top)/2-textSize.cy/2; + DrawText(hdc,text,-1,&rc,DT_SINGLELINE|DT_VCENTER|DT_LEFT); + } + + if (parts>1 && i!=parts-1 && !(flags&SBT_NOBORDERS) && separator.right #include #include +#include /////////////////////////////////////////////////////////////////////////////// @@ -229,6 +230,17 @@ LRESULT CALLBACK CBandWindow::ToolbarSubclassProc( HWND hWnd, UINT uMsg, WPARAM // Also refresh the buttons when the folder settings change if (pThis->HasFolderSettings()) ::PostMessage((HWND)dwRefData,CBandWindow::BWM_UPDATETOOLBAR,0,0); + + if (IsColorSchemeChangeMessage(lParam)) + { + HWND tooltips=(HWND)::SendMessage(pThis->GetToolbar(),TB_GETTOOLTIPS,0,0); + if (tooltips) + SetWindowTheme(tooltips,ShouldAppsUseDarkMode()?L"DarkMode_Explorer":NULL,NULL); + + HWND rebar=::GetParent((HWND)dwRefData); + if (rebar) + ::RedrawWindow(rebar,NULL,NULL,RDW_INVALIDATE|RDW_ERASE|RDW_FRAME|RDW_ALLCHILDREN); + } } if (uMsg==WM_PAINT) { @@ -239,8 +251,149 @@ LRESULT CALLBACK CBandWindow::ToolbarSubclassProc( HWND hWnd, UINT uMsg, WPARAM return DefSubclassProc(hWnd,uMsg,wParam,lParam); } +// Custom-draw the Classic Explorer toolbar on Windows 10+ so it follows the +// Windows application theme. Keeping this on the band window preserves the +// existing toolbar message handling and right-click behavior. +LRESULT CALLBACK CBandWindow::BandSubclassProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData ) +{ + CBandWindow *pThis=(CBandWindow*)uIdSubclass; + if (uMsg!=WM_NOTIFY || !lParam || !pThis || GetWinVersion()hwndFrom!=pThis->GetToolbar() || header->code!=NM_CUSTOMDRAW) + return DefSubclassProc(hWnd,uMsg,wParam,lParam); + + LPNMTBCUSTOMDRAW draw=(LPNMTBCUSTOMDRAW)lParam; + DWORD stage=draw->nmcd.dwDrawStage; + if (!ShouldAppsUseDarkMode()) + return DefSubclassProc(hWnd,uMsg,wParam,lParam); + + if (stage==CDDS_PREPAINT) + { + FillRect(draw->nmcd.hdc,&draw->nmcd.rc,pThis->m_DarkBackgroundBrush); + return CDRF_DODEFAULT|CDRF_NOTIFYITEMDRAW; + } + + if (stage==CDDS_ITEMPREPAINT) + { + TBBUTTONINFO info={sizeof(info),TBIF_STYLE|TBIF_STATE|TBIF_IMAGE}; + if (::SendMessage(pThis->GetToolbar(),TB_GETBUTTONINFO,(WPARAM)draw->nmcd.dwItemSpec,(LPARAM)&info)==-1) + return CDRF_DODEFAULT; + + bool dropDown=(info.fsStyle&(BTNS_DROPDOWN|BTNS_WHOLEDROPDOWN))!=0; + bool checked=(info.fsState&TBSTATE_CHECKED)!=0; + + // Checked toolbar buttons need explicit rendering. The standard toolbar + // theme otherwise paints a light checked background in dark mode. + if (checked && !dropDown && info.iImage!=I_IMAGENONE) + { + size_t itemIndex=(size_t)draw->nmcd.lItemlParam; + HIMAGELIST images=(HIMAGELIST)::SendMessage(header->hwndFrom,TB_GETIMAGELIST,0,0); + if (images && itemIndexm_Items.size()) + { + const wchar_t *label=pThis->m_Items[itemIndex].label; + int iconSize=GetSettingInt(GetSettingBool(L"UseBigButtons")?L"LargeIconSize":L"SmallIconSize"); + SIZE textSize={}; + RECT itemRect=draw->nmcd.rc; + RECT paintRect=itemRect; + paintRect.right++; + HDC hdc=draw->nmcd.hdc; + + int saved=SaveDC(hdc); + HBRUSH brush=(HBRUSH)GetStockObject(DC_BRUSH); + SetDCBrushColor(hdc,RGB(0x4D,0x4D,0x4D)); + HRGN background=CreateRoundRectRgn(paintRect.left,paintRect.top,paintRect.right,paintRect.bottom,4,4); + if (background) + { + FillRgn(hdc,background,brush); + SetDCBrushColor(hdc,RGB(0x63,0x63,0x63)); + FrameRgn(hdc,background,brush,1,1); + } + + if (label) + { + SetBkMode(hdc,TRANSPARENT); + SetTextColor(hdc,RGB(0xFF,0xFF,0xFF)); + SelectObject(hdc,(HGDIOBJ)::SendMessage(header->hwndFrom,WM_GETFONT,0,0)); + GetTextExtentPoint32(hdc,L"T",1,&textSize); + } + + if (!GetSettingBool(L"ToolbarListMode")) + { + int iconY=itemRect.top+((itemRect.bottom-itemRect.top)/2-(iconSize+(label?textSize.cy:0))/2)+1-(label?2:0); + ImageList_Draw(images,info.iImage,hdc,itemRect.left+((itemRect.right-itemRect.left)/2-iconSize/2)+1,iconY,ILD_NORMAL); + if (label) + { + RECT textRect=itemRect; + textRect.left++; + textRect.top=iconY+iconSize+1; + DrawText(hdc,label,-1,&textRect,DT_SINGLELINE|DT_TOP|DT_CENTER); + } + } + else + { + int iconX=itemRect.left+(label?4:5); + ImageList_Draw(images,info.iImage,hdc,iconX,itemRect.top+((itemRect.bottom-itemRect.top)/2-iconSize/2)+1,ILD_NORMAL); + if (label) + { + RECT textRect=itemRect; + textRect.left=iconX+iconSize+4; + textRect.top+=(textRect.bottom-textRect.top)/2-textSize.cy/2; + DrawText(hdc,label,-1,&textRect,DT_SINGLELINE|DT_TOP|DT_LEFT); + } + } + + if (background) + DeleteObject(background); + RestoreDC(hdc,saved); + return CDRF_SKIPDEFAULT; + } + } + + draw->clrText=RGB(0xFF,0xFF,0xFF); + UINT flags=CDRF_DODEFAULT|TBCDRF_USECDCOLORS; + if (dropDown) + flags|=CDRF_NOTIFYPOSTPAINT; + else + { + draw->clrHighlightHotTrack=RGB(0x47,0x47,0x47); + flags|=TBCDRF_HILITEHOTTRACK; + } + return flags; + } + + if (stage==CDDS_ITEMPOSTPAINT) + { + RECT rc=draw->nmcd.rc; + UINT dpi=GetDpi(hWnd); + int triangleLeft=rc.right-(dpi>USER_DEFAULT_SCREEN_DPI?ScaleForDpi(hWnd,10):9); + int triangleTop=(rc.bottom-rc.top)/2+MulDiv(2,USER_DEFAULT_SCREEN_DPI,dpi); + int width=ScaleForDpi(hWnd,6); + width-=width%2; + POINT vertices[]={{triangleLeft,triangleTop},{triangleLeft+width,triangleTop},{triangleLeft+width/2,triangleTop+width/2}}; + + int saved=SaveDC(draw->nmcd.hdc); + SetDCPenColor(draw->nmcd.hdc,RGB(0xDE,0xDE,0xDE)); + SetDCBrushColor(draw->nmcd.hdc,RGB(0xDE,0xDE,0xDE)); + SelectObject(draw->nmcd.hdc,GetStockObject(DC_PEN)); + SelectObject(draw->nmcd.hdc,GetStockObject(DC_BRUSH)); + Polygon(draw->nmcd.hdc,vertices,_countof(vertices)); + RestoreDC(draw->nmcd.hdc,saved); + return CDRF_DODEFAULT; + } + + return CDRF_DODEFAULT; +} + LRESULT CBandWindow::OnCreate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { + if (GetWinVersion()>=WIN_VER_WIN10) + { + m_DarkBackgroundBrush=CreateSolidBrush(RGB(0x19,0x19,0x19)); + m_DarkBorderBrush=CreateSolidBrush(RGB(0x3A,0x3A,0x3A)); + } + ParseToolbar(); bool bLabels=false; @@ -261,6 +414,14 @@ LRESULT CBandWindow::OnCreate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bH m_Toolbar.SendMessage(TB_SETMAXTEXTROWS,1); SetWindowSubclass(m_Toolbar,ToolbarSubclassProc,(UINT_PTR)this,(DWORD_PTR)m_hWnd); + if (GetWinVersion()>=WIN_VER_WIN10) + { + SetWindowSubclass(m_hWnd,BandSubclassProc,(UINT_PTR)this,(DWORD_PTR)m_hWnd); + HWND tooltips=(HWND)m_Toolbar.SendMessage(TB_GETTOOLTIPS,0,0); + if (tooltips && ShouldAppsUseDarkMode()) + SetWindowTheme(tooltips,L"DarkMode_Explorer",NULL); + } + int iconSize=GetSettingInt(GetSettingBool(L"UseBigButtons")?L"LargeIconSize":L"SmallIconSize"); if (iconSize<8) iconSize=8; else if (iconSize>128) iconSize=128; @@ -397,6 +558,20 @@ LRESULT CBandWindow::OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& b if (it->menuIconD) DeleteObject(it->menuIconD); } m_Items.clear(); + + if (GetWinVersion()>=WIN_VER_WIN10) + RemoveWindowSubclass(m_hWnd,BandSubclassProc,(UINT_PTR)this); + if (m_DarkBackgroundBrush) + { + DeleteObject(m_DarkBackgroundBrush); + m_DarkBackgroundBrush=NULL; + } + if (m_DarkBorderBrush) + { + DeleteObject(m_DarkBorderBrush); + m_DarkBorderBrush=NULL; + } + bHandled=FALSE; return 0; } @@ -2031,6 +2206,34 @@ LRESULT CALLBACK CExplorerBand::RebarSubclassProc( HWND hWnd, UINT uMsg, WPARAM } } + if (GetWinVersion()>=WIN_VER_WIN10 && ShouldAppsUseDarkMode()) + { + CExplorerBand *pThis=(CExplorerBand*)uIdSubclass; + if (uMsg==WM_ERASEBKGND) + { + HDC hdc=(HDC)wParam; + RECT rc; + if (GetClipBox(hdc,&rc)!=ERROR) + FillRect(hdc,&rc,pThis->m_BandWindow.GetDarkBackgroundBrush()); + return 1; + } + if (uMsg==WM_NCPAINT) + { + LRESULT result=DefSubclassProc(hWnd,uMsg,wParam,lParam); + HDC hdc=GetWindowDC(hWnd); + if (hdc) + { + RECT rc; + GetWindowRect(hWnd,&rc); + OffsetRect(&rc,-rc.left,-rc.top); + rc.top=rc.bottom-1; + FillRect(hdc,&rc,pThis->m_BandWindow.GetDarkBorderBrush()); + ReleaseDC(hWnd,hdc); + } + return result; + } + } + return DefSubclassProc(hWnd,uMsg,wParam,lParam); } diff --git a/Src/ClassicExplorer/ExplorerBand.h b/Src/ClassicExplorer/ExplorerBand.h index ee8af4de9..579e1371e 100644 --- a/Src/ClassicExplorer/ExplorerBand.h +++ b/Src/ClassicExplorer/ExplorerBand.h @@ -97,6 +97,9 @@ class CBandWindow: public CWindowImpl bool HasPanes( void ) const; bool HasFolderSettings( void ) const; + HBRUSH GetDarkBackgroundBrush( void ) const { return m_DarkBackgroundBrush; } + HBRUSH GetDarkBorderBrush( void ) const { return m_DarkBorderBrush; } + protected: // Handler prototypes: // LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); @@ -121,6 +124,8 @@ class CBandWindow: public CWindowImpl CComPtr m_pBrowserBag; HIMAGELIST m_ImgEnabled; HIMAGELIST m_ImgDisabled; + HBRUSH m_DarkBackgroundBrush=NULL; + HBRUSH m_DarkBorderBrush=NULL; int m_MenuIconSize; struct StdToolbarItem @@ -162,6 +167,7 @@ class CBandWindow: public CWindowImpl void UpdateFolderSettings( void ); static LRESULT CALLBACK ToolbarSubclassProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData ); + static LRESULT CALLBACK BandSubclassProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData ); static HRESULT __stdcall BagWriteHook( IPropertyBag *pThis, LPCOLESTR pszPropName, VARIANT *pVar ); }; diff --git a/Src/Lib/ResourceHelper.cpp b/Src/Lib/ResourceHelper.cpp index b7388a554..0c53f1ad9 100644 --- a/Src/Lib/ResourceHelper.cpp +++ b/Src/Lib/ResourceHelper.cpp @@ -752,6 +752,43 @@ bool IsWin11(void) return bIsWin11; } +typedef bool (WINAPI *FShouldAppsUseDarkMode)( void ); + +// UxTheme ordinal 132 is the application-theme query used by Explorer on +// supported Windows 10/11 builds. Resolve it dynamically so older supported +// systems simply keep the existing light rendering path. +bool ShouldAppsUseDarkMode( void ) +{ + // Dark application mode was introduced with Windows 10 1809. Ordinal + // exports are not a stable API, so never call ordinal 132 on older builds. + auto version=GetOSVersion(); + if (version.dwMajorVersion<10 || version.dwBuildNumber<17763) + return false; + + HIGHCONTRAST highContrast={sizeof(highContrast)}; + if (SystemParametersInfo(SPI_GETHIGHCONTRAST,sizeof(highContrast),&highContrast,0) && + (highContrast.dwFlags&HCF_HIGHCONTRASTON)) + { + return false; + } + + static FShouldAppsUseDarkMode pShouldAppsUseDarkMode=[]() -> FShouldAppsUseDarkMode + { + HMODULE module=GetModuleHandle(L"uxtheme.dll"); + if (!module) + module=LoadLibrary(L"uxtheme.dll"); + return module?(FShouldAppsUseDarkMode)GetProcAddress(module,MAKEINTRESOURCEA(132)):NULL; + }(); + + return pShouldAppsUseDarkMode?pShouldAppsUseDarkMode():false; +} + +bool IsColorSchemeChangeMessage( LPARAM lParam ) +{ + return GetWinVersion()>=WIN_VER_WIN10 && lParam && + CompareStringOrdinal((LPCWCH)lParam,-1,L"ImmersiveColorSet",-1,TRUE)==CSTR_EQUAL; +} + // Wrapper for IShellFolder::ParseDisplayName HRESULT ShParseDisplayName( const wchar_t *pszName, PIDLIST_ABSOLUTE *ppidl, SFGAOF sfgaoIn, SFGAOF *psfgaoOut ) { diff --git a/Src/Lib/ResourceHelper.h b/Src/Lib/ResourceHelper.h index 5afc1d1dc..185330526 100644 --- a/Src/Lib/ResourceHelper.h +++ b/Src/Lib/ResourceHelper.h @@ -70,6 +70,15 @@ bool IsWin10RS4( void ); // Returns true if the version is Windows11 or later bool IsWin11(); +// Returns true if Windows is currently using the dark application theme. +// The underlying uxtheme entry point is resolved dynamically because older +// supported Windows versions don't expose it. +bool ShouldAppsUseDarkMode( void ); + +// Returns true for the WM_SETTINGCHANGE notification Windows sends when the +// immersive application color scheme changes. +bool IsColorSchemeChangeMessage( LPARAM lParam ); + // Wrapper for IShellFolder::ParseDisplayName HRESULT ShParseDisplayName( const wchar_t *pszName, PIDLIST_ABSOLUTE *ppidl, SFGAOF sfgaoIn, SFGAOF *psfgaoOut ); diff --git a/Src/Setup/Setup.cpp b/Src/Setup/Setup.cpp index c7c144e20..44a4e7b4e 100644 --- a/Src/Setup/Setup.cpp +++ b/Src/Setup/Setup.cpp @@ -47,6 +47,69 @@ struct Chunk int start1, start2, len; }; +static bool IsExplorerShellRunning( void ) +{ + HWND shellWindow=GetShellWindow(); + if (!shellWindow) + return false; + + DWORD shellProcessId=0; + GetWindowThreadProcessId(shellWindow,&shellProcessId); + if (!shellProcessId) + return false; + + DWORD currentSession=0; + DWORD shellSession=0; + if (!ProcessIdToSessionId(GetCurrentProcessId(),¤tSession) || + !ProcessIdToSessionId(shellProcessId,&shellSession) || + currentSession!=shellSession) + { + return false; + } + + HANDLE process=OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,FALSE,shellProcessId); + if (!process) + return false; + + wchar_t path[_MAX_PATH]; + DWORD pathSize=_countof(path); + bool isExplorer=false; + if (QueryFullProcessImageName(process,0,path,&pathSize)) + isExplorer=_wcsicmp(PathFindFileName(path),L"explorer.exe")==0; + + CloseHandle(process); + return isExplorer; +} + +static void RestoreExplorerShellIfNeeded( bool wasRunning ) +{ + if (!wasRunning) + return; + + // Restart Manager normally brings Explorer back before msiexec exits. + // Give it a short grace period before applying our fallback so we don't + // race a normal restart or create an extra Explorer process. + for (int i=0;i<50;i++) + { + if (IsExplorerShellRunning()) + return; + Sleep(100); + } + + wchar_t explorerPath[_MAX_PATH]; + UINT len=GetWindowsDirectory(explorerPath,_countof(explorerPath)); + if (!len || len>=_countof(explorerPath) || !PathAppend(explorerPath,L"explorer.exe")) + return; + + STARTUPINFO startupInfo={sizeof(startupInfo)}; + PROCESS_INFORMATION processInfo={}; + if (CreateProcess(explorerPath,NULL,NULL,NULL,FALSE,0,NULL,NULL,&startupInfo,&processInfo)) + { + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + } +} + static void WriteFileXOR( HANDLE hFile, const unsigned char *buf, int size ) { if (size>0) @@ -409,6 +472,11 @@ int APIENTRY wWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCm if (extractType == ARM64) Wow64DisableWow64FsRedirection(&wow64FsRedirVal); + // Remember whether this interactive session had the Explorer desktop shell. + // If Restart Manager closes it during MSI processing but fails to bring it + // back, restore only the shell that was present before setup started. + const bool explorerWasRunning=IsExplorerShellRunning(); + // start the installer STARTUPINFO startupInfo={sizeof(startupInfo)}; PROCESS_INFORMATION processInfo; @@ -438,6 +506,12 @@ int APIENTRY wWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCm GetExitCodeProcess(processInfo.hProcess,&code); CloseHandle(processInfo.hProcess); DeleteFile(msiName); + + // Only repair the shell after a successful transaction. Reboot-required + // outcomes deliberately keep Windows Installer in control of recovery. + if (code==ERROR_SUCCESS) + RestoreExplorerShellIfNeeded(explorerWasRunning); + return code; } } diff --git a/Src/StartMenu/StartMenuDLL/MenuContainer.cpp b/Src/StartMenu/StartMenuDLL/MenuContainer.cpp index 856ead1e4..3337b66a8 100644 --- a/Src/StartMenu/StartMenuDLL/MenuContainer.cpp +++ b/Src/StartMenu/StartMenuDLL/MenuContainer.cpp @@ -2537,10 +2537,11 @@ void CMenuContainer::InitItems( void ) } } - if (m_Items.empty() && m_Path1[0] && m_pDropFolder[0]) + if (!m_bSubMenu && m_Items.empty() && m_Path1[0] && m_pDropFolder[0]) { - // add (Empty) item to the empty submenus - MenuItem item(m_bSubMenu?MENU_EMPTY:MENU_EMPTY_TOP); + // Keep the top-level drop target placeholder. Submenus defer their + // empty-state decision until after custom/standard items are appended. + MenuItem item(MENU_EMPTY_TOP); item.name=FindTranslation(L"Menu.Empty",L"(Empty)"); m_Items.push_back(item); } diff --git a/Src/StartMenu/StartMenuDLL/MenuPaint.cpp b/Src/StartMenu/StartMenuDLL/MenuPaint.cpp index 32bccd387..9ea9d6710 100644 --- a/Src/StartMenu/StartMenuDLL/MenuPaint.cpp +++ b/Src/StartMenu/StartMenuDLL/MenuPaint.cpp @@ -1185,13 +1185,23 @@ void CMenuContainer::CreateBackground( int width1, int width2, int height1, int if (opacity!=MenuSkin::OPACITY_SOLID && !bMask) { - // set to opaque + // set the visible part to opaque. BitBlt/AlphaBlend above clip to + // the destination bitmap automatically, but this direct pixel pass + // must do the same before forming a pointer into the DIB. SelectObject(hdc,bmp0); // deselect m_Bitmap so all the GDI operations get flushed - unsigned int *bits2=bits+pos.y*totalWidth+pos.x; - alpha<<=24; - for (int y=0;y @@ -2643,6 +2644,7 @@ void UpdateTaskBars( TUpdateTaskbar update ) InvalidateRect(taskBar.taskBar,NULL,TRUE); PostMessage(taskBar.taskBar,WM_THEMECHANGED,0,0); } + UpdateWin11StartButtonMonitor(); } /////////////////////////////////////////////////////////////////////////////// @@ -3227,6 +3229,7 @@ if (!g_bTrimHooks) UpdateTaskBars(TASKBAR_RECREATE_BUTTONS); UpdateTaskBars(TASKBAR_UPDATE_TEXTURE); + StartWin11StartButtonMonitor(); } static void RecreateStartButton( size_t taskbarId ) @@ -3288,6 +3291,8 @@ static DWORD WINAPI ExitThreadProc( void *param ) static void CleanStartMenuDLL( void ) { + StopWin11StartButtonMonitor(); + ClearIatHook(g_DwmpBTRHook); g_DwmpBTRHook=NULL; ClearIatHook(g_DwmpTWWRHook); diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj index 0412e588c..416c5a52f 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj @@ -89,6 +89,7 @@ Create + @@ -110,6 +111,7 @@ + diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters index fc2bd40cb..bfb839860 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters @@ -72,6 +72,9 @@ Source Files + + Source Files + @@ -128,6 +131,9 @@ Header Files + + Header Files + Resource Files diff --git a/Src/StartMenu/StartMenuDLL/Win11StartButton.cpp b/Src/StartMenu/StartMenuDLL/Win11StartButton.cpp new file mode 100644 index 000000000..edb68920d --- /dev/null +++ b/Src/StartMenu/StartMenuDLL/Win11StartButton.cpp @@ -0,0 +1,976 @@ +// Windows 11 native Start button suppression. +// +// Open-Shell's replacement button is a separate layered window. Windows 11 +// renders its own Start button in XAML, so the old HWND hiding logic cannot +// remove the native glyph. This file uses the public XAML diagnostics API to +// suppress only the native Start glyph and its hit target. The XAML element is +// left in layout, so centered taskbar positioning remains owned by Windows. + +#include "stdafx.h" +#include "Win11StartButton.h" +#include "Settings.h" +#include "LogManager.h" +#include "ResourceHelper.h" + +#include +#include +#include +#include +#include + +static const GUID CLSID_OpenShellStartButtonTap = +{ 0x7d15741f, 0x2f3b, 0x4971, { 0xb8, 0x91, 0x6a, 0x5d, 0x42, 0xd7, 0x1a, 0x34 } }; + +static const UINT WM_OS_STARTBUTTON_APPLY = WM_APP + 0x35B; +static const UINT WM_OS_STARTBUTTON_SHUTDOWN = WM_APP + 0x35C; + +static volatile LONG g_StartButtonActive = 0; +static volatile LONG g_ConnectStarted = 0; + +static HMODULE GetThisModule( void ) +{ + HMODULE module = NULL; + GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCTSTR)&GetThisModule, &module); + return module; +} + +struct StartElement +{ + InstanceHandle parent; + CString type; + CString name; + bool visibilityOverride; + bool hitTestOverride; + bool startControlResolved; + bool isStartControl; + unsigned int discoveryOrder; + + StartElement( void ) + { + parent = 0; + visibilityOverride = false; + hitTestOverride = false; + startControlResolved = false; + isStartControl = false; + discoveryOrder = 0; + } +}; + +class CWin11StartButtonTap; +static CWin11StartButtonTap *g_Tap = NULL; +static SRWLOCK g_TapLock = SRWLOCK_INIT; + +static bool ContainsText( const CString &text, const wchar_t *part ) +{ + return text.Find(part) >= 0; +} + +static bool IsStartControlCandidate( const StartElement &element ) +{ + if (!ContainsText(element.type, L"ExperienceToggleButton")) + return false; + return element.name == L"LaunchListButton" || element.name == L"StartButton"; +} + +static bool IsStartGlyph( const StartElement &element ) +{ + if (element.name == L"Icon") + return true; + if (ContainsText(element.type, L"AnimatedVisualPlayer") || ContainsText(element.type, L"AepAnimatedIcon")) + return true; + if (ContainsText(element.type, L"FontIcon") || ContainsText(element.type, L"PathIcon") || + ContainsText(element.type, L"ImageIcon") || ContainsText(element.type, L"BitmapIcon") || + ContainsText(element.type, L"SymbolIcon")) + return true; + return false; +} + +class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCallback2 +{ +public: + CWin11StartButtonTap( void ) + { + m_Refs = 1; + m_Advised = false; + m_Dispatch = NULL; + m_PrimaryStart = 0; + m_NextDiscoveryOrder = 0; + m_InjectionReferenceBalanced = false; + InitializeCriticalSection(&m_Lock); + } + + ~CWin11StartButtonTap( void ) + { + if (m_Visual && m_Advised) + m_Visual->UnadviseVisualTreeChange(static_cast(this)); + if (m_Dispatch && GetWindowThreadProcessId(m_Dispatch, NULL) == GetCurrentThreadId()) + DestroyWindow(m_Dispatch); + + AcquireSRWLockExclusive(&g_TapLock); + if (g_Tap == this) + g_Tap = NULL; + ReleaseSRWLockExclusive(&g_TapLock); + + InterlockedExchange(&g_ConnectStarted, 0); + DeleteCriticalSection(&m_Lock); + } + + STDMETHODIMP QueryInterface( REFIID riid, void **ppv ) + { + if (!ppv) + return E_POINTER; + *ppv = NULL; + + if (riid == IID_IUnknown || riid == IID_IObjectWithSite) + *ppv = static_cast(this); + else if (riid == __uuidof(IVisualTreeServiceCallback) || riid == __uuidof(IVisualTreeServiceCallback2)) + *ppv = static_cast(this); + else + return E_NOINTERFACE; + + AddRef(); + return S_OK; + } + + STDMETHODIMP_(ULONG) AddRef( void ) + { + return (ULONG)InterlockedIncrement(&m_Refs); + } + + STDMETHODIMP_(ULONG) Release( void ) + { + LONG refs = InterlockedDecrement(&m_Refs); + if (!refs) + delete this; + return (ULONG)refs; + } + + STDMETHODIMP SetSite( IUnknown *site ) + { + if (m_Visual && m_Advised) + { + ApplyState(false); + m_Visual->UnadviseVisualTreeChange(static_cast(this)); + m_Advised = false; + } + m_Visual.Release(); + m_Site.Release(); + + if (!site) + { + AcquireSRWLockExclusive(&g_TapLock); + if (g_Tap == this) + g_Tap = NULL; + ReleaseSRWLockExclusive(&g_TapLock); + InterlockedExchange(&g_ConnectStarted, 0); + return S_OK; + } + + EnterCriticalSection(&m_Lock); + m_Elements.clear(); + m_PrimaryStart = 0; + m_NextDiscoveryOrder = 0; + LeaveCriticalSection(&m_Lock); + + m_Site = site; + + // Keep the module reference acquired by InitializeXamlDiagnosticsEx for + // the whole TAP session. It is balanced only after the callback and all + // XAML/COM references have been released. StartMenuDLL has its own module + // reference while Open-Shell is active, so balancing the diagnostics + // reference during teardown cannot unload code that is still executing. + // + // Exit may race a connection attempt. Do not attach a new callback after + // shutdown has already begun. + if (!InterlockedCompareExchange(&g_StartButtonActive, 0, 0)) + { + m_Site.Release(); + BalanceInjectionReference(); + InterlockedExchange(&g_ConnectStarted, 0); + return S_OK; + } + + HRESULT hr = site->QueryInterface(__uuidof(IVisualTreeService), (void**)&m_Visual); + if (FAILED(hr) || !m_Visual) + { + m_Visual.Release(); + m_Site.Release(); + BalanceInjectionReference(); + InterlockedExchange(&g_ConnectStarted, 0); + return hr; + } + + if (!CreateDispatchWindow()) + { + DWORD error = GetLastError(); + m_Visual.Release(); + m_Site.Release(); + BalanceInjectionReference(); + InterlockedExchange(&g_ConnectStarted, 0); + return HRESULT_FROM_WIN32(error); + } + + // Advise replays the existing tree. OnVisualTreeChange only records + // element handles; all property access is dispatched afterwards. + hr = m_Visual->AdviseVisualTreeChange(static_cast(this)); + if (SUCCEEDED(hr)) + { + m_Advised = true; + + // Publish the TAP only after the subscription is fully established. + // Otherwise an Advise failure can leave g_Tap pointing at an object + // that the XAML runtime is about to release. + AcquireSRWLockExclusive(&g_TapLock); + g_Tap = this; + ReleaseSRWLockExclusive(&g_TapLock); + + RequestApply(false); + } + else + { + if (m_Dispatch) + { + HWND dispatch = m_Dispatch; + m_Dispatch = NULL; + SetWindowLongPtr(dispatch, GWLP_USERDATA, 0); + DestroyWindow(dispatch); + } + m_Visual.Release(); + m_Site.Release(); + BalanceInjectionReference(); + InterlockedExchange(&g_ConnectStarted, 0); + } + LogToFile(STARTUP_LOG, L"Win11StartButton: visual tree advise 0x%08X", hr); + return hr; + } + + STDMETHODIMP GetSite( REFIID riid, void **ppv ) + { + if (!ppv) + return E_POINTER; + *ppv = NULL; + if (!m_Site) + return E_FAIL; + return m_Site->QueryInterface(riid, ppv); + } + + STDMETHODIMP OnVisualTreeChange( ParentChildRelation relation, VisualElement element, VisualMutationType mutationType ) + { + bool interesting = false; + + EnterCriticalSection(&m_Lock); + if (mutationType == Remove) + { + auto it = m_Elements.find(element.Handle); + if (it != m_Elements.end()) + { + interesting = it->second.visibilityOverride || it->second.hitTestOverride || + it->second.isStartControl || IsStartControlCandidate(it->second); + bool wasPrimary = element.Handle == m_PrimaryStart; + m_Elements.erase(it); + if (wasPrimary) + m_PrimaryStart = FindPrimaryStartLocked(); + } + } + else if (mutationType == Add) + { + StartElement record; + record.parent = relation.Parent; + record.type = element.Type ? element.Type : L""; + record.name = element.Name ? element.Name : L""; + + auto previous = m_Elements.find(element.Handle); + if (previous != m_Elements.end()) + { + record.visibilityOverride = previous->second.visibilityOverride; + record.hitTestOverride = previous->second.hitTestOverride; + record.startControlResolved = previous->second.startControlResolved; + record.isStartControl = previous->second.isStartControl; + record.discoveryOrder = previous->second.discoveryOrder; + } + else + { + record.discoveryOrder = ++m_NextDiscoveryOrder; + } + m_Elements[element.Handle] = record; + + interesting = IsStartControlCandidate(record) || IsStartGlyph(record) || + IsUnderStartButtonLocked(record.parent); + } + LeaveCriticalSection(&m_Lock); + + if (interesting) + RequestApply(false); + return S_OK; + } + + STDMETHODIMP OnElementStateChanged( InstanceHandle, VisualElementState, LPCWSTR ) + { + return S_OK; + } + + void RequestApply( bool synchronous ) + { + if (!m_Dispatch) + return; + if (synchronous) + SendMessage(m_Dispatch, WM_OS_STARTBUTTON_APPLY, 0, 0); + else + PostMessage(m_Dispatch, WM_OS_STARTBUTTON_APPLY, 0, 0); + } + + HRESULT Shutdown( void ) + { + // SetSite creates the dispatch window on the XAML thread. Run the complete + // XAML/COM teardown on that same thread rather than releasing apartment- + // sensitive interfaces from whichever thread initiated Open-Shell Exit. + if (!m_Dispatch) + return E_UNEXPECTED; + + HRESULT hr = (HRESULT)SendMessage(m_Dispatch, WM_OS_STARTBUTTON_SHUTDOWN, 0, 0); + if (FAILED(hr)) + return hr; + + AcquireSRWLockExclusive(&g_TapLock); + if (g_Tap == this) + g_Tap = NULL; + ReleaseSRWLockExclusive(&g_TapLock); + + InterlockedExchange(&g_ConnectStarted, 0); + BalanceInjectionReference(); + return S_OK; + } + +private: + static LRESULT CALLBACK DispatchProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) + { + CWin11StartButtonTap *tap = (CWin11StartButtonTap*)GetWindowLongPtr(hwnd, GWLP_USERDATA); + if (msg == WM_NCCREATE) + { + CREATESTRUCT *create = (CREATESTRUCT*)lParam; + tap = (CWin11StartButtonTap*)create->lpCreateParams; + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)tap); + } + if (msg == WM_OS_STARTBUTTON_APPLY && tap) + { + bool enabled = InterlockedCompareExchange(&g_StartButtonActive, 0, 0) != 0 && + GetSettingBool(L"EnableStartButton"); + tap->ApplyState(enabled); + return 0; + } + if (msg == WM_OS_STARTBUTTON_SHUTDOWN && tap) + return tap->ShutdownOnDispatchThread(); + return DefWindowProc(hwnd, msg, wParam, lParam); + } + + HRESULT ShutdownOnDispatchThread( void ) + { + // The window is owned by the same thread on which SetSite ran. Keep all + // operations touching the XAML diagnostics interfaces on this thread. + ApplyState(false); + + if (m_Visual && m_Advised) + { + HRESULT hr = m_Visual->UnadviseVisualTreeChange(static_cast(this)); + if (FAILED(hr)) + { + LogToFile(STARTUP_LOG, L"Win11StartButton: visual tree unadvise failed 0x%08X", hr); + return hr; + } + m_Advised = false; + } + + m_Visual.Release(); + m_Site.Release(); + + if (m_Dispatch) + { + HWND dispatch = m_Dispatch; + m_Dispatch = NULL; + SetWindowLongPtr(dispatch, GWLP_USERDATA, 0); + DestroyWindow(dispatch); + } + return S_OK; + } + + void BalanceInjectionReference( void ) + { + if (m_InjectionReferenceBalanced) + return; + + HMODULE module = GetThisModule(); + if (module) + { + // Mark first: FreeLibrary can run loader callbacks before it returns. + m_InjectionReferenceBalanced = true; + FreeLibrary(module); + } + } + + bool CreateDispatchWindow( void ) + { + if (m_Dispatch) + return true; + + static const wchar_t CLASS_NAME[] = L"OpenShell.Win11StartButtonTap"; + WNDCLASS wc = {}; + HMODULE module = GetThisModule(); + if (!module) + return false; + + wc.lpfnWndProc = DispatchProc; + wc.hInstance = module; + wc.lpszClassName = CLASS_NAME; + if (!RegisterClass(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) + return false; + + m_Dispatch = CreateWindowEx(0, CLASS_NAME, L"", 0, 0, 0, 0, 0, + HWND_MESSAGE, NULL, module, this); + return m_Dispatch != NULL; + } + + bool IsUnderStartButtonLocked( InstanceHandle parent ) const + { + for (int depth = 0; depth < 24 && parent; depth++) + { + auto it = m_Elements.find(parent); + if (it == m_Elements.end()) + break; + if (it->second.isStartControl) + return true; + parent = it->second.parent; + } + return false; + } + + InstanceHandle GetStartAncestor( InstanceHandle handle ) + { + InstanceHandle result = 0; + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(handle); + if (it != m_Elements.end()) + { + InstanceHandle parent = it->second.parent; + for (int depth = 0; depth < 24 && parent; depth++) + { + auto pit = m_Elements.find(parent); + if (pit == m_Elements.end()) + break; + if (pit->second.isStartControl) + { + result = parent; + break; + } + parent = pit->second.parent; + } + } + LeaveCriticalSection(&m_Lock); + return result; + } + + static void FreeProperties( PropertyChainSource *sources, unsigned int sourceCount, + PropertyChainValue *values, unsigned int valueCount ) + { + if (sources) + { + for (unsigned int i = 0; i < sourceCount; i++) + { + SysFreeString(sources[i].TargetType); + SysFreeString(sources[i].Name); + SysFreeString(sources[i].SrcInfo.FileName); + SysFreeString(sources[i].SrcInfo.Hash); + } + CoTaskMemFree(sources); + } + if (values) + { + for (unsigned int i = 0; i < valueCount; i++) + { + SysFreeString(values[i].Type); + SysFreeString(values[i].DeclaringType); + SysFreeString(values[i].ValueType); + SysFreeString(values[i].ItemType); + SysFreeString(values[i].Value); + SysFreeString(values[i].PropertyName); + } + CoTaskMemFree(values); + } + } + + InstanceHandle FindPrimaryStartLocked( void ) const + { + InstanceHandle primary = 0; + unsigned int bestOrder = 0; + for (auto it = m_Elements.begin(); it != m_Elements.end(); ++it) + { + if (!it->second.isStartControl) + continue; + if (!primary || it->second.discoveryOrder < bestOrder) + { + primary = it->first; + bestOrder = it->second.discoveryOrder; + } + } + return primary; + } + + HRESULT ResolveStartControl( InstanceHandle handle, const StartElement &element, bool *isStartControl ) + { + *isStartControl = false; + if (!IsStartControlCandidate(element)) + return S_OK; + + // Older taskbar implementations may expose a distinct x:Name. + if (element.name.CompareNoCase(L"StartButton") == 0) + { + *isStartControl = true; + return S_OK; + } + + unsigned int sourceCount = 0; + unsigned int valueCount = 0; + PropertyChainSource *sources = NULL; + PropertyChainValue *values = NULL; + HRESULT hr = m_Visual->GetPropertyValuesChain(handle, &sourceCount, &sources, &valueCount, &values); + if (FAILED(hr)) + { + FreeProperties(sources, sourceCount, values, valueCount); + return hr; + } + + for (unsigned int i = 0; i < valueCount; i++) + { + if (!values[i].PropertyName || !values[i].Value) + continue; + if (_wcsicmp(values[i].PropertyName, L"AutomationId") != 0 && + _wcsicmp(values[i].PropertyName, L"AutomationProperties.AutomationId") != 0) + continue; + if (_wcsicmp(values[i].Value, L"StartButton") == 0) + { + *isStartControl = true; + break; + } + } + + FreeProperties(sources, sourceCount, values, valueCount); + return S_OK; + } + + void SetControlClassification( InstanceHandle handle, bool isStartControl ) + { + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(handle); + if (it != m_Elements.end()) + { + it->second.startControlResolved = true; + it->second.isStartControl = isStartControl; + m_PrimaryStart = FindPrimaryStartLocked(); + } + LeaveCriticalSection(&m_Lock); + } + + HRESULT FindProperty( InstanceHandle handle, const wchar_t *name, unsigned int *index, CString *typeName ) + { + unsigned int sourceCount = 0; + unsigned int valueCount = 0; + PropertyChainSource *sources = NULL; + PropertyChainValue *values = NULL; + HRESULT hr = m_Visual->GetPropertyValuesChain(handle, &sourceCount, &sources, &valueCount, &values); + if (FAILED(hr)) + { + FreeProperties(sources, sourceCount, values, valueCount); + return hr; + } + + bool found = false; + for (unsigned int i = 0; i < valueCount; i++) + { + if (!values[i].PropertyName || wcscmp(values[i].PropertyName, name) != 0) + continue; + if (values[i].MetadataBits & 0x2) // IsPropertyReadOnly + continue; + + *index = values[i].Index; + if (typeName) + *typeName = values[i].Type ? values[i].Type : L""; + found = true; + break; + } + FreeProperties(sources, sourceCount, values, valueCount); + return found ? S_OK : HRESULT_FROM_WIN32(ERROR_NOT_FOUND); + } + + HRESULT SetPropertyText( InstanceHandle handle, const wchar_t *name, const wchar_t *valueText ) + { + unsigned int index = 0; + CString typeName; + HRESULT hr = FindProperty(handle, name, &index, &typeName); + if (FAILED(hr) || typeName.IsEmpty()) + return FAILED(hr) ? hr : E_FAIL; + + CComBSTR type(typeName); + CComBSTR value(valueText); + InstanceHandle created = 0; + hr = m_Visual->CreateInstance(type, value, &created); + if (FAILED(hr)) + return hr; + return m_Visual->SetProperty(handle, created, index); + } + + HRESULT ClearPropertyByName( InstanceHandle handle, const wchar_t *name ) + { + unsigned int index = 0; + HRESULT hr = FindProperty(handle, name, &index, NULL); + if (FAILED(hr)) + return hr; + return m_Visual->ClearProperty(handle, index); + } + + void SetOverrideFlags( InstanceHandle handle, bool *visibility, bool *hitTest ) + { + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(handle); + if (it != m_Elements.end()) + { + if (visibility) + it->second.visibilityOverride = *visibility; + if (hitTest) + it->second.hitTestOverride = *hitTest; + } + LeaveCriticalSection(&m_Lock); + } + + void ApplyState( bool enabled ) + { + if (!m_Visual) + return; + + std::vector> elements; + EnterCriticalSection(&m_Lock); + for (auto it = m_Elements.begin(); it != m_Elements.end(); ++it) + elements.push_back(*it); + LeaveCriticalSection(&m_Lock); + + // Start and Task View share ExperienceToggleButton#LaunchListButton on + // current Windows 11 builds. Resolve the attached AutomationId on the XAML + // UI thread before changing any control or descendant. + for (size_t i = 0; i < elements.size(); i++) + { + StartElement &record = elements[i].second; + if (!IsStartControlCandidate(record) || record.startControlResolved) + continue; + + bool isStartControl = false; + if (SUCCEEDED(ResolveStartControl(elements[i].first, record, &isStartControl))) + { + record.startControlResolved = true; + record.isStartControl = isStartControl; + SetControlClassification(elements[i].first, isStartControl); + } + } + + InstanceHandle primaryStart = 0; + EnterCriticalSection(&m_Lock); + primaryStart = m_PrimaryStart; + LeaveCriticalSection(&m_Lock); + + const bool allTaskbars = GetSettingBool(L"AllTaskbars"); + + for (size_t i = 0; i < elements.size(); i++) + { + InstanceHandle handle = elements[i].first; + StartElement record = elements[i].second; + + if (record.isStartControl) + { + bool target = allTaskbars || !primaryStart || handle == primaryStart; + if (enabled && target) + { + if (!record.hitTestOverride) + { + HRESULT hr = SetPropertyText(handle, L"IsHitTestVisible", L"False"); + if (SUCCEEDED(hr)) + { + bool value = true; + SetOverrideFlags(handle, NULL, &value); + } + } + } + else if (record.hitTestOverride) + { + HRESULT hr = ClearPropertyByName(handle, L"IsHitTestVisible"); + if (SUCCEEDED(hr)) + { + bool value = false; + SetOverrideFlags(handle, NULL, &value); + } + } + continue; + } + + if (!IsStartGlyph(record)) + continue; + + InstanceHandle startAncestor = GetStartAncestor(handle); + if (!startAncestor) + continue; + bool target = allTaskbars || !primaryStart || startAncestor == primaryStart; + + if (enabled && target) + { + if (!record.visibilityOverride) + { + HRESULT hr = SetPropertyText(handle, L"Visibility", L"Collapsed"); + if (SUCCEEDED(hr)) + { + bool value = true; + SetOverrideFlags(handle, &value, NULL); + } + } + } + else if (record.visibilityOverride) + { + HRESULT hr = ClearPropertyByName(handle, L"Visibility"); + if (SUCCEEDED(hr)) + { + bool value = false; + SetOverrideFlags(handle, &value, NULL); + } + } + } + } + + LONG m_Refs; + bool m_Advised; + HWND m_Dispatch; + CRITICAL_SECTION m_Lock; + CComPtr m_Site; + CComPtr m_Visual; + InstanceHandle m_PrimaryStart; + unsigned int m_NextDiscoveryOrder; + bool m_InjectionReferenceBalanced; + std::unordered_map m_Elements; +}; + +static CWin11StartButtonTap *GetTapRef( void ) +{ + CWin11StartButtonTap *tap = NULL; + AcquireSRWLockShared(&g_TapLock); + tap = g_Tap; + if (tap) + tap->AddRef(); + ReleaseSRWLockShared(&g_TapLock); + return tap; +} + +class CStartButtonTapFactory: public IClassFactory +{ +public: + CStartButtonTapFactory( void ) { m_Refs = 1; } + + STDMETHODIMP QueryInterface( REFIID riid, void **ppv ) + { + if (!ppv) + return E_POINTER; + *ppv = NULL; + if (riid != IID_IUnknown && riid != IID_IClassFactory) + return E_NOINTERFACE; + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + + STDMETHODIMP_(ULONG) AddRef( void ) { return (ULONG)InterlockedIncrement(&m_Refs); } + STDMETHODIMP_(ULONG) Release( void ) { return (ULONG)InterlockedDecrement(&m_Refs); } + + STDMETHODIMP CreateInstance( IUnknown *outer, REFIID riid, void **ppv ) + { + if (!ppv) + return E_POINTER; + *ppv = NULL; + if (outer) + return CLASS_E_NOAGGREGATION; + + CWin11StartButtonTap *tap = new CWin11StartButtonTap(); + if (!tap) + return E_OUTOFMEMORY; + HRESULT hr = tap->QueryInterface(riid, ppv); + tap->Release(); + return hr; + } + + STDMETHODIMP LockServer( BOOL ) { return S_OK; } + +private: + LONG m_Refs; +}; + +static CStartButtonTapFactory g_Factory; + +// xamlom.h declares DllGetClassObject through the platform headers, so export +// our implementation under that name via a linker alias. +extern "C" HRESULT STDMETHODCALLTYPE OpenShellStartButtonDllGetClassObject( REFCLSID clsid, REFIID riid, LPVOID *ppv ) +{ + if (!IsEqualGUID(clsid, CLSID_OpenShellStartButtonTap)) + return CLASS_E_CLASSNOTAVAILABLE; + return g_Factory.QueryInterface(riid, ppv); +} + +#ifdef _M_IX86 +#pragma comment(linker, "/EXPORT:DllGetClassObject=_OpenShellStartButtonDllGetClassObject@12,PRIVATE") +#else +#pragma comment(linker, "/EXPORT:DllGetClassObject=OpenShellStartButtonDllGetClassObject,PRIVATE") +#endif + +typedef HRESULT (WINAPI *InitXamlDiagnosticsEx_t)( LPCWSTR, DWORD, LPCWSTR, LPCWSTR, CLSID, LPCWSTR ); + +struct ConnectAttempt +{ + InitXamlDiagnosticsEx_t init; + const wchar_t *endpoint; + wchar_t dllPath[MAX_PATH]; + HRESULT hr; +}; + +static DWORD WINAPI ConnectAttemptThread( LPVOID param ) +{ + ConnectAttempt *attempt = (ConnectAttempt*)param; + attempt->hr = attempt->init(attempt->endpoint, GetCurrentProcessId(), NULL, + attempt->dllPath, CLSID_OpenShellStartButtonTap, NULL); + return 0; +} + +static DWORD FinishConnectThread( HMODULE moduleReference, HMODULE runtime, bool resetConnectionState ) +{ + if (runtime) + FreeLibrary(runtime); + if (resetConnectionState) + InterlockedExchange(&g_ConnectStarted, 0); + + // The connection worker executes from StartMenuDLL and can outlive the normal + // Open-Shell unload path. Release its private module reference and terminate + // atomically so the DLL cannot disappear underneath the thread's return path. + FreeLibraryAndExitThread(moduleReference, 0); + return 0; +} + +static DWORD WINAPI ConnectThread( LPVOID param ) +{ + HMODULE moduleReference = (HMODULE)param; + HMODULE runtime = LoadLibraryEx(L"Windows.UI.Xaml.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!runtime) + return FinishConnectThread(moduleReference, NULL, true); + + InitXamlDiagnosticsEx_t init = (InitXamlDiagnosticsEx_t)GetProcAddress(runtime, "InitializeXamlDiagnosticsEx"); + if (!init) + return FinishConnectThread(moduleReference, runtime, true); + + HMODULE module = GetThisModule(); + wchar_t dllPath[MAX_PATH]; + if (!module || !GetModuleFileName(module, dllPath, _countof(dllPath))) + return FinishConnectThread(moduleReference, runtime, true); + + const wchar_t *endpoints[] = { L"VisualDiagConnection1", L"VisualDiagConnection2" }; + HRESULT last = E_FAIL; + for (int retry = 0; retry < 8 && InterlockedCompareExchange(&g_StartButtonActive, 0, 0); retry++) + { + for (int i = 0; i < _countof(endpoints); i++) + { + ConnectAttempt attempt = {}; + attempt.init = init; + attempt.endpoint = endpoints[i]; + Strcpy(attempt.dllPath, _countof(attempt.dllPath), dllPath); + attempt.hr = E_FAIL; + + HANDLE thread = CreateThread(NULL, 0, ConnectAttemptThread, &attempt, 0, NULL); + if (!thread) + continue; + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + last = attempt.hr; + if (SUCCEEDED(last)) + { + LogToFile(STARTUP_LOG, L"Win11StartButton: connected using %s", endpoints[i]); + return FinishConnectThread(moduleReference, runtime, false); + } + } + Sleep(500); + } + + LogToFile(STARTUP_LOG, L"Win11StartButton: connection failed 0x%08X", last); + return FinishConnectThread(moduleReference, runtime, true); +} + +static void EnsureConnection( void ) +{ + if (InterlockedCompareExchange(&g_ConnectStarted, 1, 0) != 0) + return; + + HMODULE moduleReference = NULL; + if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCTSTR)&ConnectThread, &moduleReference)) + { + InterlockedExchange(&g_ConnectStarted, 0); + return; + } + + HANDLE thread = CreateThread(NULL, 0, ConnectThread, moduleReference, 0, NULL); + if (thread) + { + CloseHandle(thread); + } + else + { + FreeLibrary(moduleReference); + InterlockedExchange(&g_ConnectStarted, 0); + } +} + +void StartWin11StartButtonMonitor( void ) +{ + if (!IsWin11()) + return; + + InterlockedExchange(&g_StartButtonActive, 1); + CWin11StartButtonTap *tap = GetTapRef(); + if (tap) + { + tap->RequestApply(false); + tap->Release(); + return; + } + EnsureConnection(); +} + +void UpdateWin11StartButtonMonitor( void ) +{ + if (!IsWin11()) + return; + + CWin11StartButtonTap *tap = GetTapRef(); + if (tap) + { + tap->RequestApply(false); + tap->Release(); + } + else if (InterlockedCompareExchange(&g_StartButtonActive, 0, 0)) + { + EnsureConnection(); + } +} + +void StopWin11StartButtonMonitor( void ) +{ + if (!IsWin11()) + return; + + InterlockedExchange(&g_StartButtonActive, 0); + CWin11StartButtonTap *tap = GetTapRef(); + if (tap) + { + HRESULT hr = tap->Shutdown(); + if (FAILED(hr)) + LogToFile(STARTUP_LOG, L"Win11StartButton: TAP shutdown failed 0x%08X", hr); + tap->Release(); + } +} diff --git a/Src/StartMenu/StartMenuDLL/Win11StartButton.h b/Src/StartMenu/StartMenuDLL/Win11StartButton.h new file mode 100644 index 000000000..b4bc15b56 --- /dev/null +++ b/Src/StartMenu/StartMenuDLL/Win11StartButton.h @@ -0,0 +1,8 @@ +#pragma once + +// Windows 11 draws the native Start button in XAML. These helpers hide only +// the native glyph/hit target while Open-Shell's replacement button is active. +// They never move or resize either button. +void StartWin11StartButtonMonitor( void ); +void UpdateWin11StartButtonMonitor( void ); +void StopWin11StartButtonMonitor( void );