# Orca # # Copyright 2005-2008 Sun Microsystems Inc. # # 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. """Custom script for Evolution.""" __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." __license__ = "LGPL" import pyatspi import orca.debug as debug import orca.scripts.default as default import orca.input_event as input_event import orca.rolenames as rolenames import orca.braille as braille import orca.orca as orca import orca.orca_state as orca_state import orca.speech as speech import orca.speechserver as speechserver import orca.settings as settings from orca.orca_i18n import _ # for gettext support from formatting import Formatting from speech_generator import SpeechGenerator from script_utilities import Utilities _settingsManager = getattr(orca, '_settingsManager') ######################################################################## # # # 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 # A handle to the Spellcheck dialog. # self.spellCheckDialog = None # Last Setup Assistant panel spoken. # self.lastSetupPanel = None # The last row and column we were on in the mail message header list. self.lastMessageColumn = -1 self.lastMessageRow = -1 # The last locusOfFocusChanged roles hierarchy. # self.rolesList = [] # 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. # Pylint is confused and flags several errors of the type: # # E1101:282:Script.__init__: Module 'orca.rolenames' has no # 'ROLE_CALENDAR_VIEW' member # # So for now, we just disable these errors in this method. # # pylint: disable-msg=E1101 rolenames.ROLE_CALENDAR_VIEW = "Calendar View" rolenames.rolenames[rolenames.ROLE_CALENDAR_VIEW] = rolenames.Rolename( rolenames.ROLE_CALENDAR_VIEW, # Translators: short braille for the rolename of a calendar view. # _("calv"), # Translators: long braille for the rolename of a calendar view. # _("CalendarView"), # Translators: spoken words for the rolename of a calendar view. # _("calendar view")) rolenames.ROLE_CALENDAR_EVENT = "Calendar Event" rolenames.rolenames[rolenames.ROLE_CALENDAR_EVENT] = \ rolenames.Rolename( rolenames.ROLE_CALENDAR_EVENT, # Translators: short braille for the rolename of a calendar event. # _("cale"), # Translators: long braille for the rolename of a calendar event. # _("CalendarEvent"), # Translators: spoken words for the rolename of a calendar event. # _("calendar event")) def getSpeechGenerator(self): """Returns the speech generator for this script. """ return SpeechGenerator(self) def getFormatting(self): """Returns the formatting strings for this script.""" return Formatting(self) def getUtilities(self): """Returns the utilites for this script.""" return Utilities(self) 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, # Translators: this tells Orca to act like 'biff', or let # the user know when new mail has arrived, even if Evolution # doesn't have focus. # _("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) keymap = ( ("n", settings.defaultModifierMask, settings.ORCA_MODIFIER_MASK, "toggleReadMailHandler"), ) keyBindings.load(keymap, self.inputEventHandlers) return keyBindings def getListeners(self): """Sets up the AT-SPI event listeners for this script. """ listeners = default.Script.getListeners(self) listeners["object:state-changed:showing"] = \ self.onStateChanged return listeners def isActivatableEvent(self, event): """Returns True if the given event is one that should cause this script to become the active script. This is only a hint to the focus tracking manager and it is not guaranteed this request will be honored. Note that by the time the focus tracking manager calls this method, it thinks the script should become active. This is an opportunity for the script to say it shouldn't. """ # If the Evolution window is not focused, ignore this event. # window = self.utilities.topLevelObject(event.source) if window and not window.getState().contains(pyatspi.STATE_ACTIVE): return False return True 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.") # Translators: this tells Orca to act like 'biff', or let # the user know when new mail has arrived, even if Evolution # doesn't have focus. # line = _("present new mail if this script is not active.") self.presentIfInactive = not self.presentIfInactive if not self.presentIfInactive: # Translators: this tells Orca to act like 'biff', or let # the user know when new mail has arrived, even if Evolution # doesn't have focus. # line = _("do not present new mail if this script is not active.") self.presentMessage(line) 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. # [[[TODO - JD: This method is way, way too huge]]] # 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 """ # Pylint is confused and flags several errors of the type: # # E1101:1040:Script.locusOfFocusChanged: Module 'orca.rolenames' # has no 'ROLE_CALENDAR_EVENT' member # # So for now, we just disable these errors in this method. # # pylint: disable-msg=E1101 brailleGen = self.brailleGenerator speechGen = self.speechGenerator details = debug.getAccessibleDetails(self.debugLevel, event.source) debug.printObjectEvent(self.debugLevel, event, details) # We always automatically go back to focus tracking mode when # the focus changes. # if self.flatReviewContext: self.toggleFlatReviewMode() # 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. if self.utilities.isMessageBody(event.source) \ and not event.source.getState().contains(pyatspi.STATE_EDITABLE): debug.println(self.debugLevel, "evolution.locusOfFocusChanged - mail view: " \ + "current message pane: " \ + "individual lines of text.") self.presentMessageLine(event.source, newLocusOfFocus) 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. self.rolesList = [pyatspi.ROLE_TEXT, \ pyatspi.ROLE_PANEL, \ pyatspi.ROLE_TABLE_CELL] if _settingsManager.getSetting('readTableCellRow') \ and (self.utilities.hasMatchingHierarchy( event.source, self.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.getRole() == pyatspi.ROLE_TABLE: parentTable = parent.queryTable() index = self.utilities.cellIndex(obj) row = parentTable.getRowAtIndex(index) utterances = [] regions = [] for i in range(0, parentTable.nColumns): cell = parentTable.getAccessibleAt(row, i) while cell.childCount: cell = cell[0] if cell.getRole() == pyatspi.ROLE_TEXT: regions.append(self.getNewBrailleText(cell)) [string, caretOffset, startOffset] = \ self.getTextLineAtCaret(cell) utterances.append(string) self.displayBrailleRegions([regions, regions[0]]) speech.speak(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. self.rolesList = [pyatspi.ROLE_TABLE_CELL, \ pyatspi.ROLE_TREE_TABLE] if _settingsManager.getSetting('readTableCellRow') \ and self.utilities.hasMatchingHierarchy(event.source, self.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. # _settingsManager.setSetting('readTableCellRow', False) parent = event.source.parent parentTable = parent.queryTable() index = self.utilities.cellIndex(event.source) row = parentTable.getRowAtIndex(index) column = parentTable.getColumnAtIndex(index) # If we are on the same row, then just speak/braille the table # cell as if settings.readTableCellRow was False. # See bug #503874 for more details. # if self.lastMessageRow == row: default.Script.locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus) _settingsManager.setSetting('readTableCellRow', True) return # 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 # deleted a message), 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. # justDeleted = False string, mods = self.utilities.lastKeyAndModifiers() if string == "Delete": justDeleted = True speakAll = (self.lastMessageRow != row) or \ ((row == 0 or row == parentTable.nRows-1) and \ self.lastMessageColumn == column) or \ justDeleted savedBrailleVerbosityLevel = \ _settingsManager.getSetting('brailleVerbosityLevel') savedSpeechVerbosityLevel = \ _settingsManager.getSetting('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.getRole() != pyatspi.ROLE_TABLE_CELL: speakAll = True message = "%d messages" % parentTable.nRows brailleRegions.append(self.brailleRegionsFromStrings(message)) speech.speak(message) for i in range(0, parentTable.nColumns): cell = parentTable.getAccessibleAt(row, i) if cell: verbose = (cell.getIndexInParent() == \ event.source.getIndexInParent()) # 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 = parentTable.getColumnHeader(i) checkbox = False toRead = True # Whether or not we want to replace the cell's column # header with a more user-friendly alternative name. # Currently we only do this with the "status" column, # which we replace with "unread" if it is not checked. # useAlternativeName = False try: action = cell.queryAction() except NotImplementedError: action = None if action: for j in range(0, action.nActions): # Translators: this is the action name for # the 'toggle' action. It must be the same # string used in the *.po file for gail. # if action.getName(j) in ["toggle", _("toggle")]: checkbox = True checked = cell.getState().contains( \ pyatspi.STATE_CHECKED) if speakAll: # Translators: this is the name of the # status column header in the message # list in Evolution. The name needs to # match what Evolution is using. # if header.name == _("Status"): toRead = not checked useAlternativeName = True break # Translators: this is the name of the # flagged column header in the message # list in Evolution. The name needs to # match what Evolution is using. # elif header.name == _("Flagged"): toRead = 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 (verbose or (checkbox and column == i)) \ and not useAlternativeName: _settingsManager.setSetting( 'brailleVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) _settingsManager.setSetting( 'speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) utterances = speechGen.generateSpeech( header, includeContext=False, priorObj=oldLocusOfFocus) [headerRegions, focusedRegion] = \ brailleGen.generateBraille(header) brailleRegions.extend(headerRegions) brailleRegions.append(braille.Region(" ")) if column == i: cellWithFocus = focusedRegion if speakAll or (column == i): speech.speak(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: level = settings.VERBOSITY_LEVEL_VERBOSE else: level = settings.VERBOSITY_LEVEL_BRIEF _settingsManager.setSetting( 'brailleVerbosityLevel', level) _settingsManager.setSetting( 'speechVerbosityLevel', savedSpeechVerbosityLevel) utterances = speechGen.generateSpeech( cell, includeContext=False, priorObj=oldLocusOfFocus) [cellRegions, focusedRegion] = \ brailleGen.generateBraille(cell) # Translators: this is the name of the # status column header in the message # list in Evolution. The name needs to # match what Evolution is using. # if header.name == _("Status"): # Translators: we present this to the user to # indicate that an email message has not been # marked as having been read. # text = _("unread") utterances = [ text ] brailleRegions.append(braille.Region(text)) brailleRegions.append(braille.Region(" ")) # Translators: this is the name of the # attachment column header in the message # list in Evolution. The name needs to # match what Evolution is using. # elif header.name == _("Attachment"): text = header.name utterances = [ text ] if column != i: brailleRegions.append(braille.Region(text)) brailleRegions.append(braille.Region(" ")) # Translators: this is the name of the # flagged column header in the message # list in Evolution. The name needs to # match what Evolution is using. # elif header.name == _("Flagged"): text = header.name utterances = [ text ] if column != i: 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.speak(utterances) if brailleRegions != []: self.displayBrailleRegions([brailleRegions, cellWithFocus]) _settingsManager.setSetting( 'brailleVerbosityLevel', savedBrailleVerbosityLevel) _settingsManager.setSetting( 'speechVerbosityLevel', savedSpeechVerbosityLevel) self.lastMessageColumn = column self.lastMessageRow = row _settingsManager.setSetting('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. self.rolesList = [rolenames.ROLE_CALENDAR_EVENT, \ rolenames.ROLE_CALENDAR_VIEW] if self.utilities.hasMatchingHierarchy(event.source, self.rolesList): debug.println(self.debugLevel, "evolution.locusOfFocusChanged - calendar view: " \ + "day view: tabbing to day with appts.") parent = event.source.parent utterances = speechGen.generateSpeech(parent, includeContext=False, priorObj=oldLocusOfFocus) [brailleRegions, focusedRegion] = \ brailleGen.generateBraille(parent) speech.speak(utterances) apptExtents = event.source.queryComponent().getExtents(0) for child in parent: if (child.getRole() == pyatspi.ROLE_TABLE): childTable = child.queryTable() noRows = childTable.nRows for j in range(0, noRows): row = childTable.getRowAtIndex(j) appt = childTable.getAccessibleAt(row, 0) extents = appt.queryComponent().getExtents(0) if extents.y == apptExtents.y: utterances = speechGen.generateSpeech( event.source, includeContext=False, priorObj=oldLocusOfFocus) [apptRegions, focusedRegion] = \ brailleGen.generateBraille(event.source) brailleRegions.extend(apptRegions) speech.speak(utterances) startTime = 'Start time ' + \ self.utilities.timeForCalRow(j, noRows) brailleRegions.append(braille.Region(startTime)) speech.speak(startTime) apptLen = apptExtents.height / extents.height endTime = 'End time ' + \ self.utilities.timeForCalRow(j + apptLen, noRows) brailleRegions.append(braille.Region(endTime)) speech.speak(endTime) self.displayBrailleRegions([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. self.rolesList = [pyatspi.ROLE_UNKNOWN, \ pyatspi.ROLE_TABLE, \ rolenames.ROLE_CALENDAR_VIEW] if self.utilities.hasMatchingHierarchy(event.source, self.rolesList): debug.println(self.debugLevel, "evolution.locusOfFocusChanged - calendar view: " \ + "day view: moving with arrow keys.") brailleRegions = [] index = event.source.getIndexInParent() parent = event.source.parent calendarView = event.source.parent.parent extents = event.source.queryComponent().getExtents(0) noRows = parent.queryTable().nRows found = False for child in calendarView: if (child.getRoleName() == rolenames.ROLE_CALENDAR_EVENT): apptExtents = child.queryComponent().getExtents(0) if extents.y == apptExtents.y: utterances = speechGen.generateSpeech( child, includeContext=False, priorObj=oldLocusOfFocus) [apptRegions, focusedRegion] = \ brailleGen.generateBraille(child) brailleRegions.extend(apptRegions) speech.speak(utterances) startTime = 'Start time ' + \ self.utilities.timeForCalRow(index, noRows) brailleRegions.append(braille.Region(startTime)) speech.speak(startTime) apptLen = apptExtents.height / extents.height endTime = 'End time ' + \ self.utilities.timeForCalRow(index + apptLen, noRows) brailleRegions.append(braille.Region(endTime)) speech.speak(endTime) self.displayBrailleRegions([brailleRegions, brailleRegions[0]]) found = True if not found: startTime = 'Start time ' + \ self.utilities.timeForCalRow(index, noRows) brailleRegions.append(braille.Region(startTime)) speech.speak(startTime) # Translators: this means there are no scheduled entries # in the calendar. # utterance = _("No appointments") speech.speak(utterance) brailleRegions.append(braille.Region(utterance)) self.displayBrailleRegions([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. self.rolesList = [pyatspi.ROLE_TABLE_CELL, \ pyatspi.ROLE_TABLE, \ pyatspi.ROLE_UNKNOWN, \ pyatspi.ROLE_SCROLL_PANE] if self.utilities.hasMatchingHierarchy(event.source, self.rolesList): debug.println(self.debugLevel, "evolution.locusOfFocusChanged - preferences dialog: " \ + "table cell in options list.") index = event.source.getIndexInParent() obj = event.source.parent.parent.parent parent = obj.parent if parent.getRole() == pyatspi.ROLE_FILLER: for child in parent: if (child.getRole() == pyatspi.ROLE_PAGE_TAB_LIST): tabList = child tab = tabList[index-1] if (tab.getRole() == pyatspi.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. self.rolesList = [pyatspi.ROLE_PUSH_BUTTON, \ pyatspi.ROLE_PANEL, \ pyatspi.ROLE_FILLER, \ pyatspi.ROLE_FILLER, \ pyatspi.ROLE_SPLIT_PANE, \ pyatspi.ROLE_FILLER, \ pyatspi.ROLE_FILLER, \ pyatspi.ROLE_FILLER, \ pyatspi.ROLE_FILLER, \ pyatspi.ROLE_DIALOG] if self.utilities.hasMatchingHierarchy(event.source, self.rolesList): debug.println(self.debugLevel, "evolution.locusOfFocusChanged - mail insert " \ + "attachment dialog: unlabelled button.") brailleRegions = [] # Translators: this is the unlabelled arrow button near the # top of the mail view Insert Attachment dialog in Evolution. # We give it a name. # utterance = _("Directories button") speech.speak(utterance) brailleRegions.append(braille.Region(utterance)) self.displayBrailleRegions([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. if self.utilities.isMessageBody(event.source) \ and event.source.getState().contains(pyatspi.STATE_EDITABLE): debug.println(self.debugLevel, "evolution.locusOfFocusChanged - mail " \ + "compose window: message area.") # We are getting extra (bogus?) "focus:" events from Evolution # when we type the first character at the beginning of a line. # If the last input event was a keyboard event and the parent # of the locusOfFocus and the event.source are the same, and # the last roles hierarchy is the same as this one and # the last key pressed wasn't a navigation key, then just # ignore it. See bug #490317 for more details. # if self.utilities.isSameObject(event.source.parent, orca_state.locusOfFocus.parent): lastKey, mods = self.utilities.lastKeyAndModifiers() if self.utilities.isMessageBody(orca_state.locusOfFocus) \ and lastKey not in ["Left", "Right", "Up", "Down", "Home", "End", "Return", "Tab"]: return # If the last keyboard event was a "same line" # navigation key, then pass this event onto the # onCaretMoved() method in the parent class for # speaking. See bug #516565 for more details. # if lastKey in ["Left", "Right", "Home", "End"]: default.Script.onCaretMoved(self, event) return self.message_panel = event.source.parent.parent self.presentMessageLine(event.source, newLocusOfFocus) return # 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. if self.utilities.isSpellingSuggestionsList(event.source) \ or self.utilities.isSpellingSuggestionsList(newLocusOfFocus): debug.println(self.debugLevel, "evolution.locusOfFocusChanged - spell checking dialog.") # Braille the default action for this component. # self.updateBraille(orca_state.locusOfFocus) if not self.pointOfReference.get('activeDescendantInfo'): [badWord, allTokens] = \ self.utilities.misspelledWordAndBody(event.source, self.message_panel) self.speakMisspeltWord(allTokens, badWord) # 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. self.rolesList = [pyatspi.ROLE_PUSH_BUTTON, \ pyatspi.ROLE_FILLER, \ pyatspi.ROLE_PANEL, \ pyatspi.ROLE_PANEL, \ pyatspi.ROLE_TABLE_CELL, \ pyatspi.ROLE_TABLE, \ pyatspi.ROLE_PANEL] if self.utilities.hasMatchingHierarchy(event.source, self.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) tmp = event.source.parent.parent table = tmp.parent.parent.parent cell = table[table.childCount-1] allText = self.utilities.descendantsWithRole( cell, pyatspi.ROLE_TEXT) utterance = "for " + self.utilities.substring(allText[0], 0, -1) speech.speak(utterance) return # 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 stopSpeechOnActiveDescendantChanged(self, event): """Whether or not speech should be stopped prior to setting the locusOfFocus in onActiveDescendantChanged. Arguments: - event: the Event Returns True if speech should be stopped; False otherwise. """ return False ######################################################################## # # # AT-SPI OBJECT EVENT HANDLERS # # # ######################################################################## def onActiveDescendantChanged(self, event): """Called when an object who manages its own descendants detects a change in one of its children. Arguments: - event: the Event """ # The default script's onActiveDescendantChanged method is cutting # off speech with a speech.stop. If we're in the spellcheck dialog, # this interrupts the presentation of the context. # if self.utilities.isSpellingSuggestionsList(event.source): orca.setLocusOfFocus(event, event.any_data) # We'll tuck away the activeDescendant information for future # reference since the AT-SPI gives us little help in finding # this. # if orca_state.locusOfFocus \ and (orca_state.locusOfFocus != event.source): self.pointOfReference['activeDescendantInfo'] = \ [orca_state.locusOfFocus.parent, orca_state.locusOfFocus.getIndexInParent()] return default.Script.onActiveDescendantChanged(self, event) def onFocus(self, event): """Called whenever an object gets focus. Arguments: - event: the Event """ # When a message is deleted from within the table of messages, we get # two focus events: One for the index of the new message prior to # deletion and one for the index of the new message after deletion. # This causes us to speak the message after the one that gets focus # prior to speaking the actual message that gets focus. # See bug #347964. # string, mods = self.utilities.lastKeyAndModifiers() if string == "Delete": roles = [pyatspi.ROLE_TABLE_CELL, pyatspi.ROLE_TREE_TABLE, pyatspi.ROLE_UNKNOWN, pyatspi.ROLE_SCROLL_PANE] oldLocusOfFocus = orca_state.locusOfFocus if self.utilities.hasMatchingHierarchy(event.source, roles) \ and self.utilities.hasMatchingHierarchy(oldLocusOfFocus, roles): parent = event.source.parent parentTable = parent.queryTable() newIndex = self.utilities.cellIndex(event.source) newRow = parentTable.getRowAtIndex(newIndex) oldIndex = self.utilities.cellIndex(oldLocusOfFocus) oldRow = parentTable.getRowAtIndex(oldIndex) nRows = parentTable.nRows if (newRow != oldRow) and (oldRow != nRows): return # For everything else, pass the event onto the parent class # to be handled in the default way. # default.Script.onFocus(self, event) 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 self.utilities.isWizardNewInfoEvent(event): if event.source.getRole() == pyatspi.ROLE_PANEL: self.lastSetupPanel = event.source self.presentWizardNewInfo( self.utilities.topLevelObject(event.source)) return # For everything else, pass the event onto the parent class # to be handled in the default way. # default.Script.onStateChanged(self, event) def onTextInserted(self, event): """Called whenever text is inserted into an object. Arguments: - event: the Event """ # When the active descendant in the list of misspelled words # changes, we typically get an object:active-descendant-changed # event. Unfortunately, we don't seem to get this event (or a # focus: event) when the user presses a button without moving # focus there explicitly. (e.g. pressing Alt+R) The label which # is associated with the spelling list gets new text. So we'll # try to look for that instead. # if event.source.getRole() == pyatspi.ROLE_LABEL: relations = event.source.getRelationSet() for relation in relations: if relation.getRelationType() == pyatspi.RELATION_LABEL_FOR: target = relation.getTarget(0) if self.utilities.isSpellingSuggestionsList(target): [badWord, allTokens] = \ self.utilities.misspelledWordAndBody( target, self.message_panel) self.speakMisspeltWord(allTokens, badWord) try: selection = target.querySelection() except NotImplementedError: selection = None if selection and selection.nSelectedChildren > 0: newFocus = selection.getSelectedChild(0) orca.setLocusOfFocus(event, newFocus) self.pointOfReference['activeDescendantInfo'] = \ [target, newFocus.getIndexInParent()] return default.Script.onTextInserted(self, event) ######################################################################## # # # Methods for presenting content # # # ######################################################################## def presentMessageLine(self, obj, newLocusOfFocus): """Speak/braille the line at the current text caret offset. """ [string, caretOffset, startOffset] = self.getTextLineAtCaret(obj) self.updateBraille(newLocusOfFocus) result = self.speechGenerator.generateTextIndentation(obj, line=string) if result: speech.speak(result[0]) line = self.utilities.adjustForRepeats(string) if self.utilities.speakBlankLine(obj): # Translators: "blank" is a short word to mean the # user has navigated to an empty line. # speech.speak(_("blank"), None, False) else: speech.speak(line, None, False) def presentWizardNewInfo(self, obj): """Causes the new information displayed in a wizard to be presented to the user. Arguments: - obj: the Accessible object """ if not obj: return # TODO - JD: Presenting the Setup Assistant (or any Wizard) as a # dialog means that we will repeat the dialog's name for each new # "screen". We should consider a 'ROLE_WIZARD' or some other means # for presenting these objects. # utterances = \ self.speechGenerator.generateSpeech(obj, role=pyatspi.ROLE_DIALOG) # The following falls under the heading of "suck it and see." The # worst case scenario is that we present the push button and then # process a focus:/object:state-changed:focused event and present # it. # if orca_state.locusOfFocus \ and orca_state.locusOfFocus.getRole() == pyatspi.ROLE_PUSH_BUTTON \ and orca_state.locusOfFocus.getState().\ contains(pyatspi.STATE_FOCUSED): utterances.append( self.speechGenerator.generateSpeech(orca_state.locusOfFocus)) speech.speak(utterances) 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 _settingsManager.setSetting( 'speechVerbosityLevel', settings.VERBOSITY_LEVEL_BRIEF) utterances = speechGen.generateSpeech(tab) speech.speak(utterances) _settingsManager.setSetting( 'speechVerbosityLevel', savedSpeechVerbosityLevel) self.displayBrailleRegions(brailleGen.generateBraille(tab)) def textLines(self, obj): """Creates a generator that can be used to iterate over each line of a text object, starting at the caret offset. We have to subclass this because Evolution lays out its messages such that each paragraph is in its own panel, each of which is in a higher level panel. So, we just traverse through the children. Arguments: - obj: an Accessible that has a text specialization Returns 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. """ if not obj: return try: text = obj.queryText() except NotImplementedError: return panel = obj.parent htmlPanel = panel.parent startIndex = panel.getIndexInParent() i = startIndex total = htmlPanel.childCount textObjs = [] startOffset = text.caretOffset offset = text.caretOffset string = "" done = False # Determine the correct "say all by" mode to use. # sayAllStyle = _settingsManager.getSetting('sayAllStyle') if sayAllStyle == settings.SAYALL_STYLE_SENTENCE: mode = pyatspi.TEXT_BOUNDARY_SENTENCE_END elif sayAllStyle == settings.SAYALL_STYLE_LINE: mode = pyatspi.TEXT_BOUNDARY_LINE_START else: mode = pyatspi.TEXT_BOUNDARY_LINE_START voices = _settingsManager.getSetting('voices') while not done: panel = htmlPanel.getChildAtIndex(i) if panel != None: textObj = panel.getChildAtIndex(0) try: text = textObj.queryText() except NotImplementedError: return textObjs.append(textObj) length = text.characterCount while offset <= length: [mystr, start, end] = text.getTextAtOffset(offset, mode) endOffset = end if len(mystr) != 0: string += " " + mystr if mode == pyatspi.TEXT_BOUNDARY_LINE_START or \ len(mystr) == 0 or mystr[len(mystr)-1] in '.?!': string = self.utilities.adjustForRepeats(string) if string.decode("UTF-8").isupper(): voice = voices[settings.UPPERCASE_VOICE] else: voice = voices[settings.DEFAULT_VOICE] if not textObjs: textObjs.append(textObj) if len(string) != 0: yield [speechserver.SayAllContext(textObjs, string, startOffset, endOffset), voice] textObjs = [] string = "" startOffset = endOffset if len(mystr) == 0 or end == length: break else: offset = end offset = 0 i += 1 if i == total: done = True # If there is anything left unspoken, speak it now. # if len(string) != 0: string = self.utilities.adjustForRepeats(string) if string.decode("UTF-8").isupper(): voice = voices[settings.UPPERCASE_VOICE] else: voice = voices[settings.DEFAULT_VOICE] yield [speechserver.SayAllContext(textObjs, string, startOffset, endOffset), voice] def __sayAllProgressCallback(self, context, callbackType): """Provide feedback during the sayAll operation. """ if callbackType == speechserver.SayAllContext.PROGRESS: #print "PROGRESS", context.utterance, context.currentOffset return elif callbackType == speechserver.SayAllContext.INTERRUPTED: #print "INTERRUPTED", context.utterance, context.currentOffset offset = context.currentOffset for i in range(0, len(context.obj)): obj = context.obj[i] charCount = obj.queryText().characterCount if offset > charCount: offset -= charCount else: obj.queryText().setCaretOffset(offset) break elif callbackType == speechserver.SayAllContext.COMPLETED: #print "COMPLETED", context.utterance, context.currentOffset obj = context.obj[len(context.obj)-1] obj.queryText().setCaretOffset(context.currentOffset) orca.setLocusOfFocus(None, obj, notifyScript=False) # If there is a selection, clear it. See bug #489504 for more details. # This is not straight forward with Evolution. all the text is in # an HTML panel which contains multiple panels, each containing a # single text object. # panel = obj.parent htmlPanel = panel.parent for i in range(0, htmlPanel.childCount): panel = htmlPanel.getChildAtIndex(i) if panel != None: textObj = panel.getChildAtIndex(0) try: text = textObj.queryText() except: pass else: if text.getNSelections(): text.removeSelection(0) 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.") try: if orca_state.locusOfFocus and orca_state.locusOfFocus.queryText(): speech.sayAll(self.textLines(orca_state.locusOfFocus), self.__sayAllProgressCallback) except: default.Script.sayAll(self, inputEvent) return True