From c22509baa5972c9b546c79ed4896b9bb4076fc55 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 7 May 2021 08:45:09 +0200 Subject: [PATCH] Python Console: Jump to previous/next result (PR #9785) Fixes #9784 Summary of the issue: In the output pane of the Python Console, it can be tedious to inspect a series of lengthy output results. Description of how this pull request fixes the issue: Provide key bindings to jump to the previous/next result, select a whole result and clear the output pane. Co-authored-by: Reef Turner --- devDocs/developerGuide.t2t | 2 + source/appModules/nvda.py | 150 +++++++++++++++++++++++++++++++++++-- source/gui/__init__.py | 7 +- source/pythonConsole.py | 20 +++-- user_docs/en/changes.t2t | 3 + 5 files changed, 171 insertions(+), 11 deletions(-) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 45e20975111..2291fd243d1 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -824,6 +824,8 @@ You can navigate through the history of previously entered lines using the up an Output (responses from the interpreter) will be spoken when enter is pressed. The f6 key toggles between the input and output controls. +When on the output control, alt+up/down jumps to the previous/next result (add shift for selecting). +Pressing control+l clears the output. The result of the last executed command is stored in the "_" global variable. This shadows the gettext function which is stored as a built-in with the same name. diff --git a/source/appModules/nvda.py b/source/appModules/nvda.py index ba37154d879..4f0fd2a529b 100755 --- a/source/appModules/nvda.py +++ b/source/appModules/nvda.py @@ -1,20 +1,30 @@ -#appModules/nvda.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2008-2017 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2008-2021 NV Access Limited, James Teh, Michael Curran, Leonard de Ruijter, Reef Turner, +# Julien Cochuyt +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + + +import typing import appModuleHandler import api import controlTypes import versionInfo from NVDAObjects.IAccessible import IAccessible +from baseObject import ScriptableObject import gui +from scriptHandler import script import speech +import textInfos import braille import config from logHandler import log +if typing.TYPE_CHECKING: + import inputCore + + nvdaMenuIaIdentity = None class NvdaDialog(IAccessible): @@ -44,6 +54,118 @@ def _get_description(self): """ return "" + +# Translators: The name of a category of NVDA commands. +SCRCAT_PYTHON_CONSOLE = _("Python Console") + + +class NvdaPythonConsoleUIOutputClear(ScriptableObject): + + # Allow the bound gestures to be edited through the Input Gestures dialog (see L{gui.prePopup}) + isPrevFocusOnNvdaPopup = True + + @script( + gesture="kb:control+l", + # Translators: Description of a command to clear the Python Console output pane + description=_("Clear the output pane"), + category=SCRCAT_PYTHON_CONSOLE, + ) + def script_clearOutput(self, gesture: "inputCore.InputGesture"): + from pythonConsole import consoleUI + consoleUI.clear() + + +class NvdaPythonConsoleUIOutputCtrl(ScriptableObject): + + # Allow the bound gestures to be edited through the Input Gestures dialog (see L{gui.prePopup}) + isPrevFocusOnNvdaPopup = True + + @script( + gesture="kb:alt+downArrow", + # Translators: Description of a command to move to the next result in the Python Console output pane + description=_("Move to the next result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_moveToNextResult(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="next", select=False) + + @script( + gesture="kb:alt+upArrow", + # Translators: Description of a command to move to the previous result + # in the Python Console output pane + description=_("Move to the previous result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_moveToPrevResult(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="previous", select=False) + + @script( + gesture="kb:alt+downArrow+shift", + # Translators: Description of a command to select from the current caret position to the end + # of the current result in the Python Console output pane + description=_("Select until the end of the current result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_selectToResultEnd(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="next", select=True) + + @script( + gesture="kb:alt+shift+upArrow", + # Translators: Description of a command to select from the current caret position to the start + # of the current result in the Python Console output pane + description=_("Select until the start of the current result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_selectToResultStart(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="previous", select=True) + + def _resultNavHelper(self, direction: str = "next", select: bool = False): + from pythonConsole import consoleUI + startPos, endPos = consoleUI.outputCtrl.GetSelection() + if self.isTextSelectionAnchoredAtStart: + curPos = endPos + else: + curPos = startPos + if direction == "previous": + for pos in reversed(consoleUI.outputPositions): + if pos < curPos: + break + else: + # Translators: Reported when attempting to move to the previous result in the Python Console + # output pane while there is no previous result. + speech.speakMessage(_("Top")) + return + elif direction == "next": + for pos in consoleUI.outputPositions: + if pos > curPos: + break + else: + # Translators: Reported when attempting to move to the next result in the Python Console + # output pane while there is no next result. + speech.speakMessage(_("Bottom")) + return + else: + raise ValueError(u"Unexpected direction: {!r}".format(direction)) + if select: + consoleUI.outputCtrl.Freeze() + anchorPos = startPos if self.isTextSelectionAnchoredAtStart else endPos + consoleUI.outputCtrl.SetSelection(anchorPos, pos) + consoleUI.outputCtrl.Thaw() + self.detectPossibleSelectionChange() + self.isTextSelectionAnchoredAtStart = anchorPos < pos + else: + consoleUI.outputCtrl.SetSelection(pos, pos) + info = self.makeTextInfo(textInfos.POSITION_CARET) + copy = info.copy() + info.expand(textInfos.UNIT_LINE) + if ( + copy.move(textInfos.UNIT_CHARACTER, 4, endPoint="end") == 4 + and copy.text == ">>> " + ): + info.move(textInfos.UNIT_CHARACTER, 4, endPoint="start") + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET) + + class AppModule(appModuleHandler.AppModule): # The configuration profile that has been previously edited. # This ought to be a class property. @@ -108,8 +230,26 @@ def isNvdaSettingsDialog(self, obj): return True return False + def isNvdaPythonConsoleUIInputCtrl(self, obj): + from pythonConsole import consoleUI + if not consoleUI: + return + return obj.windowHandle == consoleUI.inputCtrl.GetHandle() + + def isNvdaPythonConsoleUIOutputCtrl(self, obj): + from pythonConsole import consoleUI + if not consoleUI: + return + return obj.windowHandle == consoleUI.outputCtrl.GetHandle() + def chooseNVDAObjectOverlayClasses(self, obj, clsList): if obj.windowClassName == "#32770" and obj.role == controlTypes.ROLE_DIALOG: clsList.insert(0, NvdaDialog) if self.isNvdaSettingsDialog(obj): clsList.insert(0, NvdaDialogEmptyDescription) + return + if self.isNvdaPythonConsoleUIInputCtrl(obj): + clsList.insert(0, NvdaPythonConsoleUIOutputClear) + elif self.isNvdaPythonConsoleUIOutputCtrl(obj): + clsList.insert(0, NvdaPythonConsoleUIOutputClear) + clsList.insert(0, NvdaPythonConsoleUIOutputCtrl) diff --git a/source/gui/__init__.py b/source/gui/__init__.py index a8d8fc928a7..9aaab0b7a87 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -84,7 +84,12 @@ def prePopup(self): """ nvdaPid = os.getpid() focus = api.getFocusObject() - if focus.processID != nvdaPid: + # Do not set prevFocus if the focus is on a control rendered by NVDA itself, such as the NVDA menu. + # This allows to refer to the control that had focus before opening the menu while still using NVDA + # on its own controls. The L{nvdaPid} check can be bypassed by setting the optional attribute + # L{isPrevFocusOnNvdaPopup} to L{True} when a NVDA dialog offers customizable bound gestures, + # eg. the NVDA Python Console. + if focus.processID != nvdaPid or getattr(focus, "isPrevFocusOnNvdaPopup", False): self.prevFocus = focus self.prevFocusAncestors = api.getFocusAncestors() if winUser.getWindowThreadProcessID(winUser.getForegroundWindow())[0] != nvdaPid: diff --git a/source/pythonConsole.py b/source/pythonConsole.py index 33cc9384d93..4dc8a7590ec 100755 --- a/source/pythonConsole.py +++ b/source/pythonConsole.py @@ -1,8 +1,8 @@ -#pythonConsole.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2008-2019 NV Access Limited, Leonard de Ruijter +# pythonConsole.py +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2008-2020 NV Access Limited, Leonard de Ruijter, Julien Cochuyt import watchdog @@ -12,6 +12,7 @@ import builtins import os +from typing import Sequence import code import codeop import sys @@ -253,6 +254,7 @@ def __init__(self, parent): # Even the most recent line has a position in the history, so initialise with one blank line. self.inputHistory = [""] self.inputHistoryPos = 0 + self.outputPositions: Sequence[int] = [0] def onActivate(self, evt): if evt.GetActive(): @@ -268,6 +270,12 @@ def output(self, data): if data and not data.isspace(): queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, data) + def clear(self): + """Clear the output. + """ + self.outputCtrl.Clear() + self.outputPositions[:] = [0] + def echo(self, data): self.outputCtrl.write(data) @@ -292,6 +300,8 @@ def execute(self): self.inputHistory.append("") self.inputHistoryPos = len(self.inputHistory) - 1 self.inputCtrl.ChangeValue("") + if self.console.prompt != "...": + self.outputPositions.append(self.outputCtrl.GetInsertionPoint()) def historyMove(self, movement): newIndex = self.inputHistoryPos + movement diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0c9fe36fbce..f1fca8e0e10 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -8,6 +8,9 @@ What's New in NVDA == New Features == - Early support for UIA with Chromium based browsers (such as Edge). (#12025) - Optional experimental support for Microsoft Excel via UI Automation. Only recommended for Microsoft Excel build 16.0.13522.10000 or higher. (#12210) +- Easier navigation of output in NVDA Python Console. (#9784) + - alt+up/down jumps to the previous/next output result (add shift for selecting). + - control+l clears the output pane. == Changes ==