Coverage Report - orca.flat_review

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