~stomato463/+junk/nvdajp

« back to all changes in this revision

Viewing changes to source/compoundDocuments.py

  • Committer: Masataka Shinke
  • Date: 2011-10-25 12:35:26 UTC
  • mfrom: (4185 jpmain)
  • mto: This revision was merged to the branch mainline in revision 4211.
  • Revision ID: mshinke@users.sourceforge.jp-20111025123526-ze527a2rl3z0g2ky
lp:~nishimotz/nvdajp/main : 4185 をマージ

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#compoundDocuments.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) 2010 James Teh <jamie@jantrid.net>
 
6
 
 
7
import winUser
 
8
import textInfos
 
9
import controlTypes
 
10
import eventHandler
 
11
from NVDAObjects import NVDAObject
 
12
from editableText import EditableText
 
13
from treeInterceptorHandler import TreeInterceptor
 
14
import speech
 
15
import braille
 
16
from NVDAObjects import behaviors
 
17
 
 
18
class CompoundTextInfo(textInfos.TextInfo):
 
19
 
 
20
        def _getObjectPosition(self, obj):
 
21
                indexes = []
 
22
                rootObj = self.obj.rootNVDAObject
 
23
                while obj and obj != rootObj:
 
24
                        indexes.insert(0, obj.indexInParent)
 
25
                        obj = obj.parent
 
26
                return indexes
 
27
 
 
28
        def compareEndPoints(self, other, which):
 
29
                if which in ("startToStart", "startToEnd"):
 
30
                        selfTi = self._start
 
31
                        selfObj = self._startObj
 
32
                else:
 
33
                        selfTi = self._end
 
34
                        selfObj = self._endObj
 
35
                if which in ("startToStart", "endToStart"):
 
36
                        otherTi = other._start
 
37
                        otherObj = other._startObj
 
38
                else:
 
39
                        otherTi = other._end
 
40
                        otherObj = other._endObj
 
41
 
 
42
                if selfObj == otherObj:
 
43
                        # Same object, so just compare the two TextInfos normally.
 
44
                        return selfTi.compareEndPoints(otherTi, which)
 
45
 
 
46
                # Different objects, so we have to compare the hierarchical positions of the objects.
 
47
                return cmp(self._getObjectPosition(selfObj), other._getObjectPosition(otherObj))
 
48
 
 
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
 
55
                        if obj:
 
56
                                self._startObj = obj
 
57
                                self._start = obj.makeTextInfo(textInfos.POSITION_FIRST)
 
58
 
 
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
 
64
                else:
 
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")
 
69
 
 
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
 
85
                else:
 
86
                        raise ValueError("which=%s" % which)
 
87
                self._normalizeStartAndEnd()
 
88
 
 
89
        def collapse(self, end=False):
 
90
                if end:
 
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
 
97
                                if obj:
 
98
                                        self._endObj = obj
 
99
                                        self._end = obj.makeTextInfo(textInfos.POSITION_FIRST)
 
100
                                else:
 
101
                                        # There are no more objects, so just collapse to the end of this object.
 
102
                                        self._end.collapse(end=True)
 
103
                        else:
 
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
 
108
 
 
109
                else:
 
110
                        self._start.collapse()
 
111
                        self._end = self._start
 
112
                        self._endObj = self._startObj
 
113
 
 
114
        def copy(self):
 
115
                return self.__class__(self.obj, self)
 
116
 
 
117
        def updateCaret(self):
 
118
                self._startObj.setFocus()
 
119
                self._start.updateCaret()
 
120
 
 
121
        def updateSelection(self):
 
122
                self._startObj.setFocus()
 
123
                self._start.updateSelection()
 
124
                if self._end is not self._start:
 
125
                        self._end.updateSelection()
 
126
 
 
127
        def _get_bookmark(self):
 
128
                return self.copy()
 
129
 
 
130
        def _get_NVDAObjectAtStart(self):
 
131
                return self._startObj
 
132
 
 
133
        def _get_pointAtStart(self):
 
134
                return self._start.pointAtStart
 
135
 
 
136
        def _getControlFieldForObject(self, obj, ignoreEditableText=True):
 
137
                role = obj.role
 
138
                if ignoreEditableText and role in (controlTypes.ROLE_PARAGRAPH, controlTypes.ROLE_EDITABLETEXT):
 
