%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/orca/ |
| Current File : //lib/python3/dist-packages/orca/flat_review.py |
# Orca
#
# Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2016 Igalia, S.L.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
"""Provides the default implementation for flat review for Orca."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
"Copyright (c) 2016 Igalia, S.L."
__license__ = "LGPL"
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import re
from . import braille
from . import debug
from . import focus_manager
from . import script_manager
from . import settings
from .ax_component import AXComponent
from .ax_event_synthesizer import AXEventSynthesizer
from .ax_object import AXObject
from .ax_text import AXText
from .ax_utilities import AXUtilities
EMBEDDED_OBJECT_CHARACTER = '\ufffc'
class Char:
"""A character's worth of presentable information."""
def __init__(self, word, index, startOffset, string, x, y, width, height):
"""Creates a new char.
Arguments:
- word: the Word instance this belongs to
- startOffset: the start offset with respect to the accessible
- string: the actual char
- x, y, width, height: the extents of this Char on the screen
"""
self.word = word
self.index = index
self.startOffset = startOffset
self.endOffset = startOffset + 1
self.string = string
self.x = x
self.y = y
self.width = width
self.height = height
class Word:
"""A single chunk (word or object) of presentable information."""
def __init__(self, zone, index, startOffset, string, x, y, width, height):
"""Creates a new Word.
Arguments:
- zone: the Zone instance this belongs to
- index: the index of this Word in the Zone
- startOffset: the start offset with respect to the accessible
- string: the actual string
- x, y, width, height: the extents of this Word on the screen
"""
self.zone = zone
self.index = index
self.startOffset = startOffset
self.string = string
self.length = len(string)
self.endOffset = self.startOffset + len(string)
self.x = x
self.y = y
self.width = width
self.height = height
self.chars = []
def __str__(self):
return "WORD: '%s' (%i-%i) %s" % \
(self.string.replace("\n", "\\n"),
self.startOffset,
self.endOffset,
self.zone.accessible)
def __getattribute__(self, attr):
if attr != "chars":
return super().__getattribute__(attr)
chars = []
for i, char in enumerate(self.string):
start = i + self.startOffset
rect1 = AXText.get_character_rect(self.zone.accessible, start)
extents = rect1.x, rect1.y, rect1.width, rect1.height
chars.append(Char(self, i, start, char, *extents))
return chars
def getRelativeOffset(self, offset):
"""Returns the char offset with respect to this word or -1."""
if self.startOffset <= offset < self.startOffset + len(self.string):
return offset - self.startOffset
return -1
class Zone:
"""Represents text that is a portion of a single horizontal line."""
WORDS_RE = re.compile(r"(\S+\s*)", re.UNICODE)
def __init__(self, accessible, string, x, y, width, height, role=None):
"""Creates a new Zone.
Arguments:
- accessible: the Accessible associated with this Zone
- string: the string being displayed for this Zone
- extents: x, y, width, height in screen coordinates
- role: Role to override accessible's role.
"""
self.accessible = accessible
self.startOffset = 0
self._string = string
self.length = len(string)
self.x = x
self.y = y
self.width = width
self.height = height
self.role = role or AXObject.get_role(accessible)
self._words = []
def __str__(self):
return "ZONE: '%s' %s" % (self._string.replace("\n", "\\n"), self.accessible)
def __getattribute__(self, attr):
"""To ensure we update the content."""
if attr not in ["words", "string"]:
return super().__getattribute__(attr)
if attr == "string":
return self._string
if not self._shouldFakeText():
return self._words
# TODO - JD: For now, don't fake character and word extents.
# The main goal is to improve reviewability.
extents = self.x, self.y, self.width, self.height
words = []
for i, word in enumerate(re.finditer(self.WORDS_RE, self._string)):
words.append(Word(self, i, word.start(), word.group(), *extents))
self._words = words
return words
def _shouldFakeText(self):
"""Returns True if we should try to fake the text interface"""
textRoles = [Atspi.Role.LABEL,
Atspi.Role.MENU,
Atspi.Role.MENU_ITEM,
Atspi.Role.CHECK_MENU_ITEM,
Atspi.Role.RADIO_MENU_ITEM,
Atspi.Role.PAGE_TAB,
Atspi.Role.PUSH_BUTTON,
Atspi.Role.TABLE_CELL]
if self.role in textRoles:
return True
return False
def _extentsAreOnSameLine(self, zone, pixelDelta=5):
"""Returns True if this Zone is physically on the same line as zone."""
if self.width == 0 and self.height == 0:
return zone.y <= self.y <= zone.y + zone.height
if zone.width == 0 and self.height == 0:
return self.y <= zone.y <= self.y + self.height
highestBottom = min(self.y + self.height, zone.y + zone.height)
lowestTop = max(self.y, zone.y)
if lowestTop >= highestBottom:
return False
middle = self.y + self.height / 2
zoneMiddle = zone.y + zone.height / 2
if abs(middle - zoneMiddle) > pixelDelta:
return False
return True
def onSameLine(self, zone):
"""Returns True if we treat this Zone and zone as being on one line."""
if Atspi.Role.SCROLL_BAR in [self.role, zone.role]:
return self.accessible == zone.accessible
thisParent = AXObject.get_parent(self.accessible)
thisParentRole = AXObject.get_role(thisParent)
zoneParent = AXObject.get_parent(zone.accessible)
zoneParentRole = AXObject.get_role(zoneParent)
if Atspi.Role.MENU_BAR in [thisParentRole, zoneParentRole]:
return thisParent == zoneParent
return self._extentsAreOnSameLine(zone)
def getWordAtOffset(self, charOffset):
msg = f"FLAT REVIEW: Searching for word at offset {charOffset}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
for word in self.words:
tokens = ["FLAT REVIEW: Checking", word]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
offset = word.getRelativeOffset(charOffset)
if offset >= 0:
return word, offset
if self.length == charOffset and self.words:
lastWord = self.words[-1]
return lastWord, lastWord.length
return None, -1
def hasCaret(self):
"""Returns True if this Zone contains the caret."""
return False
def wordWithCaret(self):
"""Returns the Word and relative offset with the caret."""
return None, -1
class TextZone(Zone):
"""A Zone whose purpose is to display text of an object."""
def __init__(self, accessible, startOffset, string, x, y, width, height, role=None):
super().__init__(accessible, string, x, y, width, height, role)
self.startOffset = startOffset
self.endOffset = self.startOffset + len(string)
def __getattribute__(self, attr):
"""To ensure we update the content."""
if attr not in ["words", "string"]:
return super().__getattribute__(attr)
string = AXText.get_substring(self.accessible, self.startOffset, self.endOffset)
words = []
for i, word in enumerate(re.finditer(self.WORDS_RE, string)):
start, end = map(lambda x: x + self.startOffset, word.span())
rect = AXText.get_range_rect(self.accessible, start, end)
extents = rect.x, rect.y, rect.width, rect.height
words.append(Word(self, i, start, word.group(), *extents))
self._string = string
self._words = words
return super().__getattribute__(attr)
def hasCaret(self):
"""Returns True if this Zone contains the caret."""
if self.startOffset <= AXText.get_caret_offset(self.accessible) < self.endOffset:
return True
return self.endOffset == AXText.get_character_count(self.accessible)
def wordWithCaret(self):
"""Returns the Word and relative offset with the caret."""
if not self.hasCaret():
return None, -1
return self.getWordAtOffset(AXText.get_caret_offset(self.accessible))
class StateZone(Zone):
"""A Zone whose purpose is to display the state of an object."""
def __init__(self, accessible, x, y, width, height, role=None):
super().__init__(accessible, "", x, y, width, height, role)
def __getattribute__(self, attr):
"""To ensure we update the state."""
if attr not in ["string", "brailleString"]:
return super().__getattribute__(attr)
script = script_manager.getManager().getActiveScript()
if attr == "string":
generator = script.speechGenerator
else:
generator = script.brailleGenerator
result = generator.getStateIndicator(self.accessible, role=self.role)
if result:
return result[0]
return ""
class ValueZone(Zone):
"""A Zone whose purpose is to display the value of an object."""
def __init__(self, accessible, x, y, width, height, role=None):
super().__init__(accessible, "", x, y, width, height, role)
def __getattribute__(self, attr):
"""To ensure we update the value."""
if attr not in ["string", "brailleString"]:
return super().__getattribute__(attr)
script = script_manager.getManager().getActiveScript()
if attr == "string":
generator = script.speechGenerator
else:
generator = script.brailleGenerator
result = ""
# TODO - JD: This cobbling together beats what we had, but the
# generators should also be doing the assembly.
rolename = generator.getLocalizedRoleName(self.accessible)
value = generator.getValue(self.accessible)
if rolename and value:
result = f"{rolename} {value[0]}"
return result
class Line:
"""A Line is a single line across a window and is composed of Zones."""
def __init__(self,
index,
zones):
"""Creates a new Line, which is a horizontal region of text.
Arguments:
- index: the index of this Line in the window
- zones: the Zones that make up this line
"""
self.index = index
self.zones = zones
self.brailleRegions = None
def __getattribute__(self, attr):
if attr == "string":
return " ".join([zone.string for zone in self.zones])
if attr == "x":
return min([zone.x for zone in self.zones])
if attr == "y":
return min([zone.y for zone in self.zones])
if attr == "width":
return sum([zone.width for zone in self.zones])
if attr == "height":
return max([zone.height for zone in self.zones])
return super().__getattribute__(attr)
def getBrailleRegions(self):
# [[[WDW - We'll always compute the braille regions. This
# allows us to handle StateZone and ValueZone zones whose
# states might be changing on us.]]]
#
if True or not self.brailleRegions:
self.brailleRegions = []
brailleOffset = 0
for zone in self.zones:
# The 'isinstance(zone, TextZone)' test is a sanity check
# to handle problems with Java text. See Bug 435553.
if isinstance(zone, TextZone) and \
((AXObject.get_role(zone.accessible) in \
(Atspi.Role.TEXT,
Atspi.Role.PASSWORD_TEXT,
Atspi.Role.TERMINAL)) or \
# [[[TODO: Eitan - HACK:
# This is just to get FF3 cursor key routing support.
# We really should not be determining all this stuff here,
# it should be in the scripts.
# Same applies to roles above.]]]
(AXObject.get_role(zone.accessible) in \
(Atspi.Role.PARAGRAPH,
Atspi.Role.HEADING,
Atspi.Role.LINK))):
region = braille.ReviewText(zone.accessible,
zone.string,
zone.startOffset,
zone)
else:
try:
brailleString = zone.brailleString
except Exception:
brailleString = zone.string
region = braille.ReviewComponent(zone.accessible,
brailleString,
0, # cursor offset
zone)
if len(self.brailleRegions):
pad = braille.Region(" ")
pad.brailleOffset = brailleOffset
self.brailleRegions.append(pad)
brailleOffset += 1
zone.brailleRegion = region
region.brailleOffset = brailleOffset
self.brailleRegions.append(region)
regionString = region.string
brailleOffset += len(regionString)
if not settings.disableBrailleEOL:
if len(self.brailleRegions):
pad = braille.Region(" ")
pad.brailleOffset = brailleOffset
self.brailleRegions.append(pad)
brailleOffset += 1
eol = braille.Region("$l")
eol.brailleOffset = brailleOffset
self.brailleRegions.append(eol)
return self.brailleRegions
class Context:
"""Contains the flat review regions for the current top-level object."""
ZONE = 0
CHAR = 1
WORD = 2
LINE = 3 # includes all zones on same line
WINDOW = 4
WRAP_NONE = 0
WRAP_LINE = 1 << 0
WRAP_TOP_BOTTOM = 1 << 1
WRAP_ALL = (WRAP_LINE | WRAP_TOP_BOTTOM)
def __init__(self, script, root=None):
"""Create a new Context for script."""
self.script = script
self.zones = []
self.lines = []
self.lineIndex = 0
self.zoneIndex = 0
self.wordIndex = 0
self.charIndex = 0
self.targetCharInfo = None
self.focusZone = None
self.container = None
self.focusObj = focus_manager.getManager().get_locus_of_focus()
self.topLevel = None
self.bounds = Atspi.Rect()
frame, dialog = script.utilities.frameAndDialog(self.focusObj)
if root is not None:
self.topLevel = root
tokens = ["FLAT REVIEW: Restricting flat review to", root]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
else:
self.topLevel = dialog or frame
tokens = ["FLAT REVIEW: Frame:", frame, "Dialog:", dialog, ". Top level:", self.topLevel]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
self.bounds = AXComponent.get_rect(self.topLevel)
containerRoles = [Atspi.Role.MENU]
def isContainer(x):
return AXObject.get_role(x) in containerRoles
container = AXObject.find_ancestor(self.focusObj, isContainer)
if not container and isContainer(self.focusObj):
container = self.focusObj
self.container = container or self.topLevel
self.zones, self.focusZone = self.getShowingZones(self.container)
self.lines = self.clusterZonesByLine(self.zones)
if not (self.lines and self.focusZone):
return
for i, line in enumerate(self.lines):
if self.focusZone in line.zones:
self.lineIndex = i
self.zoneIndex = line.zones.index(self.focusZone)
word, offset = self.focusZone.wordWithCaret()
if word:
self.wordIndex = word.index
self.charIndex = offset
break
msg = (
f"FLAT REVIEW: On line {self.lineIndex}, zone {self.zoneIndex} "
f"word {self.wordIndex}, char {self.charIndex}"
)
debug.printMessage(debug.LEVEL_INFO, msg, True)
def splitTextIntoZones(self, accessible, string, startOffset, cliprect):
"""Traverses the string, splitting it up into separate zones if the
string contains the EMBEDDED_OBJECT_CHARACTER, which is used by apps
such as Firefox to handle containment of things such as links in
paragraphs.
Arguments:
- accessible: the accessible
- string: a substring from the accessible's text specialization
- startOffset: the starting character offset of the string
- cliprect: the extents that the Zones must fit inside.
Returns a list of Zones for the visible text.
"""
zones = []
substrings = [(*m.span(), m.group(0)) for m in re.finditer(r"[^\ufffc]+", string)]
substrings = list(map(lambda x: (x[0] + startOffset, x[1] + startOffset, x[2]), substrings))
for (start, end, substring) in substrings:
rect = AXText.get_range_rect(accessible, start, end)
intersection = AXComponent.get_rect_intersection(rect, cliprect)
if not AXComponent.is_empty_rect(intersection):
clipping = intersection.x, intersection.y, intersection.width, intersection.height
zones.append(TextZone(accessible, start, substring, *clipping))
return zones
def getZonesFromText(self, accessible, cliprect):
"""Gets a list of Zones from an object that implements the
AccessibleText specialization.
Arguments:
- accessible: the accessible
- cliprect: the extents that the Zones must fit inside.
Returns a list of Zones.
"""
if not self.script.utilities.hasPresentableText(accessible):
return []
zones = []
def _is_container(x):
return AXUtilities.is_scroll_pane(x) or AXUtilities.is_document(x)
container = AXObject.find_ancestor(accessible, _is_container)
if container:
rect = AXComponent.get_rect(container)
intersection = AXComponent.get_rect_intersection(rect, cliprect)
if AXComponent.is_same_rect(rect, intersection):
tokens = ["FLAT REVIEW: Cliprect", cliprect, "->", rect, "from", container]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
cliprect = rect
if AXObject.supports_editable_text(accessible) and AXUtilities.is_single_line(accessible):
rect = AXComponent.get_rect(accessible)
extents = rect.x, rect.y, rect.width, rect.height
return [TextZone(accessible, 0, AXText.get_all_text(accessible), *extents)]
tokens = ["FLAT REVIEW: Getting lines for", accessible]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
lines = AXText.get_visible_lines(accessible, cliprect)
tokens = ["FLAT REVIEW:", len(lines), "lines found for", accessible]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
for string, startOffset, endOffset in lines:
zones.extend(self.splitTextIntoZones(accessible, string, startOffset, cliprect))
return zones
def _insertStateZone(self, zones, accessible, extents):
"""If the accessible presents non-textual state, such as a
checkbox or radio button, insert a StateZone representing
that state."""
# TODO - JD: This whole thing is pretty hacky. Either do it
# right or nuke it.
indicatorExtents = [extents[0], extents[1], 1, extents[3]]
role = AXObject.get_role(accessible)
if role == Atspi.Role.TOGGLE_BUTTON:
zone = StateZone(accessible, *indicatorExtents, role=role)
if zone:
zones.insert(0, zone)
return
if role == Atspi.Role.TABLE_CELL \
and self.script.utilities.hasMeaningfulToggleAction(accessible):
role = Atspi.Role.CHECK_BOX
if role not in [Atspi.Role.CHECK_BOX,
Atspi.Role.CHECK_MENU_ITEM,
Atspi.Role.RADIO_BUTTON,
Atspi.Role.RADIO_MENU_ITEM]:
return
zone = None
stateOnLeft = True
if len(zones) == 1 and isinstance(zones[0], TextZone):
textZone = zones[0]
textToLeftEdge = textZone.x - extents.x
textToRightEdge = (extents[0] + extents[2]) - (textZone.x + textZone.width)
stateOnLeft = textToLeftEdge > 20
if stateOnLeft:
indicatorExtents[2] = textToLeftEdge
else:
indicatorExtents[0] = textZone.x + textZone.width
indicatorExtents[2] = textToRightEdge
zone = StateZone(accessible, *indicatorExtents, role=role)
if zone:
if stateOnLeft:
zones.insert(0, zone)
else:
zones.append(zone)
def getZonesFromAccessible(self, accessible, cliprect):
"""Returns a list of Zones for the given accessible."""
rect = AXComponent.get_rect(accessible)
extents = rect.x, rect.y, rect.width, rect.height
role = AXObject.get_role(accessible)
zones = self.getZonesFromText(accessible, cliprect)
if not zones and role in [Atspi.Role.SCROLL_BAR,
Atspi.Role.SLIDER,
Atspi.Role.PROGRESS_BAR]:
zones.append(ValueZone(accessible, *extents))
elif not zones:
string = ""
redundant = [Atspi.Role.TABLE_ROW]
if role not in redundant:
string = self.script.speechGenerator.getName(accessible, inFlatReview=True)
useless = [Atspi.Role.TABLE_CELL, Atspi.Role.LABEL]
if not string and role not in useless:
string = self.script.speechGenerator.getRoleName(accessible)
if string:
zones.append(Zone(accessible, string, *extents))
self._insertStateZone(zones, accessible, extents)
return zones
def _isOrIsIn(self, child, parent):
if not (child and parent):
return False
if child == parent:
return True
return AXObject.find_ancestor(child, lambda x: x == parent)
def setCurrentToZoneWithObject(self, obj):
"""Attempts to set the current zone to obj, if obj is in the current context."""
tokens = ["FLAT REVIEW: Current", self.getCurrentAccessible(),
f"line: {self.lineIndex}, zone: {self.zoneIndex},",
f"word: {self.wordIndex}, char: {self.charIndex})"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
zone = self._findZoneWithObject(obj)
tokens = ["FLAT REVIEW: Zone with", obj, "is", zone]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if zone is None:
return False
for i, line in enumerate(self.lines):
if zone in line.zones:
self.lineIndex = i
self.zoneIndex = line.zones.index(zone)
word, offset = zone.wordWithCaret()
if word:
self.wordIndex = word.index
self.charIndex = offset
msg = "FLAT REVIEW: Updated current zone."
debug.printMessage(debug.LEVEL_INFO, msg, True)
break
else:
msg = "FLAT REVIEW: Failed to update current zone."
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
tokens = ["FLAT REVIEW: Updated", self.getCurrentAccessible(),
f"line: {self.lineIndex}, zone: {self.zoneIndex},",
f"word: {self.wordIndex}, char: {self.charIndex})"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return True
def _findZoneWithObject(self, obj):
"""Returns the existing zone which contains obj."""
if obj is None:
return None
for zone in self.zones:
if zone.accessible == obj:
return zone
# Some items get pruned from the flat review tree. For instance, a
# tree item which has a descendant section whose text is the displayed
# text of the tree item, that section will be in the flat review tree
# but the ancestor item might not.
if AXObject.is_ancestor(zone.accessible, obj):
tokens = ["FLAT REVIEW:", zone.accessible, "is ancestor of zone accessible", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return zone
return None
def getShowingZones(self, root, boundingbox=None):
"""Returns an unsorted list of all the zones under root and the focusZone."""
if boundingbox is None:
boundingbox = self.bounds
objs = self.script.utilities.getOnScreenObjects(root, boundingbox)
tokens = ["FLAT REVIEW:", len(objs), "on-screen objects found for", root]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
allZones, focusZone = [], None
for o in objs:
zones = self.getZonesFromAccessible(o, boundingbox)
if not zones:
descendant = self.script.utilities.realActiveDescendant(o)
if descendant:
zones = self.getZonesFromAccessible(descendant, boundingbox)
if not zones:
continue
allZones.extend(zones)
if not focusZone and zones and self.focusObj and self._isOrIsIn(o, self.focusObj):
zones = list(filter(lambda z: z.hasCaret(), zones)) or zones
focusZone = zones[0]
tokens = ["FLAT REVIEW:", len(allZones), "zones found for", root]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return allZones, focusZone
def clusterZonesByLine(self, zones):
"""Returns a sorted list of Line clusters containing sorted Zones."""
if not zones:
return []
lineClusters = []
sortedZones = sorted(zones, key=lambda z: z.y)
newCluster = [sortedZones.pop(0)]
for zone in sortedZones:
if zone.onSameLine(newCluster[-1]):
newCluster.append(zone)
else:
lineClusters.append(sorted(newCluster, key=lambda z: z.x))
newCluster = [zone]
if newCluster:
lineClusters.append(sorted(newCluster, key=lambda z: z.x))
lines = []
for lineIndex, lineCluster in enumerate(lineClusters):
lines.append(Line(lineIndex, lineCluster))
for zoneIndex, zone in enumerate(lineCluster):
zone.line = lines[lineIndex]
zone.index = zoneIndex
tokens = ["FLAT REVIEW: Zones clustered into", len(lines), "lines"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return lines
def getCurrent(self, flatReviewType=ZONE):
"""Returns the current string, offset, and extent information."""
# TODO - JD: This method has not (yet) been renamed. But we have a
# getter and setter which do totally different things....
zone = self._getCurrentZone()
if not zone:
return None, -1, -1, -1, -1
current = zone
if flatReviewType == Context.LINE:
current = zone.line
elif flatReviewType != Context.ZONE and zone.words:
current = zone.words[self.wordIndex]
if flatReviewType == Context.CHAR and current.chars:
try:
current = current.chars[self.charIndex]
except Exception:
return None, -1, -1, -1, -1
return current.string, current.x, current.y, current.width, current.height
def setCurrent(self, lineIndex, zoneIndex, wordIndex, charIndex):
"""Sets the current character of interest.
Arguments:
- lineIndex: index into lines
- zoneIndex: index into lines[lineIndex].zones
- wordIndex: index into lines[lineIndex].zones[zoneIndex].words
- charIndex: index lines[lineIndex].zones[zoneIndex].words[wordIndex].chars
"""
self.lineIndex = lineIndex
self.zoneIndex = zoneIndex
self.wordIndex = wordIndex
self.charIndex = charIndex
self.targetCharInfo = self.getCurrent(Context.CHAR)
def _getClickPoint(self):
string, x, y, width, height = self.getCurrent(Context.CHAR)
if (x < 0 and y < 0) or (width <= 0 and height <=0):
return -1, -1
# Click left of center to position the caret there.
x = int(max(x, x + (width / 2) - 1))
y = int(y + height / 2)
return x, y
def routeToCurrent(self):
"""Routes the mouse pointer to the current accessible."""
x, y = self._getClickPoint()
if x < 0 or y < 0:
return False
return AXEventSynthesizer.route_to_point(x, y)
def clickCurrent(self, button=1):
"""Performs a mouse click on the current accessible."""
x, y = self._getClickPoint()
if x >= 0 and y >= 0 and AXEventSynthesizer.click_point(x, y, button):
return True
if AXEventSynthesizer.click_object(self.getCurrentAccessible(), button):
return True
return False
def _getCurrentZone(self):
if not (self.lines and 0 <= self.lineIndex < len(self.lines)):
return None
line = self.lines[self.lineIndex]
if not (line and 0 <= self.zoneIndex < len(line.zones)):
return None
return line.zones[self.zoneIndex]
def getCurrentAccessible(self):
"""Returns the current accessible."""
zone = self._getCurrentZone()
if not zone:
return None
return zone.accessible
def getCurrentBrailleRegions(self):
"""Gets the braille for the entire current line.
Returns [regions, regionWithFocus]
"""
if (not self.lines) \
or (not self.lines[self.lineIndex].zones):
return [None, None]
regionWithFocus = None
line = self.lines[self.lineIndex]
regions = line.getBrailleRegions()
# Now find the current region and the current character offset
# into that region.
#
for zone in line.zones:
if zone.index == self.zoneIndex:
regionWithFocus = zone.brailleRegion
regionWithFocus.cursorOffset = 0
if zone.words:
regionWithFocus.cursorOffset += zone.words[0].startOffset - zone.startOffset
for wordIndex in range(0, self.wordIndex):
regionWithFocus.cursorOffset += \
len(zone.words[wordIndex].string)
regionWithFocus.cursorOffset += self.charIndex
regionWithFocus.repositionCursor()
break
return [regions, regionWithFocus]
def goBegin(self, flatReviewType=WINDOW):
"""Moves this context's locus of interest to the first char
of the first relevant zone.
Arguments:
- flatReviewType: one of ZONE, LINE or WINDOW
Returns True if the locus of interest actually changed.
"""
if (flatReviewType == Context.LINE) or (flatReviewType == Context.ZONE):
lineIndex = self.lineIndex
elif flatReviewType == Context.WINDOW:
lineIndex = 0
else:
raise Exception("Invalid type: %d" % flatReviewType)
if flatReviewType == Context.ZONE:
zoneIndex = self.zoneIndex
else:
zoneIndex = 0
wordIndex = 0
charIndex = 0
moved = (self.lineIndex != lineIndex) \
or (self.zoneIndex != zoneIndex) \
or (self.wordIndex != wordIndex) \
or (self.charIndex != charIndex) \
if moved:
self.lineIndex = lineIndex
self.zoneIndex = zoneIndex
self.wordIndex = wordIndex
self.charIndex = charIndex
self.targetCharInfo = self.getCurrent(Context.CHAR)
return moved
def goEnd(self, flatReviewType=WINDOW):
"""Moves this context's locus of interest to the last char
of the last relevant zone.
Arguments:
- flatReviewType: one of ZONE, LINE, or WINDOW
Returns True if the locus of interest actually changed.
"""
if (flatReviewType == Context.LINE) or (flatReviewType == Context.ZONE):
lineIndex = self.lineIndex
elif flatReviewType == Context.WINDOW:
lineIndex = len(self.lines) - 1
else:
raise Exception("Invalid type: %d" % flatReviewType)
if flatReviewType == Context.ZONE:
zoneIndex = self.zoneIndex
else:
zoneIndex = len(self.lines[lineIndex].zones) - 1
zone = self.lines[lineIndex].zones[zoneIndex]
if zone.words:
wordIndex = len(zone.words) - 1
chars = zone.words[wordIndex].chars
if chars:
charIndex = len(chars) - 1
else:
charIndex = 0
else:
wordIndex = 0
charIndex = 0
moved = (self.lineIndex != lineIndex) \
or (self.zoneIndex != zoneIndex) \
or (self.wordIndex != wordIndex) \
or (self.charIndex != charIndex) \
if moved:
self.lineIndex = lineIndex
self.zoneIndex = zoneIndex
self.wordIndex = wordIndex
self.charIndex = charIndex
self.targetCharInfo = self.getCurrent(Context.CHAR)
return moved
def goPrevious(self, flatReviewType=ZONE,
wrap=WRAP_ALL, omitWhitespace=True):
"""Moves this context's locus of interest to the first char
of the previous type.
Arguments:
- flatReviewType: one of ZONE, CHAR, WORD, LINE
- wrap: if True, will cross boundaries, including top and
bottom; if False, will stop on boundaries.
Returns True if the locus of interest actually changed.
"""
if not self.lines:
debug.printMessage(debug.LEVEL_INFO, 'goPrevious(): no lines in context')
return False
moved = False
if flatReviewType == Context.ZONE:
if self.zoneIndex > 0:
self.zoneIndex -= 1
self.wordIndex = 0
self.charIndex = 0
moved = True
elif wrap & Context.WRAP_LINE:
if self.lineIndex > 0:
self.lineIndex -= 1
self.zoneIndex = len(self.lines[self.lineIndex].zones) - 1
self.wordIndex = 0
self.charIndex = 0
moved = True
elif wrap & Context.WRAP_TOP_BOTTOM:
self.lineIndex = len(self.lines) - 1
self.zoneIndex = len(self.lines[self.lineIndex].zones) - 1
self.wordIndex = 0
self.charIndex = 0
moved = True
elif flatReviewType == Context.CHAR:
if self.charIndex > 0:
self.charIndex -= 1
moved = True
else:
moved = self.goPrevious(Context.WORD, wrap, False)
if moved:
zone = self.lines[self.lineIndex].zones[self.zoneIndex]
if zone.words:
chars = zone.words[self.wordIndex].chars
if chars:
self.charIndex = len(chars) - 1
elif flatReviewType == Context.WORD:
zone = self.lines[self.lineIndex].zones[self.zoneIndex]
accessible = zone.accessible
lineIndex = self.lineIndex
zoneIndex = self.zoneIndex
wordIndex = self.wordIndex
charIndex = self.charIndex
if self.wordIndex > 0:
self.wordIndex -= 1
self.charIndex = 0
moved = True
else:
moved = self.goPrevious(Context.ZONE, wrap)
if moved:
zone = self.lines[self.lineIndex].zones[self.zoneIndex]
if zone.words:
self.wordIndex = len(zone.words) - 1
# If we landed on a whitespace word or something with no words,
# we might need to move some more.
#
zone = self.lines[self.lineIndex].zones[self.zoneIndex]
if omitWhitespace \
and moved \
and ((len(zone.string) == 0) \
or (len(zone.words) \
and zone.words[self.wordIndex].string.isspace())):
hasMoreText = False
if self.lineIndex > 0 and isinstance(zone, TextZone):
prevZone = self.lines[self.lineIndex - 1].zones[-1]
if prevZone.accessible == zone.accessible:
hasMoreText = True
# If we're on whitespace in the same zone, then let's
# try to move on. If not, we've definitely moved
# across accessibles. If that's the case, let's try
# to find the first 'real' word in the accessible.
# If we cannot, then we're just stuck on an accessible
# with no words and we should do our best to announce
# this to the user (e.g., "whitespace" or "blank").
#
if zone.accessible == accessible or hasMoreText:
moved = self.goPrevious(Context.WORD, wrap)
else:
wordIndex = self.wordIndex - 1
while wordIndex >= 0:
if (not zone.words[wordIndex].string) \
or not len(zone.words[wordIndex].string) \
or zone.words[wordIndex].string.isspace():
wordIndex -= 1
else:
break
if wordIndex >= 0:
self.wordIndex = wordIndex
if not moved:
self.lineIndex = lineIndex
self.zoneIndex = zoneIndex
self.wordIndex = wordIndex
self.charIndex = charIndex
elif flatReviewType == Context.LINE:
if wrap & Context.WRAP_LINE:
if self.lineIndex > 0:
self.lineIndex -= 1
self.zoneIndex = 0
self.wordIndex = 0
self.charIndex = 0
moved = True
elif (wrap & Context.WRAP_TOP_BOTTOM) \
and (len(self.lines) != 1):
self.lineIndex = len(self.lines) - 1
self.zoneIndex = 0
self.wordIndex = 0
self.charIndex = 0
moved = True
else:
raise Exception("Invalid type: %d" % flatReviewType)
if moved and (flatReviewType != Context.LINE):
self.targetCharInfo = self.getCurrent(Context.CHAR)
return moved
def goNext(self, flatReviewType=ZONE, wrap=WRAP_ALL, omitWhitespace=True):
"""Moves this context's locus of interest to first char of
the next type.
Arguments:
- flatReviewType: one of ZONE, CHAR, WORD, LINE
- wrap: if True, will cross boundaries, including top and
bottom; if False, will stop on boundaries.
"""
if not self.lines:
debug.printMessage(debug.LEVEL_INFO, 'goNext(): no lines in context')
return False
moved = False
if flatReviewType == Context.ZONE:
if self.zoneIndex < (len(self.lines[self.lineIndex].zones) - 1):
self.zoneIndex += 1
self.wordIndex = 0
self.charIndex = 0
moved = True
elif wrap & Context.WRAP_LINE:
if self.lineIndex < (len(self.lines) - 1):
self.lineIndex += 1
self.zoneIndex = 0
self.wordIndex = 0
self.charIndex = 0
moved = True
braille.clear()
elif wrap & Context.WRAP_TOP_BOTTOM:
self.lineIndex = 0
self.zoneIndex = 0
self.wordIndex = 0
self.charIndex = 0
moved = True
braille.clear()
elif flatReviewType == Context.CHAR:
zone = self.lines[self.lineIndex].zones[self.zoneIndex]
if zone.words:
chars = zone.words[self.wordIndex].chars
if chars:
if self.charIndex < (len(chars) - 1):
self.charIndex += 1
moved = True
else:
moved = self.goNext(Context.WORD, wrap, False)
else:
moved = self.goNext(Context.WORD, wrap)
else:
moved = self.goNext(Context.ZONE, wrap)
elif flatReviewType == Context.WORD:
zone = self.lines[self.lineIndex].zones[self.zoneIndex]
accessible = zone.accessible
lineIndex = self.lineIndex
zoneIndex = self.zoneIndex
wordIndex = self.wordIndex
charIndex = self.charIndex
if zone.words:
if self.wordIndex < (len(zone.words) - 1):
self.wordIndex += 1
self.charIndex = 0
moved = True
else:
moved = self.goNext(Context.ZONE, wrap)
else:
moved = self.goNext(Context.ZONE, wrap)
# If we landed on a whitespace word or something with no words,
# we might need to move some more.
#
zone = self.lines[self.lineIndex].zones[self.zoneIndex]
if omitWhitespace \
and moved \
and ((len(zone.string) == 0) \
or (len(zone.words) \
and zone.words[self.wordIndex].string.isspace())):
# If we're on whitespace in the same zone, then let's
# try to move on. If not, we've definitely moved
# across accessibles. If that's the case, let's try
# to find the first 'real' word in the accessible.
# If we cannot, then we're just stuck on an accessible
# with no words and we should do our best to announce
# this to the user (e.g., "whitespace" or "blank").
#
if zone.accessible == accessible:
moved = self.goNext(Context.WORD, wrap)
else:
wordIndex = self.wordIndex + 1
while wordIndex < len(zone.words):
if (not zone.words[wordIndex].string) \
or not len(zone.words[wordIndex].string) \
or zone.words[wordIndex].string.isspace():
wordIndex += 1
else:
break
if wordIndex < len(zone.words):
self.wordIndex = wordIndex
if not moved:
self.lineIndex = lineIndex
self.zoneIndex = zoneIndex
self.wordIndex = wordIndex
self.charIndex = charIndex
elif flatReviewType == Context.LINE:
if wrap & Context.WRAP_LINE:
if self.lineIndex < (len(self.lines) - 1):
self.lineIndex += 1
self.zoneIndex = 0
self.wordIndex = 0
self.charIndex = 0
moved = True
elif (wrap & Context.WRAP_TOP_BOTTOM) \
and (self.lineIndex != 0):
self.lineIndex = 0
self.zoneIndex = 0
self.wordIndex = 0
self.charIndex = 0
moved = True
else:
raise Exception("Invalid type: %d" % flatReviewType)
if moved and (flatReviewType != Context.LINE):
self.targetCharInfo = self.getCurrent(Context.CHAR)
return moved
def goAbove(self, flatReviewType=LINE, wrap=WRAP_ALL):
"""Moves this context's locus of interest to first char
of the type that's closest to and above the current locus of
interest.
Arguments:
- flatReviewType: LINE
- wrap: if True, will cross top/bottom boundaries; if False, will
stop on top/bottom boundaries.
Returns: [string, startOffset, endOffset, x, y, width, height]
"""
moved = False
if flatReviewType == Context.CHAR:
# We want to shoot for the closest character, which we've
# saved away as self.targetCharInfo, which is the list
# [string, x, y, width, height].
#
if not self.targetCharInfo:
self.targetCharInfo = self.getCurrent(Context.CHAR)
target = self.targetCharInfo
[string, x, y, width, height] = target
middleTargetX = x + (width / 2)
moved = self.goPrevious(Context.LINE, wrap)
if moved:
while True:
[string, bx, by, bwidth, bheight] = \
self.getCurrent(Context.CHAR)
if (bx + width) >= middleTargetX:
break
elif not self.goNext(Context.CHAR, Context.WRAP_NONE):
break
# Moving around might have reset the current targetCharInfo,
# so we reset it to our saved value.
#
self.targetCharInfo = target
elif flatReviewType == Context.LINE:
return self.goPrevious(flatReviewType, wrap)
else:
raise Exception("Invalid type: %d" % flatReviewType)
return moved
def goBelow(self, flatReviewType=LINE, wrap=WRAP_ALL):
"""Moves this context's locus of interest to the first
char of the type that's closest to and below the current
locus of interest.
Arguments:
- flatReviewType: one of WORD, LINE
- wrap: if True, will cross top/bottom boundaries; if False, will
stop on top/bottom boundaries.
Returns: [string, startOffset, endOffset, x, y, width, height]
"""
moved = False
if flatReviewType == Context.CHAR:
# We want to shoot for the closest character, which we've
# saved away as self.targetCharInfo, which is the list
# [string, x, y, width, height].
#
if not self.targetCharInfo:
self.targetCharInfo = self.getCurrent(Context.CHAR)
target = self.targetCharInfo
[string, x, y, width, height] = target
middleTargetX = x + (width / 2)
moved = self.goNext(Context.LINE, wrap)
if moved:
while True:
[string, bx, by, bwidth, bheight] = \
self.getCurrent(Context.CHAR)
if (bx + width) >= middleTargetX:
break
elif not self.goNext(Context.CHAR, Context.WRAP_NONE):
break
# Moving around might have reset the current targetCharInfo,
# so we reset it to our saved value.
#
self.targetCharInfo = target
elif flatReviewType == Context.LINE:
moved = self.goNext(flatReviewType, wrap)
else:
raise Exception("Invalid type: %d" % flatReviewType)
return moved