Skip to content

Commit 365e8a1

Browse files
authored
Merge d8af08e into 02f1dff
2 parents 02f1dff + d8af08e commit 365e8a1

10 files changed

Lines changed: 174 additions & 67 deletions

File tree

developerGuide.t2t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ These variables are:
790790
- focus: The current focus object
791791
- focusAnc: The ancestors of the current focus object
792792
- fdl: Focus difference level; i.e. the level at which the ancestors for the current and previous focus differ
793+
- caret: The current caret object
793794
- fg: The current foreground object
794795
- nav: The current navigator object
795796
- mouse: The current mouse object

source/NVDAObjects/UIA/winConsoleUIA.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ class WinConsoleUIA(KeyboardHandlerBasedTypedCharSupport):
271271
STABILIZE_DELAY = 0.03
272272
#: the caret in consoles can take a while to move on Windows 10 1903 and later.
273273
_caretMovementTimeoutMultiplier = 1.5
274+
# For this reason, don't rely on textInfo to speak typed words
275+
useTextInfoToSpeakTypedWords =False
274276

275277
def _get_TextInfo(self):
276278
"""Overriding _get_TextInfo and thus the TextInfo property

source/NVDAObjects/__init__.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,32 +1003,6 @@ def _get_landmark(self):
10031003
"""
10041004
return None
10051005

1006-
def _reportErrorInPreviousWord(self):
1007-
try:
1008-
# self might be a descendant of the text control; e.g. Symphony.
1009-
# We want to deal with the entire text, so use the caret object.
1010-
info = api.getCaretObject().makeTextInfo(textInfos.POSITION_CARET)
1011-
# This gets called for characters which might end a word; e.g. space.
1012-
# The character before the caret is the word end.
1013-
# The one before that is the last of the word, which is what we want.
1014-
info.move(textInfos.UNIT_CHARACTER, -2)
1015-
info.expand(textInfos.UNIT_CHARACTER)
1016-
fields = info.getTextWithFields()
1017-
except RuntimeError:
1018-
return
1019-
except:
1020-
# Focus probably moved.
1021-
log.debugWarning("Error fetching last character of previous word", exc_info=True)
1022-
return
1023-
for command in fields:
1024-
if isinstance(command, textInfos.FieldCommand) and command.command == "formatChange" and command.field.get("invalid-spelling"):
1025-
break
1026-
else:
1027-
# No error.
1028-
return
1029-
import nvwave
1030-
nvwave.playWaveFile(r"waves\textError.wav")
1031-
10321006
def event_liveRegionChange(self):
10331007
"""
10341008
A base implementation for live region change events.
@@ -1038,12 +1012,6 @@ def event_liveRegionChange(self):
10381012
ui.message(name)
10391013

10401014
def event_typedCharacter(self,ch):
1041-
if config.conf["documentFormatting"]["reportSpellingErrors"] and config.conf["keyboard"]["alertForSpellingErrors"] and (
1042-
# Not alpha, apostrophe or control.
1043-
ch.isspace() or (ch >= u" " and ch not in u"'\x7f" and not ch.isalpha())
1044-
):
1045-
# Reporting of spelling errors is enabled and this character ends a word.
1046-
self._reportErrorInPreviousWord()
10471015
speech.speakTypedCharacters(ch)
10481016
import winUser
10491017
if config.conf["keyboard"]["beepForLowercaseWithCapslock"] and ch.islower() and winUser.getKeyState(winUser.VK_CAPITAL)&1:

source/appModules/soffice.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
#appModules/soffice.py
2-
#A part of NonVisual Desktop Access (NVDA)
3-
#This file is covered by the GNU General Public License.
4-
#See the file COPYING for more details.
5-
#Copyright (C) 2006-2019 NV Access Limited, Bill Dengler
1+
# appModules/soffice.py
2+
# A part of NonVisual Desktop Access (NVDA)
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
# Copyright (C) 2006-2019 NV Access Limited, Bill Dengler, Babbage B.V.
66

77
from comtypes import COMError
88
import IAccessibleHandler
@@ -78,6 +78,7 @@ def _get_columnNumber(self):
7878
return 0
7979

8080
class SymphonyTextInfo(IA2TextTextInfo):
81+
useUniscribe = False
8182

8283
def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
8384
obj = self.obj
@@ -181,6 +182,12 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
181182

182183
return formatField,(startOffset,endOffset)
183184

185+
def _getWordOffsets(self, offset):
186+
# #8065: At least in LibreOffice, the word offsets returned by IA2TextTextInfo do not match reality.
187+
# As long as uniscribe is disabled, The LibreOffice implementation much more closely matches
188+
# the OffsetsTextInfo implementation to fetch word offsets
189+
return super(IA2TextTextInfo, self)._getWordOffsets(offset)
190+
184191
def _getLineOffsets(self, offset):
185192
start, end = super(SymphonyTextInfo, self)._getLineOffsets(offset)
186193
if offset == 0 and start == 0 and end == 0:

source/documentBase.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
#A part of NonVisual Desktop Access (NVDA)
2-
#Copyright (C) 2017 NV Access Limited
3-
#This file is covered by the GNU General Public License.
4-
#See the file COPYING for more details.
1+
# documentBase.py
2+
# A part of NonVisual Desktop Access (NVDA)
3+
# Copyright (C) 2017-2019 NV Access Limited, Babbage B.V.
4+
# This file is covered by the GNU General Public License.
5+
# See the file COPYING for more details.
56

67
from baseObject import AutoPropertyObject, ScriptableObject
78
from scriptHandler import isScriptWaiting
@@ -29,6 +30,12 @@ def _get_selection(self):
2930
def _set_selection(self,info):
3031
info.updateSelection()
3132

33+
def _get_caret(self):
34+
return self.makeTextInfo(textInfos.POSITION_CARET)
35+
36+
def _set_caret(self, info):
37+
info.updateCaret()
38+
3239
class DocumentWithTableNavigation(TextContainerObject,ScriptableObject):
3340
"""
3441
A document that supports standard table navigiation comments (E.g. control+alt+arrows to move between table cells).

