Package Adapters :: Package ATSPI :: Module TextAdapter
[hide private]
[frames] | no frames]

Source Code for Module Adapters.ATSPI.TextAdapter

  1  ''' 
  2  Defines L{AccAdapt.Adapter}s for AT-SPI accessibles implementing the Text  
  3  interface. 
  4   
  5  @author: Pete Brunet 
  6  @author: Peter Parente 
  7  @author: Brett Clippingdale 
  8  @author: Scott Haeger 
  9  @organization: IBM Corporation 
 10  @copyright: Copyright (c) 2005, 2007 IBM Corporation 
 11  @license: The BSD License 
 12   
 13  All rights reserved. This program and the accompanying materials are made 
 14  available under the terms of the BSD license which accompanies 
 15  this distribution, and is available at 
 16  U{http://www.opensource.org/licenses/bsd-license.php} 
 17  ''' 
 18  from POR import POR 
 19  from AEEvent import * 
 20  from AEInterfaces import * 
 21  from DefaultInfo import DefaultAccInfoAdapter 
 22  from DefaultNav import * 
 23  from DefaultEventHandler import DefaultEventHandlerAdapter 
 24  from pyLinAcc import Constants, Interfaces 
 25  import pyLinAcc, AEConstants 
 26  from i18n import _ 
 27  import time 
 28   
 29  FUDGE_PX = 3 
 30   
