Re: Orca Document reading function only works partially
- From: Rich Burridge <Rich Burridge Sun COM>
- To: Hermann <steppenwolf2 onlinehome de>
- Cc: orca-list gnome org
- Subject: Re: Orca Document reading function only works partially
- Date: Wed, 14 Mar 2007 10:25:19 -0700
Hi hermann,
> as an attachment I send the debug file.
> I tested e-mail reading, reading a text file containing blank lines
between
> paragraphs, and reading an OO document without blank lines - BTW. the
> latter works correctly.
Thanks. I found two problems here:
1/
Traceback (most recent call last):
...
File "/usr/lib/python2.4/site-packages/orca/braille.py", line 697, in
refresh
writeStruct.text += " "
TypeError: unsupported operand type(s) for +=: 'NoneType' and 'str'
2/
Traceback (most recent call last):
...
File "/usr/lib/python2.4/site-packages/orca/scripts/Evolution.py",
line 309, in textLines
for [context, acss] in default.Script.textLines(textObj):
TypeError: unbound method textLines() must be called with Script
instance as first argument (got Accessible instance instead)
Taking the second one first. This was caused by the recent refactor. It
was a problem specific to just the Evolution.py script. I've attached a
new version of that script that hopefully now works. Please copy it to
your .../src/orca/scripts directory and give it a try (after reinstalling).
The first problem has apparently been there for quite a while. It's a
problem with brailling empty lines. Will suggested a rework of the code
in this area that we hope should fix it.
So I've attached a new version of braille.py. This needs to go in the
.../src/orca directory. I suggest you take a copy of the existing one
first, just in case I've made a boo boo. Don't forget the reinstall.
Could you retry the tests and attach a new debug.out. If those two
tracebacks have gone away (and say all in Evolution is now working),
then I'll check them in.
Thanks
# Orca
#
# Copyright 2005-2006 Sun Microsystems Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 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
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
"""Custom script for Evolution."""
__id__ = "$Id: Evolution.py 2140 2007-03-07 20:04:06Z richb $"
__version__ = "$Revision: 2140 $"
__date__ = "$Date: 2007-03-07 12:04:06 -0800 (Wed, 07 Mar 2007) $"
__copyright__ = "Copyright (c) 2005-2006 Sun Microsystems Inc."
__license__ = "LGPL"
import orca.debug as debug
import orca.default as default
import orca.atspi as atspi
import orca.input_event as input_event
import orca.keybindings as keybindings
import orca.rolenames as rolenames
import orca.braille as braille
import orca.orca_state as orca_state
import orca.speech as speech
import orca.settings as settings
import orca.chnames as chnames
from orca.orca_i18n import _ # for gettext support
########################################################################
# #
# The Evolution script class. #
# #
########################################################################
class Script(default.Script):
def __init__(self, app):
"""Creates a new script for the given application.
Arguments:
- app: the application to create a script for.
"""
# Set the debug level for all the methods in this script.
#
self.debugLevel = debug.LEVEL_FINEST
default.Script.__init__(self, app)
# This will be used to cache a handle to the message area in the
# Mail compose window.
self.message_panel = None
# Dictionary of Setup Assistant panels already handled.
#
self.setupPanels = {}
# Dictionary of Setup Assistant labels already handled.
#
self.setupLabels = {}
# The last row and column we were on in the mail message header list.
self.lastMessageColumn = -1
self.lastMessageRow = -1
# By default, don't present if Evolution is not the active application.
#
self.presentIfInactive = False
# Evolution defines new custom roles. We need to make them known
# to Orca for Speech and Braille output.
rolenames.ROLE_CALENDAR_VIEW = "Calendar View"
rolenames.rolenames[rolenames.ROLE_CALENDAR_VIEW] = \
rolenames.Rolename(rolenames.ROLE_CALENDAR_VIEW,
_("calv"),
_("CalendarView"),
_("calendar view"))
rolenames.ROLE_CALENDAR_EVENT = "Calendar Event"
rolenames.rolenames[rolenames.ROLE_CALENDAR_EVENT] = \
rolenames.Rolename(rolenames.ROLE_CALENDAR_EVENT,
_("cale"),
_("CalendarEvent"),
_("calendar event"))
def setupInputEventHandlers(self):
"""Defines InputEventHandler fields for this script that can be
called by the key and braille bindings. In this particular case,
we just want to be able to define our own sayAll() method.
"""
default.Script.setupInputEventHandlers(self)
self.inputEventHandlers["sayAllHandler"] = \
input_event.InputEventHandler(
Script.sayAll,
_("Speaks entire document."))
self.inputEventHandlers["toggleReadMailHandler"] = \
input_event.InputEventHandler(
Script.toggleReadMail,
_("Toggle whether we present new mail if we are not the active script."))
def getKeyBindings(self):
"""Defines the new key binding for this script. Setup the default
key bindings, then add one in for toggling whether we present new
mail if we not not the active script.
Returns an instance of keybindings.KeyBindings.
"""
debug.println(self.debugLevel, "Evolution.getKeyBindings.")
keyBindings = default.Script.getKeyBindings(self)
keyBindings.add(
keybindings.KeyBinding(
"n",
1 << settings.MODIFIER_ORCA,
1 << settings.MODIFIER_ORCA,
self.inputEventHandlers["toggleReadMailHandler"]))
return keyBindings
def toggleReadMail(self, inputEvent):
""" Toggle whether we present new mail if we not not the active script.+
Arguments:
- inputEvent: if not None, the input event that caused this action.
"""
debug.println(self.debugLevel, "Evolution.toggleReadMail.")
line = _("present new mail if this script is not active.")
self.presentIfInactive = not self.presentIfInactive
if not self.presentIfInactive:
line = _("do not present new mail if this script is not active.")
speech.speak(line)
return True
def speakSetupAssistantLabel(self, label):
"""Perform a variety of tests on this Setup Assistant label to see
if we want to speak it.
Arguments:
- label: the Setup Assistant Label.
"""
if label.state.count(atspi.Accessibility.STATE_SHOWING):
# We are only interested in a label if all the panels in the
# component hierarchy have states of ENABLED, SHOWING and VISIBLE.
# If this is not the case, then just return.
#
obj = label.parent
while obj and obj.role != rolenames.ROLE_APPLICATION:
if obj.role == rolenames.ROLE_PANEL:
state = obj.state
if not state.count(atspi.Accessibility.STATE_ENABLED) or \
not state.count(atspi.Accessibility.STATE_SHOWING) or \
not state.count(atspi.Accessibility.STATE_VISIBLE):
return
obj = obj.parent
# Each Setup Assistant screen has one label in the top left
# corner that describes what this screen is for. It has a text
# weight attribute of 800. We always speak those labels with
# " screen" appended.
#
if label.text:
charAttributes = label.text.getAttributes(0)
if charAttributes[0]:
[charKeys, charDict] = \
self.textAttrsToDictionary(charAttributes[0])
weight = charDict.get('weight')
if weight and weight == '800':
text = self.getDisplayedText(label)
# Only speak the screen label if we haven't already
# done so.
#
if text and not self.setupLabels.has_key(label):
speech.speak(text + _(" screen"), None, False)
self.setupLabels[label] = True
# If the locus of focus is a push button that's
# insensitive, speak/braille about it. (The
# Identity screen has such a component).
#
if orca_state.locusOfFocus and \
orca_state.locusOfFocus.role == \
rolenames.ROLE_PUSH_BUTTON and \
(orca_state.locusOfFocus.state.count( \
atspi.Accessibility.STATE_SENSITIVE) == 0):
self.updateBraille(orca_state.locusOfFocus)
speech.speakUtterances(
self.speechGenerator.getSpeech( \
orca_state.locusOfFocus, False))
# It's possible to get multiple "object:state-changed:showing"
# events for the same label. If we've already handled this
# label, then just ignore it.
#
text = self.getDisplayedText(label)
if text and not self.setupLabels.has_key(label):
# Most of the Setup Assistant screens have a useful piece
# of text starting with the word "Please". We want to speak
# these. For the first screen, the useful piece of text
# starts with "Welcome". For the last screen, it starts
# with "Congratulations". Speak those too.
#
if text.startswith(_("Please")) or \
text.startswith(_("Welcome")) or \
text.startswith(_("Congratulations")):
speech.speak(text, None, False)
self.setupLabels[label] = True
def handleSetupAssistantPanel(self, panel):
"""Find all the labels in this Setup Assistant panel and see if
we want to speak them.
Arguments:
- panel: the Setup Assistant panel.
"""
allLabels = self.findByRole(panel, rolenames.ROLE_LABEL)
for label in allLabels:
self.speakSetupAssistantLabel(label)
def readPageTab(self, tab):
"""Speak/Braille the given page tab. The speech verbosity is set
to VERBOSITY_LEVEL_BRIEF for this operation and then restored
to its previous value on completion.
Arguments:
- tab: the page tab to speak/braille.
"""
brailleGen = self.brailleGenerator
speechGen = self.speechGenerator
savedSpeechVerbosityLevel = settings.speechVerbosityLevel
settings.speechVerbosityLevel = settings.VERBOSITY_LEVEL_BRIEF
utterances = speechGen.getSpeech(tab, False)
speech.speakUtterances(utterances)
settings.speechVerbosityLevel = savedSpeechVerbosityLevel
braille.displayRegions(brailleGen.getBrailleRegions(tab))
def getTimeForCalRow(self, row, noIncs):
"""Return a string equivalent to the time of the given row in
the calendar day view. Each calendar row is equivalent to
a certain time interval (from 5 minutes upto 1 hour), with
time (row 0) starting at 12 am (midnight).
Arguments:
- row: the row number.
- noIncs: the number of equal increments that the 24 hour period
is divided into.
Returns the time as a string.
"""
totalMins = timeIncrements[noIncs] * row
if totalMins < 720:
suffix = 'A.M.'
else:
totalMins -= 720
suffix = 'P.M.'
hrs = hours[totalMins / 60]
mins = minutes[totalMins % 60]
return hrs + ' ' + mins + ' ' + suffix
def readTextLines(self):
"""This an iterator that produces elements of the form:
[SayAllContext, acss], where SayAllContext has the text to be
spoken and acss is an ACSS instance for speaking the text. We
do this because of the way Evolution lays out its messages:
each paragraph tends to be in its own panel, each of which is
in a higher level panel. So, we just traverse through the
children.
"""
panel = orca_state.locusOfFocus.parent
htmlPanel = orca_state.locusOfFocus.parent.parent
startIndex = panel.index
for i in range(startIndex, htmlPanel.childCount):
accPanel = htmlPanel.accessible.getChildAtIndex(i)
panel = atspi.Accessible.makeAccessible(accPanel)
accTextObj = panel.accessible.getChildAtIndex(0)
textObj = atspi.Accessible.makeAccessible(accTextObj)
for [context, acss] in self.textLines(textObj):
yield [context, acss]
def sayAll(self, inputEvent):
"""Speak all the text associated with the text object that has
focus. We have to define our own method here because Evolution
does not implement the FLOWS_TO relationship and all the text
are in an HTML panel which contains multiple panels, each
containing a single text object.
Arguments:
- inputEvent: if not None, the input event that caused this action.
"""
debug.println(self.debugLevel, "evolution.sayAll.")
if orca_state.locusOfFocus and orca_state.locusOfFocus.text:
speech.sayAll(self.readTextLines(),
self.__sayAllProgressCallback)
else:
default.Script.sayAll(self, inputEvent)
return True
# This method tries to detect and handle the following cases:
# 1) Mail view: current message pane: individual lines of text.
# 2) Mail view: current message pane: "standard" mail header lines.
# 3) Mail view: message header list
# 4) Calendar view: day view: tabbing to day with appts.
# 5) Calendar view: day view: moving with arrow keys.
# 6) Preferences Dialog: options list.
# 7) Mail view: insert attachment dialog: unlabelled arrow button.
# 8) Mail compose window: message area
# 9) Spell Checking Dialog
# 10) Mail view: message area - attachments.
# 11) Setup Assistant
def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
"""Called when the visual object with focus changes.
Arguments:
- event: if not None, the Event that caused the change
- oldLocusOfFocus: Accessible that is the old locus of focus
- newLocusOfFocus: Accessible that is the new locus of focus
"""
brailleGen = self.brailleGenerator
speechGen = self.speechGenerator
debug.printObjectEvent(self.debugLevel,
event,
event.source.toString())
# self.printAncestry(event.source)
# 1) Mail view: current message pane: individual lines of text.
#
# When the focus is in the pane containing the lines of an
# actual mail message, then, for each of those lines, we
# don't want to speak "text", the role of the component that
# currently has focus.
#
# The situation is determine by checking the roles of the current
# component, plus its parent, plus its parent. We are looking for
# "text", "panel" and "unknown". If we find that, then (hopefully)
# it's a line in the mail message and we get the utterances to
# speak for that Text.
rolesList = [rolenames.ROLE_TEXT, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_UNKNOWN]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - mail view: " \
+ "current message pane: " \
+ "individual lines of text.")
[string, caretOffset, startOffset] = \
self.getTextLineAtCaret(event.source)
braille.displayMessage(string)
if settings.enableSpeechIndentation:
self.speakTextIndentation(event.source, string)
line = self.adjustForRepeats(string)
if self.speakNewLine(event.source):
speech.speak(chnames.getCharacterName("\n"), None, False)
if self.speakBlankLine(event.source):
speech.speak(_("blank"), None, False)
else:
speech.speak(line, None, False)
return
# 2) Mail view: current message pane: "standard" mail header lines.
#
# Check if the focus is in the From:, To:, Subject: or Date: headers
# of a message in the message area, and that we want to speak all of
# the tables cells for that current row.
#
# The situation is determine by checking the roles of the current
# component, plus its parent, plus its parent. We are looking for
# "text", "panel" and "table cell". If we find that, then (hopefully)
# it's a header line in the mail message.
#
# For each of the table cells in the current row in the table, we
# have to work our way back down the component hierarchy until we
# get a component with no children. We then use the role of that
# component to determine how to speak its contents.
#
# NOTE: the code assumes that there is only one child within each
# component and that the final component (with no children) is of
# role TEXT.
rolesList = [rolenames.ROLE_TEXT, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_TABLE_CELL]
if settings.readTableCellRow \
and (self.isDesiredFocusedItem(event.source, rolesList)):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - mail view: " \
+ "current message pane: " \
+ "standard mail header lines.")
obj = event.source.parent.parent
parent = obj.parent
if parent.role == rolenames.ROLE_TABLE:
row = parent.table.getRowAtIndex(obj.index)
utterances = []
regions = []
for i in range(0, parent.table.nColumns):
obj = parent.table.getAccessibleAt(row, i)
cell = atspi.Accessible.makeAccessible(obj)
while cell.childCount:
cell = cell.child(0)
if cell.role == rolenames.ROLE_TEXT:
regions.append(braille.Text(cell))
[string, caretOffset, startOffset] = \
self.getTextLineAtCaret(cell)
utterances.append(string)
braille.displayRegions([regions, regions[0]])
speech.speakUtterances(utterances)
return
# 3) Mail view: message header list
#
# Check if the focus is in the message header list. If this focus
# event is for a different row that the last time we got a similar
# focus event, we want to speak all of the tables cells (and the
# header for the one that currently has focus) in the current
# highlighted message. (The role is only spoken/brailled for the
# table cell that currently has focus).
#
# If this focus event is just for a different table cell on the same
# row as last time, then we just speak the current cell (and its
# header).
#
# The braille cursor to set to point to the current cell.
#
# Note that the Evolution user can adjust which columns appear in
# the message list and the order in which they appear, so that
# Orca will just speak the ones that they are interested in.
rolesList = [rolenames.ROLE_TABLE_CELL, \
rolenames.ROLE_TREE_TABLE]
if settings.readTableCellRow \
and (self.isDesiredFocusedItem(event.source, rolesList)):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - mail view: " \
+ "message header list.")
# Unfortunately the default read table cell row handling won't
# just work with Evolution (see bogusity comment below). We
# quickly solve this by setting readTableCellRow to False
# for the duration of this code section, then resetting it to
# True at the end.
#
settings.readTableCellRow = False
parent = event.source.parent
row = parent.table.getRowAtIndex(event.source.index)
column = parent.table.getColumnAtIndex(event.source.index)
# This is an indication of whether we should speak all the table
# cells (the user has moved focus up or down the list), or just
# the current one (focus has moved left or right in the same row).
# If we at the start or the end of the message header list and
# the row and column haven't changed, then speak all the table
# cells.
speakAll = (self.lastMessageRow != row) or \
((row == 0 or row == parent.table.nRows-1) and \
self.lastMessageColumn == column)
savedBrailleVerbosityLevel = settings.brailleVerbosityLevel
savedSpeechVerbosityLevel = settings.speechVerbosityLevel
brailleRegions = []
cellWithFocus = None
# If the current locus of focus is not a table cell, then we
# are entering the mail message header list (rather than moving
# around inside it), so speak/braille the number of mail messages
# total.
#
# This code section handles one other bogusity. As Evolution is
# initially being rendered on the screen, the focus at some point
# is given to the highlighted row in the mail message header list.
# Because of this, self.lastMessageRow and self.lastMessageColumn
# will be set to that row number and column number, making the
# setting of the speakAll variable above, incorrect. We fix that
# up here.
if orca_state.locusOfFocus.role != rolenames.ROLE_TABLE_CELL:
speakAll = True
message = "%d messages" % \
parent.table.nRows
brailleRegions.append(braille.Region(message))
speech.speak(message)
for i in range(0, parent.table.nColumns):
obj = parent.table.getAccessibleAt(row, i)
if obj:
cell = atspi.Accessible.makeAccessible(obj)
verbose = (cell.index == event.source.index)
# Check if the current table cell is a check box. If it
# is, then to reduce verbosity, only speak and braille it,
# if it's checked or if we are moving the focus from to the
# left or right on the same row.
#
# The one exception to the above is if this is for the
# Status checkbox, in which case we speake/braille it if
# the message is unread (not checked).
#
header_obj = parent.table.getColumnHeader(i)
header = atspi.Accessible.makeAccessible(header_obj)
checkbox = False
toRead = True
action = cell.action
if action:
for j in range(0, action.nActions):
if action.getName(j) == "toggle":
checkbox = True
checked = cell.state.count( \
atspi.Accessibility.STATE_CHECKED)
if speakAll:
if header.name == _("Status"):
toRead = not checked
break
if not checked:
toRead = False
break
if toRead:
# Speak/braille the column header for this table cell
# if it has focus (unless it's a checkbox).
#
if not checkbox and verbose:
settings.brailleVerbosityLevel = \
settings.VERBOSITY_LEVEL_BRIEF
settings.speechVerbosityLevel = \
settings.VERBOSITY_LEVEL_BRIEF
utterances = speechGen.getSpeech(header, False)
[headerRegions, focusedRegion] = \
brailleGen.getBrailleRegions(header)
brailleRegions.extend(headerRegions)
brailleRegions.append(braille.Region(" "))
if column == i:
cellWithFocus = focusedRegion
if speakAll or (column == i):
speech.speakUtterances(utterances)
# Speak/braille the table cell.
#
# If this cell has a column header of "Status",
# then speak/braille "read".
# If this cell has a column header of "Attachment",
# then speak/braille "attachment".
#
if verbose:
settings.brailleVerbosityLevel = \
settings.VERBOSITY_LEVEL_VERBOSE
else:
settings.brailleVerbosityLevel = \
settings.VERBOSITY_LEVEL_BRIEF
settings.speechVerbosityLevel = \
savedSpeechVerbosityLevel
utterances = speechGen.getSpeech(cell, False)
[cellRegions, focusedRegion] = \
brailleGen.getBrailleRegions(cell)
if header.name == _("Status"):
text = _("unread")
utterances = [ text ]
brailleRegions.append(braille.Region(text))
brailleRegions.append(braille.Region(" "))
elif header.name == _("Attachment"):
text = header.name
utterances = [ text ]
brailleRegions.append(braille.Region(text))
brailleRegions.append(braille.Region(" "))
else:
brailleRegions.extend(cellRegions)
brailleRegions.append(braille.Region(" "))
# If the current focus is on a checkbox then we won't
# have set braille line focus to its header above, so
# set it to the cell instead.
#
if column == i and cellWithFocus == None:
cellWithFocus = focusedRegion
if speakAll or (column == i):
speech.speakUtterances(utterances)
if brailleRegions != []:
braille.displayRegions([brailleRegions, cellWithFocus])
settings.brailleVerbosityLevel = savedBrailleVerbosityLevel
settings.speechVerbosityLevel = savedSpeechVerbosityLevel
self.lastMessageColumn = column
self.lastMessageRow = row
settings.readTableCellRow = True
return
# 4) Calendar view: day view: tabbing to day with appts.
#
# If the focus is in the Calendar Day View on an appointment, then
# provide the user with useful feedback. First we get the current
# date and appointment summary from the parent. This is then followed
# by getting the information on the current appointment.
#
# The start time for the appointment is determined by detecting the
# equivalent child in the parent Calendar View's table has the same
# y position on the screen.
#
# The end time for the appointment is determined by using the height
# of the current appointment component divided by the height of a
# single child in the parent Calendar View's table
#
# Both of these time values depend upon the value of a time increment
# which is determined by the number of children in the parent Calendar
# View's table.
rolesList = [rolenames.ROLE_CALENDAR_EVENT, \
rolenames.ROLE_CALENDAR_VIEW]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - calendar view: " \
+ "day view: tabbing to day with appts.")
parent = event.source.parent
utterances = speechGen.getSpeech(parent, False)
[brailleRegions, focusedRegion] = \
brailleGen.getBrailleRegions(parent)
speech.speakUtterances(utterances)
apptExtents = event.source.component.getExtents(0)
for i in range(0, parent.childCount):
child = parent.child(i)
if (child.role == rolenames.ROLE_TABLE):
noRows = child.table.nRows
for j in range(0, noRows):
row = child.table.getRowAtIndex(j)
obj = child.table.getAccessibleAt(row, 0)
appt = atspi.Accessible.makeAccessible(obj)
extents = appt.component.getExtents(0)
if extents.y == apptExtents.y:
utterances = speechGen.getSpeech(event.source, \
False)
[apptRegions, focusedRegion] = \
brailleGen.getBrailleRegions(event.source)
brailleRegions.extend(apptRegions)
speech.speakUtterances(utterances)
startTime = 'Start time ' + \
self.getTimeForCalRow(j, noRows)
brailleRegions.append(braille.Region(startTime))
speech.speak(startTime)
apptLen = apptExtents.height / extents.height
endTime = 'End time ' + \
self.getTimeForCalRow(j + apptLen, noRows)
brailleRegions.append(braille.Region(endTime))
speech.speak(endTime)
braille.displayRegions([brailleRegions,
brailleRegions[0]])
return
# 5) Calendar view: day view: moving with arrow keys.
#
# If the focus is in the Calendar Day View, check to see if there
# are any appointments starting at the current time. If there are,
# then provide the user with useful feedback for that appointment,
# otherwise output the current time and state that there are no
# appointments.
#
# First get the y position of the current table entry. Then compare
# this will any Calendar Events in the parent Calendar View. If their
# y position is the same, then speak that information.
#
# The end time for the appointment is determined by using the height
# of the current appointment component divided by the height of a
# single child in the parent Calendar View's table
#
# Both of these time values depend upon the value of a time increment
# which is determined by the number of children in the parent Calendar
# View's table.
rolesList = [rolenames.ROLE_UNKNOWN, \
rolenames.ROLE_TABLE, \
rolenames.ROLE_CALENDAR_VIEW]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - calendar view: " \
+ "day view: moving with arrow keys.")
brailleRegions = []
index = event.source.index
parent = event.source.parent
calendarView = event.source.parent.parent
extents = event.source.component.getExtents(0)
noRows = parent.table.nRows
found = False
for i in range(0, calendarView.childCount):
child = calendarView.child(i)
if (child.role == rolenames.ROLE_CALENDAR_EVENT):
apptExtents = child.component.getExtents(0)
if extents.y == apptExtents.y:
utterances = speechGen.getSpeech(child, False)
[apptRegions, focusedRegion] = \
brailleGen.getBrailleRegions(child)
brailleRegions.extend(apptRegions)
speech.speakUtterances(utterances)
startTime = 'Start time ' + \
self.getTimeForCalRow(index, noRows)
brailleRegions.append(braille.Region(startTime))
speech.speak(startTime)
apptLen = apptExtents.height / extents.height
endTime = 'End time ' + \
self.getTimeForCalRow(index + apptLen, noRows)
brailleRegions.append(braille.Region(endTime))
speech.speak(endTime)
braille.displayRegions([brailleRegions,
brailleRegions[0]])
found = True
if not found:
startTime = 'Start time ' + self.getTimeForCalRow(index, noRows)
brailleRegions.append(braille.Region(startTime))
speech.speak(startTime)
utterance = _("No appointments")
speech.speak(utterance)
brailleRegions.append(braille.Region(utterance))
braille.displayRegions([brailleRegions,
brailleRegions[0]])
return
# 6) Preferences Dialog: options list.
#
# Check if the focus is in one of the various options on the left
# side of the Preferences dialog. If it is, then we just want to
# speak the name of the page we are currently on.
#
# Even though it looks like the focus is on one of the page tabs
# in this dialog, it's possible that it's actually on a table cell,
# within a table which is contained within a scroll pane. We check
# for this my looking for a component hierarchy of "table cell",
# "table", "unknown" and "scroll pane".
#
# If this is the case, then we get the parent of the scroll pane
# and look to see if one of its other children is a "page tab list".
# If that's true, then we get the Nth child, when N is the index of
# the initial table cell minus 1. We double check that this is a
# "page tab", then if so, speak and braille that component.
#
# NOTE: assumes there is only one "page tab list" in the "filler"
# component.
rolesList = [rolenames.ROLE_TABLE_CELL, \
rolenames.ROLE_TABLE, \
rolenames.ROLE_UNKNOWN, \
rolenames.ROLE_SCROLL_PANE]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - preferences dialog: " \
+ "table cell in options list.")
index = event.source.index
obj = event.source.parent.parent.parent
parent = obj.parent
if parent.role == rolenames.ROLE_FILLER:
for i in range(0, parent.childCount):
child = parent.child(i)
if (child.role == rolenames.ROLE_PAGE_TAB_LIST):
tabList = child
tab = tabList.child(index-1)
if (tab.role == rolenames.ROLE_PAGE_TAB):
self.readPageTab(tab)
return
# 7) Mail view: insert attachment dialog: unlabelled arrow button.
#
# Check if the focus is on the unlabelled arrow button near the
# top of the mail view Insert Attachment dialog. If it is, then
# rather than just speak/braille "button", output something a
# little more useful.
rolesList = [rolenames.ROLE_PUSH_BUTTON, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_SPLIT_PANE, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_DIALOG]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - mail insert " \
+ "attachment dialog: unlabelled button.")
brailleRegions = []
utterance = _("Directories button")
speech.speak(utterance)
brailleRegions.append(braille.Region(utterance))
braille.displayRegions([brailleRegions,
brailleRegions[0]])
return
# 8) Mail compose window: message area
#
# This works in conjunction with code in section 9). Check to see if
# focus is currently in the Mail compose window message area. If it
# is, then, if this is the first time, save a pointer to the HTML
# panel that will contain a variety of components that will, in turn,
# contain the message text.
#
# Note that this drops through to then use the default event
# processing in the parent class for this "focus:" event.
rolesList = [rolenames.ROLE_TEXT, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_SCROLL_PANE]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - mail " \
+ "compose window: message area.")
self.message_panel = event.source.parent.parent
if self.speakNewLine(event.source):
speech.speak(chnames.getCharacterName("\n"), None, False)
if self.speakBlankLine(event.source):
speech.speak(_("blank"), None, False)
# 9) Spell Checking Dialog
#
# This works in conjunction with code in section 8). Check to see if
# current focus is in the table of possible replacement words in the
# spell checking dialog. If it is, then we use a cached handle to
# the Mail compose window message area, to find out where the text
# caret currently is, and use this to speak a selection of the
# surrounding text, to give the user context for the current misspelt
# word.
rolesList = [rolenames.ROLE_TABLE, \
rolenames.ROLE_SCROLL_PANE, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_DIALOG]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - spell checking dialog.")
# Braille the default action for this component.
#
self.updateBraille(orca_state.locusOfFocus)
# Look for the "Suggestions for 'xxxxx' label in the spell
# checker dialog panel. Extract out the xxxxx. This will be the
# misspelt word.
#
panel = event.source.parent.parent
allLabels = self.findByRole(panel, rolenames.ROLE_LABEL)
found = False
for label in allLabels:
if not found:
text = self.getDisplayedText(label)
if text:
tokens = text.split()
else:
tokens = []
for token in tokens:
if token.startswith("'"):
badWord = token
badWord = badWord[1:len(badWord)-1]
found = True
break
# If we have a handle to the HTML message panel, then extract out
# all the text objects, and create a list of all the words found
# in them.
#
if self.message_panel != None:
allTokens = []
panel = self.message_panel
allText = self.findByRole(panel, rolenames.ROLE_TEXT)
for i in range(0, len(allText)):
text = self.getText(allText[i], 0, -1)
tokens = text.split()
allTokens += tokens
self.speakMisspeltWord(allTokens, badWord)
return
# 10) Mail view: message area - attachments.
#
# Check if the focus is on the "go forward" button or the
# "attachment button" for an attachment in the mail message
# attachment area. (There will be a pair of these buttons
# for each attachment in the mail message).
#
# If it is, then get the text which describes the current
# attachment and speak it after doing the default action
# for the button.
#
# NOTE: it is assumed that the last table cell in the table
# contains this information.
rolesList = [rolenames.ROLE_PUSH_BUTTON, \
rolenames.ROLE_FILLER, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_PANEL, \
rolenames.ROLE_TABLE_CELL, \
rolenames.ROLE_TABLE, \
rolenames.ROLE_PANEL]
if self.isDesiredFocusedItem(event.source, rolesList):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - " \
+ "mail message area attachments.")
# Speak/braille the default action for this component.
#
default.Script.locusOfFocusChanged(self, event,
oldLocusOfFocus, newLocusOfFocus)
table = event.source.parent.parent.parent.parent.parent
cell = table.child(table.childCount-1)
allText = self.findByRole(cell, rolenames.ROLE_TEXT)
utterance = "for " + self.getText(allText[0], 0, -1)
speech.speak(utterance)
return
# 11) Setup Assistant.
#
# If the name of the frame of the object that currently has focus is
# "Evolution Setup Assistant", then empty out the two dictionaries
# containing which setup assistant panels and labels we've already
# seen.
obj = event.source.parent
while obj and obj.role != rolenames.ROLE_APPLICATION:
if obj.role == rolenames.ROLE_FRAME and \
obj.name.endswith(_("Assistant")):
debug.println(self.debugLevel,
"evolution.locusOfFocusChanged - " \
+ "setup assistant.")
self.setupPanels = {}
self.setupLabels = {}
break
obj = obj.parent
# For everything else, pass the focus event onto the parent class
# to be handled in the default way.
#
# Note that this includes table cells if we only want to read the
# current cell.
default.Script.locusOfFocusChanged(self, event,
oldLocusOfFocus, newLocusOfFocus)
def speakNewLine(self, obj):
"""Returns True if a newline should be spoken.
Otherwise, returns False.
"""
# Get the the AccessibleText interrface.
text = obj.text
if not text:
return False
# Was a left or right-arrow key pressed?
if not (orca_state.lastInputEvent and \
orca_state.lastInputEvent.__dict__.has_key("event_string")):
return False
lastKey = orca_state.lastInputEvent.event_string
if lastKey != "Left" and lastKey != "Right":
return False
# Was a control key pressed?
mods = orca_state.lastInputEvent.modifiers
isControlKey = mods & (1 << atspi.Accessibility.MODIFIER_CONTROL)
# Get the line containing the caret
caretOffset = text.caretOffset
line = text.getTextAtOffset(caretOffset, \
atspi.Accessibility.TEXT_BOUNDARY_LINE_START)
lineStart = line[1]
lineEnd = line[2]
if isControlKey: # control-right-arrow or control-left-arrow
# Get the word containing the caret.
word = text.getTextAtOffset(caretOffset, \
atspi.Accessibility.TEXT_BOUNDARY_WORD_START)
wordStart = word[1]
wordEnd = word[2]
if lastKey == "Right":
if wordStart == lineStart:
return True
else:
if wordEnd == lineEnd:
return True
else: # right arrow or left arrow
if lastKey == "Right":
if caretOffset == lineStart:
return True
else:
if caretOffset == lineEnd:
return True
return False
def speakBlankLine(self, obj):
"""Returns True if a blank line should be spoken.
Otherwise, returns False.
"""
# Get the the AccessibleText interrface.
text = obj.text
if not text:
return False
# Get the line containing the caret
caretOffset = text.caretOffset
line = text.getTextAtOffset(caretOffset, \
atspi.Accessibility.TEXT_BOUNDARY_LINE_START)
debug.println(debug.LEVEL_FINEST, "speakBlankLine: start=%d, end=%d, line=<%s>" % (line[1], line[2],
line[0]))
# If this is a blank line, announce it if the user requested
# that blank lines be spoken.
if line[1] == 0 and line[2] == 0:
return settings.speakBlankLines
else:
return False
def onStateChanged(self, event):
"""Called whenever an object's state changes. We are only
interested in "object:state-changed:showing" events for any
object in the Setup Assistant.
Arguments:
- event: the Event
"""
if event.type.endswith("showing"):
# Check to see if this "object:state-changed:showing" event is
# for an object in the Setup Assistant by walking back up the
# object hierarchy until we get to the frame object and check
# to see if it has a name that ends with "Assistant", which is
# what we see when we configure Evolution for the first time
# and when we add new accounts.
#
obj = event.source.parent
while obj and obj.role != rolenames.ROLE_APPLICATION:
if obj.role == rolenames.ROLE_FRAME and \
obj.name.endswith(_("Assistant")):
debug.println(self.debugLevel,
"evolution.onStateChanged - " \
+ "setup assistant.")
# If the event is for a label see if we want to speak it.
#
if event.source.role == rolenames.ROLE_LABEL:
self.speakSetupAssistantLabel(event.source)
break
# If the event is for a panel and we haven't already
# seen this panel, then handle it.
#
elif event.source.role == rolenames.ROLE_PANEL and \
not self.setupPanels.has_key(event.source):
self.handleSetupAssistantPanel(event.source)
self.setupPanels[event.source] = True
break
obj = obj.parent
# For everything else, pass the event onto the parent class
# to be handled in the default way.
#
default.Script.onStateChanged(self, event)
# Values used to construct a time string for calendar appointments.
#
timeIncrements = {}
timeIncrements[288] = 5
timeIncrements[144] = 10
timeIncrements[96] = 15
timeIncrements[48] = 30
timeIncrements[24] = 60
minutes = {}
minutes[0] = ''
minutes[5] = '5'
minutes[10] = '10'
minutes[15] = '15'
minutes[20] = '20'
minutes[25] = '25'
minutes[30] = '30'
minutes[35] = '35'
minutes[40] = '40'
minutes[45] = '45'
minutes[50] = '50'
minutes[55] = '55'
hours = ['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
# Orca
#
# Copyright 2005-2007 Sun Microsystems Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 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
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
"""A very experimental approach to the refreshable Braille display. This
module treats each line of the display as a sequential set of regions, where
each region can potentially backed by an Accessible object. Depending upon
the Accessible object, the cursor routing keys can be used to perform
operations on the Accessible object, such as invoking default actions or
moving the text caret.
"""
__id__ = "$Id: braille.py 2126 2007-03-06 21:35:17Z richb $"
__version__ = "$Revision: 2126 $"
__date__ = "$Date: 2007-03-06 13:35:17 -0800 (Tue, 06 Mar 2007) $"
__copyright__ = "Copyright (c) 2005-2006 Sun Microsystems Inc."
__license__ = "LGPL"
import logging
log = logging.getLogger("braille")
import signal
import threading
import atspi
# We'll use the official BrlAPI pythons (as of BrlTTY 3.8) if they
# are available. Otherwise, we'll fall back to our own bindings.
#
try:
import brlapi
brlAPI = None
useBrlAPIBindings = True
brlAPIRunning = False
except:
import brl
useBrlAPIBindings = False
brlAPIRunning = False
try:
# This can fail due to gtk not being available. We want to
# be able to recover from that if possible. The main driver
# for this is to allow "orca --text-setup" to work even if
# the desktop is not running.
#
import brlmon
except:
pass
import debug
import eventsynthesizer
import orca_state
import settings
from orca_i18n import _ # for gettext support
from rolenames import getShortBrailleForRoleName # localized role names
from rolenames import getLongBrailleForRoleName # localized role names
# If True, this module has been initialized.
#
_initialized = False
# The braille monitor
#
monitor = None
# Each of these maps to BrlAPI's brldefs.h file.
#
CMD_NOOP = 0x00
CMD_LNUP = 0x01
CMD_LNDN = 0x02
CMD_WINUP = 0x03
CMD_WINDN = 0x04
CMD_PRDIFLN = 0x05
CMD_NXDIFLN = 0x06
CMD_ATTRUP = 0x07
CMD_ATTRDN = 0x08
CMD_TOP = 0x09
CMD_BOT = 0x0a
CMD_TOP_LEFT = 0x0b
CMD_BOT_LEFT = 0x0c
CMD_PRPGRPH = 0x0d
CMD_NXPGRPH = 0x0e
CMD_PRPROMPT = 0x0f
CMD_NXPROMPT = 0x10
CMD_PRSEARCH = 0x11
CMD_NXSEARCH = 0x12
CMD_CHRLT = 0x13
CMD_CHRRT = 0x14
CMD_HWINLT = 0x15
CMD_HWINRT = 0x16
CMD_FWINLT = 0x17
CMD_FWINRT = 0x18
CMD_FWINLTSKIP = 0x19
CMD_FWINRTSKIP = 0x1a
CMD_LNBEG = 0x1b
CMD_LNEND = 0x1c
CMD_HOME = 0x1d
CMD_BACK = 0x1e
CMD_FREEZE = 0x1f
CMD_DISPMD = 0x20
CMD_SIXDOTS = 0x21
CMD_SLIDEWIN = 0x22
CMD_SKPIDLNS = 0x23
CMD_SKPBLNKWINS = 0x24
CMD_CSRVIS = 0x25
CMD_CSRHIDE = 0x26
CMD_CSRTRK = 0x27
CMD_CSRSIZE = 0x28
CMD_CSRBLINK = 0x29
CMD_ATTRVIS = 0x2a
CMD_ATTRBLINK = 0x2b
CMD_CAPBLINK = 0x2c
CMD_TUNES = 0x2d
CMD_HELP = 0x2e
CMD_INFO = 0x2f
CMD_LEARN = 0x30
CMD_PREFMENU = 0x31
CMD_PREFSAVE = 0x32
CMD_PREFLOAD = 0x33
CMD_MENU_FIRST_ITEM = 0x34
CMD_MENU_LAST_ITEM = 0x35
CMD_MENU_PREV_ITEM = 0x36
CMD_MENU_NEXT_ITEM = 0x37
CMD_MENU_PREV_SETTING = 0x38
CMD_MENU_NEXT_SETTING = 0x39
CMD_SAY_LINE = 0x3a
CMD_SAY_ABOVE = 0x3b
CMD_SAY_BELOW = 0x3c
CMD_MUTE = 0x3d
CMD_SPKHOME = 0x3e
CMD_SWITCHVT_PREV = 0x3f
CMD_SWITCHVT_NEXT = 0x40
CMD_CSRJMP_VERT = 0x41
CMD_PASTE = 0x42
CMD_RESTARTBRL = 0x43
CMD_RESTARTSPEECH = 0x44
CMD_MAX = 0x44
BRL_FLG_REPEAT_INITIAL= 0x800000
BRL_FLG_REPEAT_DELAY = 0x400000
# Common names for most used BrlTTY commands, to be shown in the GUI:
# ATM, the ones used in default.py are:
#
command_name = {}
command_name[CMD_FWINLT] = _("Line Left")
command_name[CMD_FWINRT] = _("Line Right")
command_name[CMD_LNUP] = _("Line Up")
command_name[CMD_LNDN] = _("Line Down")
command_name[CMD_TOP_LEFT] = _("Top Left")
command_name[CMD_BOT_LEFT] = _("Bottom Right")
command_name[CMD_HOME] = _("Cursor Position")
# The size of the physical display (width, height). The coordinate system of
# the display is set such that the upper left is (0,0), x values increase from
# left to right, and y values increase from top to bottom.
#
# For the purposes of testing w/o a braille display, we'll set the display
# size to width=32 and height=1.
#
# [[[TODO: WDW - Only a height of 1 is support at this time.]]]
#
_displaySize = [32, 1]
# The list of lines on the display. This represents the entire amount of data
# to be drawn on the display. It will be clipped by the viewport if too large.
#
_lines = []
# The region with focus. This will be displayed at the home position.
#
_regionWithFocus = None
# The viewport is a rectangular region of size _displaySize whose upper left
# corner is defined by the point (x, line number). As such, the viewport is
# identified solely by its upper left point.
#
_viewport = [0, 0]
# The callback to call on a BrlTTY input event. This is passed to
# the init method.
#
_callback = None
# If True, the given portion of the currently displayed line is showing
# on the display.
#
endIsShowing = False
beginningIsShowing = False
# 1-based offset saying which braille cell has the cursor. A value
# of 0 means no cell has the cursor.
#
cursorCell = 0
def _printBrailleEvent(level, command):
"""Prints out a Braille event. The given level may be overridden
if the eventDebugLevel (see debug.setEventDebugLevel) is greater in
debug.py.
Arguments:
- command: the BrlAPI command for the key that was pressed.
"""
debug.printInputEvent(
level,
"BRAILLE EVENT: %x" % command)
class Region:
"""A Braille region to be displayed on the display. The width of
each region is determined by its string.
"""
def __init__(self, string, cursorOffset=0):
"""Creates a new Region containing the given string.
Arguments:
- string: the string to be displayed
- cursorOffset: a 0-based index saying where to draw the cursor
for this Region if it gets focus.
"""
if not string:
string = ""
string = string.decode("UTF-8")
if string[-1:] == "\n":
string = string[:-1]
self.string = string.encode("UTF-8")
self.cursorOffset = cursorOffset
def processCursorKey(self, offset):
"""Processes a cursor key press on this Component. The offset is
0-based, where 0 represents the leftmost character of string
associated with this region. Note that the zeroeth character may have
been scrolled off the display."""
pass
class Component(Region):
"""A subclass of Region backed by an accessible. This Region will react
to any cursor routing key events and perform the default action on the
accessible, if a default action exists.
"""
def __init__(self, accessible, string, cursorOffset=0):
"""Creates a new Component.
Arguments:
- accessible: the accessible
- string: the string to use to represent the component
- cursorOffset: a 0-based index saying where to draw the cursor
for this Region if it gets focus.
"""
Region.__init__(self, string, cursorOffset)
self.accessible = accessible
def processCursorKey(self, offset):
"""Processes a cursor key press on this Component. The offset is
0-based, where 0 represents the leftmost character of string
associated with this region. Note that the zeroeth character may have
been scrolled off the display."""
actions = self.accessible.action
if actions:
actions.doAction(0)
else:
# [[[WDW - HACK to do a mouse button 1 click if we have
# to. For example, page tabs don't have any actions but
# we want to be able to select them with the cursor
# routing key.]]]
#
debug.println(debug.LEVEL_FINEST,
"braille.Component.processCursorKey: no action")
try:
eventsynthesizer.clickObject(self.accessible, 1)
except:
debug.printException(debug.LEVEL_SEVERE)
class Text(Region):
"""A subclass of Region backed by a Text object. This Region will
react to any cursor routing key events by positioning the caret in
the associated text object. The line displayed will be the
contents of the text object preceded by an optional label.
[[[TODO: WDW - need to add in text selection capabilities. Logged
as bugzilla bug 319754.]]]"""
def __init__(self, accessible, label=None):
"""Creates a new Text region.
Arguments:
- accessible: the accessible that implements AccessibleText
- label: an optional label to display
"""
self.accessible = accessible
if orca_state.activeScript:
[string, self.caretOffset, self.lineOffset] = \
orca_state.activeScript.getTextLineAtCaret(self.accessible)
# Sometimes, gnome-terminal will give us very odd values when
# the user is editing using 'vi' and has positioned the caret
# at the first character of the first line. In this case, we
# end up getting a very large negative number for the line offset.
# So, we just assume the user is at the first character.
#
if self.lineOffset < 0:
self.caretOffset = 0
self.lineOffset = 0
[string, startOffset, endOffset] = \
self.accessible.text.getTextAtOffset(
0,
atspi.Accessibility.TEXT_BOUNDARY_LINE_START)
cursorOffset = self.caretOffset - self.lineOffset
self.label = label
if self.label:
string = self.label + " " + string
cursorOffset += len(self.label.decode("UTF-8")) + 1
Region.__init__(self, string, cursorOffset)
def repositionCursor(self):
"""Attempts to reposition the cursor in response to a new
caret position. If it is possible (i.e., the caret is on
the same line as it was), reposition the cursor and return
True. Otherwise, return False.
"""
[string, caretOffset, lineOffset] = \
orca_state.activeScript.getTextLineAtCaret(self.accessible)
cursorOffset = caretOffset - lineOffset
if self.label:
cursorOffset += len(self.label.decode("UTF-8")) + 1
if lineOffset != self.lineOffset:
return False
else:
self.caretOffset = caretOffset
self.lineOffset = lineOffset
self.cursorOffset = cursorOffset
return True
def processCursorKey(self, offset):
"""Processes a cursor key press on this Component. The offset is
0-based, where 0 represents the leftmost character of text associated
with this region. Note that the zeroeth character may have been
scrolled off the display."""
if self.label:
offset = offset - len(self.label.decode("UTF-8")) - 1
if offset < 0:
return
newCaretOffset = self.lineOffset + offset
self.accessible.text.setCaretOffset(newCaretOffset)
class ReviewComponent(Component):
"""A subclass of Component that is to be used for flat review mode."""
def __init__(self, accessible, string, cursorOffset, zone):
"""Creates a new Component.
Arguments:
- accessible: the accessible
- string: the string to use to represent the component
- cursorOffset: a 0-based index saying where to draw the cursor
for this Region if it gets focus.
- zone: the flat review Zone associated with this component
"""
Component.__init__(self, accessible, string, cursorOffset)
self.zone = zone
class ReviewText(Region):
"""A subclass of Region backed by a Text object. This Region will
does not react to the caret changes, but will react if one updates
the cursorPosition. This class is meant to be used by flat review
mode to show the current character position.
"""
def __init__(self, accessible, string, lineOffset, zone):
"""Creates a new Text region.
Arguments:
- accessible: the accessible that implements AccessibleText
- string: the string to use to represent the component
- lineOffset: the character offset into where the text line starts
- zone: the flat review Zone associated with this component
"""
Region.__init__(self, string)
self.accessible = accessible
self.lineOffset = lineOffset
self.zone = zone
def processCursorKey(self, offset):
"""Processes a cursor key press on this Component. The offset is
0-based, where 0 represents the leftmost character of text associated
with this region. Note that the zeroeth character may have been
scrolled off the display."""
newCaretOffset = self.lineOffset + offset
self.accessible.text.setCaretOffset(newCaretOffset)
class Line:
"""A horizontal line on the display. Each Line is composed of a sequential
set of Regions.
"""
def __init__(self, region=None):
self.regions = []
self.string = ""
if region:
self.addRegion(region)
def addRegion(self, region):
self.regions.append(region)
def addRegions(self, regions):
self.regions.extend(regions)
def getLineInfo(self):
"""Computes the complete string for this line as well as a
0-based index where the focused region starts on this line.
If the region with focus is not on this line, then the index
will be -1.
Returns [string, offsetIndex]
"""
string = ""
focusOffset = -1
for region in self.regions:
if region == _regionWithFocus:
focusOffset = len(string.decode("UTF-8"))
if region.string:
# [[[TODO: WDW - HACK: Replace UTF-8 ellipses with "..."
# The ultimate solution is to get i18n support into
# BrlTTY.]]]
#
string += region.string.replace("\342\200\246", "...")
return [string, focusOffset]
def getRegionAtOffset(self, offset):
"""Finds the Region at the given 0-based offset in this line.
Returns the [region, offsetinregion] where the region is
the region at the given offset, and offsetinregion is the
0-based offset from the beginning of the region, representing
where in the region the given offset is."""
# Translate the cursor offset for this line into a cursor offset
# for a region, and then pass the event off to the region for
# handling.
#
region = None
string = ""
pos = 0
for region in self.regions:
string = string + region.string
if len(string.decode("UTF-8")) > offset:
break
else:
pos = len(string.decode("UTF-8"))
return [region, offset - pos]
def processCursorKey(self, offset):
"""Processes a cursor key press on this Component. The offset is
0-based, where 0 represents the leftmost character of string
associated with this line. Note that the zeroeth character may have
been scrolled off the display."""
[region, regionOffset] = self.getRegionAtOffset(offset)
region.processCursorKey(regionOffset)
def getRegionAtCell(cell):
"""Given a 1-based cell offset, return the braille region
associated with that cell in the form of [region, offsetinregion]
where 'region' is the region associated with the cell and
'offsetinregion' is the 0-based offset of where the cell is
in the region, where 0 represents the beginning of the region, """
if len(_lines) > 0:
offset = (cell - 1) + _viewport[0]
lineNum = _viewport[1]
return _lines[lineNum].getRegionAtOffset(offset)
else:
return [None, -1]
def clear():
"""Clears the logical structure, but keeps the Braille display as is
(until a refresh operation).
"""
global _lines
global _regionWithFocus
global _viewport
_lines = []
_regionWithFocus = None
_viewport = [0, 0]
def setLines(lines):
global _lines
_lines = lines
def addLine(line):
"""Adds a line to the logical display for painting. The line is added to
the end of the current list of known lines. It is necessary for the
viewport to be over the lines and for refresh to be called for the new
line to be painted.
Arguments:
- line: an instance of Line to add.
"""
global _lines
_lines.append(line)
line._index = len(_lines)
def getShowingLine():
"""Returns the Line that is currently being painted on the display.
"""
return _lines[_viewport[1]]
def setFocus(region, panToFocus=True):
"""Specififes the region with focus. This region will be positioned
at the home position if panToFocus is True.
Arguments:
- region: the given region, which much be in a line that has been
added to the logical display
"""
global _regionWithFocus
_regionWithFocus = region
if not panToFocus or (not _regionWithFocus):
return
# Adjust the viewport according to the new region with focus.
# The goal is to have the first cell of the region be in the
# home position, but we will give priority to make sure the
# cursor for the region is on the display. For example, when
# faced with a long text area, we'll show the position with
# the caret vs. showing the beginning of the region.
lineNum = 0
done = False
for line in _lines:
for reg in line.regions:
if reg == _regionWithFocus:
_viewport[1] = lineNum
done = True
break
if done:
break
else:
lineNum += 1
line = _lines[_viewport[1]]
[string, offset] = line.getLineInfo()
# If the cursor is too far right, we scroll the viewport
# so the cursor will be on the last cell of the display.
#
if _regionWithFocus.cursorOffset >= _displaySize[0]:
offset += _regionWithFocus.cursorOffset - _displaySize[0] + 1
_viewport[0] = max(0, offset)
def refresh(panToCursor=True, targetCursorCell=0):
"""Repaints the Braille on the physical display. This clips the entire
logical structure by the viewport and also sets the cursor to the
appropriate location. [[[TODO: WDW - I'm not sure how BrlTTY handles
drawing to displays with more than one line, so I'm only going to handle
drawing one line right now.]]]
Arguments:
- panToCursor: if True, will adjust the viewport so the cursor is
showing.
- targetCursorCell: Only effective if panToCursor is True.
0 means automatically place the cursor somewhere
on the display so as to minimize movement but
show as much of the line as possible. A positive
value is a 1-based target cell from the left side
of the display and a negative value is a 1-based
target cell from the right side of the display.
"""
global endIsShowing
global beginningIsShowing
global cursorCell
global monitor
if len(_lines) == 0:
if useBrlAPIBindings:
if brlAPIRunning:
brlAPI.writeText("", 0)
else:
brl.writeText(0, "")
return
# Now determine the location of the cursor. First, we'll figure
# out the 1-based offset for where we want the cursor to be. If
# the target cell is less than zero, it means an offset from the
# right hand side of the display.
#
if targetCursorCell < 0:
targetCursorCell = _displaySize[0] + targetCursorCell + 1
# Now, we figure out the 0-based offset for where the cursor
# actually is in the string.
#
line = _lines[_viewport[1]]
[string, focusOffset] = line.getLineInfo()
cursorOffset = -1
if focusOffset >= 0:
cursorOffset = focusOffset + _regionWithFocus.cursorOffset
# Now, if desired, we'll automatically pan the viewport to show
# the cursor. If there's no targetCursorCell, then we favor the
# left of the display if we need to pan left, or we favor the
# right of the display if we need to pan right.
#
if panToCursor and (cursorOffset >= 0):
if len(string.decode("UTF-8")) <= _displaySize[0]:
_viewport[0] = 0
elif targetCursorCell:
_viewport[0] = max(0, cursorOffset - targetCursorCell + 1)
elif cursorOffset < _viewport[0]:
_viewport[0] = max(0, cursorOffset)
elif cursorOffset >= (_viewport[0] + _displaySize[0]):
_viewport[0] = max(0, cursorOffset - _displaySize[0] + 1)
startPos = _viewport[0]
endPos = startPos + _displaySize[0]
# Now normalize the cursor position to BrlTTY, which uses 1 as
# the first cursor position as opposed to 0.
#
cursorCell = cursorOffset - startPos
if (cursorCell < 0) or (cursorCell >= _displaySize[0]):
cursorCell = 0
else:
cursorCell += 1 # Normalize to 1-based offset
debug.println(debug.LEVEL_INFO, "BRAILLE LINE: '%s'" % string)
log.info("line:'%s'" % string)
debug.println(debug.LEVEL_INFO, " VISIBLE: '%s', cursor=%d" \
% (string[startPos:endPos], cursorCell))
log.info("visible:'%s'" % string[startPos:endPos])
log.info("cursor:%d" % cursorCell)
string = string.decode("UTF-8")
substring = string[startPos:endPos].encode("UTF-8")
if useBrlAPIBindings:
if brlAPIRunning:
try:
# The name after (and including) BrlTTY v3.8 revision 2810
#
writeStruct = brlapi.WriteStruct()
except:
# The name before BrlTTY v3.8 revision 2810
#
writeStruct = brlapi.Write()
writeStruct.regionBegin = 1
writeStruct.regionSize = len(substring.decode("UTF-8"))
while writeStruct.regionSize < _displaySize[0]:
substring += " "
writeStruct.regionSize += 1
writeStruct.text = substring
writeStruct.cursor = cursorCell
writeStruct.charset = "UTF-8"
# [[[WDW - if you want to muck around with the dots on the
# display to do things such as add underlines, you can use
# the attrOr field of the write structure to do so. The
# attrOr field is a string whose length must be the same
# length as the display and whose dots will end up showing
# up on the display. Each character represents a bitfield
# where each bit corresponds to a dot (i.e., bit 0 = dot 1,
# bit 1 = dot 2, and so on). Here's an example that underlines
# all the text.]]]
#
#myUnderline = ""
#for i in range(0, _displaySize[0]):
# myUnderline += '\xc0'
#writeStruct.attrOr = myUnderline
brlAPI.write(writeStruct)
else:
brl.writeText(cursorCell, substring)
if settings.enableBrailleMonitor:
if not monitor:
monitor = brlmon.BrlMon(_displaySize[0])
monitor.show_all()
monitor.writeText(cursorCell, substring)
elif monitor:
monitor.destroy()
beginningIsShowing = startPos == 0
endIsShowing = endPos >= len(string)
def displayRegions(regionInfo):
"""Displays a list of regions on a single line, setting focus to the
specified region. The regionInfo parameter is something that is
typically returned by a call to braillegenerator.getBrailleRegions.
Arguments:
- regionInfo: a list where the first element is a list of regions
to display and the second element is the region
with focus (must be in the list from element 0)
"""
regions = regionInfo[0]
focusedRegion = regionInfo[1]
clear()
line = Line()
for item in regions:
line.addRegion(item)
addLine(line)
setFocus(focusedRegion)
refresh()
def displayMessage(message, cursor=-1):
"""Displays a single line, setting the cursor to the given position,
ensuring that the cursor is in view.
Arguments:
- message: the string to display
- cursor: the 0-based cursor position, where -1 (default) means no cursor
"""
clear()
region = Region(message, cursor)
addLine(Line(region))
setFocus(region)
refresh(True)
def panLeft(panAmount=0):
"""Pans the display to the left, limiting the pan to the beginning
of the line being displayed.
Arguments:
- panAmount: the amount to pan. A value of 0 means the entire
width of the physical display.
Returns True if a pan actually happened.
"""
oldX = _viewport[0]
if panAmount == 0:
panAmount = _displaySize[0]
if _viewport[0] > 0:
_viewport[0] = max(0, _viewport[0] - panAmount)
return oldX != _viewport[0]
def panRight(panAmount=0):
"""Pans the display to the right, limiting the pan to the length
of the line being displayed.
Arguments:
- panAmount: the amount to pan. A value of 0 means the entire
width of the physical display.
Returns True if a pan actually happened.
"""
oldX = _viewport[0]
if panAmount == 0:
panAmount = _displaySize[0]
if len(_lines) > 0:
lineNum = _viewport[1]
newX = _viewport[0] + panAmount
[string, focusOffset] = _lines[lineNum].getLineInfo()
if newX < len(string.decode("UTF-8")):
_viewport[0] = newX
return oldX != _viewport[0]
def panToOffset(offset):
"""Automatically pan left or right to make sure the current offset is
showing."""
while offset < _viewport[0]:
debug.println(debug.LEVEL_FINEST,
"braille.panToOffset (left) %d" % offset)
if not panLeft():
break
while offset >= (_viewport[0] + _displaySize[0]):
debug.println(debug.LEVEL_FINEST,
"braille.panToOffset (right) %d" % offset)
if not panRight():
break
def returnToRegionWithFocus(inputEvent=None):
"""Pans the display so the region with focus is displayed.
Arguments:
- inputEvent: the InputEvent instance that caused this to be called.
Returns True to mean the command should be consumed.
"""
setFocus(_regionWithFocus)
refresh(True)
return True
def _processBrailleEvent(command):
"""Handles BrlTTY command events. This passes commands on to Orca for
processing. If Orca does not handle them (as indicated by a return value
of false from the callback passed to init, it will attempt to handle the
command itself - either by panning the viewport or passing cursor routing
keys to the Regions for handling.
Arguments:
- command: the BrlAPI command for the key that was pressed.
"""
_printBrailleEvent(debug.LEVEL_FINE, command)
# [[[TODO: WDW - DaveM suspects the Alva driver is sending us a
# repeat flag. So...let's kill a couple birds here until BrlTTY
# 3.8 fixes the problem: we'll disable autorepeat and we'll also
# strip out the autorepeat flag if this is the first press of a
# button.]]]
#
if command & BRL_FLG_REPEAT_INITIAL:
command &= ~(BRL_FLG_REPEAT_INITIAL | BRL_FLG_REPEAT_DELAY)
elif command & BRL_FLG_REPEAT_DELAY:
return True
consumed = False
if settings.timeoutCallback and (settings.timeoutTime > 0):
signal.signal(signal.SIGALRM, settings.timeoutCallback)
signal.alarm(settings.timeoutTime)
if _callback:
try:
# Like key event handlers, a return value of True means
# the command was consumed.
#
consumed = _callback(command)
except:
debug.printException(debug.LEVEL_WARNING)
consumed = False
if (command >= 0x100) and (command < (0x100 + _displaySize[0])):
if len(_lines) > 0:
cursor = (command - 0x100) + _viewport[0]
lineNum = _viewport[1]
_lines[lineNum].processCursorKey(cursor)
consumed = True
if settings.timeoutCallback and (settings.timeoutTime > 0):
signal.alarm(0)
return consumed
def _brlAPIKeyReader():
"""Method to read a key from the BrlAPI bindings. This is a
gidle handler.
"""
key = brlAPI.readKey(False)
if key:
flags = key >> 32
lower = key & 0xFFFFFFFF
keyType = lower >> 29
keyCode = lower & 0x1FFFFFFF
# [[TODO: WDW - HACK If we have a cursor routing key, map
# it back to the code we used to get with earlier versions
# of BrlAPI (i.e., bit 0x100 was the indicator of a cursor
# routing key instead of 0x1000). This may change before
# the offical BrlAPI Python bindings are released.]]]
#
if keyCode & 0x10000:
keyCode = 0x100 | (keyCode & 0xFF)
if keyCode:
_processBrailleEvent(keyCode)
return brlAPIRunning
def setupKeyRanges(keys):
"""Hacky method to tell BrlTTY what to send and not send us via
the readKey method. This only works with BrlTTY v3.8 and better.
Arguments:
-keys: a list of BrlAPI commands.
"""
if not brlAPIRunning:
return
try:
# First, start by ignoring everything.
#
brlAPI.ignoreKeys(brlapi.rangeType_all, [0])
# Next, enable cursor routing keys.
#
keySet = [brlapi.KEY_TYPE_CMD | brlapi.KEY_CMD_ROUTE]
# Finally, enable the commands we care about.
#
for key in keys:
keySet.append(brlapi.KEY_TYPE_CMD | key)
brlAPI.acceptKeys(brlapi.rangeType_command, keySet)
debug.println(debug.LEVEL_FINEST, "Using BrlAPI v0.5.0+")
except:
debug.printException(debug.LEVEL_FINEST)
try:
# Old, incompatible way that was in v3.8 devel, but
# changed prior to release. We need this just in case
# people have not updated yet.
# First, start by ignoring everything.
#
brlAPI.ignoreKeyRange(0,
brlapi.KEY_FLAGS_MASK \
| brlapi.KEY_TYPE_MASK \
| brlapi.KEY_CODE_MASK)
# Next, enable cursor routing keys.
#
brlAPI.acceptKeyRange(brlapi.KEY_TYPE_CMD | brlapi.KEY_CMD_ROUTE,
brlapi.KEY_TYPE_CMD \
| brlapi.KEY_CMD_ROUTE \
| brlapi.KEY_CMD_ARG_MASK)
# Finally, enable the commands we care about.
#
keySet = []
for key in keys:
keySet.append(brlapi.KEY_TYPE_CMD | key)
if len(keySet):
brlAPI.acceptKeySet(keySet)
debug.println(debug.LEVEL_FINEST,
"Using BrlAPI pre-release v0.5.0")
except:
debug.printException(debug.LEVEL_FINEST)
debug.println(
debug.LEVEL_WARNING,
"Braille module cannot listen for braille input events")
def init(callback=None, tty=7):
"""Initializes the braille module, connecting to the BrlTTY driver.
Arguments:
- callback: the method to call with a BrlTTY input event.
- tty: the tty port to take ownership of (default = 7)
Returns True if the initialization procedure was run or False if this
module has already been initialized.
"""
global _initialized
global _displaySize
global _callback
if _initialized:
return False
_callback = callback
if useBrlAPIBindings:
try:
import gobject
gobject.threads_init()
global brlAPI
global brlAPIRunning
brlAPI = brlapi.Connection()
try:
import os
windowPath = os.environ["WINDOWPATH"]
brlAPI.enterTtyModeWithPath()
brlAPIRunning = True
gobject.idle_add(_brlAPIKeyReader)
debug.println(\
debug.LEVEL_CONFIGURATION,
"Braille module has been initialized using WINDOWPATH=" \
+ "%s" % windowPath)
except:
brlAPI.enterTtyMode(tty)
brlAPIRunning = True
gobject.idle_add(_brlAPIKeyReader)
debug.println(\
debug.LEVEL_CONFIGURATION,
"Braille module has been initialized using tty=%d" % tty)
except:
debug.printException(debug.LEVEL_FINEST)
return False
else:
if brl.init(tty):
debug.println(debug.LEVEL_CONFIGURATION,
"Braille module has been initialized.")
brl.registerCallback(_processBrailleEvent)
else:
debug.println(debug.LEVEL_CONFIGURATION,
"Braille module has NOT been initialized.")
return False
# [[[TODO: WDW - For some reason, BrlTTY wants to say the height of the
# Vario is 40 so we hardcode it to 1 for now.]]]
#
#_displaySize = (brl.getDisplayWidth(), brl.getDisplayHeight())
if useBrlAPIBindings:
(x, y) = brlAPI.displaySize
_displaySize = [x, 1]
else:
_displaySize = [brl.getDisplayWidth(), 1]
debug.println(debug.LEVEL_CONFIGURATION,
"braille display size = (%d, %d)" \
% (_displaySize[0], _displaySize[1]))
clear()
refresh(True)
_initialized = True
return True
def shutdown():
"""Shuts down the braille module. Returns True if the shutdown procedure
was run or False if this module has not been initialized.
"""
global _initialized
if not _initialized:
return False
global brlAPIRunning
if useBrlAPIBindings:
if brlAPIRunning:
brlAPIRunning = False
brlAPI.leaveTtyMode()
else:
brl.shutdown()
_initialized = False
return True
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]