source/editableText.py

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
#editableText.py
2-
#A part of NonVisual Desktop Access (NVDA)
3-
#This file is covered by the GNU General Public License.
4-
#See the file COPYING for more details.
5-
#Copyright (C) 2006-2017 NV Access Limited, Davy Kager
1+
# editableText.py
2+
# A part of NonVisual Desktop Access (NVDA)
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
# Copyright (C) 2006-2019 NV Access Limited, Davy Kager
66

77
"""Common support for editable text.
88
@note: If you want editable text functionality for an NVDAObject,
@@ -23,13 +23,14 @@
2323
import textInfos
2424
import controlTypes
2525
from logHandler import log
26+
from typing import Tuple, Optional
2627

2728
class EditableText(TextContainerObject,ScriptableObject):
2829
"""Provides scripts to report appropriately when moving the caret in editable text fields.
2930
This does not handle the selection change keys.
3031
To have selection changes reported, the object must notify of selection changes.
3132
If the object supports selection but does not notify of selection changes, L{EditableTextWithoutAutoSelectDetection} should be used instead.
32-
33+
3334
If the object notifies of selection changes, the following should be done:
3435
* When the object gains focus, L{initAutoSelectDetection} must be called.
3536
* When the object notifies of a possible selection change, L{detectPossibleSelectionChange} must be called.
@@ -54,6 +55,25 @@ class EditableText(TextContainerObject,ScriptableObject):
5455

5556
_caretMovementTimeoutMultiplier = 1
5657

