diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp index 3a6b18770..6c816f3ad 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp @@ -14,6 +14,7 @@ #include "ResourceHelper.h" #include "LogManager.h" #include "TouchHelper.h" +#include "Win11StartButton.h" #include "IatHookHelper.h" #include "dllmain.h" #include @@ -1315,7 +1316,11 @@ static void UpdateStartButtonPosition(const TaskbarInfo* taskBar, const WINDOWPO } } - RECT rcOldButton; + RECT rcNativeStart = {}; + const bool hasNativeStartRect = IsWin11() && + GetWin11StartButtonRect(taskBar->taskBar, &rcNativeStart); + + RECT rcOldButton = {}; if (taskBar->oldButton) GetWindowRect(taskBar->oldButton, &rcOldButton); @@ -1328,25 +1333,44 @@ static void UpdateStartButtonPosition(const TaskbarInfo* taskBar, const WINDOWPO x = rcTask.left; else x = rcTask.right - taskBar->startButtonSize.cx; - y = taskBar->oldButton ? rcOldButton.top : rcTask.top; + + if (hasNativeStartRect) + y = (rcNativeStart.top + rcNativeStart.bottom - taskBar->startButtonSize.cy) / 2; + else + y = taskBar->oldButton ? rcOldButton.top : rcTask.top; } else { - if (GetWindowLongPtr(taskBar->rebar, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) + // Windows 11's Start button is a XAML control. Its old "Start" HWND is + // not a reliable layout anchor on centered taskbars, high DPI, or newer + // taskbar implementations. Center the replacement over the verified + // AutomationId=StartButton rectangle instead. + if (hasNativeStartRect) + x = (rcNativeStart.left + rcNativeStart.right - taskBar->startButtonSize.cx) / 2; + else if (GetWindowLongPtr(taskBar->rebar, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) x = (taskBar->oldButton ? rcOldButton.right : rcTask.right) - taskBar->startButtonSize.cx; else x = taskBar->oldButton ? rcOldButton.left : rcTask.left; + if (GetSettingInt(L"StartButtonType") != START_BUTTON_CUSTOM || !GetSettingBool(L"StartButtonAlign")) - y = (rcTask.top + rcTask.bottom - taskBar->startButtonSize.cy) / 2; + { + if (hasNativeStartRect) + y = (rcNativeStart.top + rcNativeStart.bottom - taskBar->startButtonSize.cy) / 2; + else + y = (rcTask.top + rcTask.bottom - taskBar->startButtonSize.cy) / 2; + } else if (uEdge == ABE_TOP) y = rcTask.top; else y = rcTask.bottom - taskBar->startButtonSize.cy; - // Start button on Win11 is a bit shifted to the right - // We will shift our Aero button to cover original button - if (IsWin11() && (x == info.rcMonitor.left) && (GetStartButtonType() == START_BUTTON_AERO) && !g_epTaskbar) + // Keep the legacy Win11 offset only when the actual XAML Start rectangle + // is unavailable. Applying both would double-correct the position. + if (!hasNativeStartRect && IsWin11() && (x == info.rcMonitor.left) && + (GetStartButtonType() == START_BUTTON_AERO) && !g_epTaskbar) + { x += ScaleForDpi(taskBar->taskBar, 6); + } if (GetStartButtonType() == START_BUTTON_CUSTOM) x += ScaleForDpi(taskBar->taskBar, GetSettingInt(L"StartButtonOffset")); @@ -1845,8 +1869,21 @@ static LRESULT CALLBACK SubclassTaskBarProc( HWND hWnd, UINT uMsg, WPARAM wParam pPos->hwndInsertAfter=taskBar->startButton; } } + if (uMsg==WM_OS_STARTBUTTON_RECT_READY && taskBar) + { + if (taskBar->bReplaceButton) + { + WINDOWPOS pos = {}; + pos.flags = SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE; + UpdateStartButtonPosition(taskBar, &pos); + } + return 0; + } if (uMsg==WM_WINDOWPOSCHANGED && taskBar) { + if (IsWin11()) + RefreshWin11StartButtonRects(); + if (taskBar->bReplaceButton) { UpdateStartButtonPosition(taskBar,(WINDOWPOS*)lParam); @@ -2643,6 +2680,7 @@ void UpdateTaskBars( TUpdateTaskbar update ) InvalidateRect(taskBar.taskBar,NULL,TRUE); PostMessage(taskBar.taskBar,WM_THEMECHANGED,0,0); } + UpdateWin11StartButtonMonitor(); } /////////////////////////////////////////////////////////////////////////////// @@ -3227,6 +3265,7 @@ if (!g_bTrimHooks) UpdateTaskBars(TASKBAR_RECREATE_BUTTONS); UpdateTaskBars(TASKBAR_UPDATE_TEXTURE); + StartWin11StartButtonMonitor(); } static void RecreateStartButton( size_t taskbarId ) @@ -3288,6 +3327,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..cfdbc5cb6 --- /dev/null +++ b/Src/StartMenu/StartMenuDLL/Win11StartButton.cpp @@ -0,0 +1,1186 @@ +// 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 +#include + +#pragma comment(lib, "uiautomationcore.lib") + +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 volatile LONG g_StartRectRefreshStarted = 0; +static SRWLOCK g_StartRectLock = SRWLOCK_INIT; +static std::unordered_map g_StartRects; + +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 iconOpacityOverride; + double originalOpacity; + bool hitTestOverride; + boolean originalHitTestVisible; + bool startControlResolved; + bool isStartControl; + unsigned int discoveryOrder; + + StartElement( void ) + { + parent = 0; + iconOpacityOverride = false; + originalOpacity = 1.0; + hitTestOverride = false; + originalHitTestVisible = true; + 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 IsStockStartIcon( const StartElement &element ) +{ + // Fail closed. Modern Windows 11 uses AnimatedVisualPlayer#Icon for the + // stock Start glyph. Do not hide arbitrary descendants named "Icon" because + // taskbar templates can change independently across Windows builds. + return element.name == L"Icon" && ContainsText(element.type, L"AnimatedVisualPlayer"); +} + +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_Diagnostics.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(IXamlDiagnostics), (void**)&m_Diagnostics); + if (FAILED(hr) || !m_Diagnostics) + { + m_Diagnostics.Release(); + m_Site.Release(); + BalanceInjectionReference(); + InterlockedExchange(&g_ConnectStarted, 0); + return hr; + } + + hr = site->QueryInterface(__uuidof(IVisualTreeService), (void**)&m_Visual); + if (FAILED(hr) || !m_Visual) + { + m_Visual.Release(); + m_Diagnostics.Release(); + m_Site.Release(); + BalanceInjectionReference(); + InterlockedExchange(&g_ConnectStarted, 0); + return hr; + } + + if (!CreateDispatchWindow()) + { + DWORD error = GetLastError(); + m_Visual.Release(); + m_Diagnostics.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_Diagnostics.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.iconOpacityOverride || 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.iconOpacityOverride = previous->second.iconOpacityOverride; + record.originalOpacity = previous->second.originalOpacity; + record.hitTestOverride = previous->second.hitTestOverride; + record.originalHitTestVisible = previous->second.originalHitTestVisible; + 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) || IsStockStartIcon(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_Diagnostics.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 ResolveUIElement( InstanceHandle handle, CComPtr &element ) + { + if (!m_Diagnostics) + return E_NOINTERFACE; + + CComPtr inspectable; + HRESULT hr = m_Diagnostics->GetIInspectableFromHandle(handle, &inspectable); + if (FAILED(hr) || !inspectable) + return FAILED(hr) ? hr : E_FAIL; + + return inspectable->QueryInterface(__uuidof(ABI::Windows::UI::Xaml::IUIElement), + (void**)&element); + } + + HRESULT SetStartControlHitTest( InstanceHandle handle, bool enabled ) + { + CComPtr element; + HRESULT hr = ResolveUIElement(handle, element); + if (FAILED(hr)) + return hr; + + if (enabled) + { + boolean original = true; + hr = element->get_IsHitTestVisible(&original); + if (FAILED(hr)) + return hr; + + hr = element->put_IsHitTestVisible(false); + if (SUCCEEDED(hr)) + { + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(handle); + if (it != m_Elements.end()) + { + it->second.originalHitTestVisible = original; + it->second.hitTestOverride = true; + } + LeaveCriticalSection(&m_Lock); + } + return hr; + } + + boolean original = true; + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(handle); + if (it != m_Elements.end()) + original = it->second.originalHitTestVisible; + LeaveCriticalSection(&m_Lock); + + hr = element->put_IsHitTestVisible(original); + if (SUCCEEDED(hr)) + { + EnterCriticalSection(&m_Lock); + it = m_Elements.find(handle); + if (it != m_Elements.end()) + it->second.hitTestOverride = false; + LeaveCriticalSection(&m_Lock); + } + return hr; + } + + HRESULT SetStockIconOpacity( InstanceHandle handle, bool hidden ) + { + CComPtr element; + HRESULT hr = ResolveUIElement(handle, element); + if (FAILED(hr)) + return hr; + + if (hidden) + { + double original = 1.0; + hr = element->get_Opacity(&original); + if (FAILED(hr)) + return hr; + + hr = element->put_Opacity(0.0); + if (SUCCEEDED(hr)) + { + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(handle); + if (it != m_Elements.end()) + { + it->second.originalOpacity = original; + it->second.iconOpacityOverride = true; + } + LeaveCriticalSection(&m_Lock); + } + return hr; + } + + double original = 1.0; + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(handle); + if (it != m_Elements.end()) + original = it->second.originalOpacity; + LeaveCriticalSection(&m_Lock); + + hr = element->put_Opacity(original); + if (SUCCEEDED(hr)) + { + EnterCriticalSection(&m_Lock); + it = m_Elements.find(handle); + if (it != m_Elements.end()) + it->second.iconOpacityOverride = false; + LeaveCriticalSection(&m_Lock); + } + return hr; + } + + 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) + SetStartControlHitTest(handle, true); + } + else if (record.hitTestOverride) + { + SetStartControlHitTest(handle, false); + } + continue; + } + + if (!IsStockStartIcon(record)) + continue; + + InstanceHandle startAncestor = GetStartAncestor(handle); + if (!startAncestor) + continue; + bool target = allTaskbars || !primaryStart || startAncestor == primaryStart; + + if (enabled && target) + { + if (!record.iconOpacityOverride) + SetStockIconOpacity(handle, true); + } + else if (record.iconOpacityOverride) + { + SetStockIconOpacity(handle, false); + } + } + } + + LONG m_Refs; + bool m_Advised; + HWND m_Dispatch; + CRITICAL_SECTION m_Lock; + CComPtr m_Site; + CComPtr m_Visual; + CComPtr m_Diagnostics; + 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 StartRectRefreshContext +{ + IUIAutomation *automation; + IUIAutomationCondition *condition; +}; + +static bool IsTaskbarWindowForCurrentProcess( HWND hwnd ) +{ + DWORD processId = 0; + if (!GetWindowThreadProcessId(hwnd, &processId) || processId != GetCurrentProcessId()) + return false; + + wchar_t className[64] = {}; + if (!GetClassName(hwnd, className, _countof(className))) + return false; + + return _wcsicmp(className, L"Shell_TrayWnd") == 0 || + _wcsicmp(className, L"Shell_SecondaryTrayWnd") == 0; +} + +static void CacheStartRect( HWND taskbar, const RECT &rect ) +{ + AcquireSRWLockExclusive(&g_StartRectLock); + g_StartRects[taskbar] = rect; + ReleaseSRWLockExclusive(&g_StartRectLock); + + PostMessage(taskbar, WM_OS_STARTBUTTON_RECT_READY, 0, 0); +} + +static void RemoveCachedStartRect( HWND taskbar ) +{ + AcquireSRWLockExclusive(&g_StartRectLock); + g_StartRects.erase(taskbar); + ReleaseSRWLockExclusive(&g_StartRectLock); +} + +static BOOL CALLBACK RefreshStartRectEnumProc( HWND hwnd, LPARAM lParam ) +{ + if (!IsTaskbarWindowForCurrentProcess(hwnd)) + return TRUE; + + StartRectRefreshContext *context = (StartRectRefreshContext*)lParam; + if (!context || !context->automation || !context->condition) + return TRUE; + + CComPtr root; + HRESULT hr = context->automation->ElementFromHandle(hwnd, &root); + if (FAILED(hr) || !root) + { + RemoveCachedStartRect(hwnd); + return TRUE; + } + + CComPtr start; + hr = root->FindFirst(TreeScope_Descendants, context->condition, &start); + if (FAILED(hr) || !start) + { + RemoveCachedStartRect(hwnd); + return TRUE; + } + + RECT rect = {}; + hr = start->get_CurrentBoundingRectangle(&rect); + if (FAILED(hr) || rect.right <= rect.left || rect.bottom <= rect.top) + { + RemoveCachedStartRect(hwnd); + return TRUE; + } + + RECT taskbarRect = {}; + RECT intersection = {}; + if (!GetWindowRect(hwnd, &taskbarRect) || + !IntersectRect(&intersection, &rect, &taskbarRect)) + { + RemoveCachedStartRect(hwnd); + return TRUE; + } + + if (InterlockedCompareExchange(&g_StartButtonActive, 0, 0)) + CacheStartRect(hwnd, rect); + return TRUE; +} + +static DWORD WINAPI StartRectRefreshThread( LPVOID param ) +{ + HMODULE moduleReference = (HMODULE)param; + HRESULT initHr = CoInitializeEx(NULL, COINIT_MULTITHREADED); + bool uninitialize = SUCCEEDED(initHr); + + if (SUCCEEDED(initHr) || initHr == RPC_E_CHANGED_MODE) + { + CComPtr automation; + HRESULT hr = CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&automation)); + if (SUCCEEDED(hr) && automation) + { + CComVariant automationId(L"StartButton"); + CComPtr condition; + hr = automation->CreatePropertyCondition(UIA_AutomationIdPropertyId, + automationId, &condition); + if (SUCCEEDED(hr) && condition && + InterlockedCompareExchange(&g_StartButtonActive, 0, 0)) + { + StartRectRefreshContext context = { automation, condition }; + EnumWindows(RefreshStartRectEnumProc, (LPARAM)&context); + } + } + } + + if (uninitialize) + CoUninitialize(); + + InterlockedExchange(&g_StartRectRefreshStarted, 0); + FreeLibraryAndExitThread(moduleReference, 0); + return 0; +} + +void RefreshWin11StartButtonRects( void ) +{ + if (!IsWin11() || !InterlockedCompareExchange(&g_StartButtonActive, 0, 0)) + return; + + if (InterlockedCompareExchange(&g_StartRectRefreshStarted, 1, 0) != 0) + return; + + HMODULE moduleReference = NULL; + if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (LPCTSTR)&StartRectRefreshThread, &moduleReference)) + { + InterlockedExchange(&g_StartRectRefreshStarted, 0); + return; + } + + HANDLE thread = CreateThread(NULL, 0, StartRectRefreshThread, moduleReference, 0, NULL); + if (thread) + { + CloseHandle(thread); + } + else + { + FreeLibrary(moduleReference); + InterlockedExchange(&g_StartRectRefreshStarted, 0); + } +} + +bool GetWin11StartButtonRect( HWND taskbar, RECT *rect ) +{ + if (!rect || !taskbar || !IsWin11()) + return false; + + bool found = false; + AcquireSRWLockShared(&g_StartRectLock); + auto it = g_StartRects.find(taskbar); + if (it != g_StartRects.end()) + { + *rect = it->second; + found = true; + } + ReleaseSRWLockShared(&g_StartRectLock); + + if (!found) + RefreshWin11StartButtonRects(); + return found; +} + +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); + RefreshWin11StartButtonRects(); + CWin11StartButtonTap *tap = GetTapRef(); + if (tap) + { + tap->RequestApply(false); + tap->Release(); + return; + } + EnsureConnection(); +} + +void UpdateWin11StartButtonMonitor( void ) +{ + if (!IsWin11()) + return; + + RefreshWin11StartButtonRects(); + 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); + + AcquireSRWLockExclusive(&g_StartRectLock); + g_StartRects.clear(); + ReleaseSRWLockExclusive(&g_StartRectLock); + + 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..e24bcc086 --- /dev/null +++ b/Src/StartMenu/StartMenuDLL/Win11StartButton.h @@ -0,0 +1,17 @@ +#pragma once + +// Windows 11 draws the native Start button in XAML. These helpers suppress the +// stock XAML icon and expose the real Start slot geometry to Open-Shell's +// replacement button. The geometry comes from UI Automation on a worker MTA, +// never from Explorer's UI thread. +#define WM_OS_STARTBUTTON_RECT_READY (WM_APP + 0x35D) + +void StartWin11StartButtonMonitor( void ); +void UpdateWin11StartButtonMonitor( void ); +void StopWin11StartButtonMonitor( void ); + +// Returns the most recently verified screen-space rectangle for the native +// Windows 11 Start control. A background refresh is requested automatically if +// no cached rectangle is available. +bool GetWin11StartButtonRect( HWND taskbar, RECT *rect ); +void RefreshWin11StartButtonRects( void );