139
                        # This is basically just a text node.
 
140
                        return None
 
141
                field = textInfos.ControlField()
 
142
                field["role"] = obj.role
 
143
                states = obj.states
 
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
 
160
                return field
 
161
 
 
162
        def _iterTextWithEmbeddedObjects(self, text, ti, fieldStart, textLength=None):
 
163
                if textLength is None:
 
164
                        textLength = len(text)
 
165
                chunkStart = 0
 
166
                while chunkStart < textLength:
 
167
                        try:
 
168
                                chunkEnd = text.index(u"\uFFFC", chunkStart)
 
169
                        except ValueError:
 
170
                                yield text[chunkStart:]
 
171
                                break
 
172
                        if chunkStart != chunkEnd:
 
173
                                yield text[chunkStart:chunkEnd]
 
174
                        yield ti.getEmbeddedObject(fieldStart + chunkEnd)
 
175
                        chunkStart = chunkEnd + 1
 
176
 
 
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
 
179
 
 
180
        def __ne__(self, other):
 
181
                return not self == other
 
182
 
 
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)
 
186
 
 
187
        def __init__(self, obj, position):
 
188
                super(TreeCompoundTextInfo, self).__init__(obj, position)
 
189
                rootObj = obj.rootNVDAObject
 
190
                if isinstance(position, NVDAObject):
 
191
                        # FIXME
 
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
 
198
                        else:
 
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
 
230
                        else:
 
231
                                self._end = self._endObj.makeTextInfo(position)
 
232
                else:
 
233
                        raise NotImplementedError
 
234
 
 
235
        def _findContentDescendant(self, obj):
 
236
                while obj and controlTypes.STATE_FOCUSABLE not in obj.states:
 
237
                        obj = obj.firstChild
 
238
                return obj
 
239
 
 
240
        def _getTextInfos(self):
 
241
                yield self._start
 
242
                if self._startObj == self._endObj:
 
243
                        return
 
244
                obj = self._startObj.flowsTo
 
245
                while obj and obj != self._endObj:
 
246
                        yield obj.makeTextInfo(textInfos.POSITION_ALL)
 
247
                        obj = obj.flowsTo
 
248
                yield self._end
 
249
 
 
250
        def _get_text(self):
 
251
                return "".join(ti.text for ti in self._getTextInfos())
 
252
 
 
253
        def getTextWithFields(self, formatConfig=None):
 
254
                # Get the initial control fields.
 
255
                fields = []
 
256
                rootObj = self.obj.rootNVDAObject
 
257
                obj = self._startObj
 
258
                while obj and obj != rootObj:
 
259
                        field = self._getControlFieldForObject(obj)
 
260
                        if field:
 
261
                                fields.insert(0, textInfos.FieldCommand("controlStart", field))
 
262
                        obj = obj.parent
 
263
 
 
264
                for ti in self._getTextInfos():
 
265
                        fieldStart = 0
 
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):
 
271
                                                        fields.append(chunk)
 
272
                                                else:
 
273
                                                        controlField = self._getControlFieldForObject(chunk, ignoreEditableText=False)
 
274
                                                        controlField["alwaysReportName"] = True
 
275
                                                        fields.extend((textInfos.FieldCommand("controlStart", controlField),
 
276
                                                                u"\uFFFC",
 
277
                                                                textInfos.FieldCommand("controlEnd", None)))
 
278
                                        fieldStart += textLength
 
279
 
 
280
                                else:
 
281
                                        fields.append(field)
 
282
                return fields
 
283
 
 
284
        def expand(self, unit):
 
285
                if unit == textInfos.UNIT_READINGCHUNK:
 
286
                        unit = textInfos.UNIT_LINE
 
287
 
 
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
 
293
                else:
 
294
                        raise NotImplementedError
 
295
 
 
296
        def move(self, unit, direction, endPoint=None):
 
297
                if direction == 0:
 
298
                        return 0
 
299
 
 
300
                if unit == textInfos.UNIT_READINGCHUNK:
 
301
                        unit = textInfos.UNIT_LINE
 
302
 
 
303
                if unit not in self.SINGLE_TEXTINFO_UNITS:
 
304
                        raise NotImplementedError
 
