1 |
|
# Orca |
2 |
|
# |
3 |
|
# Copyright 2005-2006 Sun Microsystems Inc. |
4 |
|
# |
5 |
|
# This library is free software; you can redistribute it and/or |
6 |
|
# modify it under the terms of the GNU Library General Public |
7 |
|
# License as published by the Free Software Foundation; either |
8 |
|
# version 2 of the License, or (at your option) any later version. |
9 |
|
# |
10 |
|
# This library is distributed in the hope that it will be useful, |
11 |
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 |
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 |
|
# Library General Public License for more details. |
14 |
|
# |
15 |
|
# You should have received a copy of the GNU Library General Public |
16 |
|
# License along with this library; if not, write to the |
17 |
|
# Free Software Foundation, Inc., 59 Temple Place - Suite 330, |
18 |
|
# Boston, MA 02111-1307, USA. |
19 |
|
|
20 |
1 |
"""Provides the default implementation for flat review for Orca.""" |
21 |
|
|
22 |
1 |
__id__ = "$Id: flat_review.py 2126 2007-03-06 21:35:17Z richb $" |
23 |
1 |
__version__ = "$Revision: 2126 $" |
24 |
1 |
__date__ = "$Date: 2007-03-06 13:35:17 -0800 (Tue, 06 Mar 2007) $" |
25 |
1 |
__copyright__ = "Copyright (c) 2005-2006 Sun Microsystems Inc." |
26 |
1 |
__license__ = "LGPL" |
27 |
|
|
28 |
1 |
import re |
29 |
1 |
import sys |
30 |
|
|
31 |
1 |
import atspi |
32 |
1 |
import braille |
33 |
1 |
import debug |
34 |
1 |
import eventsynthesizer |
35 |
1 |
import orca_state |
36 |
1 |
import rolenames |
37 |
|
|
38 |
|
# [[[WDW - HACK Regular expression to split strings on whitespace |
39 |
|
# boundaries, which is what we'll use for word dividers instead of |
40 |
|
# living at the whim of whomever decided to implement the AT-SPI |
41 |
|
# interfaces for their toolkit or app.]]] |
42 |
|
# |
43 |
1 |
whitespace_re = re.compile(r'(\s+)', re.DOTALL | re.IGNORECASE | re.M) |
44 |
|
|
45 |
2 |
class Char: |
46 |
|
"""Represents a single char of an Accessibility_Text object.""" |
47 |
|
|
48 |
1 |
def __init__(self, |
49 |
|
word, |
50 |
|
index, |
51 |
|
string, |
52 |
|
x, y, width, height): |
53 |
|
"""Creates a new char. |
54 |
|
|
55 |
|
Arguments: |
56 |
|
- word: the Word instance this belongs to |
57 |
|
- index: the index of this char in the word |
58 |
|
- string: the actual char |
59 |
|
- x, y, width, height: the extents of this Char on the screen |
60 |
|
""" |
61 |
|
|
62 |
33 |
self.word = word |
63 |
33 |
self.string = string |
64 |
33 |
self.index = index |
65 |
33 |
self.x = x |
66 |
33 |
self.y = y |
67 |
33 |
self.width = width |
68 |
33 |
self.height = height |
69 |
|
|
70 |
2 |
class Word: |
71 |
|
"""Represents a single word of an Accessibility_Text object, or |
72 |
|
the entire name of an Image or Component if the associated object |
73 |
|
does not implement the Accessibility_Text interface. As a rule of |
74 |
|
thumb, all words derived from an Accessibility_Text interface will |
75 |
|
start with the word and will end with all chars up to the |
76 |
|
beginning of the next word. That is, whitespace and punctuation |
77 |
|
will usually be tacked on to the end of words.""" |
78 |
|
|
79 |
1 |
def __init__(self, |
80 |
|
zone, |
81 |
|
index, |
82 |
|
startOffset, |
83 |
|
string, |
84 |
|
x, y, width, height): |
85 |
|
"""Creates a new Word. |
86 |
|
|
87 |
|
Arguments: |
88 |
|
- zone: the Zone instance this belongs to |
89 |
|
- index: the index of this word in the Zone |
90 |
|
- string: the actual string |
91 |
|
- x, y, width, height: the extents of this Char on the screen""" |
92 |
|
|
93 |
19 |
self.zone = zone |
94 |
19 |
self.index = index |
95 |
19 |
self.startOffset = startOffset |
96 |
19 |
self.string = string |
97 |
19 |
self.length = len(string.decode("UTF-8")) |
98 |
19 |
self.x = x |
99 |
19 |
self.y = y |
100 |
19 |
self.width = width |
101 |
19 |
self.height = height |
102 |
|
|
103 |
1 |
def __getattr__(self, attr): |
104 |
|
"""Used for lazily determining the chars of a word. We do |
105 |
|
this to reduce the total number of round trip calls to the app, |
106 |
|
and to also spread the round trip calls out over the lifetime |
107 |
|
of a flat review context. |
108 |
|
|
109 |
|
Arguments: |
110 |
|
- attr: a string indicating the attribute name to retrieve |
111 |
|
|
112 |
|
Returns the value of the given attribute. |
113 |
|
""" |
114 |
|
|
115 |
7 |
if attr == "chars": |
116 |
7 |
if isinstance(self.zone, TextZone): |
117 |
7 |
text = self.zone.accessible.text |
118 |
7 |
self.chars = [] |
119 |
7 |
i = 0 |
120 |
40 |
while i < self.length: |
121 |
33 |
[char, startOffset, endOffset] = text.getTextAtOffset( |
122 |
|
self.startOffset + i, |
123 |
33 |
atspi.Accessibility.TEXT_BOUNDARY_CHAR) |
124 |
33 |
[x, y, width, height] = text.getRangeExtents( |
125 |
|
startOffset, |
126 |
33 |
endOffset, |
127 |
33 |
0) |
128 |
33 |
self.chars.append(Char(self, |
129 |
33 |
i, |
130 |
33 |
char, |
131 |
33 |
x, y, width, height)) |
132 |
33 |
i += 1 |
133 |
|
else: |
134 |
0 |
self.chars = None |
135 |
7 |
return self.chars |
136 |
0 |
elif attr.startswith('__') and attr.endswith('__'): |
137 |
0 |
raise AttributeError, attr |
138 |
|
else: |
139 |
0 |
return self.__dict__[attr] |
140 |
|
|
141 |
2 |
class Zone: |
142 |
|
"""Represents text that is a portion of a single horizontal line.""" |
143 |
|
|
144 |
1 |
def __init__(self, |
145 |
|
accessible, |
146 |
|
string, |
147 |
|
x, y, |
148 |
|
width, height): |
149 |
|
"""Creates a new Zone, which is a horizontal region of text. |
150 |
|
|
151 |
|
Arguments: |
152 |
|
- accessible: the Accessible associated with this Zone |
153 |
|
- string: the string being displayed for this Zone |
154 |
|
- extents: x, y, width, height in screen coordinates |
155 |
|
""" |
156 |
|
|
157 |
355 |
self.accessible = accessible |
158 |
355 |
self.string = string |
159 |
355 |
self.length = len(string.decode("UTF-8")) |
160 |
355 |
self.x = x |
161 |
355 |
self.y = y |
162 |
355 |
self.width = width |
163 |
355 |
self.height = height |
164 |
|
|
165 |
1 |
def __getattr__(self, attr): |
166 |
|
"""Used for lazily determining the words in a Zone. |
167 |
|
|
168 |
|
Arguments: |
169 |
|
- attr: a string indicating the attribute name to retrieve |
170 |
|
|
171 |
|
Returns the value of the given attribute. |
172 |
|
""" |
173 |
|
|
174 |
0 |
if attr == "words": |
175 |
0 |
self.words = [] |
176 |
0 |
return self.words |
177 |
0 |
elif attr.startswith('__') and attr.endswith('__'): |
178 |
0 |
raise AttributeError, attr |
179 |
|
else: |
180 |
0 |
return self.__dict__[attr] |
181 |
|
|
182 |
1 |
def onSameLine(self, zone): |
183 |
|
"""Returns True if this Zone is on the same horiztonal line as |
184 |
|
the given zone.""" |
185 |
|
|
186 |
4129 |
highestBottom = min(self.y + self.height, zone.y + zone.height) |
187 |
4129 |
lowestTop = max(self.y, zone.y) |
188 |
|
|
189 |
|
# If we do overlap, lets see how much. We'll require a 25% overlap |
190 |
|
# for now... |
191 |
|
# |
192 |
4129 |
if lowestTop < highestBottom: |
193 |
2924 |
overlapAmount = highestBottom - lowestTop |
194 |
2924 |
shortestHeight = min(self.height, zone.height) |
195 |
2924 |
return ((1.0 * overlapAmount) / shortestHeight) > 0.25 |
196 |
|
else: |
197 |
1205 |
return False |
198 |
|
|
199 |
1 |
def getWordAtOffset(self, charOffset): |
200 |
0 |
word = None |
201 |
0 |
offset = 0 |
202 |
0 |
for word in self.words: |
203 |
0 |
nextOffset = offset + len(word.string.decode("UTF-8")) |
204 |
0 |
if nextOffset > charOffset: |
205 |
0 |
return [word, charOffset - offset] |
206 |
|
else: |
207 |
0 |
offset = nextOffset |
208 |
|
|
209 |
0 |
return [word, offset] |
210 |
|
|
211 |
2 |
class TextZone(Zone): |
212 |
|
"""Represents Accessibility_Text that is a portion of a single |
213 |
|
horizontal line.""" |
214 |
|
|
215 |
1 |
def __init__(self, |
216 |
|
accessible, |
217 |
|
startOffset, |
218 |
|
string, |
219 |
|
x, y, |
220 |
|
width, height): |
221 |
|
"""Creates a new Zone, which is a horizontal region of text. |
222 |
|
|
223 |
|
Arguments: |
224 |
|
- accessible: the Accessible associated with this Zone |
225 |
|
- startOffset: the index of the char in the Accessibility_Text |
226 |
|
interface where this Zone starts |
227 |
|
- string: the string being displayed for this Zone |
228 |
|
- extents: x, y, width, height in screen coordinates |
229 |
|
""" |
230 |
|
|
231 |
90 |
Zone.__init__(self, accessible, string, x, y, width, height) |
232 |
90 |
self.startOffset = startOffset |
233 |
|
|
234 |
1 |
def __getattr__(self, attr): |
235 |
|
"""Used for lazily determining the words in a Zone. The words |
236 |
|
will either be all whitespace (interword boundaries) or actual |
237 |
|
words. To determine if a Word is whitespace, use |
238 |
|
word.string.isspace() |
239 |
|
|
240 |
|
Arguments: |
241 |
|
- attr: a string indicating the attribute name to retrieve |
242 |
|
|
243 |
|
Returns the value of the given attribute. |
244 |
|
""" |
245 |
|
|
246 |
15 |
if attr == "words": |
247 |
15 |
text = self.accessible.text |
248 |
15 |
self.words = [] |
249 |
15 |
wordIndex = 0 |
250 |
15 |
offset = self.startOffset |
251 |
50 |
for string in whitespace_re.split(self.string): |
252 |
35 |
if len(string): |
253 |
19 |
endOffset = offset + len(string.decode("UTF-8")) |
254 |
19 |
[x, y, width, height] = text.getRangeExtents( |
255 |
|
offset, |
256 |
19 |
endOffset, |
257 |
19 |
0) |
258 |
19 |
word = Word(self, |
259 |
19 |
wordIndex, |
260 |
19 |
offset, |
261 |
19 |
string, |
262 |
19 |
x, y, width, height) |
263 |
19 |
self.words.append(word) |
264 |
19 |
wordIndex += 1 |
265 |
19 |
offset = endOffset |
266 |
|
|
267 |
15 |
return self.words |
268 |
|
|
269 |
0 |
elif attr.startswith('__') and attr.endswith('__'): |
270 |
0 |
raise AttributeError, attr |
271 |
|
else: |
272 |
0 |
return self.__dict__[attr] |
273 |
|
|
274 |
2 |
class Line: |
275 |
|
"""A Line is a single line across a window and is composed of Zones.""" |
276 |
|
|
277 |
1 |
def __init__(self, |
278 |
|
index, |
279 |
|
zones): |
280 |
|
"""Creates a new Line, which is a horizontal region of text. |
281 |
|
|
282 |
|
Arguments: |
283 |
|
- index: the index of this Line in the window |
284 |
|
- zones: the Zones that make up this line |
285 |
|
""" |
286 |
|
|
287 |
69 |
self.index = index |
288 |
69 |
self.zones = zones |
289 |
|
|
290 |
69 |
bounds = None |
291 |
69 |
self.string = "" |
292 |
424 |
for zone in self.zones: |
293 |
355 |
if not bounds: |
294 |
69 |
bounds = [zone.x, zone.y, |
295 |
|
zone.x + zone.width, zone.y + zone.height] |
296 |
|
else: |
297 |
286 |
bounds[0] = min(bounds[0], zone.x) |
298 |
286 |
bounds[1] = min(bounds[1], zone.y) |
299 |
286 |
bounds[2] = max(bounds[2], zone.x + zone.width) |
300 |
286 |
bounds[3] = max(bounds[3], zone.y + zone.height) |
301 |
355 |
if len(zone.string): |
302 |
355 |
if len(self.string): |
303 |
286 |
self.string += " " |
304 |
355 |
self.string += zone.string |
305 |
|
|
306 |
69 |
if not bounds: |
307 |
0 |
bounds = [-1, -1, -1, -1] |
308 |
|
|
309 |
69 |
self.x = bounds[0] |
310 |
69 |
self.y = bounds[1] |
311 |
69 |
self.width = bounds[2] - bounds[0] |
312 |
69 |
self.height = bounds[3] - bounds[1] |
313 |
|
|
314 |
69 |
self.brailleRegions = None |
315 |
|
|
316 |
1 |
def getBrailleRegions(self): |
317 |
23 |
if not self.brailleRegions: |
318 |
10 |
self.brailleRegions = [] |
319 |
10 |
brailleOffset = 0 |
320 |
26 |
for zone in self.zones: |
321 |
16 |
if (zone.accessible.role == rolenames.ROLE_TEXT) \ |
322 |
|
or (zone.accessible.role == rolenames.ROLE_PASSWORD_TEXT) \ |
323 |
|
or (zone.accessible.role == rolenames.ROLE_TERMINAL): |
324 |
9 |
region = braille.ReviewText(zone.accessible, |
325 |
9 |
zone.string, |
326 |
9 |
zone.startOffset, |
327 |
9 |
zone) |
328 |
|
else: |
329 |
7 |
region = braille.ReviewComponent(zone.accessible, |
330 |
7 |
zone.string, |
331 |
7 |
0, # cursor offset |
332 |
7 |
zone) |
333 |
16 |
if len(self.brailleRegions): |
334 |
6 |
pad = braille.Region(" ") |
335 |
6 |
pad.brailleOffset = brailleOffset |
336 |
6 |
self.brailleRegions.append(pad) |
337 |
6 |
brailleOffset += 1 |
338 |
|
|
339 |
16 |
zone.brailleRegion = region |
340 |
16 |
region.brailleOffset = brailleOffset |
341 |
16 |
self.brailleRegions.append(region) |
342 |
|
|
343 |
16 |
brailleOffset += len(region.string.decode("UTF-8")) |
344 |
|
|
345 |
10 |
if len(self.brailleRegions): |
346 |
10 |
pad = braille.Region(" ") |
347 |
10 |
pad.brailleOffset = brailleOffset |
348 |
10 |
self.brailleRegions.append(pad) |
349 |
10 |
brailleOffset += 1 |
350 |
10 |
eol = braille.Region("$l") |
351 |
10 |
eol.brailleOffset = brailleOffset |
352 |
10 |
self.brailleRegions.append(eol) |
353 |
|
|
354 |
23 |
return self.brailleRegions |
355 |
|
|
356 |
2 |
class Context: |
357 |
|
"""Information regarding where a user happens to be exploring |
358 |
|
right now. |
359 |
|
""" |
360 |
|
|
361 |
1 |
ZONE = 0 |
362 |
1 |
CHAR = 1 |
363 |
1 |
WORD = 2 |
364 |
1 |
LINE = 3 # includes all zones on same line |
365 |
1 |
WINDOW = 4 |
366 |
|
|
367 |
1 |
WRAP_NONE = 0 |
368 |
1 |
WRAP_LINE = 1 << 0 |
369 |
1 |
WRAP_TOP_BOTTOM = 1 << 1 |
370 |
1 |
WRAP_ALL = (WRAP_LINE | WRAP_TOP_BOTTOM) |
371 |
|
|
372 |
1 |
def __init__(self, script): |
373 |
|
"""Create a new Context that will be used for handling flat |
374 |
|
review mode. |
375 |
|
""" |
376 |
|
|
377 |
5 |
self.script = script |
378 |
|
|
379 |
5 |
if (not orca_state.locusOfFocus) \ |
380 |
|
or (orca_state.locusOfFocus.app != self.script.app): |
381 |
0 |
self.lines = [] |
382 |
|
else: |
383 |
|
# We want to stop at the window or frame or equivalent level. |
384 |
|
# |
385 |
5 |
obj = orca_state.locusOfFocus |
386 |
11 |
while obj \ |
387 |
|
and obj.parent \ |
388 |
|
and (obj.parent.role != rolenames.ROLE_APPLICATION) \ |
389 |
|
and (obj != obj.parent): |
390 |
6 |
obj = obj.parent |
391 |
5 |
if obj: |
392 |
5 |
self.lines = self.clusterZonesByLine(self.getShowingZones(obj)) |
393 |
|
else: |
394 |
0 |
self.lines = [] |
395 |
|
|
396 |
5 |
currentLineIndex = 0 |
397 |
5 |
currentZoneIndex = 0 |
398 |
5 |
currentWordIndex = 0 |
399 |
5 |
currentCharIndex = 0 |
400 |
|
|
401 |
5 |
if orca_state.locusOfFocus and \ |
402 |
|
orca_state.locusOfFocus.role == rolenames.ROLE_TABLE_CELL: |
403 |
0 |
searchZone = orca_state.activeScript.getRealActiveDescendant(\ |
404 |
|
orca_state.locusOfFocus) |
405 |
|
else: |
406 |
5 |
searchZone = orca_state.locusOfFocus |
407 |
|
|
408 |
5 |
foundZoneWithFocus = False |
409 |
50 |
while currentLineIndex < len(self.lines): |
410 |
46 |
line = self.lines[currentLineIndex] |
411 |
46 |
currentZoneIndex = 0 |
412 |
376 |
while currentZoneIndex < len(line.zones): |
413 |
331 |
zone = line.zones[currentZoneIndex] |
414 |
331 |
if zone.accessible == searchZone: |
415 |
1 |
foundZoneWithFocus = True |
416 |
1 |
break |
417 |
|
else: |
418 |
330 |
currentZoneIndex += 1 |
419 |
46 |
if foundZoneWithFocus: |
420 |
1 |
break |
421 |
|
else: |
422 |
45 |
currentLineIndex += 1 |
423 |
|
|
424 |
|
# Fallback to the first Zone if we didn't find anything. |
425 |
|
# |
426 |
5 |
if not foundZoneWithFocus: |
427 |
4 |
currentLineIndex = 0 |
428 |
4 |
currentZoneIndex = 0 |
429 |
1 |
elif isinstance(zone, TextZone): |
430 |
|
# If we're on an accessible text object, try to set the |
431 |
|
# review cursor to the caret position of that object. |
432 |
|
# |
433 |
1 |
accessible = zone.accessible |
434 |
1 |
lineIndex = currentLineIndex |
435 |
1 |
zoneIndex = currentZoneIndex |
436 |
1 |
caretOffset = zone.accessible.text.caretOffset |
437 |
1 |
foundZoneWithCaret = False |
438 |
1 |
checkForEOF = False |
439 |
2 |
while lineIndex < len(self.lines): |
440 |
2 |
line = self.lines[lineIndex] |
441 |
4 |
while zoneIndex < len(line.zones): |
442 |
3 |
zone = line.zones[zoneIndex] |
443 |
3 |
if zone.accessible == accessible: |
444 |
2 |
if (caretOffset >= zone.startOffset): |
445 |
2 |
if (caretOffset \ |
446 |
|
< (zone.startOffset + zone.length)): |
447 |
1 |
foundZoneWithCaret = True |
448 |
1 |
break |
449 |
1 |
elif (caretOffset \ |
450 |
|
== (zone.startOffset + zone.length)): |
451 |
0 |
checkForEOF = True |
452 |
0 |
lineToCheck = lineIndex |
453 |
0 |
zoneToCheck = zoneIndex |
454 |
2 |
zoneIndex += 1 |
455 |
2 |
if foundZoneWithCaret: |
456 |
1 |
currentLineIndex = lineIndex |
457 |
1 |
currentZoneIndex = zoneIndex |
458 |
1 |
currentWordIndex = 0 |
459 |
1 |
currentCharIndex = 0 |
460 |
1 |
offset = zone.startOffset |
461 |
2 |
while currentWordIndex < len(zone.words): |
462 |
2 |
word = zone.words[currentWordIndex] |
463 |
2 |
if (word.length + offset) > caretOffset: |
464 |
1 |
currentCharIndex = caretOffset - offset |
465 |
1 |
break |
466 |
|
else: |
467 |
1 |
currentWordIndex += 1 |
468 |
1 |
offset += word.length |
469 |
1 |
break |
470 |
|
else: |
471 |
1 |
zoneIndex = 0 |
472 |
1 |
lineIndex += 1 |
473 |
1 |
atEOF = not foundZoneWithCaret and checkForEOF |
474 |
1 |
if atEOF: |
475 |
0 |
line = self.lines[lineToCheck] |
476 |
0 |
zone = line.zones[zoneToCheck] |
477 |
0 |
currentLineIndex = lineToCheck |
478 |
0 |
currentZoneIndex = zoneToCheck |
479 |
0 |
if caretOffset: |
480 |
0 |
currentWordIndex = len(zone.words) - 1 |
481 |
0 |
currentCharIndex = \ |
482 |
|
zone.words[currentWordIndex].length - 1 |
483 |
|
|
484 |
5 |
self.lineIndex = currentLineIndex |
485 |
5 |
self.zoneIndex = currentZoneIndex |
486 |
5 |
self.wordIndex = currentWordIndex |
487 |
5 |
self.charIndex = currentCharIndex |
488 |
|
|
489 |
|
# This is used to tell us where we should strive to move to |
490 |
|
# when going up and down lines to the closest character. |
491 |
|
# The targetChar is the character where we initially started |
492 |
|
# moving from, and does not change when one moves up or down |
493 |
|
# by line. |
494 |
|
# |
495 |
5 |
self.targetCharInfo = None |
496 |
|
|
497 |
1 |
def visible(self, |
498 |
|
ax, ay, awidth, aheight, |
499 |
|
bx, by, bwidth, bheight): |
500 |
|
"""Returns true if any portion of region 'a' is in region 'b' |
501 |
|
""" |
502 |
464 |
highestBottom = min(ay + aheight, by + bheight) |
503 |
464 |
lowestTop = max(ay, by) |
504 |
|
|
505 |
464 |
leftMostRightEdge = min(ax + awidth, bx + bwidth) |
506 |
464 |
rightMostLeftEdge = max(ax, bx) |
507 |
|
|
508 |
464 |
visible = False |
509 |
|
|
510 |
464 |
if (lowestTop <= highestBottom) \ |
511 |
|
and (rightMostLeftEdge <= leftMostRightEdge): |
512 |
464 |
visible = True |
513 |
0 |
elif (aheight == 0): |
514 |
0 |
if (awidth == 0): |
515 |
0 |
visible = (lowestTop == highestBottom) \ |
516 |
|
and (leftMostRightEdge == rightMostLeftEdge) |
517 |
|
else: |
518 |
0 |
visible = leftMostRightEdge <= rightMostLeftEdge |
519 |
0 |
elif (awidth == 0): |
520 |
0 |
visible = (lowestTop <= highestBottom) |
521 |
|
|
522 |
464 |
return visible |
523 |
|
|
524 |
1 |
def clip(self, |
525 |
|
ax, ay, awidth, aheight, |
526 |
|
bx, by, bwidth, bheight): |
527 |
|
"""Clips region 'a' by region 'b' and returns the new region as |
528 |
|
a list: [x, y, width, height]. |
529 |
|
""" |
530 |
|
|
531 |
395 |
x = max(ax, bx) |
532 |
395 |
x2 = min(ax + awidth, bx + bwidth) |
533 |
395 |
width = x2 - x |
534 |
|
|
535 |
395 |
y = max(ay, by) |
536 |
395 |
y2 = min(ay + aheight, by + bheight) |
537 |
395 |
height = y2 - y |
538 |
|
|
539 |
395 |
return [x, y, width, height] |
540 |
|
|
541 |
1 |
def splitTextIntoZones(self, accessible, string, startOffset, cliprect): |
542 |
|
"""Traverses the string, splitting it up into separate zones if the |
543 |
|
string contains the EMBEDDED_OBJECT_CHARACTER, which is used by apps |
544 |
|
such as Firefox to handle containment of things such as links in |
545 |
|
paragraphs. |
546 |
|
|
547 |
|
Arguments: |
548 |
|
- accessible: the accessible |
549 |
|
- string: a substring from the accessible's text specialization |
550 |
|
- startOffset: the starting character offset of the string |
551 |
|
- cliprect: the extents that the Zones must fit inside. |
552 |
|
|
553 |
|
Returns a list of Zones for the visible text or None if nothing is |
554 |
|
visible. |
555 |
|
""" |
556 |
|
|
557 |
|
# We convert the string to unicode and walk through it. While doing |
558 |
|
# this, we keep two sets of offsets: |
559 |
|
# |
560 |
|
# substring{Start,End}Offset: where in the accessible text implementation |
561 |
|
# we are |
562 |
|
# |
563 |
|
# unicodeStartOffset: where we are in the unicodeString |
564 |
|
# |
565 |
90 |
anyVisible = False |
566 |
90 |
zones = [] |
567 |
90 |
text = accessible.text |
568 |
90 |
substringStartOffset = startOffset |
569 |
90 |
substringEndOffset = startOffset |
570 |
90 |
unicodeStartOffset = 0 |
571 |
90 |
unicodeString = string.decode("UTF-8") |
572 |
|
#print "LOOKING AT '%s'" % unicodeString |
573 |
552 |
for i in range(0, len(unicodeString) + 1): |
574 |
462 |
if (i != len(unicodeString)) \ |
575 |
|
and (unicodeString[i] != orca_state.activeScript.EMBEDDED_OBJECT_CHARACTER): |
576 |
372 |
substringEndOffset += 1 |
577 |
90 |
elif (substringEndOffset == substringStartOffset): |
578 |
0 |
substringStartOffset += 1 |
579 |
0 |
substringEndOffset = substringStartOffset |
580 |
0 |
unicodeStartOffset = i + 1 |
581 |
|
else: |
582 |
90 |
[x, y, width, height] = text.getRangeExtents(substringStartOffset, |
583 |
90 |
substringEndOffset, |
584 |
90 |
0) |
585 |
90 |
if self.visible(x, y, width, height, |
586 |
90 |
cliprect.x, cliprect.y, |
587 |
90 |
cliprect.width, cliprect.height): |
588 |
|
|
589 |
90 |
anyVisible = True |
590 |
|
|
591 |
90 |
clipping = self.clip(x, y, width, height, |
592 |
90 |
cliprect.x, cliprect.y, |
593 |
90 |
cliprect.width, cliprect.height) |
594 |
|
|
595 |
|
# [[[TODO: WDW - HACK it would be nice to clip the |
596 |
|
# the text by what is really showing on the screen, |
597 |
|
# but this seems to hang Orca and the client. Logged |
598 |
|
# as bugzilla bug 319770.]]] |
599 |
|
# |
600 |
|
#ranges = text.getBoundedRanges(\ |
601 |
|
# clipping[0], |
602 |
|
# clipping[1], |
603 |
|
# clipping[2], |
604 |
|
# clipping[3], |
605 |
|
# 0, |
606 |
|
# atspi.Accessibility.TEXT_CLIP_BOTH, |
607 |
|
# atspi.Accessibility.TEXT_CLIP_BOTH) |
608 |
|
# |
609 |
|
#print |
610 |
|
#print "HERE!" |
611 |
|
#for range in ranges: |
612 |
|
# print range.startOffset |
613 |
|
# print range.endOffset |
614 |
|
# print range.content |
615 |
|
|
616 |
90 |
substring = unicodeString[unicodeStartOffset:i] |
617 |
|
#print " SUBSTRING '%s'" % substring |
618 |
90 |
zones.append(TextZone(accessible, |
619 |
90 |
substringStartOffset, |
620 |
90 |
substring.encode("UTF-8"), |
621 |
90 |
clipping[0], |
622 |
90 |
clipping[1], |
623 |
90 |
clipping[2], |
624 |
90 |
clipping[3])) |
625 |
90 |
substringStartOffset = substringEndOffset + 1 |
626 |
90 |
substringEndOffset = substringStartOffset |
627 |
90 |
unicodeStartOffset = i + 1 |
628 |
|
|
629 |
90 |
if anyVisible: |
630 |
90 |
return zones |
631 |
|
else: |
632 |
0 |
return None |
633 |
|
|
634 |
1 |
def getZonesFromText(self, accessible, cliprect): |
635 |
|
"""Gets a list of Zones from an object that implements the |
636 |
|
AccessibleText specialization. |
637 |
|
|
638 |
|
Arguments: |
639 |
|
- accessible: the accessible |
640 |
|
- cliprect: the extents that the Zones must fit inside. |
641 |
|
|
642 |
|
Returns a list of Zones. |
643 |
|
""" |
644 |
|
|
645 |
97 |
debug.println(debug.LEVEL_FINEST, " looking at text:") |
646 |
|
|
647 |
97 |
if accessible.text: |
648 |
97 |
zones = [] |
649 |
|
else: |
650 |
0 |
return [] |
651 |
|
|
652 |
97 |
text = accessible.text |
653 |
97 |
length = text.characterCount |
654 |
|
|
655 |
97 |
offset = 0 |
656 |
97 |
lastEndOffset = -1 |
657 |
187 |
while offset < length: |
658 |
|
|
659 |
90 |
[string, startOffset, endOffset] = text.getTextAtOffset( |
660 |
|
offset, |
661 |
90 |
atspi.Accessibility.TEXT_BOUNDARY_LINE_START) |
662 |
|
|
663 |
|
#NEED TO SKIP OVER EMBEDDED_OBJECT_CHARACTERS |
664 |
|
|
665 |
|
#print "STRING at %d is (start=%d end=%d): '%s'" \ |
666 |
|
# % (offset, startOffset, endOffset, string) |
667 |
|
#if startOffset > offset: |
668 |
|
# embedded = text.getText(offset, offset + 1).decode("UTF-8") |
669 |
|
# if embedded[0] == orca_state.activeScript.EMBEDDED_OBJECT_CHARACTER: |
670 |
|
# offset = startOffset |
671 |
|
|
672 |
90 |
debug.println(debug.LEVEL_FINEST, |
673 |
90 |
" line at %d is (start=%d end=%d): '%s'" \ |
674 |
|
% (offset, startOffset, endOffset, string)) |
675 |
|
|
676 |
|
# [[[WDW - HACK: well...gnome-terminal sometimes wants to |
677 |
|
# give us outrageous values back from getTextAtOffset |
678 |
|
# (see http://bugzilla.gnome.org/show_bug.cgi?id=343133), |
679 |
|
# so we try to handle it. Evolution does similar things.]]] |
680 |
|
# |
681 |
90 |
if (startOffset < 0) \ |
682 |
|
or (endOffset < 0) \ |
683 |
|
or (startOffset > offset) \ |
684 |
|
or (endOffset < offset) \ |
685 |
|
or (startOffset > endOffset) \ |
686 |
|
or (abs(endOffset - startOffset) > 666e3): |
687 |
0 |
debug.println(debug.LEVEL_WARNING, |
688 |
0 |
"flat_review:getZonesFromText detected "\ |
689 |
|
"garbage from getTextAtOffset for accessible "\ |
690 |
|
"name='%s' role'='%s': offset used=%d, "\ |
691 |
|
"start/end offset returned=(%d,%d), string='%s'"\ |
692 |
|
% (accessible.name, accessible.role, |
693 |
|
offset, startOffset, endOffset, string)) |
694 |
0 |
break |
695 |
|
|
696 |
|
# [[[WDW - HACK: this is here because getTextAtOffset |
697 |
|
# tends not to be implemented consistently across toolkits. |
698 |
|
# Sometimes it behaves properly (i.e., giving us an endOffset |
699 |
|
# that is the beginning of the next line), sometimes it |
700 |
|
# doesn't (e.g., giving us an endOffset that is the end of |
701 |
|
# the current line). So...we hack. The whole 'max' deal |
702 |
|
# is to account for lines that might be a brazillion lines |
703 |
|
# long.]]] |
704 |
|
# |
705 |
90 |
if endOffset == lastEndOffset: |
706 |
0 |
offset = max(offset + 1, lastEndOffset + 1) |
707 |
0 |
lastEndOffset = endOffset |
708 |
0 |
continue |
709 |
|
else: |
710 |
90 |
offset = endOffset |
711 |
90 |
lastEndOffset = endOffset |
712 |
|
|
713 |
90 |
textZones = self.splitTextIntoZones( |
714 |
|
accessible, string, startOffset, cliprect) |
715 |
|
|
716 |
90 |
if textZones: |
717 |
90 |
zones.extend(textZones) |
718 |
0 |
elif len(zones): |
719 |
|
# We'll break out of searching all the text - the idea |
720 |
|
# here is that we'll at least try to optimize for when |
721 |
|
# we gone below the visible clipping area. |
722 |
|
# |
723 |
|
# [[[TODO: WDW - would be nice to optimize this better. |
724 |
|
# for example, perhaps we can assume the caret will always |
725 |
|
# be visible, and we can start our text search from there. |
726 |
|
# Logged as bugzilla bug 319771.]]] |
727 |
|
# |
728 |
0 |
break |
729 |
|
|
730 |
|
# We might have a zero length text area. In that case, well, |
731 |
|
# lets hack if this is something whose sole purpose is to |
732 |
|
# act as a text entry area. |
733 |
|
# |
734 |
97 |
if len(zones) == 0: |
735 |
30 |
if (accessible.role == rolenames.ROLE_TEXT) \ |
736 |
|
or ((accessible.role == rolenames.ROLE_ENTRY)) \ |
737 |
|
or ((accessible.role == rolenames.ROLE_PASSWORD_TEXT)): |
738 |
0 |
extents = accessible.component.getExtents(0) |
739 |
0 |
zones.append(TextZone(accessible, |
740 |
0 |
0, |
741 |
0 |
"", |
742 |
0 |
extents.x, extents.y, |
743 |
0 |
extents.width, extents.height)) |
744 |
|
|
745 |
97 |
return zones |
746 |
|
|
747 |
1 |
def getZonesFromAccessible(self, accessible, cliprect): |
748 |
|
"""Returns a list of Zones for the given accessible. |
749 |
|
|
750 |
|
Arguments: |
751 |
|
- accessible: the accessible |
752 |
|
- cliprect: the extents that the Zones must fit inside. |
753 |
|
""" |
754 |
|
|
755 |
374 |
if not accessible.component: |
756 |
0 |
return [] |
757 |
|
|
758 |
|
# Get the component extents in screen coordinates. |
759 |
|
# |
760 |
374 |
extents = accessible.component.getExtents(0) |
761 |
|
|
762 |
374 |
if not self.visible(extents.x, extents.y, |
763 |
374 |
extents.width, extents.height, |
764 |
374 |
cliprect.x, cliprect.y, |
765 |
374 |
cliprect.width, cliprect.height): |
766 |
0 |
return [] |
767 |
|
|
768 |
374 |
debug.println( |
769 |
|
debug.LEVEL_FINEST, |
770 |
374 |
"flat_review.getZonesFromAccessible (name=%s role=%s)" \ |
771 |
|
% (accessible.name, accessible.role)) |
772 |
|
|
773 |
|
# Now see if there is any accessible text. If so, find new zones, |
774 |
|
# where each zone represents a line of this text object. When |
775 |
|
# creating the zone, only keep track of the text that is actually |
776 |
|
# showing on the screen. |
777 |
|
# |
778 |
374 |
if accessible.text: |
779 |
97 |
zones = self.getZonesFromText(accessible, cliprect) |
780 |
|
else: |
781 |
277 |
zones = [] |
782 |
|
|
783 |
|
# We really want the accessible text information. But, if we have |
784 |
|
# an image, and it has a description, we can fall back on it. |
785 |
|
# |
786 |
374 |
if (len(zones) == 0) \ |
787 |
|
and accessible.image \ |
788 |
|
and accessible.image.imageDescription \ |
789 |
|
and len(accessible.image.imageDescription): |
790 |
|
|
791 |
0 |
[x, y] = accessible.image.getImagePosition(0) |
792 |
0 |
[width, height] = accessible.image.getImageSize() |
793 |
|
|
794 |
0 |
if (width != 0) and (height != 0) \ |
795 |
|
and self.visible(x, y, width, height, |
796 |
0 |
cliprect.x, cliprect.y, |
797 |
0 |
cliprect.width, cliprect.height): |
798 |
|
|
799 |
0 |
clipping = self.clip(x, y, width, height, |
800 |
0 |
cliprect.x, cliprect.y, |
801 |
0 |
cliprect.width, cliprect.height) |
802 |
|
|
803 |
0 |
if (clipping[2] != 0) or (clipping[3] != 0): |
804 |
0 |
zones.append(Zone(accessible, |
805 |
0 |
accessible.image.imageDescription, |
806 |
0 |
clipping[0], |
807 |
0 |
clipping[1], |
808 |
0 |
clipping[2], |
809 |
0 |
clipping[3])) |
810 |
|
|
811 |
|
# If the accessible is a parent, we really only looked at it for |
812 |
|
# its accessible text. So...we'll skip the hacking here if that's |
813 |
|
# the case. [[[TODO: WDW - HACK That is, except in the case of |
814 |
|
# combo boxes, which don't implement the accesible text |
815 |
|
# interface. We also hack with MENU items for similar reasons.]]] |
816 |
|
# |
817 |
|
# Otherwise, even if we didn't get anything of use, we certainly |
818 |
|
# know there's something there. If that's the case, we'll just |
819 |
|
# use the component extents and the name or description of the |
820 |
|
# accessible. |
821 |
|
# |
822 |
374 |
if (accessible.role != rolenames.ROLE_COMBO_BOX) \ |
823 |
|
and (accessible.role != rolenames.ROLE_EMBEDDED) \ |
824 |
|
and (accessible.role != rolenames.ROLE_MENU) \ |
825 |
|
and accessible.childCount > 0: |
826 |
2 |
pass |
827 |
372 |
elif len(zones) == 0: |
828 |
305 |
clipping = self.clip(extents.x, extents.y, |
829 |
305 |
extents.width, extents.height, |
830 |
305 |
cliprect.x, cliprect.y, |
831 |
305 |
cliprect.width, cliprect.height) |
832 |
305 |
if accessible.name and len(accessible.name): |
833 |
196 |
string = accessible.name |
834 |
109 |
elif accessible.description and len(accessible.description): |
835 |
0 |
string = accessible.description |
836 |
|
else: |
837 |
109 |
string = "" |
838 |
|
|
839 |
305 |
if string == "": |
840 |
|
# [[[TODO: WDW - ooohhhh....this is going to be a headache. |
841 |
|
# We want to synthesize a string for objects that are there, |
842 |
|
# but have no text. For example, scroll bars. The rub is |
843 |
|
# that we will sometimes want to do different strings for |
844 |
|
# speech and braille (e.g., checkbox cells in tables - the |
845 |
|
# speech will say "checkbox checked" and the braille will |
846 |
|
# display "checkbox <x>". Yikes. That may be a challenge. |
847 |
|
# So...for now we punt on table cells. Logged as bugzilla |
848 |
|
# bug 319772.]]] |
849 |
|
# |
850 |
109 |
if accessible.role != rolenames.ROLE_TABLE_CELL: |
851 |
109 |
string = accessible.role |
852 |
|
|
853 |
305 |
if len(string) and ((clipping[2] != 0) or (clipping[3] != 0)): |
854 |
265 |
zones.append(Zone(accessible, |
855 |
265 |
string, |
856 |
265 |
clipping[0], |
857 |
265 |
clipping[1], |
858 |
265 |
clipping[2], |
859 |
265 |
clipping[3])) |
860 |
|
|
861 |
374 |
return zones |
862 |
|
|
863 |
1 |
def getShowingDescendants(self, parent): |
864 |
|
"""Given a parent that manages its descendants, return a list of |
865 |
|
Accessible children that are actually showing. This algorithm |
866 |
|
was inspired a little by the srw_elements_from_accessible logic |
867 |
|
in Gnopernicus, and makes the assumption that the children of |
868 |
|
an object that manages its descendants are arranged in a row |
869 |
|
and column format. |
870 |
|
""" |
871 |
|
|
872 |
0 |
if (not parent) or (not parent.component): |
873 |
0 |
return [] |
874 |
|
|
875 |
|
# A minimal chunk to jump around should we not really know where we |
876 |
|
# are going. |
877 |
|
# |
878 |
0 |
GRID_SIZE = 7 |
879 |
|
|
880 |
0 |
descendants = [] |
881 |
|
|
882 |
0 |
parentExtents = parent.component.getExtents(0) |
883 |
|
|
884 |
|
# [[[TODO: WDW - HACK related to GAIL bug where table column headers |
885 |
|
# seem to be ignored: http://bugzilla.gnome.org/show_bug.cgi?id=325809. |
886 |
|
# The problem is that this causes getAccessibleAtPoint to return the |
887 |
|
# cell effectively below the real cell at a given point, making a mess |
888 |
|
# of everything. So...we just manually add in showing headers for now. |
889 |
|
# The remainder of the logic below accidentally accounts for this offset, |
890 |
|
# yet it should also work when bug 325809 is fixed.]]] |
891 |
|
# |
892 |
0 |
table = parent.table |
893 |
0 |
if table: |
894 |
0 |
for i in range(0, table.nColumns): |
895 |
0 |
obj = table.getColumnHeader(i) |
896 |
0 |
if obj: |
897 |
0 |
header = atspi.Accessible.makeAccessible(obj) |
898 |
0 |
extents = header.extents |
899 |
0 |
if header.state.count(atspi.Accessibility.STATE_SHOWING) \ |
900 |
|
and (extents.x >= 0) and (extents.y >= 0) \ |
901 |
|
and (extents.width > 0) and (extents.height > 0) \ |
902 |
|
and self.visible(extents.x, extents.y, |
903 |
0 |
extents.width, extents.height, |
904 |
0 |
parentExtents.x, parentExtents.y, |
905 |
0 |
parentExtents.width, |
906 |
0 |
parentExtents.height): |
907 |
0 |
descendants.append(header) |
908 |
|
|
909 |
|
# This algorithm goes left to right, top to bottom while attempting |
910 |
|
# to do *some* optimization over queries. It could definitely be |
911 |
|
# improved. |
912 |
|
# |
913 |
0 |
currentY = parentExtents.y |
914 |
0 |
while currentY < (parentExtents.y + parentExtents.height): |
915 |
0 |
currentX = parentExtents.x |
916 |
0 |
minHeight = sys.maxint |
917 |
0 |
while currentX < (parentExtents.x + parentExtents.width): |
918 |
0 |
obj = parent.component.getAccessibleAtPoint(currentX, |
919 |
0 |
currentY, |
920 |
0 |
0) |
921 |
0 |
if obj: |
922 |
0 |
child = atspi.Accessible.makeAccessible(obj) |
923 |
0 |
extents = child.extents |
924 |
0 |
if extents.x >= 0 and extents.y >= 0: |
925 |
0 |
newX = extents.x + extents.width |
926 |
0 |
minHeight = min(minHeight, extents.height) |
927 |
0 |
if not descendants.count(child): |
928 |
0 |
descendants.append(child) |
929 |
|
else: |
930 |
0 |
newX = currentX + GRID_SIZE |
931 |
|
else: |
932 |
0 |
newX = currentX + GRID_SIZE |
933 |
0 |
if newX <= currentX: |
934 |
0 |
currentX += GRID_SIZE |
935 |
|
else: |
936 |
0 |
currentX = newX |
937 |
0 |
if minHeight == sys.maxint: |
938 |
0 |
minHeight = GRID_SIZE |
939 |
0 |
currentY += minHeight |
940 |
|
|
941 |
0 |
return descendants |
942 |
|
|
943 |
1 |
def getShowingZones(self, root): |
944 |
|
"""Returns a list of all interesting, non-intersecting, regions |
945 |
|
that are drawn on the screen. Each element of the list is the |
946 |
|
Accessible object associated with a given region. The term |
947 |
|
'zone' here is inherited from OCR algorithms and techniques. |
948 |
|
|
949 |
|
The Zones are returned in no particular order. |
950 |
|
|
951 |
|
Arguments: |
952 |
|
- root: the Accessible object to traverse |
953 |
|
|
954 |
|
Returns: a list of Zones under the specified object |
955 |
|
""" |
956 |
|
|
957 |
435 |
if not root: |
958 |
0 |
return [] |
959 |
|
|
960 |
|
# If we're at a leaf node, then we've got a good one on our hands. |
961 |
|
# |
962 |
435 |
if root.childCount <= 0: |
963 |
318 |
return self.getZonesFromAccessible(root, root.extents) |
964 |
|
|
965 |
|
# We'll stop at various objects because, while they do have |
966 |
|
# children, we logically think of them as one region on the |
967 |
|
# screen. [[[TODO: WDW - HACK stopping at menu bars for now |
968 |
|
# because their menu items tell us they are showing even though |
969 |
|
# they are not showing. Until I can figure out a reliable way to |
970 |
|
# get past these lies, I'm going to ignore them.]]] |
971 |
|
# |
972 |
117 |
if (root.parent and (root.parent.role == rolenames.ROLE_MENU_BAR)) \ |
973 |
|
or (root.role == rolenames.ROLE_COMBO_BOX) \ |
974 |
|
or (root.role == rolenames.ROLE_EMBEDDED) \ |
975 |
|
or (root.role == rolenames.ROLE_TEXT): |
976 |
54 |
return self.getZonesFromAccessible(root, root.extents) |
977 |
|
|
978 |
|
# Otherwise, dig deeper. |
979 |
|
# |
980 |
|
# We'll include page tabs: while they are parents, their extents do |
981 |
|
# not contain their children. [[[TODO: WDW - need to consider all |
982 |
|
# parents, especially those that implement accessible text. Logged |
983 |
|
# as bugzilla bug 319773.]]] |
984 |
|
# |
985 |
63 |
zones = [] |
986 |
63 |
if root.role == rolenames.ROLE_PAGE_TAB: |
987 |
1 |
zones.extend(self.getZonesFromAccessible(root, root.extents)) |
988 |
|
|
989 |
63 |
if (len(zones) == 0) and root.text: |
990 |
1 |
zones = self.getZonesFromAccessible(root, root.extents) |
991 |
|
|
992 |
63 |
if root.state.count(atspi.Accessibility.STATE_MANAGES_DESCENDANTS) \ |
993 |
|
and (root.childCount > 50): |
994 |
0 |
for child in self.getShowingDescendants(root): |
995 |
0 |
zones.extend(self.getShowingZones(child)) |
996 |
|
else: |
997 |
561 |
for i in range(0, root.childCount): |
998 |
498 |
child = root.child(i) |
999 |
498 |
if child == root: |
1000 |
0 |
debug.println(debug.LEVEL_WARNING, |
1001 |
0 |
"flat_review.getShowingZones: " + |
1002 |
|
"WARNING CHILD == PARENT!!!") |
1003 |
498 |
elif not child: |
1004 |
0 |
debug.println(debug.LEVEL_WARNING, |
1005 |
0 |
"flat_review.getShowingZones: " + |
1006 |
|
"WARNING CHILD IS NONE!!!") |
1007 |
498 |
elif child.parent != root: |
1008 |
0 |
debug.println(debug.LEVEL_WARNING, |
1009 |
0 |
"flat_review.getShowingZones: " + |
1010 |
|
"WARNING CHILD.PARENT != PARENT!!!") |
1011 |
498 |
elif self.script.pursueForFlatReview(child): |
1012 |
430 |
zones.extend(self.getShowingZones(child)) |
1013 |
|
|
1014 |
63 |
return zones |
1015 |
|
|
1016 |
1 |
def clusterZonesByLine(self, zones): |
1017 |
|
"""Given a list of interesting accessible objects (the Zones), |
1018 |
|
returns a list of lines in order from the top to bottom, where |
1019 |
|
each line is a list of accessible objects in order from left |
1020 |
|
to right. |
1021 |
|
""" |
1022 |
|
|
1023 |
5 |
if len(zones) == 0: |
1024 |
0 |
return [] |
1025 |
|
|
1026 |
|
# Sort the zones and also find the top most zone - we'll bias |
1027 |
|
# the clustering to the top of the window. That is, if an |
1028 |
|
# object can be part of multiple clusters, for now it will |
1029 |
|
# become a part of the top most cluster. |
1030 |
|
# |
1031 |
5 |
numZones = len(zones) |
1032 |
360 |
for i in range(0, numZones): |
1033 |
13780 |
for j in range(0, numZones - 1 - i): |
1034 |
13425 |
a = zones[j] |
1035 |
13425 |
b = zones[j + 1] |
1036 |
13425 |
if b.y < a.y: |
1037 |
423 |
zones[j] = b |
1038 |
423 |
zones[j + 1] = a |
1039 |
|
|
1040 |
|
# Now we cluster the zones. We create the clusters on the |
1041 |
|
# fly, adding a zone to an existing cluster only if it's |
1042 |
|
# rectangle horizontally overlaps all other zones in the |
1043 |
|
# cluster. |
1044 |
|
# |
1045 |
5 |
lineClusters = [] |
1046 |
360 |
for clusterCandidate in zones: |
1047 |
355 |
addedToCluster = False |
1048 |
1560 |
for lineCluster in lineClusters: |
1049 |
1491 |
inCluster = True |
1050 |
4415 |
for zone in lineCluster: |
1051 |
4129 |
if not zone.onSameLine(clusterCandidate): |
1052 |
1205 |
inCluster = False |
1053 |
1205 |
break |
1054 |
1491 |
if inCluster: |
1055 |
|
# Add to cluster based on the x position. |
1056 |
|
# |
1057 |
286 |
i = 0 |
1058 |
2874 |
while i < len(lineCluster): |
1059 |
2640 |
zone = lineCluster[i] |
1060 |
2640 |
if clusterCandidate.x < zone.x: |
1061 |
52 |
break |
1062 |
|
else: |
1063 |
2588 |
i += 1 |
1064 |
286 |
lineCluster.insert(i, clusterCandidate) |
1065 |
286 |
addedToCluster = True |
1066 |
286 |
break |
1067 |
355 |
if not addedToCluster: |
1068 |
69 |
lineClusters.append([clusterCandidate]) |
1069 |
|
|
1070 |
|
# Now, adjust all the indeces. |
1071 |
|
# |
1072 |
5 |
lines = [] |
1073 |
5 |
lineIndex = 0 |
1074 |
74 |
for lineCluster in lineClusters: |
1075 |
69 |
lines.append(Line(lineIndex, lineCluster)) |
1076 |
69 |
zoneIndex = 0 |
1077 |
424 |
for zone in lineCluster: |
1078 |
355 |
zone.line = lines[lineIndex] |
1079 |
355 |
zone.index = zoneIndex |
1080 |
355 |
zoneIndex += 1 |
1081 |
69 |
lineIndex += 1 |
1082 |
|
|
1083 |
5 |
return lines |
1084 |
|
|
1085 |
1 |
def _dumpCurrentState(self): |
1086 |
0 |
print "line=%d, zone=%d, word=%d, char=%d" \ |
1087 |
|
% (self.lineIndex, |
1088 |
|
self.zoneIndex, |
1089 |
|
self.wordIndex, |
1090 |
|
self.zoneIndex) |
1091 |
|
|
1092 |
0 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1093 |
0 |
text = zone.accessible.text |
1094 |
|
|
1095 |
0 |
if not text: |
1096 |
0 |
print " Not Accessibility_Text" |
1097 |
0 |
return |
1098 |
|
|
1099 |
0 |
print " getTextBeforeOffset: %d" % text.caretOffset |
1100 |
0 |
[string, startOffset, endOffset] = text.getTextBeforeOffset( |
1101 |
|
text.caretOffset, |
1102 |
0 |
atspi.Accessibility.TEXT_BOUNDARY_WORD_START) |
1103 |
0 |
print " WORD_START: start=%d end=%d string='%s'" \ |
1104 |
|
% (startOffset, endOffset, string) |
1105 |
0 |
[string, startOffset, endOffset] = text.getTextBeforeOffset( |
1106 |
|
text.caretOffset, |
1107 |
0 |
atspi.Accessibility.TEXT_BOUNDARY_WORD_END) |
1108 |
0 |
print " WORD_END: start=%d end=%d string='%s'" \ |
1109 |
|
% (startOffset, endOffset, string) |
1110 |
|
|
1111 |
0 |
print " getTextAtOffset: %d" % text.caretOffset |
1112 |
0 |
[string, startOffset, endOffset] = text.getTextAtOffset( |
1113 |
|
text.caretOffset, |
1114 |
0 |
atspi.Accessibility.TEXT_BOUNDARY_WORD_START) |
1115 |
0 |
print " WORD_START: start=%d end=%d string='%s'" \ |
1116 |
|
% (startOffset, endOffset, string) |
1117 |
0 |
[string, startOffset, endOffset] = text.getTextAtOffset( |
1118 |
|
text.caretOffset, |
1119 |
0 |
atspi.Accessibility.TEXT_BOUNDARY_WORD_END) |
1120 |
0 |
print " WORD_END: start=%d end=%d string='%s'" \ |
1121 |
|
% (startOffset, endOffset, string) |
1122 |
|
|
1123 |
0 |
print " getTextAfterOffset: %d" % text.caretOffset |
1124 |
0 |
[string, startOffset, endOffset] = text.getTextAfterOffset( |
1125 |
|
text.caretOffset, |
1126 |
0 |
atspi.Accessibility.TEXT_BOUNDARY_WORD_START) |
1127 |
0 |
print " WORD_START: start=%d end=%d string='%s'" \ |
1128 |
|
% (startOffset, endOffset, string) |
1129 |
0 |
[string, startOffset, endOffset] = text.getTextAfterOffset( |
1130 |
|
text.caretOffset, |
1131 |
0 |
atspi.Accessibility.TEXT_BOUNDARY_WORD_END) |
1132 |
0 |
print " WORD_END: start=%d end=%d string='%s'" \ |
1133 |
|
% (startOffset, endOffset, string) |
1134 |
|
|
1135 |
1 |
def setCurrent(self, lineIndex, zoneIndex, wordIndex, charIndex): |
1136 |
|
"""Sets the current character of interest. |
1137 |
|
|
1138 |
|
Arguments: |
1139 |
|
- lineIndex: index into lines |
1140 |
|
- zoneIndex: index into lines[lineIndex].zones |
1141 |
|
- wordIndex: index into lines[lineIndex].zones[zoneIndex].words |
1142 |
|
- charIndex: index lines[lineIndex].zones[zoneIndex].words[wordIndex].chars |
1143 |
|
""" |
1144 |
|
|
1145 |
0 |
self.lineIndex = lineIndex |
1146 |
0 |
self.zoneIndex = zoneIndex |
1147 |
0 |
self.wordIndex = wordIndex |
1148 |
0 |
self.charIndex = charIndex |
1149 |
0 |
self.targetCharInfo = self.getCurrent(Context.CHAR) |
1150 |
|
|
1151 |
|
#print "Current line=%d zone=%d word=%d char=%d" \ |
1152 |
|
# % (lineIndex, zoneIndex, wordIndex, charIndex) |
1153 |
|
|
1154 |
1 |
def clickCurrent(self, button=1): |
1155 |
|
"""Performs a mouse click on the current accessible.""" |
1156 |
|
|
1157 |
0 |
if (not self.lines) \ |
1158 |
|
or (not self.lines[self.lineIndex].zones): |
1159 |
0 |
return |
1160 |
|
|
1161 |
0 |
[string, x, y, width, height] = self.getCurrent(Context.CHAR) |
1162 |
0 |
try: |
1163 |
|
|
1164 |
|
# We try to click to the left of center. This is to |
1165 |
|
# handle toolkits that will offset the caret position to |
1166 |
|
# the right if you click dead on center of a character. |
1167 |
|
# |
1168 |
0 |
eventsynthesizer.clickPoint(x, |
1169 |
0 |
y + height/ 2, |
1170 |
0 |
button) |
1171 |
0 |
except: |
1172 |
0 |
debug.printException(debug.LEVEL_SEVERE) |
1173 |
|
|
1174 |
1 |
def getCurrentAccessible(self): |
1175 |
|
"""Returns the accessible associated with the current locus of |
1176 |
|
interest. |
1177 |
|
""" |
1178 |
|
|
1179 |
0 |
if (not self.lines) \ |
1180 |
|
or (not self.lines[self.lineIndex].zones): |
1181 |
0 |
return [None, -1, -1, -1, -1] |
1182 |
|
|
1183 |
0 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1184 |
|
|
1185 |
0 |
return zone.accessible |
1186 |
|
|
1187 |
1 |
def getCurrent(self, type=ZONE): |
1188 |
|
"""Gets the string, offset, and extent information for the |
1189 |
|
current locus of interest. |
1190 |
|
|
1191 |
|
Arguments: |
1192 |
|
- type: one of ZONE, CHAR, WORD, LINE |
1193 |
|
|
1194 |
|
Returns: [string, x, y, width, height] |
1195 |
|
""" |
1196 |
|
|
1197 |
59 |
if (not self.lines) \ |
1198 |
|
or (not self.lines[self.lineIndex].zones): |
1199 |
0 |
return [None, -1, -1, -1, -1] |
1200 |
|
|
1201 |
59 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1202 |
|
|
1203 |
59 |
if type == Context.ZONE: |
1204 |
0 |
return [zone.string, |
1205 |
|
zone.x, |
1206 |
|
zone.y, |
1207 |
|
zone.width, |
1208 |
|
zone.height] |
1209 |
59 |
elif type == Context.CHAR: |
1210 |
24 |
if isinstance(zone, TextZone): |
1211 |
24 |
words = zone.words |
1212 |
24 |
if words: |
1213 |
24 |
chars = zone.words[self.wordIndex].chars |
1214 |
24 |
if chars: |
1215 |
24 |
char = chars[self.charIndex] |
1216 |
24 |
return [char.string, |
1217 |
|
char.x, |
1218 |
|
char.y, |
1219 |
|
char.width, |
1220 |
|
char.height] |
1221 |
|
else: |
1222 |
0 |
word = words[self.wordIndex] |
1223 |
0 |
return [word.string, |
1224 |
|
word.x, |
1225 |
|
word.y, |
1226 |
|
word.width, |
1227 |
|
word.height] |
1228 |
0 |
return self.getCurrent(Context.ZONE) |
1229 |
35 |
elif type == Context.WORD: |
1230 |
12 |
if isinstance(zone, TextZone): |
1231 |
12 |
words = zone.words |
1232 |
12 |
if words: |
1233 |
12 |
word = words[self.wordIndex] |
1234 |
12 |
return [word.string, |
1235 |
|
word.x, |
1236 |
|
word.y, |
1237 |
|
word.width, |
1238 |
|
word.height] |
1239 |
0 |
return self.getCurrent(Context.ZONE) |
1240 |
23 |
elif type == Context.LINE: |
1241 |
23 |
line = self.lines[self.lineIndex] |
1242 |
23 |
return [line.string, |
1243 |
|
line.x, |
1244 |
|
line.y, |
1245 |
|
line.width, |
1246 |
|
line.height] |
1247 |
|
else: |
1248 |
0 |
raise Exception("Invalid type: %d" % type) |
1249 |
|
|
1250 |
1 |
def getCurrentBrailleRegions(self): |
1251 |
|
"""Gets the braille for the entire current line. |
1252 |
|
|
1253 |
|
Returns [regions, regionWithFocus] |
1254 |
|
""" |
1255 |
|
|
1256 |
23 |
if (not self.lines) \ |
1257 |
|
or (not self.lines[self.lineIndex].zones): |
1258 |
0 |
return [None, None] |
1259 |
|
|
1260 |
23 |
regionWithFocus = None |
1261 |
23 |
line = self.lines[self.lineIndex] |
1262 |
23 |
regions = line.getBrailleRegions() |
1263 |
|
|
1264 |
|
# Now find the current region and the current character offset |
1265 |
|
# into that region. |
1266 |
|
# |
1267 |
53 |
for zone in line.zones: |
1268 |
53 |
if zone.index == self.zoneIndex: |
1269 |
23 |
regionWithFocus = zone.brailleRegion |
1270 |
23 |
regionWithFocus.cursorOffset = 0 |
1271 |
23 |
if zone.words: |
1272 |
23 |
for wordIndex in range(0, self.wordIndex): |
1273 |
0 |
regionWithFocus.cursorOffset += \ |
1274 |
|
len(zone.words[wordIndex].string.decode("UTF-8")) |
1275 |
23 |
regionWithFocus.cursorOffset += self.charIndex |
1276 |
23 |
break |
1277 |
|
|
1278 |
23 |
return [regions, regionWithFocus] |
1279 |
|
|
1280 |
1 |
def goBegin(self, type=WINDOW): |
1281 |
|
"""Moves this context's locus of interest to the first char |
1282 |
|
of the first relevant zone. |
1283 |
|
|
1284 |
|
Arguments: |
1285 |
|
- type: one of ZONE, LINE or WINDOW |
1286 |
|
|
1287 |
|
Returns True if the locus of interest actually changed. |
1288 |
|
""" |
1289 |
|
|
1290 |
0 |
if (type == Context.LINE) or (type == Context.ZONE): |
1291 |
0 |
lineIndex = self.lineIndex |
1292 |
0 |
elif type == Context.WINDOW: |
1293 |
0 |
lineIndex = 0 |
1294 |
|
else: |
1295 |
0 |
raise Exception("Invalid type: %d" % type) |
1296 |
|
|
1297 |
0 |
if type == Context.ZONE: |
1298 |
0 |
zoneIndex = self.zoneIndex |
1299 |
|
else: |
1300 |
0 |
zoneIndex = 0 |
1301 |
|
|
1302 |
0 |
wordIndex = 0 |
1303 |
0 |
charIndex = 0 |
1304 |
|
|
1305 |
0 |
moved = (self.lineIndex != lineIndex) \ |
1306 |
|
or (self.zoneIndex != zoneIndex) \ |
1307 |
|
or (self.wordIndex != wordIndex) \ |
1308 |
|
or (self.charIndex != charIndex) \ |
1309 |
|
|
1310 |
0 |
if moved: |
1311 |
0 |
self.lineIndex = lineIndex |
1312 |
0 |
self.zoneIndex = zoneIndex |
1313 |
0 |
self.wordIndex = wordIndex |
1314 |
0 |
self.charIndex = charIndex |
1315 |
0 |
self.targetCharInfo = self.getCurrent(Context.CHAR) |
1316 |
|
|
1317 |
0 |
return moved |
1318 |
|
|
1319 |
1 |
def goEnd(self, type=WINDOW): |
1320 |
|
"""Moves this context's locus of interest to the last char |
1321 |
|
of the last relevant zone. |
1322 |
|
|
1323 |
|
Arguments: |
1324 |
|
- type: one of ZONE, LINE, or WINDOW |
1325 |
|
|
1326 |
|
Returns True if the locus of interest actually changed. |
1327 |
|
""" |
1328 |
|
|
1329 |
0 |
if (type == Context.LINE) or (type == Context.ZONE): |
1330 |
0 |
lineIndex = self.lineIndex |
1331 |
0 |
elif type == Context.WINDOW: |
1332 |
0 |
lineIndex = len(self.lines) - 1 |
1333 |
|
else: |
1334 |
0 |
raise Exception("Invalid type: %d" % type) |
1335 |
|
|
1336 |
0 |
if type == Context.ZONE: |
1337 |
0 |
zoneIndex = self.zoneIndex |
1338 |
|
else: |
1339 |
0 |
zoneIndex = len(self.lines[lineIndex].zones) - 1 |
1340 |
|
|
1341 |
0 |
zone = self.lines[lineIndex].zones[zoneIndex] |
1342 |
0 |
if zone.words: |
1343 |
0 |
wordIndex = len(zone.words) - 1 |
1344 |
0 |
chars = zone.words[wordIndex].chars |
1345 |
0 |
if chars: |
1346 |
0 |
charIndex = len(chars) - 1 |
1347 |
|
else: |
1348 |
0 |
charIndex = 0 |
1349 |
|
else: |
1350 |
0 |
wordIndex = 0 |
1351 |
0 |
charIndex = 0 |
1352 |
|
|
1353 |
0 |
moved = (self.lineIndex != lineIndex) \ |
1354 |
|
or (self.zoneIndex != zoneIndex) \ |
1355 |
|
or (self.wordIndex != wordIndex) \ |
1356 |
|
or (self.charIndex != charIndex) \ |
1357 |
|
|
1358 |
0 |
if moved: |
1359 |
0 |
self.lineIndex = lineIndex |
1360 |
0 |
self.zoneIndex = zoneIndex |
1361 |
0 |
self.wordIndex = wordIndex |
1362 |
0 |
self.charIndex = charIndex |
1363 |
0 |
self.targetCharInfo = self.getCurrent(Context.CHAR) |
1364 |
|
|
1365 |
0 |
return moved |
1366 |
|
|
1367 |
1 |
def goPrevious(self, type=ZONE, wrap=WRAP_ALL, omitWhitespace=True): |
1368 |
|
"""Moves this context's locus of interest to the first char |
1369 |
|
of the previous type. |
1370 |
|
|
1371 |
|
Arguments: |
1372 |
|
- type: one of ZONE, CHAR, WORD, LINE |
1373 |
|
- wrap: if True, will cross boundaries, including top and |
1374 |
|
bottom; if False, will stop on boundaries. |
1375 |
|
|
1376 |
|
Returns True if the locus of interest actually changed. |
1377 |
|
""" |
1378 |
|
|
1379 |
18 |
moved = False |
1380 |
|
|
1381 |
18 |
if type == Context.ZONE: |
1382 |
6 |
if self.zoneIndex > 0: |
1383 |
5 |
self.zoneIndex -= 1 |
1384 |
5 |
self.wordIndex = 0 |
1385 |
5 |
self.charIndex = 0 |
1386 |
5 |
moved = True |
1387 |
1 |
elif wrap & Context.WRAP_LINE: |
1388 |
1 |
if self.lineIndex > 0: |
1389 |
1 |
self.lineIndex -= 1 |
1390 |
1 |
self.zoneIndex = len(self.lines[self.lineIndex].zones) - 1 |
1391 |
1 |
self.wordIndex = 0 |
1392 |
1 |
self.charIndex = 0 |
1393 |
1 |
moved = True |
1394 |
0 |
elif wrap & Context.WRAP_TOP_BOTTOM: |
1395 |
0 |
self.lineIndex = len(self.lines) - 1 |
1396 |
0 |
self.zoneIndex = len(self.lines[self.lineIndex].zones) - 1 |
1397 |
0 |
self.wordIndex = 0 |
1398 |
0 |
self.charIndex = 0 |
1399 |
0 |
moved = True |
1400 |
12 |
elif type == Context.CHAR: |
1401 |
0 |
if self.charIndex > 0: |
1402 |
0 |
self.charIndex -= 1 |
1403 |
0 |
moved = True |
1404 |
|
else: |
1405 |
0 |
moved = self.goPrevious(Context.WORD, wrap, False) |
1406 |
0 |
if moved: |
1407 |
0 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1408 |
0 |
if zone.words: |
1409 |
0 |
chars = zone.words[self.wordIndex].chars |
1410 |
0 |
if chars: |
1411 |
0 |
self.charIndex = len(chars) - 1 |
1412 |
12 |
elif type == Context.WORD: |
1413 |
6 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1414 |
6 |
accessible = zone.accessible |
1415 |
6 |
lineIndex = self.lineIndex |
1416 |
6 |
zoneIndex = self.zoneIndex |
1417 |
6 |
wordIndex = self.wordIndex |
1418 |
6 |
charIndex = self.charIndex |
1419 |
|
|
1420 |
6 |
if self.wordIndex > 0: |
1421 |
0 |
self.wordIndex -= 1 |
1422 |
0 |
self.charIndex = 0 |
1423 |
0 |
moved = True |
1424 |
|
else: |
1425 |
6 |
moved = self.goPrevious(Context.ZONE, wrap) |
1426 |
6 |
if moved: |
1427 |
6 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1428 |
6 |
if zone.words: |
1429 |
6 |
self.wordIndex = len(zone.words) - 1 |
1430 |
|
|
1431 |
|
# If we landed on a whitespace word or something with no words, |
1432 |
|
# we might need to move some more. |
1433 |
|
# |
1434 |
6 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1435 |
6 |
if omitWhitespace \ |
1436 |
|
and moved \ |
1437 |
|
and ((len(zone.words) == 0) \ |
1438 |
|
or zone.words[self.wordIndex].string.isspace()): |
1439 |
|
|
1440 |
|
# If we're on whitespace in the same zone, then let's |
1441 |
|
# try to move on. If not, we've definitely moved |
1442 |
|
# across accessibles. If that's the case, let's try |
1443 |
|
# to find the first 'real' word in the accessible. |
1444 |
|
# If we cannot, then we're just stuck on an accessible |
1445 |
|
# with no words and we should do our best to announce |
1446 |
|
# this to the user (e.g., "whitespace" or "blank"). |
1447 |
|
# |
1448 |
0 |
if zone.accessible == accessible: |
1449 |
0 |
moved = self.goPrevious(Context.WORD, wrap) |
1450 |
|
else: |
1451 |
0 |
wordIndex = self.wordIndex - 1 |
1452 |
0 |
while wordIndex >= 0: |
1453 |
0 |
if (not zone.words[wordIndex].string) \ |
1454 |
|
or not len(zone.words[wordIndex].string) \ |
1455 |
|
or zone.words[wordIndex].string.isspace(): |
1456 |
0 |
wordIndex -= 1 |
1457 |
|
else: |
1458 |
0 |
break |
1459 |
0 |
if wordIndex >= 0: |
1460 |
0 |
self.wordIndex = wordIndex |
1461 |
|
|
1462 |
6 |
if not moved: |
1463 |
0 |
self.lineIndex = lineIndex |
1464 |
0 |
self.zoneIndex = zoneIndex |
1465 |
0 |
self.wordIndex = wordIndex |
1466 |
0 |
self.charIndex = charIndex |
1467 |
|
|
1468 |
6 |
elif type == Context.LINE: |
1469 |
6 |
if wrap & Context.WRAP_LINE: |
1470 |
6 |
if self.lineIndex > 0: |
1471 |
2 |
self.lineIndex -= 1 |
1472 |
2 |
self.zoneIndex = 0 |
1473 |
2 |
self.wordIndex = 0 |
1474 |
2 |
self.charIndex = 0 |
1475 |
2 |
moved = True |
1476 |
4 |
elif (wrap & Context.WRAP_TOP_BOTTOM) \ |
1477 |
|
and (len(self.lines) != 1): |
1478 |
0 |
self.lineIndex = len(self.lines) - 1 |
1479 |
0 |
self.zoneIndex = 0 |
1480 |
0 |
self.wordIndex = 0 |
1481 |
0 |
self.charIndex = 0 |
1482 |
0 |
moved = True |
1483 |
|
else: |
1484 |
0 |
raise Exception("Invalid type: %d" % type) |
1485 |
|
|
1486 |
18 |
if moved and (type != Context.LINE): |
1487 |
12 |
self.targetCharInfo = self.getCurrent(Context.CHAR) |
1488 |
|
|
1489 |
18 |
return moved |
1490 |
|
|
1491 |
1 |
def goNext(self, type=ZONE, wrap=WRAP_ALL, omitWhitespace=True): |
1492 |
|
"""Moves this context's locus of interest to first char of |
1493 |
|
the next type. |
1494 |
|
|
1495 |
|
Arguments: |
1496 |
|
- type: one of ZONE, CHAR, WORD, LINE |
1497 |
|
- wrap: if True, will cross boundaries, including top and |
1498 |
|
bottom; if False, will stop on boundaries. |
1499 |
|
""" |
1500 |
|
|
1501 |
21 |
moved = False |
1502 |
|
|
1503 |
21 |
if type == Context.ZONE: |
1504 |
6 |
if self.zoneIndex < (len(self.lines[self.lineIndex].zones) - 1): |
1505 |
5 |
self.zoneIndex += 1 |
1506 |
5 |
self.wordIndex = 0 |
1507 |
5 |
self.charIndex = 0 |
1508 |
5 |
moved = True |
1509 |
1 |
elif wrap & Context.WRAP_LINE: |
1510 |
1 |
if self.lineIndex < (len(self.lines) - 1): |
1511 |
1 |
self.lineIndex += 1 |
1512 |
1 |
self.zoneIndex = 0 |
1513 |
1 |
self.wordIndex = 0 |
1514 |
1 |
self.charIndex = 0 |
1515 |
1 |
moved = True |
1516 |
0 |
elif wrap & Context.WRAP_TOP_BOTTOM: |
1517 |
0 |
self.lineIndex = 0 |
1518 |
0 |
self.zoneIndex = 0 |
1519 |
0 |
self.wordIndex = 0 |
1520 |
0 |
self.charIndex = 0 |
1521 |
0 |
moved = True |
1522 |
15 |
elif type == Context.CHAR: |
1523 |
0 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1524 |
0 |
if zone.words: |
1525 |
0 |
chars = zone.words[self.wordIndex].chars |
1526 |
0 |
if chars: |
1527 |
0 |
if self.charIndex < (len(chars) - 1): |
1528 |
0 |
self.charIndex += 1 |
1529 |
0 |
moved = True |
1530 |
|
else: |
1531 |
0 |
moved = self.goNext(Context.WORD, wrap, False) |
1532 |
|
else: |
1533 |
0 |
moved = self.goNext(Context.WORD, wrap) |
1534 |
|
else: |
1535 |
0 |
moved = self.goNext(Context.ZONE, wrap) |
1536 |
15 |
elif type == Context.WORD: |
1537 |
6 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1538 |
6 |
accessible = zone.accessible |
1539 |
6 |
lineIndex = self.lineIndex |
1540 |
6 |
zoneIndex = self.zoneIndex |
1541 |
6 |
wordIndex = self.wordIndex |
1542 |
6 |
charIndex = self.charIndex |
1543 |
|
|
1544 |
6 |
if zone.words: |
1545 |
6 |
if self.wordIndex < (len(zone.words) - 1): |
1546 |
0 |
self.wordIndex += 1 |
1547 |
0 |
self.charIndex = 0 |
1548 |
0 |
moved = True |
1549 |
|
else: |
1550 |
6 |
moved = self.goNext(Context.ZONE, wrap) |
1551 |
|
else: |
1552 |
0 |
moved = self.goNext(Context.ZONE, wrap) |
1553 |
|
|
1554 |
|
# If we landed on a whitespace word or something with no words, |
1555 |
|
# we might need to move some more. |
1556 |
|
# |
1557 |
6 |
zone = self.lines[self.lineIndex].zones[self.zoneIndex] |
1558 |
6 |
if omitWhitespace \ |
1559 |
|
and moved \ |
1560 |
|
and ((len(zone.words) == 0) \ |
1561 |
|
or zone.words[self.wordIndex].string.isspace()): |
1562 |
|
|
1563 |
|
# If we're on whitespace in the same zone, then let's |
1564 |
|
# try to move on. If not, we've definitely moved |
1565 |
|
# across accessibles. If that's the case, let's try |
1566 |
|
# to find the first 'real' word in the accessible. |
1567 |
|
# If we cannot, then we're just stuck on an accessible |
1568 |
|
# with no words and we should do our best to announce |
1569 |
|
# this to the user (e.g., "whitespace" or "blank"). |
1570 |
|
# |
1571 |
0 |
if zone.accessible == accessible: |
1572 |
0 |
moved = self.goNext(Context.WORD, wrap) |
1573 |
|
else: |
1574 |
0 |
wordIndex = self.wordIndex + 1 |
1575 |
0 |
while wordIndex < len(zone.words): |
1576 |
0 |
if (not zone.words[wordIndex].string) \ |
1577 |
|
or not len(zone.words[wordIndex].string) \ |
1578 |
|
or zone.words[wordIndex].string.isspace(): |
1579 |
0 |
wordIndex += 1 |
1580 |
|
else: |
1581 |
0 |
break |
1582 |
0 |
if wordIndex < len(zone.words): |
1583 |
0 |
self.wordIndex = wordIndex |
1584 |
|
|
1585 |
6 |
if not moved: |
1586 |
0 |
self.lineIndex = lineIndex |
1587 |
0 |
self.zoneIndex = zoneIndex |
1588 |
0 |
self.wordIndex = wordIndex |
1589 |
0 |
self.charIndex = charIndex |
1590 |
|
|
1591 |
9 |
elif type == Context.LINE: |
1592 |
9 |
if wrap & Context.WRAP_LINE: |
1593 |
9 |
if self.lineIndex < (len(self.lines) - 1): |
1594 |
9 |
self.lineIndex += 1 |
1595 |
9 |
self.zoneIndex = 0 |
1596 |
9 |
self.wordIndex = 0 |
1597 |
9 |
self.charIndex = 0 |
1598 |
9 |
moved = True |
1599 |
0 |
elif (wrap & Context.WRAP_TOP_BOTTOM) \ |
1600 |
|
and (self.lineIndex != 0): |
1601 |
0 |
self.lineIndex = 0 |
1602 |
0 |
self.zoneIndex = 0 |
1603 |
0 |
self.wordIndex = 0 |
1604 |
0 |
self.charIndex = 0 |
1605 |
0 |
moved = True |
1606 |
|
else: |
1607 |
0 |
raise Exception("Invalid type: %d" % type) |
1608 |
|
|
1609 |
21 |
if moved and (type != Context.LINE): |
1610 |
12 |
self.targetCharInfo = self.getCurrent(Context.CHAR) |
1611 |
|
|
1612 |
21 |
return moved |
1613 |
|
|
1614 |
1 |
def goAbove(self, type=LINE, wrap=WRAP_ALL): |
1615 |
|
"""Moves this context's locus of interest to first char |
1616 |
|
of the type that's closest to and above the current locus of |
1617 |
|
interest. |
1618 |
|
|
1619 |
|
Arguments: |
1620 |
|
- type: LINE |
1621 |
|
- wrap: if True, will cross top/bottom boundaries; if False, will |
1622 |
|
stop on top/bottom boundaries. |
1623 |
|
|
1624 |
|
Returns: [string, startOffset, endOffset, x, y, width, height] |
1625 |
|
""" |
1626 |
|
|
1627 |
0 |
moved = False |
1628 |
0 |
if type == Context.CHAR: |
1629 |
|
# We want to shoot for the closest character, which we've |
1630 |
|
# saved away as self.targetCharInfo, which is the list |
1631 |
|
# [string, x, y, width, height]. |
1632 |
|
# |
1633 |
0 |
if not self.targetCharInfo: |
1634 |
0 |
self.targetCharInfo = self.getCurrent(Context.CHAR) |
1635 |
0 |
target = self.targetCharInfo |
1636 |
|
|
1637 |
0 |
[string, x, y, width, height] = target |
1638 |
0 |
middleTargetX = x + (width / 2) |
1639 |
|
|
1640 |
0 |
moved = self.goPrevious(Context.LINE, wrap) |
1641 |
0 |
if moved: |
1642 |
0 |
while True: |
1643 |
0 |
[string, bx, by, bwidth, bheight] = \ |
1644 |
|
self.getCurrent(Context.CHAR) |
1645 |
0 |
if (bx + width) >= middleTargetX: |
1646 |
0 |
break |
1647 |
0 |
elif not self.goNext(Context.CHAR, Context.WRAP_NONE): |
1648 |
0 |
break |
1649 |
|
|
1650 |
|
# Moving around might have reset the current targetCharInfo, |
1651 |
|
# so we reset it to our saved value. |
1652 |
|
# |
1653 |
0 |
self.targetCharInfo = target |
1654 |
0 |
elif type == Context.LINE: |
1655 |
0 |
return self.goPrevious(type, wrap) |
1656 |
|
else: |
1657 |
0 |
raise Exception("Invalid type: %d" % type) |
1658 |
|
|
1659 |
0 |
return moved |
1660 |
|
|
1661 |
1 |
def goBelow(self, type=LINE, wrap=WRAP_ALL): |
1662 |
|
"""Moves this context's locus of interest to the first |
1663 |
|
char of the type that's closest to and below the current |
1664 |
|
locus of interest. |
1665 |
|
|
1666 |
|
Arguments: |
1667 |
|
- type: one of WORD, LINE |
1668 |
|
- wrap: if True, will cross top/bottom boundaries; if False, will |
1669 |
|
stop on top/bottom boundaries. |
1670 |
|
|
1671 |
|
Returns: [string, startOffset, endOffset, x, y, width, height] |
1672 |
|
""" |
1673 |
|
|
1674 |
0 |
moved = False |
1675 |
0 |
if type == Context.CHAR: |
1676 |
|
# We want to shoot for the closest character, which we've |
1677 |
|
# saved away as self.targetCharInfo, which is the list |
1678 |
|
# [string, x, y, width, height]. |
1679 |
|
# |
1680 |
0 |
if not self.targetCharInfo: |
1681 |
0 |
self.targetCharInfo = self.getCurrent(Context.CHAR) |
1682 |
0 |
target = self.targetCharInfo |
1683 |
|
|
1684 |
0 |
[string, x, y, width, height] = target |
1685 |
0 |
middleTargetX = x + (width / 2) |
1686 |
|
|
1687 |
0 |
moved = self.goNext(Context.LINE, wrap) |
1688 |
0 |
if moved: |
1689 |
0 |
while True: |
1690 |
0 |
[string, bx, by, bwidth, bheight] = \ |
1691 |
|
self.getCurrent(Context.CHAR) |
1692 |
0 |
if (bx + width) >= middleTargetX: |
1693 |
0 |
break |
1694 |
0 |
elif not self.goNext(Context.CHAR, Context.WRAP_NONE): |
1695 |
0 |
break |
1696 |
|
|
1697 |
|
# Moving around might have reset the current targetCharInfo, |
1698 |
|
# so we reset it to our saved value. |
1699 |
|
# |
1700 |
0 |
self.targetCharInfo = target |
1701 |
0 |
elif type == Context.LINE: |
1702 |
0 |
moved = self.goNext(type, wrap) |
1703 |
|
else: |
1704 |
0 |
raise Exception("Invalid type: %d" % type) |
1705 |
|
|
1706 |
0 |
return moved |