Module compoundDocuments
[hide private]
[frames] | no frames]

Source Code for Module compoundDocuments

  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):
389
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, unit=textInfos.UNIT_LINE, reason=speech.REASON_CARET) 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):
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
429