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
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
44
45 return True
46 elif r in (c.ROLE_PAGE_TAB, c.ROLE_LABEL, c.ROLE_EDITBAR):
47
48
49 return False
50 s = acc.getState()
51 if s.contains(c.STATE_SINGLE_LINE):
52
53 return False
54
55
56 return s.contains(c.STATE_MULTI_LINE)
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
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
98 return super(TextAccInfoAdapter, self).getAccName()
99 else:
100
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123 @pyLinAcc.errorToLookupError
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
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
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
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
201 first = text.getOffsetAtPoint(e.x, e.y, Constants.DESKTOP_COORDS)
202
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
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
233 first, last = self._getVisibleItemExtents(only_visible)
234
235 if off is None:
236
237 return POR(acc, first, 0)
238
239
240 line, lstart, lend = \
241 text.getTextAtOffset(off, Constants.TEXT_BOUNDARY_LINE_START)
242
243 nlstart = lend
244
245 if nlstart < first:
246
247 por = POR(acc, first, 0)
248 if nlstart < last:
249
250 por = POR(acc, nlstart, 0)
251 else:
252
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
275 first, last = self._getVisibleItemExtents(only_visible)
276
277 if off is None:
278
279 raise IndexError
280 elif off == 0:
281
282 return POR(acc, None, 0)
283
284
285 line, lstart, lend = \
286 text.getTextAtOffset(off, Constants.TEXT_BOUNDARY_LINE_START)
287
288 line, plstart, plend = \
289 text.getTextAtOffset(lstart-1, Constants.TEXT_BOUNDARY_LINE_START)
290
291 if plstart > last:
292
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
298 por = POR(acc, plstart, 0)
299 else:
300
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
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
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
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
416 text = Interfaces.IText(self.subject)
417 cOff = event.detail1
418
419 line, sOff, eOff = \
420 text.getTextAtOffset(cOff, Constants.TEXT_BOUNDARY_LINE_START)
421
422 por = POR(self.subject, sOff, cOff - sOff)
423
424
425
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
445 text = Interfaces.IText(self.subject)
446
447 caret_offset = event.detail1
448
449
450
451
452
453
454
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
462 line, sOff, eOff = \
463 text.getTextAtOffset(qo, Constants.TEXT_BOUNDARY_LINE_START)
464
465
466 por = POR(self.subject, sOff, caret_offset - sOff)
467
468
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
485 text = Interfaces.IText(self.subject)
486
487 n = text.getNSelections()
488
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
495 return (SelectorChange(por, u'', AEConstants.EVENT_CHANGE_TEXT_SELECT,
496 **kwargs),)
497 else:
498
499
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
521 return super(SimpleTextEventHandlerAdapter,
522 self)._handleFocusEvent(event, **kwargs)
523
524 kwargs['focused'] = True
525
526 line = text.getText(0, -1)
527
528 por = POR(self.subject, 0, 0)
529 char_offset = text.caretOffset
530
531
532
533
534
535 if self.subject.getApplication().toolkitName == 'Gecko':
536 return (FocusChange(por, True, **kwargs),)
537 else:
538
539
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
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
585 line, sOff, eOff = \
586 text.getTextAtOffset(char_offset, Constants.TEXT_BOUNDARY_LINE_START)
587
588 por = POR(self.subject, sOff, char_offset - sOff)
589
590
591
592 return (FocusChange(por, True, **kwargs),
593 CaretChange(por, unicode(line, 'utf-8'), char_offset-sOff,
594 **kwargs))
595