# Orca # # Copyright 2009-2010 Sun Microsystems Inc. # Copyright 2010 Joanmarie Diggs # # 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 Packagemanager.""" __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" __copyright__ = "Copyright (c) 2009-2010 Sun Microsystems Inc." \ "Copyright (c) 2010 Joanmarie Diggs" __license__ = "LGPL" import gtk import pyatspi import orca.scripts.default as default import orca.orca as orca import orca.orca_state as orca_state import orca.settings as settings import orca.speech as speech from orca.orca_i18n import _ from braille_generator import BrailleGenerator from speech_generator import SpeechGenerator from tutorialgenerator import TutorialGenerator from script_utilities import Utilities import script_settings ######################################################################## # # # The Packagemanager 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. """ default.Script.__init__(self, app) self._isBusy = False self._lastObjectPresented = None self._presentedStatusBarIcon = False # Initialize variable to None to make pylint happy. # self.presentLoggedErrorsCheckButton = None def getListeners(self): """Sets up the AT-SPI event listeners for this script.""" listeners = default.Script.getListeners(self) listeners["object:state-changed:busy"] = self.onStateChanged return listeners def getBrailleGenerator(self): """Returns the braille generator for this script.""" return BrailleGenerator(self) def getSpeechGenerator(self): """Returns the speech generator for this script.""" return SpeechGenerator(self) def getTutorialGenerator(self): """Returns the tutorial generator for this script.""" return TutorialGenerator(self) def getUtilities(self): """Returns the utilites for this script.""" return Utilities(self) def getAppPreferencesGUI(self): """Return a GtkVBox contain the application unique configuration GUI items for the current application. """ vbox = gtk.VBox(False, 0) vbox.set_border_width(12) gtk.Widget.show(vbox) # Translators: The Package Manager application notifies the # user of minor errors by displaying an icon in the status # bar and adding them to an error log rather than displaying # the error in a dialog box. This string is the label for a # checkbox. If it is checked, Orca will inform the user when # the notification icon has appeared. # label = _("Notify me when errors have been logged.") self.presentLoggedErrorsCheckButton = gtk.CheckButton(label) gtk.Widget.show(self.presentLoggedErrorsCheckButton) gtk.Box.pack_start(vbox, self.presentLoggedErrorsCheckButton, False, False, 0) gtk.ToggleButton.set_active( self.presentLoggedErrorsCheckButton, script_settings.presentLoggedErrors) return vbox def setAppPreferences(self, prefs): """Write out the application specific preferences lines and set the new values. Arguments: - prefs: file handle for application preferences. """ prefs.writelines("\n") script_settings.presentLoggedErrors = \ self.presentLoggedErrorsCheckButton.get_active() prefs.writelines("%s.presentLoggedErrors = %s\n" % \ ("orca.scripts.apps.packagemanager.script_settings", script_settings.presentLoggedErrors)) 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 """ # Prevent chattiness when arrowing out of or into a link. # lastKey, mods = self.utilities.lastKeyAndModifiers() if lastKey in ["Left", "Right", "Up", "Down"] \ and event and event.type.startswith("focus:") \ and (self.utilities.isLink(oldLocusOfFocus) \ or self.utilities.isLink(newLocusOfFocus)): orca.setLocusOfFocus(event, newLocusOfFocus, False) return default.Script.locusOfFocusChanged( self, event, oldLocusOfFocus, newLocusOfFocus) def onCaretMoved(self, event): """Called whenever the caret moves. Arguments: - event: the Event """ # When arrowing into a link, we get a focus: event followed by two # identical caret-moved events. # lastPos = self.pointOfReference.get("lastCursorPosition") if lastPos and lastPos[0] == event.source \ and lastPos[1] == event.detail1: return # Quietly set the locusOfFocus in HTML containers so that the default # script doesn't ignore this event. # if event.source != orca_state.locusOfFocus \ and self.utilities.ancestorWithRole( event.source, [pyatspi.ROLE_HTML_CONTAINER], [pyatspi.ROLE_FRAME]): orca.setLocusOfFocus(event, event.source, False) default.Script.onCaretMoved(self, event) def onFocus(self, event): """Called whenever an object gets focus. Arguments: - event: the Event """ # For some reason we're getting quite a few claims of focus from # nameless page tabs. This seems to occur in conjunction with PM's # new Recent Searches feature. We need to ignore these events. # if event and event.source and not event.source.name \ and event.source.getRole() == pyatspi.ROLE_PAGE_TAB: return default.Script.onFocus(self, event) def onStateChanged(self, event): """Called whenever an object's state changes. Arguments: - event: the Event """ if event.source.getRole() == pyatspi.ROLE_FRAME \ and event.type.startswith("object:state-changed:busy"): # The busy cursor gets set/unset frequently. It's only worth # presenting if we're in the Search entry (handles both search # and catalog refreshes). # if not self.isSearchEntry(orca_state.locusOfFocus): return if event.detail1 == 1 and not self._isBusy: # Translators: this is in reference to loading a web page # or some other content. # msg = _("Loading. Please wait.") speech.speak(msg) self.displayBrailleMessage( msg, flashTime=settings.brailleFlashTime) self._isBusy = True elif event.detail1 == 0 and self._isBusy: # Translators: this is in reference to loading a web page # or some other content. # msg = _("Finished loading.") speech.speak(msg) self.displayBrailleMessage( msg, flashTime=settings.brailleFlashTime) self._isBusy = False return if script_settings.presentLoggedErrors \ and not self._presentedStatusBarIcon \ and event.source.getRole() == pyatspi.ROLE_PANEL \ and event.type.startswith("object:state-changed:showing") \ and event.detail1: obj = self.findStatusBarIcon() while obj and not self.utilities.isSameObject(obj, event.source): obj = obj.parent if obj: # Translators: The Package Manager application notifies the # user of minor errors by displaying an icon in the status # bar and adding them to an error log rather than displaying # the error in a dialog box. This is the message Orca will # present to inform the user that this has occurred. # msg = _("An error occurred. View the error log for details.") speech.speak(msg) self.displayBrailleMessage( msg, flashTime=settings.brailleFlashTime) self._presentedStatusBarIcon = True default.Script.onStateChanged(self, event) def onWindowActivated(self, event): """Called whenever a toplevel window is activated. Arguments: - event: the Event """ if self._presentedStatusBarIcon and not self.findStatusBarIcon(): self._presentedStatusBarIcon = False default.Script.onWindowActivated(self, event) def findStatusBarIcon(self, statusBar=None): """Locates the icon which is sometimes found to the left of the packagemanager status bar. Arguments: - statusBar: packagemanager's status bar Returns the accessible for the icon if the icon is found and showing. """ icon = None if not statusBar and self.app.childCount: # Be sure we're looking in PM's main window. # statusBar = self.utilities.statusBar(self.app[0]) if statusBar: i = statusBar.getIndexInParent() if i > 0: icons = self.utilities.descendantsWithRole( statusBar.parent[i - 1], pyatspi.ROLE_ICON) if icons: icon = icons[0] return icon 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. """ # Intentionally doing an equality check for performance # purposes. # if event.any_data == orca_state.locusOfFocus: return False return True 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 """ # If the user arrows into the "recent searches" portion of the # categories tree, the tree will stop claiming STATE_FOCUSED, # but continue to emit active-descendant-changed events. (Doing # an equality check seems to be safe here.) # if not event.source.getState().contains(pyatspi.STATE_FOCUSED) \ and orca_state.locusOfFocus \ and orca_state.locusOfFocus.parent != event.source: return # There can be cases when the object that fires an # active-descendant-changed event has no children. In this case, # use the object that fired the event, otherwise, use the child. # child = event.any_data if child: if self.stopSpeechOnActiveDescendantChanged(event): speech.stop() orca.setLocusOfFocus(event, child) else: orca.setLocusOfFocus(event, event.source) # 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()] def _presentTextAtNewCaretPosition(self, event, otherObj=None): """Updates braille, magnification, and outputs speech for the event.source or the otherObj. Overridden here to force the braille display to be updated when the user arrows left/right into another object.""" default.Script._presentTextAtNewCaretPosition(self, event, otherObj) if not self.utilities.isSameObject( event.source, self._lastObjectPresented) \ and self.utilities.ancestorWithRole( event.source, [pyatspi.ROLE_HTML_CONTAINER], [pyatspi.ROLE_FRAME]): self.updateBraille(event.source) self._lastObjectPresented = event.source def updateBraille(self, obj, extraRegion=None): """Updates the braille display to show the given object. Arguments: - obj: the Accessible - extra: extra Region to add to the end """ if not self.utilities.ancestorWithRole( obj, [pyatspi.ROLE_HTML_CONTAINER], [pyatspi.ROLE_FRAME]): return default.Script.updateBraille(self, obj, extraRegion) try: text = obj.queryText() except: return default.Script.updateBraille(self, obj, extraRegion) line = self.getNewBrailleLine(clearBraille=True, addLine=True) focusedRegion = None contents = self.getLineContentsAtOffset(obj, text.caretOffset) for i, content in enumerate(contents): child, startOffset, endOffset, string = content isFocusedObj = self.utilities.isSameObject(child, obj) regions = [self.getNewBrailleText(child, startOffset=startOffset, endOffset=endOffset)] if isFocusedObj: focusedRegion = regions[0] self.addBrailleRegionsToLine(regions, line) if extraRegion: self.addBrailleRegionToLine(extraRegion, line) self.setBrailleFocus(focusedRegion, getLinkMask=False) self.refreshBraille(panToCursor=True, getLinkMask=False) def sayCharacter(self, obj): """Speak the character at the caret. Overridden here because the event we get when crossing object boundaries is for the object being left. Arguments: - obj: an Accessible object that implements the AccessibleText interface """ if not self.utilities.ancestorWithRole( obj, [pyatspi.ROLE_HTML_CONTAINER], [pyatspi.ROLE_FRAME]): return default.Script.sayCharacter(self, obj) try: text = obj.queryText() except: return lastKey, mods = self.utilities.lastKeyAndModifiers() if text.caretOffset == text.characterCount and lastKey == "Right": nextObj = self.getRelationTarget(obj, pyatspi.RELATION_FLOWS_TO) if nextObj and nextObj.getRole() == pyatspi.ROLE_TEXT: obj = nextObj default.Script.sayCharacter(self, obj) def sayLine(self, obj): """Speaks the line at the current caret position.""" if not self.utilities.ancestorWithRole( obj, [pyatspi.ROLE_HTML_CONTAINER], [pyatspi.ROLE_FRAME]): return default.Script.sayLine(self, obj) try: text = obj.queryText() except: return default.Script.sayLine(self, obj) contents = self.getLineContentsAtOffset(obj, text.caretOffset) for content in contents: child, startOffset, endOffset, line = content if len(contents) > 1 and not line.strip(): continue isLink = self.utilities.isLink(child) if len(line) and line != "\n": if line.decode("UTF-8").isupper(): voice = self.voices[settings.UPPERCASE_VOICE] elif isLink: voice = self.voices[settings.HYPERLINK_VOICE] else: voice = self.voices[settings.DEFAULT_VOICE] line = self.utilities.adjustForRepeats(line) speech.speak(line, voice) if isLink: speech.speak(self.speechGenerator.getRoleName( child, role=pyatspi.ROLE_LINK)) else: self.sayCharacter(child) def getRelationTarget(self, obj, relationType): """Gets the target of the specified relation for obj. Arguments: - obj: the current object - relationType: the pyatspi relation type Returns the target that relation points to; otherwise, None. """ for relation in obj.getRelationSet(): if relation.getRelationType() == relationType: return relation.getTarget(0) return None def getLineContentsAtOffset(self, obj, offset): """Returns an ordered list where each element is composed of an [obj, startOffset, endOffset, string] tuple. The list is created via an in-order traversal of the document contents starting at the given object and characterOffset. The first element in the list represents the beginning of the line. The last element in the list represents the character just before the beginning of the next line. Arguments: -obj: the object to start at -offset: the character offset in the object """ try: text = obj.queryText() except: return [] contents = [] # Get the line contents with respect to this object. # line = text.getTextAtOffset(offset, pyatspi.TEXT_BOUNDARY_LINE_START) contents.append([obj, line[1], line[2], line[0]]) extents = text.getCharacterExtents(offset, 0) index = obj.getIndexInParent() # Get the line contents for all objects to the left. # prevObj = self.getRelationTarget(obj, pyatspi.RELATION_FLOWS_FROM) while prevObj: try: text = prevObj.queryText() except: break line = text.getTextAtOffset( text.characterCount - 1, pyatspi.TEXT_BOUNDARY_LINE_START) newExtents = text.getCharacterExtents(line[1], 0) if extents != newExtents and self.onSameLine(extents, newExtents): content = [prevObj, line[1], line[2], line[0]] # Sanity check due to some circular FLOWS_TO/FROM relations. # if contents.count(content): break # Sanity check due to gtkhtml2 flat out lying about the value # of y. # if extents[0] <= newExtents[0]: break contents[0:0] = [content] prevObj = self.getRelationTarget( prevObj, pyatspi.RELATION_FLOWS_FROM) else: break # Get the line contents for all objects to the right. # nextObj = self.getRelationTarget(obj, pyatspi.RELATION_FLOWS_TO) while nextObj: try: text = nextObj.queryText() except: break line = text.getTextAtOffset(0, pyatspi.TEXT_BOUNDARY_LINE_START) newExtents = text.getCharacterExtents(line[1], 0) if extents != newExtents and self.onSameLine(extents, newExtents): content = [nextObj, line[1], line[2], line[0]] # Sanity check due to some circular FLOWS_TO/FROM relations. # if contents.count(content): break # Sanity check due to gtkhtml2 flat out lying about the value # of y. # if extents[0] >= newExtents[0]: break contents.append(content) nextObj = \ self.getRelationTarget(nextObj, pyatspi.RELATION_FLOWS_TO) else: break return contents def onSameLine(self, extents1, extents2): """Determine if extents1 and extents2 are on the same line. Arguments: -extents1: [x, y, width, height] -extents2: [x, y, width, height] Returns True if extents1 and extents2 are on the same line. """ verticalCenter1 = extents1[1] + extents1[3] / 2 verticalCenter2 = extents2[1] + extents2[3] / 2 return abs(verticalCenter1 - verticalCenter2) <= 5 def isSearchEntry(self, obj): """Attempts to distinguish the Search entry from other accessibles. Arguments: -obj: the accessible being examined Returns True if we think obj is the Search entry. """ # The Search entry is the only entry inside a toolbar. If that # should change, we'll need to make our criteria more specific. # if obj and obj.getRole() == pyatspi.ROLE_TEXT \ and self.utilities.ancestorWithRole( obj, [pyatspi.ROLE_TOOL_BAR], [pyatspi.ROLE_FRAME]): return True return False def isPackageListToggle(self, obj): """Attempts to identify the toggle-able cell in the package list. Arguments: -obj: the accessible being examined Returns True if we think obj is the toggle-able cell in the package list. """ if obj and obj.getRole() == pyatspi.ROLE_TABLE_CELL: try: action = obj.queryAction() except NotImplementedError: action = None if action: for i in range(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(i) in ["toggle", _("toggle")]: try: table = obj.parent.queryTable() except: col = -1 else: index = self.utilities.cellIndex(obj) col = table.getColumnAtIndex(index) if col == 0: top = self.utilities.topLevelObject(obj) if top and top.getRole() == pyatspi.ROLE_FRAME: return True return False return False