Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve keyboard support for some terminal programs #9915

Merged
merged 27 commits into from
Aug 1, 2019
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f7e7e68
Factor out the UIA keyboard support into a mixin and add it to PuTTY/…
codeofdusk Jul 10, 2019
db72da0
Enable TerminalKeyboardSupport for legacy consoles on Windows 10 1703…
codeofdusk Jul 11, 2019
ba8488c
Make TerminalKeyboardSupport configurable for legacy consoles.
codeofdusk Jul 11, 2019
187c452
Make the TerminalKeyboardSupport mixin inherit from object, so that T…
codeofdusk Jul 13, 2019
4698f94
TerminalKeyboardSupport -> TerminalWithKeyboardSupport.
codeofdusk Jul 14, 2019
0aac443
Fix ambiguous imports.
codeofdusk Jul 14, 2019
530460f
Enable new keyboard handling and TerminalWithKeyboardSupport on Windo…
codeofdusk Jul 14, 2019
a94d384
Clarify GUI option text.
codeofdusk Jul 14, 2019
6dbe58e
Merge branch 'master' into cmduia7-keyboard-refactor
codeofdusk Jul 16, 2019
98b6931
Introduce changes from #9936.
codeofdusk Jul 16, 2019
f59dd6a
TerminalWithKeyboardSupport -> TerminalWithoutTypedCharDetection.
codeofdusk Jul 16, 2019
4bc5051
Update user guide.
codeofdusk Jul 16, 2019
ef9d4cc
Revert "TerminalWithKeyboardSupport -> TerminalWithoutTypedCharDetect…
codeofdusk Jul 27, 2019
948fd5d
Review actions.
codeofdusk Jul 27, 2019
c9a134c
Only enable the KeyboardSupportInLegacyCheckBox on Windows 10 1607 an…
codeofdusk Jul 27, 2019
eb74adc
Merge branch 'master' into cmduia7-keyboard-refactor
codeofdusk Jul 27, 2019
89e853d
Fix submodules.
codeofdusk Jul 27, 2019
bb0cac9
Merge branch 'master' into cmduia7-keyboard-refactor
codeofdusk Jul 30, 2019
9be007e
Review action.
codeofdusk Jul 30, 2019
7886917
Review actions.
codeofdusk Jul 30, 2019
bd00ae2
Keyboard support -> typed character support.
codeofdusk Jul 30, 2019
5fd3874
Change accelerator.
codeofdusk Jul 30, 2019
f30db3a
Meeting actions.
codeofdusk Jul 31, 2019
71e06d9
Add newline at end of file.
codeofdusk Jul 31, 2019
73ce39f
Merge branch 'master' into cmduia7-keyboard-refactor
codeofdusk Jul 31, 2019
d52fae0
Update source/NVDAObjects/behaviors.py
codeofdusk Jul 31, 2019
d6c8a26
Review actions.
codeofdusk Jul 31, 2019
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
7 changes: 6 additions & 1 deletion source/NVDAObjects/IAccessible/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,12 @@ def findOverlayClasses(self,clsList):
elif windowClassName.startswith("Chrome_"):
from . import chromium
chromium.findExtraOverlayClasses(self, clsList)
if (
windowClassName == "ConsoleWindowClass"
and role == oleacc.ROLE_SYSTEM_CLIENT
):
from . import winConsole
winConsole.findExtraOverlayClasses(self,clsList)


#Support for Windowless richEdit
Expand Down Expand Up @@ -2012,5 +2018,4 @@ def event_alert(self):
("NUIDialog",oleacc.ROLE_SYSTEM_CLIENT):"NUIDialogClient",
("_WwB",oleacc.ROLE_SYSTEM_CLIENT):"winword.ProtectedDocumentPane",
("MsoCommandBar",oleacc.ROLE_SYSTEM_LISTITEM):"msOffice.CommandBarListItem",
("ConsoleWindowClass",oleacc.ROLE_SYSTEM_CLIENT):"winConsole.WinConsole",
}
16 changes: 13 additions & 3 deletions source/NVDAObjects/IAccessible/winConsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
#See the file COPYING for more details.
#Copyright (C) 2007-2019 NV Access Limited, Bill Dengler

import config

from winVersion import isWin10

from . import IAccessible
from ..window.winConsole import WinConsole
from ..window.winConsole import WinConsole as LegacyWinConsole
feerrenrut marked this conversation as resolved.
Show resolved Hide resolved

