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) 2010 James Teh <jamie@jantrid.net>
11
from NVDAObjects import NVDAObject
12
from editableText import EditableText
13
from treeInterceptorHandler import TreeInterceptor
16
from NVDAObjects import behaviors
18
class CompoundTextInfo(textInfos.TextInfo):
20
def _getObjectPosition(self, obj):
22
rootObj = self.obj.rootNVDAObject
23
while obj and obj != rootObj:
24
indexes.insert(0, obj.indexInParent)
28
def compareEndPoints(self, other, which):
29
if which in ("startToStart", "startToEnd"):
31
selfObj = self._startObj
34
selfObj = self._endObj
35
if which in ("startToStart", "endToStart"):
36
otherTi = other._start
37
otherObj = other._startObj
40
otherObj = other._endObj
42
if selfObj == otherObj:
43
# Same object, so just compare the two TextInfos normally.
44
return selfTi.compareEndPoints(otherTi, which)
46
# Different objects, so we have to compare the hierarchical positions of the objects.
47
return cmp(self._getObjectPosition(selfObj), other._getObjectPosition(otherObj))
49
def _normalizeStartAndEnd(self):
50
if self._start.isCollapsed and self._startObj != self._endObj:
51
# The only time start will be collapsed when start and end aren't the same is if it is at the end of the object.
52
# This is equivalent to the start of the next object.
53
# Aside from being pointless, we don't want a collapsed start object, as this will cause bogus control fields to be emitted.
54
obj = self._startObj.flowsTo
57
self._start = obj.makeTextInfo(textInfos.POSITION_FIRST)
59
if self._startObj == self._endObj:
60
# There should only be a single TextInfo and it should cover the entire range.
61
self._start.setEndPoint(self._end, "endToEnd")
62
self._end = self._start
63
self._endObj = self._startObj
65
# start needs to cover the rest of the text to the end of its object.
66
self._start.setEndPoint(self._startObj.makeTextInfo(textInfos.POSITION_ALL), "endToEnd")
67
# end needs to cover the rest of the text to the start of its object.
68
self._end.setEndPoint(self._endObj.makeTextInfo(textInfos.POSITION_ALL), "startToStart")
70
def setEndPoint(self, other, which):
71
if which == "startToStart":
72
self._start = other._start.copy()
73
self._startObj = other._startObj
74
elif which == "startToEnd":
75
self._start = other._end.copy()
76
self._start.setEndPoint(other._end, which)
77
self._startObj = other._endObj
78
elif which == "endToStart":
79
self._end = other._start.copy()
80
self._end.setEndPoint(other._start, which)
81
self._endObj = other._startObj
82
elif which == "endToEnd":
83
self._end = other._end.copy()
84
self._endObj = other._endObj
86
raise ValueError("which=%s" % which)
87
self._normalizeStartAndEnd()
89
def collapse(self, end=False):
91
if self._end.compareEndPoints(self._endObj.makeTextInfo(textInfos.POSITION_ALL), "endToEnd") == 0:
92
# The end TextInfo is at the end of its object.
93
# The end of this object is equivalent to the start of the next.
94
# As well as being silly, collapsing to the end of this object causes say all to move the caret to the end of paragraphs.
95
# Therefore, collapse to the start of the next instead.
96
obj = self._endObj.flowsTo
99
self._end = obj.makeTextInfo(textInfos.POSITION_FIRST)
101
# There are no more objects, so just collapse to the end of this object.
102
self._end.collapse(end=True)
104
# The end TextInfo is not at the end of its object, so just collapse to the end of the end TextInfo.
105
self._end.collapse(end=True)
106
self._start = self._end
107
self._startObj = self._endObj
110
self._start.collapse()
111
self._end = self._start
112
self._endObj = self._startObj
115
return self.__class__(self.obj, self)
117
def updateCaret(self):
118
self._startObj.setFocus()
119
self._start.updateCaret()
121
def updateSelection(self):
122
self._startObj.setFocus()
123
self._start.updateSelection()
124
if self._end is not self._start:
125
self._end.updateSelection()
127
def _get_bookmark(self):
130
def _get_NVDAObjectAtStart(self):
131
return self._startObj
133
def _get_pointAtStart(self):
134
return self._start.pointAtStart
136
def _getControlFieldForObject(self, obj, ignoreEditableText=True):
138
if ignoreEditableText and role in (controlTypes.ROLE_PARAGRAPH, controlTypes.ROLE_EDITABLETEXT):
139
# This is basically just a text node.
141
field = textInfos.ControlField()
142
field["role"] = obj.role
144
# The user doesn't care about certain states, as they are obvious.
145
states.discard(controlTypes.STATE_EDITABLE)
146
states.discard(controlTypes.STATE_MULTILINE)
147
states.discard(controlTypes.STATE_FOCUSED)
148
field["states"] = states
149
field["name"] = obj.name
150
field["_childcount"] = obj.childCount
151
field["level"] = obj.positionInfo.get("level")
152
if role == controlTypes.ROLE_TABLE:
153
field["table-id"] = 1 # FIXME
154
field["table-rowcount"] = obj.rowCount
155
field["table-columncount"] = obj.columnCount
156
if role in (controlTypes.ROLE_TABLECELL, controlTypes.ROLE_TABLECOLUMNHEADER, controlTypes.ROLE_TABLEROWHEADER):
157
field["table-id"] = 1 # FIXME
158
field["table-rownumber"] = obj.rowNumber
159
field["table-columnnumber"] = obj.columnNumber
162
def _iterTextWithEmbeddedObjects(self, text, ti, fieldStart, textLength=None):
163
if textLength is None:
164
textLength = len(text)
166
while chunkStart < textLength:
168
chunkEnd = text.index(u"\uFFFC", chunkStart)
170
yield text[chunkStart:]
172
if chunkStart != chunkEnd:
173
yield text[chunkStart:chunkEnd]
174
yield ti.getEmbeddedObject(fieldStart + chunkEnd)
175
chunkStart = chunkEnd + 1
177
def __eq__(self, other):
178
return self._start == other._start and self._startObj == other._startObj and self._end == other._end and self._endObj == other._endObj
180
def __ne__(self, other):
181
return not self == other
183
class TreeCompoundTextInfo(CompoundTextInfo):
184
#: Units contained within a single TextInfo.
185
SINGLE_TEXTINFO_UNITS = (textInfos.UNIT_CHARACTER, textInfos.UNIT_WORD, textInfos.UNIT_LINE, textInfos.UNIT_SENTENCE, textInfos.UNIT_PARAGRAPH)
187
def __init__(self, obj, position):
188
super(TreeCompoundTextInfo, self).__init__(obj, position)
189
rootObj = obj.rootNVDAObject
190
if isinstance(position, NVDAObject):
192
position = textInfos.POSITION_CARET
193
if isinstance(position, self.__class__):
194
self._start = position._start.copy()
195
self._startObj = position._startObj
196
if position._end is position._start:
197
self._end = self._start
199
self._end = position._end.copy()
200
self._endObj = position._endObj
201
elif position == textInfos.POSITION_FIRST:
202
self._startObj = self._endObj = self._findContentDescendant(rootObj.firstChild)
203
self._start = self._end = self._startObj.makeTextInfo(position)
204
elif position == textInfos.POSITION_LAST:
205
self._startObj = self._endObj = self._findContentDescendant(rootObj.lastChild)
206
self._start = self._end = self._startObj.makeTextInfo(position)
207
elif position == textInfos.POSITION_ALL:
208
self._startObj = self._findContentDescendant(rootObj.firstChild)
209
self._endObj = self._findContentDescendant(rootObj.lastChild)
210
self._start = self._startObj.makeTextInfo(position)
211
self._end = self._endObj.makeTextInfo(position)
212
elif position == textInfos.POSITION_CARET:
213
self._startObj = self._endObj = obj.caretObject
214
self._start = self._end = self._startObj.makeTextInfo(position)
215
elif position == textInfos.POSITION_SELECTION:
216
# Start from the caret.
217
self._startObj = self._endObj = self.obj.caretObject
218
# Find the objects which start and end the selection.
219
tempObj = self._startObj
220
while tempObj and controlTypes.STATE_SELECTED in tempObj.states:
221
self._startObj = tempObj
222
tempObj = tempObj.flowsFrom
223
tempObj = self._endObj
224
while tempObj and controlTypes.STATE_SELECTED in tempObj.states:
225
self._endObj = tempObj
226
tempObj = tempObj.flowsTo
227
self._start = self._startObj.makeTextInfo(position)
228
if self._startObj is self._endObj:
229
self._end = self._start
231
self._end = self._endObj.makeTextInfo(position)
233
raise NotImplementedError
235
def _findContentDescendant(self, obj):
236
while obj and controlTypes.STATE_FOCUSABLE not in obj.states:
240
def _getTextInfos(self):
242
if self._startObj == self._endObj:
244
obj = self._startObj.flowsTo
245
while obj and obj != self._endObj:
246
yield obj.makeTextInfo(textInfos.POSITION_ALL)
251
return "".join(ti.text for ti in self._getTextInfos())
253
def getTextWithFields(self, formatConfig=None):
254
# Get the initial control fields.
256
rootObj = self.obj.rootNVDAObject
258
while obj and obj != rootObj:
259
field = self._getControlFieldForObject(obj)
261
fields.insert(0, textInfos.FieldCommand("controlStart", field))
264
for ti in self._getTextInfos():
266
for field in ti.getTextWithFields(formatConfig=formatConfig):
267
if isinstance(field, basestring):
268
textLength = len(field)
269
for chunk in self._iterTextWithEmbeddedObjects(field, ti, fieldStart, textLength=textLength):
270
if isinstance(chunk, basestring):
273
controlField = self._getControlFieldForObject(chunk, ignoreEditableText=False)
274
controlField["alwaysReportName"] = True
275
fields.extend((textInfos.FieldCommand("controlStart", controlField),
277
textInfos.FieldCommand("controlEnd", None)))
278
fieldStart += textLength
284
def expand(self, unit):
285
if unit == textInfos.UNIT_READINGCHUNK:
286
unit = textInfos.UNIT_LINE
288
if unit in self.SINGLE_TEXTINFO_UNITS:
289
# This unit is definitely contained within a single chunk.
290
self._start.expand(unit)
291
self._end = self._start
292
self._endObj = self._startObj
294
raise NotImplementedError
296
def move(self, unit, direction, endPoint=None):
300
if unit == textInfos.UNIT_READINGCHUNK:
301
unit = textInfos.UNIT_LINE
303
if unit not in self.SINGLE_TEXTINFO_UNITS:
304
raise NotImplementedError
306
if not endPoint or endPoint == "start":
308
moveObj = self._startObj
309
elif endPoint == "end":
311
moveObj = self._endObj
313
goPrevious = direction < 0
314
remainingMovement = direction
317
movement = moveTi.move(unit, remainingMovement, endPoint=endPoint)
318
if movement == 0 and count0MoveAs != 0:
319
movement = count0MoveAs
320
remainingMovement -= movement
322
if remainingMovement == 0:
323
# The requested destination was within moveTi.
326
# The requested destination is not in this object, so move to the next.
327
tempObj = moveObj.flowsFrom if goPrevious else moveObj.flowsTo
333
moveTi = moveObj.makeTextInfo(textInfos.POSITION_ALL)
334
moveTi.collapse(end=True)
335
# We haven't moved anywhere yet, as the end of this object (where we are now) is equivalent to the start of the one we just left.
336
# Blank objects should still count as 1 step.
337
# Therefore, the next move must count as 1 even if it is 0.
340
moveTi = moveObj.makeTextInfo(textInfos.POSITION_FIRST)
341
if endPoint == "end":
342
# If we're moving the end, the previous move would have taken us to the end of the previous object,
343
# which is equivalent to the start of this object (where we are now).
344
# Therefore, moving to this new object shouldn't be counted as a move.
345
# However, ensure that blank objects will still be counted as 1 step.
348
# We've moved to the start of the next unit.
349
remainingMovement -= 1
350
if remainingMovement == 0:
351
# We just hit the requested destination.
354
if not endPoint or endPoint == "start":
356
self._startObj = moveObj
357
if not endPoint or endPoint == "end":
359
self._endObj = moveObj
360
self._normalizeStartAndEnd()
362
return direction - remainingMovement
364
class CompoundDocument(EditableText, TreeInterceptor):
365
TextInfo = TreeCompoundTextInfo
367
def __init__(self, rootNVDAObject):
368
super(CompoundDocument, self).__init__(rootNVDAObject)
370
def _get_isAlive(self):
371
root = self.rootNVDAObject
372
return winUser.isWindow(root.windowHandle)
374
def __contains__(self, obj):
375
root = self.rootNVDAObject
377
if obj.windowHandle != root.windowHandle:
384
def makeTextInfo(self, position):
385
return self.TextInfo(self, position)
387
def _get_caretObject(self):
388
return eventHandler.lastQueuedFocusObject
390
def event_treeInterceptor_gainFocus(self):
391
speech.speakObject(self.rootNVDAObject, reason=speech.REASON_FOCUS)
393
info = self.makeTextInfo(textInfos.POSITION_SELECTION)
398
info.expand(textInfos.UNIT_LINE)
399
speech.speakTextInfo(info)
401
speech.speakSelectionMessage(_("selected %s"), info.text)
402
braille.handler.handleGainFocus(self)
403
self.initAutoSelectDetection()
405
def event_caret(self, obj, nextHandler):
406
self.detectPossibleSelectionChange()
407
braille.handler.handleCaretMove(self)
409
def event_gainFocus(self, obj, nextHandler):
410
if not isinstance(obj, behaviors.EditableText):
411
# This object isn't part of the editable text; e.g. a graphic.
412
# Report it normally.
415
def event_focusEntered(self, obj, nextHandler):
418
def event_stateChange(self, obj, nextHandler):
421
def event_selection(self, obj, nextHandler):
424
def event_selectionAdd(self, obj, nextHandler):
427
def event_selectionRemove(self, obj, nextHandler):