diff --git a/addon/globalPlugins/virtualRevision.py b/addon/globalPlugins/virtualRevision.py index c4ab909..45ffabb 100644 --- a/addon/globalPlugins/virtualRevision.py +++ b/addon/globalPlugins/virtualRevision.py @@ -18,6 +18,34 @@ except: SCRCAT_TEXTREVIEW = None +def _isTermControl(obj): + # True when obj is a Windows Terminal "TermControl" (the UIA element that hosts the text + # of cmd/PowerShell/WSL tabs and panes). Reading UIAElement.currentClassName can raise a + # COM error on some objects, and this runs against every object we inspect, so it must never + # let an exception escape and abort the review command. + try: + return bool(getattr(obj, "UIAElement", None)) and obj.UIAElement.currentClassName == "TermControl" + except Exception: + return False + +def _findTermControl(obj): + # Return the focused object, or its nearest ancestor, that is a TermControl, else None. + # NVDA normally puts focus directly on the TermControl, so this usually returns obj itself, + # but walking up a few ancestors covers hosts that focus a child of the control. Capturing + # straight from the focused control (rather than searching down from the foreground window) + # is what makes every tab and split pane work, regardless of the host window's class name. + node = obj + depth = 0 + while node is not None and depth < 20: + if _isTermControl(node): + return node + try: + node = node.parent + except Exception: + break + depth += 1 + return None + def obtainUWPWindowText(): foreground = api.getForegroundObject() desktop = api.getDesktopObject() @@ -25,7 +53,7 @@ def obtainUWPWindowText(): curObject=foreground.firstChild while curObject: if curObject.name is not None: uwpTextList.append(curObject.name) - if hasattr(curObject, "UIAElement") and curObject.UIAElement and curObject.UIAElement.currentClassName == "TermControl": + if _isTermControl(curObject): info = curObject.makeTextInfo(textInfos.POSITION_FIRST) info.expand(textInfos.UNIT_STORY) text = info.clipboardText.rstrip() @@ -49,7 +77,7 @@ def obtainUWPWindowText(): curObject=parent.simpleNext except AttributeError: continue - if hasattr(foreground, "UIAElement") and foreground.UIAElement and foreground.UIAElement.currentClassName == "TermControl": + if _isTermControl(foreground): info = foreground.makeTextInfo(textInfos.POSITION_FIRST) info.expand(textInfos.UNIT_STORY) text = info.clipboardText.rstrip() @@ -71,8 +99,19 @@ def script_virtualWindowReview(self, gesture): # In case of universal apps, traverse child elements. text = None obj = api.getFocusObject() + # Modern command-line windows (Windows Terminal, and any XAML/UWP terminal host) expose + # their text through a "TermControl" UIA element instead of the legacy console buffer. + # Detect it from the focused control and capture straight from there, so every tab and + # split pane works no matter what the host window's class name is. Checked first because + # the terminal control's HWND class often matches the UWP prefixes below, and that branch's + # generic child walk is slower and can miss the active pane. + term = _findTermControl(obj) + if term is not None: + info = term.makeTextInfo(textInfos.POSITION_FIRST) + info.expand(textInfos.UNIT_STORY) + text = info.clipboardText.rstrip() # Because it may take a while to iterate through elements, play abeep to alert users of this fact and the fact it's a UWP screen. - if obj.windowClassName.startswith(("Windows.UI.Core", "Windows.UI.Input.InputSite")): + elif obj.windowClassName.startswith(("Windows.UI.Core", "Windows.UI.Input.InputSite")): import tones tones.beep(400, 300) text = "\n".join(obtainUWPWindowText())