class WinConsole(WinConsole, IAccessible):
class WinConsole(LegacyWinConsole, IAccessible):
"The legacy console implementation for situations where UIA isn't supported."
pass
pass

def findExtraOverlayClasses(obj, clsList):
if isWin10(1607) and config.conf['terminals']['keyboardSupportInLegacy']:
from NVDAObjects.behaviors import TerminalWithoutTypedCharDetection
clsList.append(TerminalWithoutTypedCharDetection)
clsList.append(WinConsole)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be a bit ambiguous. is this NVDAObjects.IAccessible.winConsole.WinConsole or NVDAObjects.window.winConsole.WinConsole. I'm sure it will be the former, but a linter might complain that you're importing WinConsole and then override it. Please change the import above to something like:
``from ..window.winConsole import WinConsole as WinConsoleWindow`

feerrenrut marked this conversation as resolved.
Show resolved Hide resolved
78 changes: 2 additions & 76 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@
# See the file COPYING for more details.
# Copyright (C) 2019 Bill Dengler

import config
import ctypes
import NVDAHelper
import speech
import time
import textInfos
import UIAHandler

from scriptHandler import script
from winVersion import isWin10
from . import UIATextInfo
from ..behaviors import Terminal
from ..behaviors import TerminalWithoutTypedCharDetection
from ..window import Window


Expand Down Expand Up @@ -223,92 +219,22 @@ def _get_focusRedirect(self):
return None


class WinConsoleUIA(Terminal):
class WinConsoleUIA(TerminalWithoutTypedCharDetection):
#: Disable the name as it won't be localized
name = ""
#: Only process text changes every 30 ms, in case the console is getting
#: a lot of text.
STABILIZE_DELAY = 0.03
_TextInfo = consoleUIATextInfo
#: A queue of typed characters, to be dispatched on C{textChange}.
#: This queue allows NVDA to suppress typed passwords when needed.
_queuedChars = []
#: Whether the console got new text lines in its last update.
#: Used to determine if typed character/word buffers should be flushed.
_hasNewLines = False
#: the caret in consoles can take a while to move on Windows 10 1903 and later.
_caretMovementTimeoutMultiplier = 1.5

def _reportNewText(self, line):
# Additional typed character filtering beyond that in LiveText
if len(line.strip()) < max(len(speech.curWordChars) + 1, 3):
return
if self._hasNewLines:
# Clear the typed word buffer for new text lines.
# This will need to be changed once #8110 is merged.
speech.curWordChars = []
self._queuedChars = []
super(WinConsoleUIA, self)._reportNewText(line)

def event_typedCharacter(self, ch):
if ch == '\t':
# Clear the typed word buffer for tab completion.
# This will need to be changed once #8110 is merged.
speech.curWordChars = []
if (
(
config.conf['keyboard']['speakTypedCharacters']
or config.conf['keyboard']['speakTypedWords']
)
and not config.conf['UIA']['winConsoleSpeakPasswords']
):
self._queuedChars.append(ch)
else:
super(WinConsoleUIA, self).event_typedCharacter(ch)

def event_textChange(self):
while self._queuedChars:
ch = self._queuedChars.pop(0)
super(WinConsoleUIA, self).event_typedCharacter(ch)
super(WinConsoleUIA, self).event_textChange()

@script(gestures=[
"kb:enter",
"kb:numpadEnter",
"kb:tab",
"kb:control+c",
"kb:control+d",
"kb:control+pause"
])
def script_flush_queuedChars(self, gesture):
"""
Flushes the typed word buffer and queue of typedCharacter events if present.
Since these gestures clear the current word/line, we should flush the
queue to avoid erroneously reporting these chars.
"""
gesture.send()
self._queuedChars = []
speech.curWordChars = []

def _getTextLines(self):
# Filter out extraneous empty lines from UIA
ptr = self.UIATextPattern.GetVisibleRanges()
res = [ptr.GetElement(i).GetText(-1) for i in range(ptr.length)]
return res

def _calculateNewText(self, newLines, oldLines):
self._hasNewLines = (
self._findNonBlankIndices(newLines)
!= self._findNonBlankIndices(oldLines)
)
return super(WinConsoleUIA, self)._calculateNewText(newLines, oldLines)

def _findNonBlankIndices(self, lines):
"""
Given a list of strings, returns a list of indices where the strings
are not empty.
"""
return [index for index, line in enumerate(lines) if line]

def findExtraOverlayClasses(obj, clsList):
if obj.UIAElement.cachedAutomationId == "Text Area":
Expand Down
95 changes: 95 additions & 0 deletions source/NVDAObjects/behaviors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import textInfos
import editableText
from logHandler import log
from scriptHandler import script
import api
import ui
import braille
Expand Down Expand Up @@ -365,6 +366,100 @@ def event_gainFocus(self):
def event_loseFocus(self):
self.stopMonitoring()


class TerminalWithoutTypedCharDetection(Terminal):
codeofdusk marked this conversation as resolved.
Show resolved Hide resolved
"""A Terminal object that also provides typed character support for
codeofdusk marked this conversation as resolved.
Show resolved Hide resolved
console applications on Windows 10 1607 and later."""
#: Whether this object reliably sends textChange events.
_supportsTextChange = True
codeofdusk marked this conversation as resolved.
Show resolved Hide resolved
#: A queue of typed characters, to be dispatched on C{textChange}.
#: This queue allows NVDA to suppress typed passwords when needed.
_queuedChars = []
#: Whether the console got new text lines in its last update.
#: Used to determine if typed character/word buffers should be flushed.
_hasNewLines = False
#: Whether the last typed character is a tab.
#: If so, we should temporarily disable filtering as completions may
#: be short.
_hasTab = False

def _reportNewText(self, line):
codeofdusk marked this conversation as resolved.
Show resolved Hide resolved
# Additional typed character filtering beyond that in LiveText
codeofdusk marked this conversation as resolved.
Show resolved Hide resolved
if (
not self._hasTab
and len(line.strip()) < max(len(speech.curWordChars) + 1, 3)
):
return
if self._hasNewLines:
# Clear the typed word buffer for new text lines.
# This will need to be changed once #8110 is merged.
speech.curWordChars = []
self._queuedChars = []
super(TerminalWithoutTypedCharDetection, self)._reportNewText(line)

def event_typedCharacter(self, ch):
if ch == '\t':
self._hasTab = True
# Clear the typed word buffer for tab completion.
# This will need to be changed once #8110 is merged.
speech.curWordChars = []
else:
self._hasTab = False
if (
(
config.conf['keyboard']['speakTypedCharacters']
or config.conf['keyboard']['speakTypedWords']
)
and not config.conf['UIA']['winConsoleSpeakPasswords']
feerrenrut marked this conversation as resolved.
Show resolved Hide resolved
and self._supportsTextChange
):
self._queuedChars.append(ch)
else:
super(TerminalWithoutTypedCharDetection, self).event_typedCharacter(ch)

def event_textChange(self):
self._dispatchQueue()
super(TerminalWithoutTypedCharDetection, self).event_textChange()

@script(gestures=[
"kb:enter",
"kb:numpadEnter",
"kb:tab",
"kb:control+c",
"kb:control+d",
"kb:control+pause"
])
def script_flush_queuedChars(self, gesture):
"""
Flushes the typed word buffer and queue of typedCharacter events if present.
Since these gestures clear the current word/line, we should flush the
queue to avoid erroneously reporting these chars.
"""
gesture.send()
codeofdusk marked this conversation as resolved.
Show resolved Hide resolved
self._queuedChars = []
speech.curWordChars = []

def _calculateNewText(self, newLines, oldLines):
feerrenrut marked this conversation as resolved.
Show resolved Hide resolved
self._hasNewLines = (
self._findNonBlankIndices(newLines)
!= self._findNonBlankIndices(oldLines)
)
return super(TerminalWithoutTypedCharDetection, self)._calculateNewText(newLines, oldLines)

def _dispatchQueue(self):
"""Sends queued typedCharacter events through to NVDA."""
while self._queuedChars:
ch = self._queuedChars.pop(0)
super(TerminalWithoutTypedCharDetection, self).event_typedCharacter(ch)

def _findNonBlankIndices(self, lines):
"""
Given a list of strings, returns a list of indices where the strings
are not empty.
"""
return [index for index, line in enumerate(lines) if line]


class CandidateItem(NVDAObject):

def getFormattedCandidateName(self,number,candidate):
Expand Down
10 changes: 8 additions & 2 deletions source/NVDAObjects/window/winConsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
#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) 2007-2012 NV Access Limited
#Copyright (C) 2007-2019 NV Access Limited, Bill Dengler

import winConsoleHandler
from . import Window
from ..behaviors import Terminal, EditableTextWithoutAutoSelectDetection
from ..behaviors import Terminal, EditableTextWithoutAutoSelectDetection, TerminalWithoutTypedCharDetection
import api
import core

Expand All @@ -18,6 +18,12 @@ class WinConsole(Terminal, EditableTextWithoutAutoSelectDetection, Window):
"""
STABILIZE_DELAY = 0.03

def initOverlayClass(self):
# Legacy consoles take quite a while to send textChange events.
# This significantly impacts typing performance, so don't queue chars.
if isinstance(self, TerminalWithoutTypedCharDetection):
self._supportsTextChange = False

def _get_TextInfo(self):
consoleObject=winConsoleHandler.consoleObject
if consoleObject and self.windowHandle == consoleObject.windowHandle:
Expand Down
6 changes: 3 additions & 3 deletions source/appModules/putty.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
#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) 2010-2014 NV Access Limited
#Copyright (C) 2010-2019 NV Access Limited, Bill Dengler

"""App module for PuTTY
"""

import oleacc
from NVDAObjects.behaviors import Terminal
from NVDAObjects.behaviors import TerminalWithoutTypedCharDetection
from NVDAObjects.window import DisplayModelEditableText, DisplayModelLiveText
import appModuleHandler
from NVDAObjects.IAccessible import IAccessible
Expand All @@ -23,4 +23,4 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList):
clsList.remove(DisplayModelEditableText)
except ValueError:
pass
clsList[0:0] = (Terminal, DisplayModelLiveText)
clsList[0:0] = (TerminalWithoutTypedCharDetection, DisplayModelLiveText)
3 changes: 3 additions & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@
winConsoleImplementation= option("auto", "legacy", "UIA", default="auto")
winConsoleSpeakPasswords = boolean(default=false)

[terminals]
keyboardSupportInLegacy = boolean(default=True)

[update]
autoCheck = boolean(default=true)
startupNotification = boolean(default=true)
Expand Down
18 changes: 18 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2066,6 +2066,21 @@ def __init__(self, parent):
self.winConsoleSpeakPasswordsCheckBox.SetValue(config.conf["UIA"]["winConsoleSpeakPasswords"])
self.winConsoleSpeakPasswordsCheckBox.defaultValue = self._getDefaultValue(["UIA", "winConsoleSpeakPasswords"])

# Translators: This is the label for a group of advanced options in the
# Advanced settings panel
label = _("Terminal programs")
terminalsGroup = guiHelper.BoxSizerHelper(
parent=self,
sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL)
)
sHelper.addItem(terminalsGroup)
# Translators: This is the label for a checkbox in the
# Advanced settings panel.
label = _("Use the new &keyboard support in legacy Windows consoles when available")
self.keyboardSupportInLegacyCheckBox=terminalsGroup.addItem(wx.CheckBox(self, label=label))
self.keyboardSupportInLegacyCheckBox.SetValue(config.conf["terminals"]["keyboardSupportInLegacy"])
self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"])

# Translators: This is the label for a group of advanced options in the
# Advanced settings panel
label = _("Browse mode")
Expand Down Expand Up @@ -2152,6 +2167,7 @@ def haveConfigDefaultsBeenRestored(self):
self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue and
self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue=='UIA') and
self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue and
self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue and
self.autoFocusFocusableElementsCheckBox.IsChecked() == self.autoFocusFocusableElementsCheckBox.defaultValue and
self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and
set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) and
Expand All @@ -2163,6 +2179,7 @@ def restoreToDefaults(self):
self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue)
self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue=='UIA')
self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue)
self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue)
self.autoFocusFocusableElementsCheckBox.SetValue(self.autoFocusFocusableElementsCheckBox.defaultValue)
self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue)
self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems
Expand All @@ -2177,6 +2194,7 @@ def onSave(self):
else:
config.conf['UIA']['winConsoleImplementation'] = "auto"
config.conf["UIA"]["winConsoleSpeakPasswords"]=self.winConsoleSpeakPasswordsCheckBox.IsChecked()
config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked()
config.conf["virtualBuffers"]["autoFocusFocusableElements"] = self.autoFocusFocusableElementsCheckBox.IsChecked()
config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue()
for index,key in enumerate(self.logCategories):
Expand Down
Loading