58+
#: A cached bookmark for the caret.
59+
#: This is cached until L{hasNewWordBeenTyped} clears it
60+
_cachedCaretBookmark = None
61+
62+
def getScript(self, gesture):
63+
script = super().getScript(gesture)
64+
if script or not self.useTextInfoToSpeakTypedWords:
65+
return script
66+
if gesture.isCharacter:
67+
return self.script_preTypedCharacter
68+
return None
69+
70+
def script_preTypedCharacter(self, gesture):
71+
try:
72+
self._cachedCaretBookmark = self.caret.bookmark
73+
except (LookupError, RuntimeError):
74+
pass # Will still be None
75+
gesture.send()
76+
5777
def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=None, origWord=None):
5878
"""
5979
Waits for the caret to move, for a timeout to elapse, or for a new focus event or script to be queued.
@@ -144,6 +164,8 @@ def _caretScriptPostMovedHelper(self, speakUnit, gesture, info=None):
144164
return
145165
# Forget the word currently being typed as the user has moved the caret somewhere else.
146166
speech.clearTypedWordBuffer()
167+
# Also clear our latest cachetd caret bookmark
168+
self._clearCachedCaretBookmark()
147169
review.handleCaretMove(info)
148170
if speakUnit and not willSayAllResume(gesture):
149171
info.expand(speakUnit)
@@ -163,6 +185,47 @@ def _caretMovementScriptHelper(self, gesture, unit):
163185
eventHandler.executeEvent("caretMovementFailed", self, gesture=gesture)
164186
self._caretScriptPostMovedHelper(unit,gesture,newInfo)
165187

188+
def _clearCachedCaretBookmark(self):
189+
self._cachedCaretBookmark = None
190+
191+
def hasNewWordBeenTyped(self, wordSeparator: str) -> Tuple[Optional[bool], textInfos.TextInfo]:
192+
"""
193+
Returns whether a new word has been typed during this core cycle.
194+
It relies on self._cachedCaretBookmark, which is cleared after every core cycle.
195+
@param wordSeparator: The word seperator that has just been typed.
196+
@returns: a tuple containing the following two values:
197+
1. Whether a new word has been typed. This could be:
198+
* False if a caret move has been detected, but no word has been typed.
199+
* True if a caret move has been detected and a new word has been typed.
200+
* None if no caret move could be detected.
201+
2. If the caret has moved and a new word has been typed, a TextInfo
202+
expanded to the word that has just been typed.
203+
"""
204+
if not self.useTextInfoToSpeakTypedWords:
205+
return (None, None)
206+
bookmark = self._cachedCaretBookmark
207+
if not bookmark:
208+
return (None, None)
209+
self._clearCachedCaretBookmark()
210+
caretMoved, caretInfo = self._hasCaretMoved(bookmark, retryInterval=0.005, timeout=0.015)
211+
if not caretMoved or not caretInfo or not caretInfo.obj:
212+
return (None, None)
213+
wordInfo = self.makeTextInfo(bookmark)
214+
# The bookmark is positioned after the end of the word.
215+
# Therefore, we need to move it one character backwards.
216+
wordInfo.move(textInfos.UNIT_CHARACTER, -1)
217+
wordInfo.expand(textInfos.UNIT_WORD)
218+
diff = wordInfo.compareEndPoints(caretInfo, "endToStart")
219+
if diff >= 0 and not wordSeparator.isspace():
220+
# This is no word boundary.
221+
return (False, None)
222+
elif wordInfo.text.isspace():
223+
# There is only space, which is not considered a word.
224+
# For example, this can occur in Notepad++ when auto indentation is on.
225+
log.debug("Word before caret contains only spaces")
226+
return (None, None)
227+
return (True, wordInfo)
228+
166229
def _get_caretMovementDetectionUsesEvents(self) -> bool:
167230
"""Returns whether or not to rely on caret and textChange events when
168231
finding out whether the caret position has changed after pressing a caret movement gesture.
@@ -177,14 +240,22 @@ def _get_caretMovementDetectionUsesEvents(self) -> bool:
177240
except AttributeError:
178241
return True
179242

180-
def script_caret_newLine(self,gesture):
243+
def _get_useTextInfoToSpeakTypedWords(self) -> bool:
244+
"""Returns whether or not to use textInfo to announce newly typed words."""
245+
# This class is a mixin that usually comes before other relevant classes in the mro.
246+
# Therefore, try to call super first, and if that fails, return the default (C{True}.
181247
try:
182-
info=self.makeTextInfo(textInfos.POSITION_CARET)
183-
except:
184-
gesture.send()
248+
return super().useTextInfoToSpeakTypedWords
249+
except AttributeError:
250+
return True
251+
252+
def script_caret_newLine(self,gesture):
253+
# Going to a new line should also speak the last word using textInfo.
254+
# Note that calling script_preTypedCharacter also executes the gesture, which is fine.
255+
self.script_preTypedCharacter(gesture)
256+
bookmark = self._cachedCaretBookmark
257+
if not bookmark:
185258
return
186-
bookmark=info.bookmark
187-
gesture.send()
188259
caretMoved,newInfo=self._hasCaretMoved(bookmark)
189260
if not caretMoved or not newInfo:
190261
return
@@ -366,7 +437,7 @@ def script_caret_changeSelection(self,gesture):
366437
try:
367438
self.reportSelectionChange(oldInfo)
368439
except:
369-
return
440+
return
370441

