Repository: nvaccess/nvda

Issue:
UIA console: when typing in the console, characters are wrongly pronounced regardless of the keyboard layout
<!--
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples
-->

### Steps to reproduce:
1. open a console window with the uia console option turned on, for example a wsl console
. switch to croatian or to the US keyboard language. I am not aware of other layouts having the problem which i will describe here thoroughly
russian keyboard is also included there
2. try to type  or ], ; or ', \ or /
### Actual behavior:
note: here are given the corresponding keys on the us keyboard. these keys are wrongly interpreted by NVDA also on oother non-us keyboard layout
for example / slash on US keyboard is interpreted as apostrophe,
; as č, the croatian diacritic mark, and so on.
### Expected behavior:
the signs should be interpreded bz NVDA as these are written on the kezboard respectivelz
### System configuration
#### NVDA installed/portable/running from source:
NVDA installed
#### NVDA version:
Latest alpha
#### Windows version:
18362 latest indows 10 1903
#### Name and version of other software in use when reproducing the issue:
n-a
#### Other information about your system:
to reproduce this bug, you should have turned on have character echo turned on
### Other questions
#### Does the issue still occur after restarting your PC?
yes
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
n/a


Discussion:
cc @codeofdusk 
> cc @codeofdusk

This is probably a bug with Windows keyboard hooks. Look at the block starting at around line 196 of the `keyboardhandler` module and the [Win32 ToUnicodeEx function](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-tounicodeex).

Resolving patch:
diff --git a/source/IAccessibleHandler.py b/source/IAccessibleHandler.py
--- a/source/IAccessibleHandler.py
+++ b/source/IAccessibleHandler.py
@@ -4,6 +4,7 @@
 #This file is covered by the GNU General Public License.
 #See the file COPYING for more details.
 
+from typing import Dict
 import heapq
 import itertools
 import struct
@@ -575,6 +576,10 @@ def winEventCallback(handle,eventID,window,objectID,childID,threadID,timestamp):
 				window=tempWindow
 
 		windowClassName=winUser.getClassName(window)
+		if windowClassName == "ConsoleWindowClass":
+			# #10113: we need to use winEvents to track the real thread for console windows.
+			consoleWindowsToThreadIDs[window] = threadID
+
 		# Modern IME candidate list windows fire menu events which confuse us
 		# and can't be used properly in conjunction with input composition support.
 		if windowClassName=="Microsoft.IME.UIManager.CandidateWindow.Host" and eventID in MENU_EVENTIDS:
@@ -1102,3 +1107,13 @@ def isMarshalledIAccessible(IAccessibleObject):
 	windll.kernel32.GetModuleHandleExW(6,addr,byref(handle))
 	windll.kernel32.GetModuleFileNameW(handle,buf,1024)
 	return not buf.value.lower().endswith('oleacc.dll')
+
+
+#: Maps from console windows (ConsoleWindowClass) to thread IDs
+# Windows hacks GetWindowThreadProcessId to return the input thread of the first attached process in a console
+# But NVDA really requires to know the actual thread the window was created in,
+# I.e. inside conhost,
+# In order to handle speaking of typed characters etc.
+# winEventCallback adds these whenever it sees an event for ConsoleWindowClass windows,
+# As winEvents always contain the true thread ID.
+consoleWindowsToThreadIDs: Dict[int, int] = {}
diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py
--- a/source/NVDAObjects/UIA/winConsoleUIA.py
+++ b/source/NVDAObjects/UIA/winConsoleUIA.py
@@ -254,6 +254,17 @@ class WinConsoleUIA(KeyboardHandlerBasedTypedCharSupport):
 	#: the caret in consoles can take a while to move on Windows 10 1903 and later.
 	_caretMovementTimeoutMultiplier = 1.5
 
+	def _get_windowThreadID(self):
+		# #10113: Windows forces the thread of console windows to match the thread of the first attached process.
+		# However, To correctly handle speaking of typed characters,
+		# NVDA really requires the real thread the window was created in,
+		# I.e. a thread inside conhost.
+		from IAccessibleHandler import consoleWindowsToThreadIDs
+		threadID = consoleWindowsToThreadIDs.get(self.windowHandle, 0)
+		if not threadID:
+			threadID = super().windowThreadID
+		return threadID
+
 	def _get_TextInfo(self):
 		"""Overriding _get_TextInfo and thus the TextInfo property
 		on NVDAObjects.UIA.UIA
diff --git a/source/NVDAObjects/window/winConsole.py b/source/NVDAObjects/window/winConsole.py
--- a/source/NVDAObjects/window/winConsole.py
+++ b/source/NVDAObjects/window/winConsole.py
@@ -26,6 +26,17 @@ def initOverlayClass(self):
 		if isinstance(self, KeyboardHandlerBasedTypedCharSupport):
 			self._supportsTextChange = False
 
+	def _get_windowThreadID(self):
+		# #10113: Windows forces the thread of console windows to match the thread of the first attached process.
+		# However, To correctly handle speaking of typed characters,
+		# NVDA really requires the real thread the window was created in,
+		# I.e. a thread inside conhost.
+		from IAccessibleHandler import consoleWindowsToThreadIDs
+		threadID = consoleWindowsToThreadIDs.get(self.windowHandle, 0)
+		if not threadID:
+			threadID = super().windowThreadID
+		return threadID
+
 	def _get_TextInfo(self):
 		consoleObject=winConsoleHandler.consoleObject
 		if consoleObject and self.windowHandle == consoleObject.windowHandle:


Test patch: