Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions source/inputCore.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ def _get_displayName(self):
"""
return self.getDisplayTextForIdentifier(self.normalizedIdentifiers[0])[1]

def _get__nameForInputHelp(self) -> List[str]:
"""The name of this gesture as presented to the user in input help mode.

The base implementation returns a list containing self.displayName.
Subclasses can override this to provide more specific behavior,
such as including the character that would be typed.

:return: The list of names to be displayed in input help mode.
"""
return [self.displayName]

#: Whether this gesture should be reported when reporting of command gestures is enabled.
#: @type: bool
shouldReportAsCommand = True
Expand Down Expand Up @@ -646,7 +657,7 @@ def _inputHelpCaptor(self, gesture):
return bypass

def _handleInputHelp(self, gesture, onlyLog=False):
textList = [gesture.displayName]
helpItems = list(gesture._nameForInputHelp)
script = gesture.script
runScript = False
logMsg = "Input help: gesture %s" % gesture.identifiers[0]
Expand All @@ -661,22 +672,25 @@ def _handleInputHelp(self, gesture, onlyLog=False):
else:
desc = script.__doc__
if desc:
textList.append(desc)
helpItems.append(desc)

log.info(logMsg)
if onlyLog:
return

import braille

braille.handler.message("\t\t".join(textList))
# Punctuation must be spoken for the gesture name (the first chunk) so that punctuation keys are spoken.
speech.speakText(
textList[0],
reason=controlTypes.OutputReason.MESSAGE,
symbolLevel=characterProcessing.SymbolLevel.ALL,
)
for text in textList[1:]:
braille.handler.message("\t\t".join(helpItems))
# Punctuation must be spoken for the gesture names (the first chunk(s))
# so that punctuation keys are spoken.
nameCount = len(gesture._nameForInputHelp)
for i in range(nameCount):
speech.speakText(
helpItems[i],
reason=controlTypes.OutputReason.MESSAGE,
symbolLevel=characterProcessing.SymbolLevel.ALL,
)
for text in helpItems[nameCount:]:
speech.speakMessage(text)

if runScript:
Expand Down
120 changes: 119 additions & 1 deletion source/keyboardHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,122 @@ def _get_displayName(self):
for key in self._keyNamesInDisplayOrder
)

def _get_character(self) -> Optional[str]:
"""Get the character this key combination would produce.

Uses ToUnicodeEx with 0x4 flag to avoid modifying keyboard state.
For dead keys, returns the dead key character itself.
Returns None for unprintable characters or when Windows key is pressed.
"""
# Key state value indicating key is pressed (high bit set)
KEY_PRESSED_STATE: int = -128

try:
threadID = api.getFocusObject().windowThreadID
except AttributeError:
return None
keyboardLayout = ctypes.windll.user32.GetKeyboardLayout(threadID)
buffer = ctypes.create_unicode_buffer(5)
states = (ctypes.c_byte * 256)()

modifierList = []
for mod, _ in self.modifiers:
if mod not in self.NORMAL_MODIFIER_KEYS.values():
modifier = self.NORMAL_MODIFIER_KEYS.get(mod)
else:
modifier = mod
if modifier:
modifierList.append(modifier)

# Check for Windows key - characters with Windows key are invalid
if VK_WIN in [self.getVkName(m, e) for m, e in self.modifiers]:
return None

for i in range(256):
if i in modifierList:
states[i] = KEY_PRESSED_STATE
else:
states[i] = ctypes.windll.user32.GetKeyState(i)

# Call ToUnicodeEx with 0x04 flag (don't modify keyboard state)
res = ctypes.windll.user32.ToUnicodeEx(
self.vkCode,
self.scanCode,
states,
buffer,
ctypes.sizeof(buffer),
0x04,
keyboardLayout,
)

# res < 0 means dead key - return the dead key character
if res < 0:
# Dead key: buffer contains the dead key character
# Call ToUnicodeEx again to get and clear the dead key from buffer
ctypes.windll.user32.ToUnicodeEx(
self.vkCode,
self.scanCode,
states,
buffer,
ctypes.sizeof(buffer),
0x04,
keyboardLayout,
)
return buffer.value[:1] if buffer.value else None

if res == 0:
return None

# Check alt key behavior - alt sometimes gives same character as without alt
if winUser.VK_MENU in modifierList:
newBuffer = ctypes.create_unicode_buffer(5)
altStates = (ctypes.c_byte * 256)()
for i in range(256):
if i in modifierList and i != winUser.VK_MENU:
altStates[i] = KEY_PRESSED_STATE
else:
altStates[i] = ctypes.windll.user32.GetKeyState(i)
ctypes.windll.user32.ToUnicodeEx(
self.vkCode,
self.scanCode,
altStates,
newBuffer,
ctypes.sizeof(newBuffer),
0x04,
keyboardLayout,
)
# If same character with and without alt, it's not valid
if buffer.value == newBuffer.value:
return None

return buffer.value[:res]

def _get__nameForInputHelp(self) -> List[str]:
"""Returns the name of this gesture for input help mode.

For keyboard gestures that produce printable characters,
the character will be included in the list,
unless it contains NVDA modifier.
"""
displayName = self.displayName

# NVDA commands keep original behavior
if any(isNVDAModifierKey(mod, ext) for mod, ext in self.modifiers):
return [displayName]

char = self.character
if not char:
return [displayName]

if not char.isprintable():
return [displayName]

# Avoid duplicating if displayName matches character (case insensitive)
if displayName.lower() == char.lower():
return [displayName]

return [char, displayName]

def _get_identifiers(self):
keyName = "+".join(self._keyNamesInDisplayOrder)
return (
Expand Down Expand Up @@ -736,7 +852,9 @@ def fromName(cls, name):
if not keys:
raise ValueError

return cls(keys[:-1], vk, 0, ext)
hkl = getInputHkl()
scanCode = user32.MapVirtualKeyEx(vk, winUser.MAPVK_VK_TO_VSC, hkl)
return cls(keys[:-1], vk, scanCode, ext)

RE_IDENTIFIER = re.compile(r"^kb(?:\((.+?)\))?:(.*)$")

Expand Down
3 changes: 3 additions & 0 deletions source/winUser.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,11 @@ class NMHdrStruct(Structure):
# Clipboard formats
CF_TEXT = 1
# mapVirtualKey constants
MAPVK_VK_TO_VSC = 0
MAPVK_VSC_TO_VK = 1
MAPVK_VK_TO_CHAR = 2
MAPVK_VSC_TO_VK_EX = 3
MAPVK_VK_TO_VSC_EX = 4
# Virtual key codes
VK_LBUTTON = 1
VK_RBUTTON = 2
Expand Down
3 changes: 3 additions & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
* A new command, assigned to `NVDA+x`, has been introduced to repeat the last information spoken by NVDA; pressing it twice shows it in a browseable message. (#625, @CyrilleB79)
* Added an unassigned command to toggle keyboard layout. (#19211, @CyrilleB79)
* Added an unassigned Quick Navigation Command for jumping to next/previous slider in browse mode. (#17005, @hdzrvcc0X74)
* Input help mode has been improved: (#17629, @Cary-rowen, @Emil-18)
* When a key combination would produce a character in normal input mode, the character is reported first, followed by the key combination.
* If the key combination corresponds to an NVDA command, the behavior remains the same as before, i.e. the description of the command is reported.

### Changes

Expand Down
Loading