31 -def _multiLineCondition(acc):
32 ''' 33 Tests whether an accessible should be treated as a multiline text area or 34 a simple one line text control. 35 36 @param acc: Accessible to test 37 @type acc: L{pyLinAcc.Accessible} 38 ''' 39 Interfaces.IText(acc) 40 r = acc.getRole() 41 c = Constants 42 if r in (c.ROLE_TERMINAL, c.ROLE_PARAGRAPH): 43 # terminals and paragraphs are always multiline 44 # note: this does not account for paragraphs that implement hypertext 45 return True 46 elif r in (c.ROLE_PAGE_TAB, c.ROLE_LABEL, c.ROLE_EDITBAR): 47 # these widgets sometimes show as multiline, but are succinct enough to 48 # treat as single line 49 return False 50 s = acc.getState() 51 if s.contains(c.STATE_SINGLE_LINE): 52 # single line widgets are not multiline (duh) 53 return False 54 # multiline widgets that weren't filtered in previous tests or editable 55 # widgets are considered multiline 56 return s.contains(c.STATE_MULTI_LINE) #or s.contains(c.STATE_EDITABLE)
57
58 -class TextAccInfoAdapter(DefaultAccInfoAdapter):
59 ''' 60 Overrides L{DefaultAccInfoAdapter} to provide information about lines of text 61 as items. Expects the subject to be a L{POR}. 62 63 Adapts accessibles that have a role of terminal or have a state of multiline, 64 single line, or editable and provide the Text interface. 65 Does not adapt L{POR} accessibles with role of page tab. 66 ''' 67 provides = [IAccessibleInfo] 68 69 @staticmethod
70 - def when(subject):
71 ''' 72 Tests if the given subject can be adapted by this class. 73 74 @param subject: L{POR} containing an accessible to test 75 @type subject: L{POR} 76 @return: True when the subject meets the condition named in the docstring 77 for this class, False otherwise 78 @rtype: boolean 79 ''' 80 return _multiLineCondition(subject.accessible)
81 82 @pyLinAcc.errorToLookupError
83 - def getAccItemText(self):
84 ''' 85 Gets the accessible text of the subject if item_offset is None and if that 86 text is not an empty string. If it is empty, gets the accessible name. If 87 item_offset is specified, gets that line of text. 88 89 @return: Accessible text of requested object 90 @rtype: string 91 @raise LookupError: When the subject accessible is dead 92 @raise IndexError: When the item offset is outside the bounds of the number 93 of lines of text in the subject accessible 94 ''' 95 text = Interfaces.IText(self.accessible) 96 if self.item_offset is None: 97 # gets the accessible name of the parent if item offset is None 98 return super(TextAccInfoAdapter, self).getAccName() 99 else: 100 # get the line at the provided offset 101 line, sOff, eOff = \ 102 text.getTextAtOffset(self.item_offset, 103 Constants.TEXT_BOUNDARY_LINE_START) 104 return unicode(line, 'utf-8') 105 return u''
106 107 #@pyLinAcc.errorToLookupError 108 #def getAccRoleName(self): 109 #''' 110 #Gets the accessible role name of the subject. Ignores the given item offset 111 #because the default adapter assumes there are no items. 112 113 #@return: Localized role name 114 #@rtype: string 115 #@raise LookupError: When the accessible object is dead 116 #''' 117 #if self.item_offset is None: 118 ## gets the accessible role name of the parent if item offset is None 119 #return super(TextAccInfoAdapter, self).getAccRoleName() 120 #else: 121 #return _('text line') 122 123 @pyLinAcc.errorToLookupError
124 - def getAccVisualExtents(self):
125 ''' 126 Gets the visual width and height of a character or the entire text area if 127 item offset is None. 128 129 @return: Width and height extents 130 @rtype: 2-tuple of integer 131 @raise LookupError: When the accessible object is dead 132 ''' 133 if self.item_offset is None: 134 return super(TextAccInfoAdapter, self).getAccVisualExtents() 135 off = self.item_offset + self.char_offset 136 text = Interfaces.IText(self.accessible) 137 x, y, w, h = text.getCharacterExtents(off, Constants.DESKTOP_COORDS) 138 return w, h
139 140 @pyLinAcc.errorToLookupError
141 - def getAccVisualPoint(self):
142 ''' 143 Gets the center of a character or the entire text area if item offset is 144 None. 145 146 @return: x,y coordinates 147 @rtype: 2-tuple of integer 148 @raise LookupError: When the accessible object is dead 149 ''' 150 if self.item_offset is None: 151 return super(TextAccInfoAdapter, self).getAccVisualPoint() 152 # compute the offset in the line 153 off = self.item_offset + self.char_offset 154 text = Interfaces.IText(self.accessible) 155 x, y, w, h = text.getCharacterExtents(off, Constants.DESKTOP_COORDS) 156 return x+w/2, y+h/2
157
158 -class TextNavAdapter(DefaultNavAdapter):
159 ''' 160 Overrides L{DefaultNavAdapter} to provide navigation over text lines as 161 items and to avoid traversing text children as separate accessible children 162 in the L{IAccessibleNav} interface. 163 164 Adapts accessibles that have a role of terminal or have a state of multiline, 165 single line, or editable and provide the Text interface. 166 Does not adapt L{POR} accessibles with role of page tab. 167 ''' 168 provides = [IItemNav, IAccessibleNav] 169 170 @staticmethod
171 - def when(subject):
172 ''' 173 Tests if the given subject can be adapted by this class. 174 175 @param subject: L{POR} containing an accessible to test 176 @type subject: L{POR} 177 @return: True when the subject meets the condition named in the docstring 178 for this class, False otherwise 179 @rtype: boolean 180 ''' 181 return _multiLineCondition(subject.accessible)
182 183 @pyLinAcc.errorToLookupError
184 - def _getVisibleItemExtents(self, only_visible):
185 ''' 186 Gets the item offsets of the first and last items in a text control. 187 188 @param only_visible: Only consider the first and last characters visible in 189 the text box (True) or the absolute first and last characters (False)? 190 @type only_visible: boolean 191 @return: First and last item offsets 192 @rtype: 2-tuple of integer 193 @raise LookupError: When the accessible does not implement IComponent 194 ''' 195 acc = self.accessible 196 comp = Interfaces.IComponent(acc) 197 text = Interfaces.IText(acc) 198 if only_visible: 199 e = comp.getExtents(Constants.DESKTOP_COORDS) 200 # get the first item 201 first = text.getOffsetAtPoint(e.x, e.y, Constants.DESKTOP_COORDS) 202 # get the last item 203 last = text.getOffsetAtPoint(e.x+e.width, e.y+e.height, \ 204 Constants.DESKTOP_COORDS) 205 else: 206 first = 0 207 last = text.characterCount-1 208 if first == last or first == -1 or last == -1: 209 # nit: protection against bad bounds reporting 210 first = 0 211 last = text.characterCount-1 212 return first, last
213 214 @pyLinAcc.errorToLookupError
215 - def getNextItem(self, only_visible=True):
216 ''' 217 Gets the next item relative to the one indicated by the L{POR} 218 providing this interface. 219 220 @param only_visible: True when Item in the returned L{POR} must be visible 221 @type only_visible: boolean 222 @return: Point of regard for the beginning of the next item in the same 223 accessible 224 @rtype: L{POR} 225 @raise IndexError: When there is no next item 226 @raise LookupError: When lookup for the next item fails even though it may 227 exist 228 ''' 229 acc = self.accessible 230 off = self.item_offset 231 text = Interfaces.IText(acc) 232 # get the first and last visible character offsets 233 first, last = self._getVisibleItemExtents(only_visible) 234 235 if off is None: 236 # use the first line if we're not on an item yet 237 return POR(acc, first, 0) 238 239 # get the current line start and end offsets 240 line, lstart, lend = \ 241 text.getTextAtOffset(off, Constants.TEXT_BOUNDARY_LINE_START) 242 # get the next line 243 nlstart = lend 244 245 if nlstart < first: 246 # return the starting offset of the first visible line 247 por = POR(acc, first, 0) 248 if nlstart < last: 249 # return the starting offset of the next line 250 por = POR(acc, nlstart, 0) 251 else: 252 # last item 253 raise IndexError 254 return por
255 256 @pyLinAcc.errorToLookupError
257 - def getPrevItem(self, only_visible=True):
258 ''' 259 Gets the previous item relative to the one indicated by the L{POR} 260 providing this interface. 261 262 @param only_visible: True when Item in the returned L{POR} must be visible 263 @type only_visible: boolean 264 @return: Point of regard for the beginning of the previous item in the same 265 accessible 266 @rtype: L{POR} 267 @raise IndexError: When there is no previous item 268 @raise LookupError: When lookup for the previous item fails even though it 269 may exist 270 ''' 271 acc = self.accessible 272 off = self.item_offset 273 text = Interfaces.IText(acc) 274 # get the first and last visible character offsets 275 first, last = self._getVisibleItemExtents(only_visible) 276 277 if off is None: 278 # on the text accessible itself 279 raise IndexError 280 elif off == 0: 281 # return the text area itself; needed to prevent negative indexing 282 return POR(acc, None, 0) 283 284 # get the current line start and end offsets 285 line, lstart, lend = \ 286 text.getTextAtOffset(off, Constants.TEXT_BOUNDARY_LINE_START) 287 # get the prev line 288 line, plstart, plend = \ 289 text.getTextAtOffset(lstart-1, Constants.TEXT_BOUNDARY_LINE_START) 290 291 if plstart > last: 292 # return the starting index of the last line 293 a, last_start, c = \ 294 text.getTextAtOffset(last, Constants.TEXT_BOUNDARY_LINE_START) 295 por = POR(acc, last_start, 0) 296 elif plstart >= first: 297 # return the starting offset of the previous line 298 por = POR(acc, plstart, 0) 299 else: 300 # return the text area itself 301 por = POR(acc, None, 0) 302 return por
303 304 @pyLinAcc.errorToLookupError
305 - def getFirstItem(self, only_visible=True):
306 ''' 307 Gets the first item relative to the one indicated by the L{POR} 308 providing this interface. 309 310 @param only_visible: True when Item in the returned L{POR} must be visible 311 @type only_visible: boolean 312 @return: Point of regard for the beginning of the first item in the same 313 accessible 314 @rtype: L{POR} 315 @raise LookupError: When lookup for the last item fails even though it may 316 exist 317 ''' 318 acc = self.accessible 319 if only_visible: 320 first, last = self._getVisibleItemExtents(only_visible) 321 return POR(acc, first, 0) 322 else: 323 return POR(acc, 0, 0)
324 325 @pyLinAcc.errorToLookupError
326 - def getLastItem(self, only_visible=True):
327 ''' 328 Gets the last item relative to the one indicated by the L{POR} 329 providing this interface. 330 331 @param only_visible: True when Item in the returned L{POR} must be visible 332 @type only_visible: boolean 333 @return: Point of regard for the beginning of the last item in the same 334 accessible 335 @rtype: L{POR} 336 @raise LookupError: When lookup for the last item fails even though it may 337 exist 338 ''' 339 acc = self.accessible 340 text = Interfaces.IText(acc) 341 first, last = self._getVisibleItemExtents(only_visible) 342 # get the starting offset of the last line 343 a, last_start, c = \ 344 text.getTextAtOffset(last-1, Constants.TEXT_BOUNDARY_LINE_START) 345 return POR(acc, last, 0)
346 347 @pyLinAcc.errorToLookupError
348 - def getFirstAccChild(self):
349 ''' 350 Always raises LookupError. Children are exposed through their parent. 351 352 @raise LookupError: Always 353 ''' 354 raise LookupError
355 356 @pyLinAcc.errorToLookupError
357 - def getLastAccChild(self):
358 ''' 359 Always raises LookupError. Children are exposed through their parent. 360 361 @raise LookupError: Always 362 ''' 363 raise LookupError
364 365 @pyLinAcc.errorToLookupError
366 - def getChildAcc(self, index):
367 ''' 368 Always raises LookupError. Children are exposed through their parent. 369 370 @raise LookupError: Always 371 ''' 372 raise LookupError
373
374 -class SimpleTextEventHandlerAdapter(DefaultEventHandlerAdapter):
375 ''' 376 Overrides L{DefaultEventHandlerAdapter} to fire L{AEEvent.CaretChange} 377 events for caret and text events. Expects the subject to be a 378 L{pyLinAcc.Accessible}. 379 380 Adapts subject accessibles that provide the Text interface. 381 ''' 382 provides = [IEventHandler] 383 384 @staticmethod
385 - def when(subject):
386 ''' 387 Tests if the given subject can be adapted by this class. 388 389 @param subject: Accessible to test 390 @type subject: L{pyLinAcc.Accessible} 391 @return: True when the subject meets the condition named in the docstring 392 for this class, False otherwise 393 @rtype: boolean 394 ''' 395 return Interfaces.IText(subject) is not None
396
397 - def _handleTextEvent(self, event, **kwargs):
398 ''' 399 Called when text is inserted or deleted (object:text-changed:insert & 400 object:text-changed:delete). Creates and returns an L{AEEvent.CaretChange} 401 to indicate a change in the caret context. 402 403 L{pyLinAcc.Event.Event}.type.minor is "insert" or "delete". 404 L{pyLinAcc.Event.Event}.detail1 has start offset of text change. 405 L{pyLinAcc.Event.Event}.detail2 has length of text change. 406 L{pyLinAcc.Event.Event}.any_data has text inserted/deleted. 407 408 @param event: Raw text-changed event 409 @type event: L{pyLinAcc.Event.Event} 410 @param kwargs: Parameters to be passed to any created L{AEEvent} 411 @type kwargs: dictionary 412 @return: L{AEEvent.CaretChange} 413 @rtype: tuple of L{AEEvent} 414 ''' 415 # get text interface 416 text = Interfaces.IText(self.subject) 417 cOff = event.detail1 418 # get the text of the caret line, it's starting and ending offsets 419 line, sOff, eOff = \ 420 text.getTextAtOffset(cOff, Constants.TEXT_BOUNDARY_LINE_START) 421 # create a POR with start offset of the line and relative caret offset 422 por = POR(self.subject, sOff, cOff - sOff) 423 424 # create Caret event with current POR, text added/removed, where it changed, 425 # and whether it was inserted. 426 return (CaretChange(por, unicode(event.any_data, 'utf-8'), 427 cOff, (event.type.minor == 'insert'), 428 **kwargs),)
429
430 - def _handleCaretEvent(self, event, **kwargs):
431 ''' 432 Creates and returns an L{AEEvent.CaretChange} indicating the caret moved in 433 the subject accessible. 434 435 L{pyLinAcc.Event.Event}.detail1 has caret offset. 436 437 @param event: Raw caret movement event 438 @type event: L{pyLinAcc.Event.Event} 439 @param kwargs: Parameters to be passed to any created L{AEEvent} 440 @type kwargs: dictionary 441 @return: L{AEEvent.CaretChange} 442 @rtype: tuple of L{AEEvent} 443 ''' 444 # get text interface 445 text = Interfaces.IText(self.subject) 446 # detail1 has new caret position 447 caret_offset = event.detail1 448 449 # 2007-05-14 450 # perform a special check account for end of line differences across 451 # implementations; if the offset is greater than the character count 452 # and the last character is not a newline, correct by going back one 453 # character offset and asking for the line; done to correct problems in 454 # single line entry fields in FF 455 qo = caret_offset 456 if (caret_offset > 0 and 457 caret_offset >= text.characterCount and 458 text.getText(caret_offset-1, -1) != '\n'): 459 qo -= 1 460 461 # get the text of the new caret line, its starting and ending offsets 462 line, sOff, eOff = \ 463 text.getTextAtOffset(qo, Constants.TEXT_BOUNDARY_LINE_START) 464 465 # create a POR with start offset of the line and relative caret offset 466 por = POR(self.subject, sOff, caret_offset - sOff) 467 468 # create Caret event with new POR, line at caret, and offset of caret 469 return (CaretChange(por, unicode(line, 'utf-8'), caret_offset, 470 **kwargs),)
471
472 - def _handleTextSelectionEvent(self, event, **kwargs):
473 ''' 474 Creates and returns a L{AEEvent.SelectorChange} event when text selection 475 changes. 476 477 @param event: Raw caret movement event 478 @type event: L{pyLinAcc.Event.Event} 479 @param kwargs: Parameters to be passed to any created L{AEEvent} 480 @type kwargs: dictionary 481 @return: L{AEEvent.SelectorChange} 482 @rtype: tuple of L{AEEvent} 483 ''' 484 # get text interface 485 text = Interfaces.IText(self.subject) 486 # get selections 487 n = text.getNSelections() 488 # create a POR with start offset of the line and relative caret offset 489 line, start, end = \ 490 text.getTextAtOffset(text.caretOffset,Constants.TEXT_BOUNDARY_LINE_START) 491 por = POR(self.subject, start, text.caretOffset - start) 492 493 if n == 0: 494 # fire an event with no text if there is no selection 495 return (SelectorChange(por, u'', AEConstants.EVENT_CHANGE_TEXT_SELECT, 496 **kwargs),) 497 else: 498 # we don't know which selection changed, so we'll just assume the first 499 # one for the time being; multi-select editors are rare 500 sel = text.getText(*text.getSelection(0)) 501 return (SelectorChange(por, unicode(sel, 'utf-8'), 502 AEConstants.EVENT_CHANGE_TEXT_SELECT, **kwargs),)
503
504 - def _handleFocusEvent(self, event, **kwargs):
505 ''' 506 Creates an L{AEEvent.FocusChange} indicating that the accessible being 507 adapted has gained the focus. Also a L{AEEvent.CaretChange}. This sequence 508 of L{AEEvent}s will be posted by the caller. 509 510 @param event: Raw focus change event 511 @type event: L{pyLinAcc.Event.Event} 512 @param kwargs: Parameters to be passed to any created L{AEEvent} 513 @type kwargs: dictionary 514 @return: L{AEEvent.FocusChange} and L{AEEvent.CaretChange} 515 @rtype: tuple of L{AEEvent} 516 ''' 517 try: 518 text = Interfaces.IEditableText(self.subject) 519 except NotImplementedError: 520 # only fire caret move when the text is editable, else fire selector 521 return super(SimpleTextEventHandlerAdapter, 522 self)._handleFocusEvent(event, **kwargs) 523 524 kwargs['focused'] = True 525 # get the text of the caret line, it's starting and ending offsets 526 line = text.getText(0, -1) 527 # create a POR with start offset of the line and relative caret offset 528 por = POR(self.subject, 0, 0) 529 char_offset = text.caretOffset 530 531 # GTK and Gecko (Firefox) toolkits handle caret events differently. 532 # GTK fails to produce a caret event while Gecko produces one for you. 533 # This code branches on the toolkit type and returns the required events 534 # for each toolkit type. 535 if self.subject.getApplication().toolkitName == 'Gecko': 536 return (FocusChange(por, True, **kwargs),) 537 else: 538 # create a focus event for this POR 539 # and add a caret event to indicate the current position 540 return (FocusChange(por, True, **kwargs), 541 CaretChange(por, unicode(line, 'utf-8'), char_offset, **kwargs))
542
543 -class TextEventHandlerAdapter(SimpleTextEventHandlerAdapter):
544 ''' 545 Overrides L{SimpleTextEventHandlerAdapter} to fire L{AEEvent.CaretChange} 546 events on focus changes for text controls. Expects the subject to be a 547 L{pyLinAcc.Accessible}. 548 549 Adapts subject accessibles that have a role of terminal or have a state of 550 multiline, single line, or editable and provide the Text 551 interface. Does not adapt accessibles with role of page tab. 552 ''' 553 provides = [IEventHandler] 554 555 @staticmethod
556 - def when(subject):
557 ''' 558 Tests if the given subject can be adapted by this class. 559 560 @param subject: Accessible to test 561 @type subject: L{pyLinAcc.Accessible} 562 @return: True when the subject meets the condition named in the docstring 563 for this class, False otherwise 564 @rtype: boolean 565 ''' 566 return _multiLineCondition(subject)
567
568 - def _handleFocusEvent(self, event, **kwargs):
569 ''' 570 Creates an L{AEEvent.FocusChange} indicating that the accessible being 571 adapted has gained the focus. Also a L{AEEvent.CaretChange}. This sequence 572 of L{AEEvent}s will be posted by the caller. 573 574 @param event: Raw focus change event 575 @type event: L{pyLinAcc.Event.Event} 576 @param kwargs: Parameters to be passed to any created L{AEEvent} 577 @type kwargs: dictionary 578 @return: L{AEEvent.FocusChange} and L{AEEvent.CaretChange} 579 @rtype: tuple of L{AEEvent} 580 ''' 581 kwargs['focused'] = True 582 text = Interfaces.IText(self.subject) 583 char_offset = text.caretOffset 584 # get the text of the caret line, it's starting and ending offsets 585 line, sOff, eOff = \ 586 text.getTextAtOffset(char_offset, Constants.TEXT_BOUNDARY_LINE_START) 587 # create a POR with start offset of the line and relative caret offset 588 por = POR(self.subject, sOff, char_offset - sOff) 589 590 # create a focus event for this POR 591 # and add a caret event to indicate the current position 592 return (FocusChange(por, True, **kwargs), 593 CaretChange(por, unicode(line, 'utf-8'), char_offset-sOff, 594 **kwargs))
595