371442
__changeSelectionGestures = (
372443
"kb:shift+upArrow",

source/pythonConsole.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
#pythonConsole.py
2-
#A part of NonVisual Desktop Access (NVDA)
3-
#This file is covered by the GNU General Public License.
4-
#See the file COPYING for more details.
5-
#Copyright (C) 2008-2019 NV Access Limited, Leonard de Ruijter
1+
# pythonConsole.py
2+
# A part of NonVisual Desktop Access (NVDA)
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
# Copyright (C) 2008-2019 NV Access Limited, Babbage B.V., Leonard de Ruijter
66

77
import watchdog
88

@@ -195,6 +195,7 @@ def updateNamespaceSnapshotVars(self):
195195
# Copy the focus ancestor list, as it gets mutated once it is replaced in api.setFocusObject.
196196
"focusAnc": list(api.getFocusAncestors()),
197197
"fdl": api.getFocusDifferenceLevel(),
198+
"caret": api.getCaretObject(),
198199
"fg": api.getForegroundObject(),
199200
"nav": api.getNavigatorObject(),
200201
"review":api.getReviewPosition(),

source/speech/__init__.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -513,20 +513,25 @@ def speakText(
513513
text: str,
514514
reason: str = controlTypes.REASON_MESSAGE,
515515
symbolLevel: Optional[int] = None,
516+
_prefixSpeechCommand: Optional[SpeechCommand] = None,
516517
priority: Optional[Spri] = None
517518
):
518519
"""Speaks some text.
519520
@param text: The text to speak.
520521
@param reason: The reason for this speech; one of the controlTypes.REASON_* constants.
521522
@param symbolLevel: The symbol verbosity level; C{None} (default) to use the user's configuration.
523+
@param _prefixSpeechCommand: A speech command to prepend to the spoken speech sequence.
522524
@param priority: The speech priority.
523525
"""
524526
if text is None:
525527
return
526528
if isBlank(text):
527529
# Translators: This is spoken when the line is considered blank.
528530
text=_("blank")
529-
speak([text], symbolLevel=symbolLevel, priority=priority)
531+
sequence = [text]
532+
if _prefixSpeechCommand is not None:
533+
sequence.insert(0, _prefixSpeechCommand)
534+
speak(sequence, symbolLevel=symbolLevel, priority=priority)
530535

531536

532537
RE_INDENTATION_SPLIT = re.compile(r"^([^\S\r\n\f\v]*)(.*)$", re.UNICODE | re.DOTALL)
@@ -846,12 +851,7 @@ def speakTypedCharacters(ch: str):
846851
# delete character produced in some apps with control+backspace
847852
return
848853
elif len(curWordChars)>0:
849-
typedWord="".join(curWordChars)
850-
curWordChars=[]
851-
if log.isEnabledFor(log.IO):
852-
log.io("typed word: %s"%typedWord)
853-
if config.conf["keyboard"]["speakTypedWords"] and not typingIsProtected:
854-
speakText(typedWord)
854+
speakPreviousWord(realChar)
855855
global _suppressSpeakTypedCharactersNumber, _suppressSpeakTypedCharactersTime
856856
if _suppressSpeakTypedCharactersNumber > 0:
857857
# We primarily suppress based on character count and still have characters to suppress.
@@ -868,6 +868,52 @@ def speakTypedCharacters(ch: str):
868868
speakSpelling(realChar)
869869

870870

871+
def speakPreviousWord(wordSeparator):
872+
word = "".join(curWordChars)
873+
typingIsProtected = api.isTypingProtected()
874+
reportSpellingError = (
875+
config.conf["documentFormatting"]["reportSpellingErrors"]
876+
and config.conf["keyboard"]["alertForSpellingErrors"]
877+
)
878+
if not (log.isEnabledFor(log.IO) or (
879+
config.conf["keyboard"]["speakTypedWords"] and not typingIsProtected
880+
) or reportSpellingError):
881+
clearTypedWordBuffer()
882+
return
883+
try:
884+
obj = api.getCaretObject()
885+
except Exception:
886+
# No caret object, nothing to report
887+
return
888+
# The caret object can be an NVDAObject or a TreeInterceptor.
889+
# Editable caret cases inherrit from EditableText.
890+
from editableText import EditableText
891+
if not isinstance(obj, EditableText) or controlTypes.STATE_READONLY in getattr(obj, "states", set()):
892+
clearTypedWordBuffer()
893+
return
894+
wordFound, wordInfo = obj.hasNewWordBeenTyped(wordSeparator)
895+
if wordFound is False:
896+
curWordChars.append(wordSeparator)
897+
return
898+
speakUsingTextInfo = wordFound is True and not isBlank(wordInfo.text)
899+
if speakUsingTextInfo:
900+
word = wordInfo.text
901+
clearTypedWordBuffer()
902+
if log.isEnabledFor(log.IO):
903+
log.io(f"typed word: {word}")
904+
if config.conf["keyboard"]["speakTypedWords"] and not typingIsProtected:
905+
prefixSpeechCommand = None
906+
if speakUsingTextInfo and reportSpellingError:
907+
for command in wordInfo.getTextWithFields():
908+
if (
909+
isinstance(command, textInfos.FieldCommand)
910+
and command.command == "formatChange"
911+
and command.field.get("invalid-spelling")
912+
):
913+
prefixSpeechCommand = WaveFileCommand(r"waves\textError.wav")
914+
speakText(word, _prefixSpeechCommand=prefixSpeechCommand)
915+
916+
871917
class SpeakTextInfoState(object):
872918
"""Caches the state of speakTextInfo such as the current controlField stack, current formatfield and indentation."""
873919

source/winConsoleHandler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def terminate():
165165
disconnectConsole()
166166

167167
class WinConsoleTextInfo(textInfos.offsets.OffsetsTextInfo):
168+
useUniscribe = False
168169

169170
_cache_consoleScreenBufferInfo=True
170171
def _get_consoleScreenBufferInfo(self):

0 commit comments

Comments
 (0)