editableText.py
10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#editableText.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) 2006-2012 NV Access Limited
"""Common support for editable text.
@note: If you want editable text functionality for an NVDAObject,
you should use the EditableText classes in L{NVDAObjects.behaviors}.
"""
import time
import sayAllHandler
import api
import review
from baseObject import ScriptableObject
import braille
import speech
import config
import eventHandler
from scriptHandler import isScriptWaiting, willSayAllResume
import textInfos
import controlTypes
class EditableText(ScriptableObject):
"""Provides scripts to report appropriately when moving the caret in editable text fields.
This does not handle the selection change keys.
To have selection changes reported, the object must notify of selection changes.
If the object supports selection but does not notify of selection changes, L{EditableTextWithoutAutoSelectDetection} should be used instead.
If the object notifies of selection changes, the following should be done:
* When the object gains focus, L{initAutoSelectDetection} must be called.
* When the object notifies of a possible selection change, L{detectPossibleSelectionChange} must be called.
* Optionally, if the object notifies of changes to its content, L{hasContentChangedSinceLastSelection} should be set to C{True}.
@ivar hasContentChangedSinceLastSelection: Whether the content has changed since the last selection occurred.
@type hasContentChangedSinceLastSelection: bool
"""
#: Whether to fire caretMovementFailed events when the caret doesn't move in response to a caret movement key.
shouldFireCaretMovementFailedEvents = False
#: Whether or not to announce text found before the caret on a new line (e.g. auto numbering)
announceNewLineText=True
def _hasCaretMoved(self, bookmark, retryInterval=0.01, timeout=0.03):
"""
Waits for the caret to move, for a timeout to elapse, or for a new focus event or script to be queued.
@param bookmark: a bookmark representing the position of the caret before it was instructed to move
@type bookmark: bookmark
@param retryInterval: the interval of time in seconds this method should wait before checking the caret each time.
@type retryInterval: float
@param timeout: the over all amount of time in seconds the method should wait before giving up completely.
@type timeout: float
@return: a tuple containing a boolean denoting whether this method timed out, and a TextInfo representing the old or updated caret position or None if interupted by a script or focus event.
@rtype: tuple
"""
elapsed = 0
newInfo=None
while elapsed < timeout:
if isScriptWaiting():
return (False,None)
api.processPendingEvents(processEventQueue=False)
if eventHandler.isPendingEvents("gainFocus"):
return (True,None)
#The caret may stop working as the focus jumps, we want to stay in the while loop though
try:
newInfo = self.makeTextInfo(textInfos.POSITION_CARET)
newBookmark = newInfo.bookmark
except (RuntimeError,NotImplementedError):
newInfo=None
else:
if newBookmark!=bookmark:
return (True,newInfo)
time.sleep(retryInterval)
elapsed += retryInterval
return (False,newInfo)
def _caretScriptPostMovedHelper(self, speakUnit, gesture, info=None):
if isScriptWaiting():
return
if not info:
try:
info = self.makeTextInfo(textInfos.POSITION_CARET)
except:
return
review.handleCaretMove(info)
if speakUnit and not willSayAllResume(gesture):
info.expand(speakUnit)
speech.speakTextInfo(info, unit=speakUnit, reason=controlTypes.REASON_CARET)
braille.handler.handleCaretMove(self)
def _caretMovementScriptHelper(self, gesture, unit):
try:
info=self.makeTextInfo(textInfos.POSITION_CARET)
except:
gesture.send()
return
bookmark=info.bookmark
gesture.send()
caretMoved,newInfo=self._hasCaretMoved(bookmark)
if not caretMoved and self.shouldFireCaretMovementFailedEvents:
eventHandler.executeEvent("caretMovementFailed", self, gesture=gesture)
self._caretScriptPostMovedHelper(unit,gesture,newInfo)
def script_caret_newLine(self,gesture):
try:
info=self.makeTextInfo(textInfos.POSITION_CARET)
except:
gesture.send()
return
bookmark=info.bookmark
gesture.send()
caretMoved,newInfo=self._hasCaretMoved(bookmark)
if not caretMoved or not newInfo:
return
# newInfo.copy should be good enough here, but in MS Word we get strange results.
try:
lineInfo=self.makeTextInfo(textInfos.POSITION_CARET)
except (RuntimeError,NotImplementedError):
return
lineInfo.expand(textInfos.UNIT_LINE)
lineInfo.setEndPoint(newInfo,"endToStart")
if lineInfo.isCollapsed:
lineInfo.expand(textInfos.UNIT_CHARACTER)
onlyInitial=True
else:
onlyInitial=False
speech.speakTextInfo(lineInfo,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET,onlyInitialFields=onlyInitial,suppressBlanks=True)
def _caretMoveBySentenceHelper(self, gesture, direction):
if isScriptWaiting():
return
try:
info=self.makeTextInfo(textInfos.POSITION_CARET)
info.move(textInfos.UNIT_SENTENCE, direction)
info.updateCaret()
self._caretScriptPostMovedHelper(textInfos.UNIT_SENTENCE,gesture,info)
except:
gesture.send()
return
def script_caret_moveByLine(self,gesture):
self._caretMovementScriptHelper(gesture, textInfos.UNIT_LINE)
script_caret_moveByLine.resumeSayAllMode=sayAllHandler.CURSOR_CARET
def script_caret_moveByCharacter(self,gesture):
self._caretMovementScriptHelper(gesture, textInfos.UNIT_CHARACTER)
def script_caret_moveByWord(self,gesture):
self._caretMovementScriptHelper(gesture, textInfos.UNIT_WORD)
def script_caret_moveByParagraph(self,gesture):
self._caretMovementScriptHelper(gesture, textInfos.UNIT_PARAGRAPH)
script_caret_moveByParagraph.resumeSayAllMode=sayAllHandler.CURSOR_CARET
def script_caret_previousSentence(self,gesture):
self._caretMoveBySentenceHelper(gesture, -1)
script_caret_previousSentence.resumeSayAllMode=sayAllHandler.CURSOR_CARET
def script_caret_nextSentence(self,gesture):
self._caretMoveBySentenceHelper(gesture, 1)
script_caret_nextSentence.resumeSayAllMode=sayAllHandler.CURSOR_CARET
def _backspaceScriptHelper(self,unit,gesture):
try:
oldInfo=self.makeTextInfo(textInfos.POSITION_CARET)
except:
gesture.send()
return
oldBookmark=oldInfo.bookmark
testInfo=oldInfo.copy()
res=testInfo.move(textInfos.UNIT_CHARACTER,-1)
if res<0:
testInfo.expand(unit)
delChunk=testInfo.text
else:
delChunk=""
gesture.send()
caretMoved,newInfo=self._hasCaretMoved(oldBookmark)
if not caretMoved:
return
if len(delChunk)>1:
speech.speakMessage(delChunk)
else:
speech.speakSpelling(delChunk)
self._caretScriptPostMovedHelper(None,gesture,newInfo)
def script_caret_backspaceCharacter(self,gesture):
self._backspaceScriptHelper(textInfos.UNIT_CHARACTER,gesture)
def script_caret_backspaceWord(self,gesture):
self._backspaceScriptHelper(textInfos.UNIT_WORD,gesture)
def script_caret_delete(self,gesture):
try:
info=self.makeTextInfo(textInfos.POSITION_CARET)
except:
gesture.send()
return
bookmark=info.bookmark
gesture.send()
# We'll try waiting for the caret to move, but we don't care if it doesn't.
caretMoved,newInfo=self._hasCaretMoved(bookmark)
self._caretScriptPostMovedHelper(textInfos.UNIT_CHARACTER,gesture,newInfo)
braille.handler.handleCaretMove(self)
__gestures = {
"kb:upArrow": "caret_moveByLine",
"kb:downArrow": "caret_moveByLine",
"kb:leftArrow": "caret_moveByCharacter",
"kb:rightArrow": "caret_moveByCharacter",
"kb:pageUp": "caret_moveByLine",
"kb:pageDown": "caret_moveByLine",
"kb:control+leftArrow": "caret_moveByWord",
"kb:control+rightArrow": "caret_moveByWord",
"kb:control+upArrow": "caret_moveByParagraph",
"kb:control+downArrow": "caret_moveByParagraph",
"kb:alt+upArrow": "caret_previousSentence",
"kb:alt+downArrow": "caret_nextSentence",
"kb:home": "caret_moveByCharacter",
"kb:end": "caret_moveByCharacter",
"kb:control+home": "caret_moveByLine",
"kb:control+end": "caret_moveByLine",
"kb:delete": "caret_delete",
"kb:numpadDelete": "caret_delete",
"kb:backspace": "caret_backspaceCharacter",
"kb:control+backspace": "caret_backspaceWord",
}
def initAutoSelectDetection(self):
"""Initialise automatic detection of selection changes.
This should be called when the object gains focus.
"""
try:
self._lastSelectionPos=self.makeTextInfo(textInfos.POSITION_SELECTION)
except:
self._lastSelectionPos=None
self.hasContentChangedSinceLastSelection=False
def detectPossibleSelectionChange(self):
"""Detects if the selection has been changed, and if so it speaks the change.
"""
try:
newInfo=self.makeTextInfo(textInfos.POSITION_SELECTION)
except:
# Just leave the old selection, which is usually better than nothing.
return
oldInfo=getattr(self,'_lastSelectionPos',None)
self._lastSelectionPos=newInfo.copy()
if not oldInfo:
# There's nothing we can do, but at least the last selection will be right next time.
return
hasContentChanged=getattr(self,'hasContentChangedSinceLastSelection',False)
self.hasContentChangedSinceLastSelection=False
speech.speakSelectionChange(oldInfo,newInfo,generalize=hasContentChanged)
class EditableTextWithoutAutoSelectDetection(EditableText):
"""In addition to L{EditableText}, provides scripts to report appropriately when the selection changes.
This should be used when an object does not notify of selection changes.
"""
def reportSelectionChange(self, oldTextInfo):
api.processPendingEvents(processEventQueue=False)
newInfo=self.makeTextInfo(textInfos.POSITION_SELECTION)
speech.speakSelectionChange(oldTextInfo,newInfo)
braille.handler.handleCaretMove(self)
def script_caret_changeSelection(self,gesture):
try:
oldInfo=self.makeTextInfo(textInfos.POSITION_SELECTION)
except:
gesture.send()
return
gesture.send()
if isScriptWaiting() or eventHandler.isPendingEvents("gainFocus"):
return
try:
self.reportSelectionChange(oldInfo)
except:
return
__changeSelectionGestures = (
"kb:shift+upArrow",
"kb:shift+downArrow",
"kb:shift+leftArrow",
"kb:shift+rightArrow",
"kb:shift+pageUp",
"kb:shift+pageDown",
"kb:shift+control+leftArrow",
"kb:shift+control+rightArrow",
"kb:shift+control+upArrow",
"kb:shift+control+downArrow",
"kb:shift+home",
"kb:shift+end",
"kb:shift+control+home",
"kb:shift+control+end",
"kb:control+a",
)
def initClass(self):
for gesture in self.__changeSelectionGestures:
self.bindGesture(gesture, "caret_changeSelection")