305
 
 
306
                if not endPoint or endPoint == "start":
 
307
                        moveTi = self._start
 
308
                        moveObj = self._startObj
 
309
                elif endPoint == "end":
 
310
                        moveTi = self._end
 
311
                        moveObj = self._endObj
 
312
 
 
313
                goPrevious = direction < 0
 
314
                remainingMovement = direction
 
315
                count0MoveAs = 0
 
316
                while True:
 
317
                        movement = moveTi.move(unit, remainingMovement, endPoint=endPoint)
 
318
                        if movement == 0 and count0MoveAs != 0:
 
319
                                movement = count0MoveAs
 
320
                        remainingMovement -= movement
 
321
                        count0MoveAs = 0
 
322
                        if remainingMovement == 0:
 
323
                                # The requested destination was within moveTi.
 
324
                                break
 
325
 
 
326
                        # The requested destination is not in this object, so move to the next.
 
327
                        tempObj = moveObj.flowsFrom if goPrevious else moveObj.flowsTo
 
328
                        if tempObj:
 
329
                                moveObj = tempObj
 
330
                        else:
 
331
                                break
 
332
                        if goPrevious:
 
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.
 
338
                                count0MoveAs = -1
 
339
                        else:
 
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.
 
346
                                        count0MoveAs = 1
 
347
                                else:
 
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.
 
352
                                                break
 
353
 
 
354
                if not endPoint or endPoint == "start":
 
355
                        self._start = moveTi
 
356
                        self._startObj = moveObj
 
357
                if not endPoint or endPoint == "end":
 
358
                        self._end = moveTi
 
359
                        self._endObj = moveObj
 
360
                self._normalizeStartAndEnd()
 
361
 
 
362
                return direction - remainingMovement
 
363
 
 
364
class CompoundDocument(EditableText, TreeInterceptor):
 
365
        TextInfo = TreeCompoundTextInfo
 
366
 
 
367
        def __init__(self, rootNVDAObject):
 
368
                super(CompoundDocument, self).__init__(rootNVDAObject)
 
369
 
 
370
        def _get_isAlive(self):
 
371
                root = self.rootNVDAObject
 
372
                return winUser.isWindow(root.windowHandle)
 
373
 
 
374
        def __contains__(self, obj):
 
375
                root = self.rootNVDAObject
 
376
                while obj:
 
377
                        if obj.windowHandle != root.windowHandle:
 
378
                                return False
 
379
                        if obj == root:
 
380
                                return True
 
381
                        obj = obj.parent
 
382
                return False
 
383
 
 
384
        def makeTextInfo(self, position):
 
385
                return self.TextInfo(self, position)
 
386
 
 
387
        def _get_caretObject(self):
 
388
                return eventHandler.lastQueuedFocusObject
 
389
 
 
390
        def event_treeInterceptor_gainFocus(self):
 
391
                speech.speakObject(self.rootNVDAObject, reason=speech.REASON_FOCUS)
 
392
                try:
 
393
                        info = self.makeTextInfo(textInfos.POSITION_SELECTION)
 
394
                except RuntimeError:
 
395
                        pass
 
396
                else:
 
397
                        if info.isCollapsed:
 
398
                                info.expand(textInfos.UNIT_LINE)
 
399
                                speech.speakTextInfo(info)
 
400
                        else:
 
401
                                speech.speakSelectionMessage(_("selected %s"), info.text)
 
402
                        braille.handler.handleGainFocus(self)
 
403
                        self.initAutoSelectDetection()
 
404
 
 
405
        def event_caret(self, obj, nextHandler):
 
406
                self.detectPossibleSelectionChange()
 
407
                braille.handler.handleCaretMove(self)
 
408
 
 
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.
 
413
                        nextHandler()
 
414
 
 
415
        def event_focusEntered(self, obj, nextHandler):
 
416
                pass
 
417
 
 
418
        def event_stateChange(self, obj, nextHandler):
 
419
                pass
 
420
 
 
421
        def event_selection(self, obj, nextHandler):
 
422
                pass
 
423
 
 
424
        def event_selectionAdd(self, obj, nextHandler):
 
425
                pass
 
426
 
 
427
        def event_selectionRemove(self, obj, nextHandler):
 
428
                pass