From bbadde62e965ac787b478c0f3cf826296bd2111b Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Mon, 10 Aug 2026 15:07:30 -0700 Subject: [PATCH] [Conhost] Make screen readers announce search results --- .github/actions/spelling/expect/expect.txt | 2 + src/host/res.rc | 5 ++ src/host/resource.h | 2 + .../inc/IAccessibilityNotifier.hpp | 2 + .../onecore/AccessibilityNotifier.cpp | 4 ++ .../onecore/AccessibilityNotifier.hpp | 2 + .../win32/AccessibilityNotifier.cpp | 69 +++++++++++++++++++ .../win32/AccessibilityNotifier.hpp | 2 + src/interactivity/win32/find.cpp | 5 ++ src/interactivity/win32/resource.h | 2 + src/interactivity/win32/window.cpp | 9 +++ src/interactivity/win32/window.hpp | 1 + src/interactivity/win32/windowUiaProvider.cpp | 27 ++++++++ src/interactivity/win32/windowUiaProvider.hpp | 1 + 14 files changed, 133 insertions(+) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 3e5b3c70f4d..c670f90e523 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1044,6 +1044,8 @@ msft MSGCMDLINEF MSGF MSGFILTER +MSGFINDNORESULT +MSGFINDRESULT MSGFLG MSGMARKMODE MSGSCROLLMODE diff --git a/src/host/res.rc b/src/host/res.rc index d77ade8c883..b44c23f7198 100644 --- a/src/host/res.rc +++ b/src/host/res.rc @@ -61,6 +61,11 @@ BEGIN ID_CONSOLE_FMT_WINDOWTITLE, "%s%s" +/* Find dialog screen reader announcements. %1 is the 1-based index of the current + match, %2 is the total number of matches. */ + ID_CONSOLE_MSGFINDRESULT, "%1!d! of %2!d!" + ID_CONSOLE_MSGFINDNORESULT, "No results" + /* Menu items that replace the standard ones. These don't have the accelerators */ SC_CLOSE, "&Close" diff --git a/src/host/resource.h b/src/host/resource.h index 053c34704a2..57545e74de0 100644 --- a/src/host/resource.h +++ b/src/host/resource.h @@ -28,6 +28,8 @@ Author(s): #define ID_CONSOLE_MSGMARKMODE 0x100C #define ID_CONSOLE_MSGSCROLLMODE 0x100D #define ID_CONSOLE_FMT_WINDOWTITLE 0x100E +#define ID_CONSOLE_MSGFINDRESULT 0x100F +#define ID_CONSOLE_MSGFINDNORESULT 0x1010 // Menu Item strings #define ID_CONSOLE_COPY 0xFFF0 diff --git a/src/interactivity/inc/IAccessibilityNotifier.hpp b/src/interactivity/inc/IAccessibilityNotifier.hpp index 786893dd656..48f14ec6daa 100644 --- a/src/interactivity/inc/IAccessibilityNotifier.hpp +++ b/src/interactivity/inc/IAccessibilityNotifier.hpp @@ -37,5 +37,7 @@ namespace Microsoft::Console::Interactivity virtual void NotifyConsoleLayoutEvent() = 0; virtual void NotifyConsoleStartApplicationEvent(_In_ DWORD processId) = 0; virtual void NotifyConsoleEndApplicationEvent(_In_ DWORD processId) = 0; + + virtual void AnnounceSearchResults(_In_ ptrdiff_t index, _In_ size_t count) = 0; }; } diff --git a/src/interactivity/onecore/AccessibilityNotifier.cpp b/src/interactivity/onecore/AccessibilityNotifier.cpp index 743bd03e309..9eefb91b152 100644 --- a/src/interactivity/onecore/AccessibilityNotifier.cpp +++ b/src/interactivity/onecore/AccessibilityNotifier.cpp @@ -38,3 +38,7 @@ void AccessibilityNotifier::NotifyConsoleStartApplicationEvent(_In_ DWORD /*proc void AccessibilityNotifier::NotifyConsoleEndApplicationEvent(_In_ DWORD /*processId*/) noexcept { } + +void AccessibilityNotifier::AnnounceSearchResults(_In_ ptrdiff_t /*index*/, _In_ size_t /*count*/) noexcept +{ +} diff --git a/src/interactivity/onecore/AccessibilityNotifier.hpp b/src/interactivity/onecore/AccessibilityNotifier.hpp index 90c044427f2..e9b916161f5 100644 --- a/src/interactivity/onecore/AccessibilityNotifier.hpp +++ b/src/interactivity/onecore/AccessibilityNotifier.hpp @@ -31,5 +31,7 @@ namespace Microsoft::Console::Interactivity::OneCore void NotifyConsoleLayoutEvent() noexcept override; void NotifyConsoleStartApplicationEvent(_In_ DWORD processId) noexcept override; void NotifyConsoleEndApplicationEvent(_In_ DWORD processId) noexcept override; + + void AnnounceSearchResults(_In_ ptrdiff_t index, _In_ size_t count) noexcept override; }; } diff --git a/src/interactivity/win32/AccessibilityNotifier.cpp b/src/interactivity/win32/AccessibilityNotifier.cpp index 015627b953b..b78b7df40f2 100644 --- a/src/interactivity/win32/AccessibilityNotifier.cpp +++ b/src/interactivity/win32/AccessibilityNotifier.cpp @@ -7,6 +7,8 @@ #include "../inc/ServiceLocator.hpp" #include "ConsoleControl.hpp" +#include "resource.h" +#include "window.hpp" using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Interactivity::Win32; @@ -133,3 +135,70 @@ void AccessibilityNotifier::NotifyConsoleEndApplicationEvent(_In_ DWORD processI 0); } } + +// Routine Description: +// - Loads a string resource and returns it. Returns an empty string on failure +static std::wstring _loadString(const UINT id) +{ + WCHAR buffer[70]; + const auto length = LoadStringW(Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().hInstance, id, buffer, ARRAYSIZE(buffer)); + return { &buffer[0], gsl::narrow_cast(std::max(length, 0)) }; +} + +// Routine Description: +// - Announces the state of the Find dialog's search results to screen readers +// Arguments: +// - index: the 0-based index of the current match +// - count: the total number of matches +void AccessibilityNotifier::AnnounceSearchResults(_In_ const ptrdiff_t index, _In_ const size_t count) +try +{ + const auto pWindow = ServiceLocator::LocateConsoleWindow(); + if (!pWindow) + { + return; + } + + std::wstring announcement; + + if (count == 0) + { + // No results found + announcement = _loadString(ID_CONSOLE_MSGFINDNORESULT); + } + else + { + // Results found. Announce as the 1-based index of the total ("2 of 5") + const auto format = _loadString(ID_CONSOLE_MSGFINDRESULT); + const auto position = std::clamp(gsl::narrow_cast(std::max(index, 0)) + 1, 1, count); + + // The resource uses positional inserts (%1, %2) so that translations can reorder them. + const DWORD_PTR args[]{ + gsl::narrow_cast(position), + gsl::narrow_cast(count), + }; + wil::unique_hlocal_string formatted; + const auto length = FormatMessageW( + FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_ARGUMENT_ARRAY, + format.c_str(), + 0, + 0, + reinterpret_cast(formatted.addressof()), + 0, + reinterpret_cast(const_cast(&args[0]))); + if (length == 0) + { + LOG_LAST_ERROR(); + return; + } + announcement.assign(formatted.get(), length); + } + + if (announcement.empty()) + { + return; + } + + LOG_IF_FAILED(pWindow->SignalUiaAnnouncement(announcement)); +} +CATCH_LOG() diff --git a/src/interactivity/win32/AccessibilityNotifier.hpp b/src/interactivity/win32/AccessibilityNotifier.hpp index 115633c511e..f5648444b33 100644 --- a/src/interactivity/win32/AccessibilityNotifier.hpp +++ b/src/interactivity/win32/AccessibilityNotifier.hpp @@ -35,5 +35,7 @@ namespace Microsoft::Console::Interactivity::Win32 void NotifyConsoleLayoutEvent(); void NotifyConsoleStartApplicationEvent(_In_ DWORD processId); void NotifyConsoleEndApplicationEvent(_In_ DWORD processId); + + void AnnounceSearchResults(_In_ ptrdiff_t index, _In_ size_t count); }; } diff --git a/src/interactivity/win32/find.cpp b/src/interactivity/win32/find.cpp index 07996c47767..2cf881d0947 100644 --- a/src/interactivity/win32/find.cpp +++ b/src/interactivity/win32/find.cpp @@ -63,6 +63,11 @@ INT_PTR CALLBACK FindDialogProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM l searcher.FindNext(reverse); } + if (const auto notifier = ServiceLocator::LocateAccessibilityNotifier()) + { + notifier->AnnounceSearchResults(searcher.CurrentMatch(), searcher.Results().size()); + } + if (searcher.SelectCurrent()) { return TRUE; diff --git a/src/interactivity/win32/resource.h b/src/interactivity/win32/resource.h index bb871e2b52f..5307155a0d5 100644 --- a/src/interactivity/win32/resource.h +++ b/src/interactivity/win32/resource.h @@ -25,6 +25,8 @@ Author(s): #define ID_CONSOLE_MSGMARKMODE 0x100C #define ID_CONSOLE_MSGSCROLLMODE 0x100D #define ID_CONSOLE_FMT_WINDOWTITLE 0x100E +#define ID_CONSOLE_MSGFINDRESULT 0x100F +#define ID_CONSOLE_MSGFINDNORESULT 0x1010 // Menu Item strings #define ID_CONSOLE_COPY 0xFFF0 diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index 3fa53f2f974..0e2307c8a02 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -1370,6 +1370,15 @@ IRawElementProviderSimple* Window::_GetUiaProvider() return S_FALSE; } +[[nodiscard]] HRESULT Window::SignalUiaAnnouncement(const std::wstring_view text) +{ + if (_pUiaProvider != nullptr) + { + return _pUiaProvider->SignalAnnouncement(text); + } + return S_FALSE; +} + [[nodiscard]] HRESULT Window::UiaSetTextAreaFocus() { if (_pUiaProvider != nullptr) diff --git a/src/interactivity/win32/window.hpp b/src/interactivity/win32/window.hpp index 2b36d960da5..7b54c8bc5d3 100644 --- a/src/interactivity/win32/window.hpp +++ b/src/interactivity/win32/window.hpp @@ -83,6 +83,7 @@ namespace Microsoft::Console::Interactivity::Win32 BOOL PostUpdateExtendedEditKeys() const; [[nodiscard]] HRESULT SignalUia(_In_ EVENTID id); + [[nodiscard]] HRESULT SignalUiaAnnouncement(const std::wstring_view text); void SetOwner(); BOOL GetCursorPosition(_Out_ til::point* lpPoint); diff --git a/src/interactivity/win32/windowUiaProvider.cpp b/src/interactivity/win32/windowUiaProvider.cpp index 664101fc561..8d48de41bd0 100644 --- a/src/interactivity/win32/windowUiaProvider.cpp +++ b/src/interactivity/win32/windowUiaProvider.cpp @@ -78,6 +78,33 @@ CATCH_RETURN(); CATCH_RETURN(); } +// Routine Description: +// - Raises a UIA notification event so that screen readers speak the given text +// Arguments: +// - text: the text to be announced +// Return Value: +// - S_OK, or a suitable HRESULT on failure +[[nodiscard]] HRESULT WindowUiaProvider::SignalAnnouncement(const std::wstring_view text) noexcept +try +{ + // Raise the announcement on the text area because + // that's the element screen readers are actually reading from + RETURN_HR_IF_NULL(E_POINTER, _pScreenInfoProvider.Get()); + + const wil::unique_bstr announcement{ SysAllocStringLen(text.data(), gsl::narrow(text.size())) }; + RETURN_IF_NULL_ALLOC(announcement); + + static const auto activityId = wil::make_bstr_nothrow(L"ConhostSearchResultAnnouncement"); + RETURN_IF_NULL_ALLOC(activityId); + + return UiaRaiseNotificationEvent(_pScreenInfoProvider.Get(), + NotificationKind_ActionCompleted, + NotificationProcessing_ImportantMostRecent, + announcement.get(), + activityId.get()); +} +CATCH_RETURN(); + #pragma region IRawElementProviderSimple // Implementation of IRawElementProviderSimple::get_ProviderOptions. diff --git a/src/interactivity/win32/windowUiaProvider.hpp b/src/interactivity/win32/windowUiaProvider.hpp index 97a35aa5535..f1b1c66a89d 100644 --- a/src/interactivity/win32/windowUiaProvider.hpp +++ b/src/interactivity/win32/windowUiaProvider.hpp @@ -49,6 +49,7 @@ namespace Microsoft::Console::Interactivity::Win32 public: [[nodiscard]] virtual HRESULT Signal(_In_ EVENTID id); [[nodiscard]] virtual HRESULT SetTextAreaFocus(); + [[nodiscard]] HRESULT SignalAnnouncement(const std::wstring_view text) noexcept; // IRawElementProviderSimple methods IFACEMETHODIMP get_ProviderOptions(_Out_ ProviderOptions* pOptions) override;