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,
2323import textInfos
2424import controlTypes
2525from logHandler import log
26+ from typing import Tuple , Optional
2627
2728class 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" ,
0 commit comments