Skip to content

Commit

Permalink
Python Console: Jump to previous/next result (PR #9785)
Browse files Browse the repository at this point in the history
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 <reef@nvaccess.org>
  • Loading branch information
JulienCochuyt and feerrenrut committed May 7, 2021
1 parent 9dc1f36 commit c22509b
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 11 deletions.
2 changes: 2 additions & 0 deletions devDocs/developerGuide.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
150 changes: 145 additions & 5 deletions source/appModules/nvda.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
7 changes: 6 additions & 1 deletion source/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 15 additions & 5 deletions source/pythonConsole.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -12,6 +12,7 @@

import builtins
import os
from typing import Sequence
import code
import codeop
import sys
Expand Down Expand Up @@ -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():
Expand All @@ -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)

Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions user_docs/en/changes.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==
Expand Down

0 comments on commit c22509b

Please sign in to comment.