diff --git a/source/inputCore.py b/source/inputCore.py index 54a638c739..1a6508b35a 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -150,6 +150,15 @@ def _get_displayName(self): """ return self.getDisplayTextForIdentifier(self.normalizedIdentifiers[0])[1] + def _get__nameForInputHelp(self): + """The name of this gesture as presented to the user in input help mode. + The base implementation returns self.displayName. + Subclasses can override this to provide a more specific implementation. + @return: The name to be displayed in input help mode. + @rtype: str + """ + return self.displayName + #: Whether this gesture should be reported when reporting of command gestures is enabled. #: @type: bool shouldReportAsCommand = True @@ -643,7 +652,7 @@ def _inputHelpCaptor(self, gesture): return bypass def _handleInputHelp(self, gesture, onlyLog=False): - textList = [gesture.displayName] + textList = [gesture._nameForInputHelp] script = gesture.script runScript = False logMsg = "Input help: gesture %s" % gesture.identifiers[0] diff --git a/source/keyboardHandler.py b/source/keyboardHandler.py index d06ddcf08c..4165cf1f31 100644 --- a/source/keyboardHandler.py +++ b/source/keyboardHandler.py @@ -494,6 +494,66 @@ def __init__(self, modifiers, vkCode, scanCode, isExtended): self.isExtended = isExtended super(KeyboardInputGesture, self).__init__() + def _get_character(self): + threadID = api.getFocusObject().windowThreadID + keyboardLayout = ctypes.windll.user32.GetKeyboardLayout(threadID) + buffer = ctypes.create_unicode_buffer(5) + states = (ctypes.c_byte * 256)() + modifierList = [] + valid = True + for i in self.modifiers: + if i[0] not in self.NORMAL_MODIFIER_KEYS.values(): + modifier = self.NORMAL_MODIFIER_KEYS.get(i[0]) + else: + modifier = i[0] + if modifier: + modifierList.append(modifier) + for i in range(256): + if i in modifierList: + states[ + i + ] = ( + -128 + ) # We tell ToUnicodeEx that the modifier is down, even tho it isn't according to Windows + else: + states[i] = ctypes.windll.user32.GetKeyState(i) + res = ctypes.windll.user32.ToUnicodeEx( + self.vkCode, + self.scanCode, + states, + buffer, + ctypes.sizeof(buffer), + 0x04, + keyboardLayout, + ) + if res < 0: + return "" + charList = [] + if res > 0: + if winUser.VK_MENU in modifierList: + # When alt is the only modifier that is down, + # ToUnicodeEx still writes the character to the provided buffer as tho alt wasn't down + # So remove alt and try again + newBuffer = ctypes.create_unicode_buffer(5) + states[winUser.VK_MENU] = 0 + ctypes.windll.user32.ToUnicodeEx( + self.vkCode, + self.scanCode, + states, + newBuffer, + ctypes.sizeof(newBuffer), + 0x04, + keyboardLayout, + ) + # Check if newBuffer.value == buffer.value. + # If they do, treat the key as invalid as it wouldn't have bin written to the focused window + valid = False if buffer.value == newBuffer.value else True + for i in buffer[:res]: + charList.append(i) + if not valid or "windows" in modifierList: + charList.clear() + return "".join(charList) + def _get_bypassInputHelp(self): # #4226: Numlock must always be handled normally otherwise the Keyboard controller and Windows can get out of synk wih each other in regard to this key state. return self.vkCode == winUser.VK_NUMLOCK @@ -553,6 +613,30 @@ def _get_displayName(self): for key in self._keyNamesInDisplayOrder ) + def _get__nameForInputHelp(self): + """Returns the name of this gesture for input help mode. + For keyboard gestures that produce printable characters, + the character will be prepended to the display name, + unless it contains NVDA modifier. + @return: The name to be displayed in input help mode + @rtype: str + """ + displayName = super()._get__nameForInputHelp() + # NVDA commands keep original behavior + if any(isNVDAModifierKey(mod[0], mod[1]) for mod in self.modifiers): + return displayName + # Get character if available + if not self.character: + return displayName + char = "".join(self.character) + if not char.isprintable(): + return displayName + # Avoid duplicating if displayName is the same as character (case insensitive) + if displayName.lower() == char.lower(): + return displayName + # Add character before display name + return f"{char} {displayName}" + def _get_identifiers(self): keyName = "+".join(self._keyNamesInDisplayOrder) return ( diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index fe7c3b241d..6c2ab11d2d 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -59,6 +59,9 @@ Prefix matching on command line flags, e.g. using `--di` for `--disable-addons` * Microsoft Speech API version 5 and Microsoft Speech Platform voices now use WASAPI for audio output, which may improve the responsiveness of those voices. (#13284, @gexgd0419) * The keyboard settings for "Speak typed characters" and "Speak typed words" now have three options: Off, Only in edit controls, and Always. (#17505, @Cary-rowen) * By default, "Speak typed characters" is now set to "Only in edit controls". +*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. ### Bug Fixes