From 7fa29f78b0a0894b17ec9a27d0ffe65cee52c775 Mon Sep 17 00:00:00 2001 From: liyuan Date: Wed, 18 Apr 2007 08:42:33 +0000 Subject: [PATCH] 2007-04-18 Li Yuan * pyatspi/__init__.py: * pyatspi/accessible.py: * pyatspi/constants.py: * pyatspi/event.py: * pyatspi/registry.py: * pyatspi/utils.py: Bug #430938. Add some files to create the uniform Python wrapper for at-spi. git-svn-id: http://svn.gnome.org/svn/at-spi/trunk@904 e2bd861d-eb25-0410-b326-f6ed22b6b98c --- ChangeLog | 11 + pyatspi/ChangeLog | 10 + pyatspi/__init__.py | 67 +++++ pyatspi/accessible.py | 449 ++++++++++++++++++++++++++++++ pyatspi/constants.py | 210 +++++++++++++++ pyatspi/event.py | 229 ++++++++++++++++ pyatspi/registry.py | 734 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyatspi/utils.py | 285 ++++++++++++++++++++ 8 files changed, 1995 insertions(+) create mode 100644 pyatspi/ChangeLog create mode 100644 pyatspi/__init__.py create mode 100644 pyatspi/accessible.py create mode 100644 pyatspi/constants.py create mode 100644 pyatspi/event.py create mode 100644 pyatspi/registry.py create mode 100644 pyatspi/utils.py diff --git a/ChangeLog b/ChangeLog index e966804..26789d6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,14 @@ +2007-04-18 Li Yuan + + * pyatspi/__init__.py: + * pyatspi/accessible.py: + * pyatspi/constants.py: + * pyatspi/event.py: + * pyatspi/registry.py: + * pyatspi/utils.py: + Bug #430938. Add some files to create the uniform Python + wrapper for at-spi. + 2007-04-12 Li Yuan * atk-bridge/bridge.c: (spi_atk_bridge_get_registry), diff --git a/pyatspi/ChangeLog b/pyatspi/ChangeLog new file mode 100644 index 0000000..51dddb9 --- /dev/null +++ b/pyatspi/ChangeLog @@ -0,0 +1,10 @@ +2007-04-18 Li Yuan + + * pyatspi/__init__.py: + * pyatspi/accessible.py: + * pyatspi/constants.py: + * pyatspi/event.py: + * pyatspi/registry.py: + * pyatspi/utils.py: + Bug #430938. Add some files to create the uniform Python + wrapper for at-spi. diff --git a/pyatspi/__init__.py b/pyatspi/__init__.py new file mode 100644 index 0000000..727e652 --- /dev/null +++ b/pyatspi/__init__.py @@ -0,0 +1,67 @@ +''' +Wraps the Gnome Assistive Technology Service Provider Interface for use in +Python. Imports the bonobo and ORBit modules. Initializes the ORBit ORB. +Activates the bonobo Accessibility Registry. Loads the Accessibility typelib +and imports the classes implementing the AT-SPI interfaces. + +@var Registry: Reference to the AT-SPI registry daemon intialized on successful + import +@type Registry: registry.Registry + +@author: Peter Parente +@organization: IBM Corporation +@copyright: Copyright (c) 2005, 2007 IBM Corporation +@license: LGPL + +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. + +Portions of this code originally licensed and copyright (c) 2005, 2007 +IBM Corporation under the BSD license, available at +U{http://www.opensource.org/licenses/bsd-license.php} +''' + +REGISTRY_IID = "OAFIID:Accessibility_Registry:1.0" +TYPELIB_NAME = "Accessibility" + +# import ORBit and bonobo first (required) +import ORBit, bonobo +# initialize the ORB +orb = ORBit.CORBA.ORB_init() +# get a reference to the gnome Accessibility registry +reg = bonobo.activation.activate_from_id(REGISTRY_IID, 0, 0) +if reg is None: + raise RuntimeError('could not activate:', REGISTRY_IID) +# generate Python code for the Accessibility module from the IDL +ORBit.load_typelib(TYPELIB_NAME) + +# import our registry module +import registry +# wrap the raw registry object in our convenience singleton +Registry = registry.Registry(reg) +# now throw the module away immediately +del registry + +# pull the cache level functions into this namespace, but nothing else +from accessible import setCacheLevel, getCacheLevel + +# pull constants and utilities directly into this namespace; rest of code +# never has to be touched externally +from constants import * +from utils import * + +# throw away extra references +del reg +del orb diff --git a/pyatspi/accessible.py b/pyatspi/accessible.py new file mode 100644 index 0000000..ff1391b --- /dev/null +++ b/pyatspi/accessible.py @@ -0,0 +1,449 @@ +''' +Creates functions at import time that are mixed into the +Accessibility.Accessible base class to make it more Pythonic. + +Based on public domain code originally posted at +http://wwwx.cs.unc.edu/~parente/cgi-bin/RuntimeClassMixins. + +@todo: PP: implement caching for basic attributes + +@var _CACHE_LEVEL: Current level of caching enabled. Checked dynamically by + L{_AccessibleMixin} +@type _CACHE_LEVEL: integer + +@author: Peter Parente +@organization: IBM Corporation +@copyright: Copyright (c) 2005, 2007 IBM Corporation +@license: LGPL + +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. + +Portions of this code originally licensed and copyright (c) 2005, 2007 +IBM Corporation under the BSD license, available at +U{http://www.opensource.org/licenses/bsd-license.php} +''' +import new +import types +import ORBit +import Accessibility +import constants +import utils + +_CACHE_LEVEL = None + +def getCacheLevel(): + ''' + Gets the current level of caching. + + @return: None indicating no caching is in effect. + L{constants.CACHE_INTERFACES} indicating all interface query results are + cached. L{constants.CACHE_PROPERTIES} indicating all basic accessible + properties are cached. + @rtype: integer + ''' + return _CACHE_LEVEL + +def setCacheLevel(val): + ''' + Sets the desired level of caching for all accessible objects created after + this function is invoked. + + @param val: None indicating no caching is in effect. + L{constants.CACHE_INTERFACES} indicating all interface query results are + cached. L{constants.CACHE_PROPERTIES} indicating all basic accessible + properties are cached. + @type val: integer + ''' + global _CACHE_LEVEL + _CACHE_LEVEL = val + +def _makeQuery(iid): + ''' + Builds a function querying to a specific interface and returns it. + + @ivar iid: Interface identifier to use when querying + @type iid: string + @return: Function querying to the given interface + @rtype: function + ''' + def _inner(self): + ''' + Queries an object for another interface. + + @return: An object with the desired interface + @rtype: object + @raise NotImplementedError: When the desired interface is not supported + ''' + try: + return self._cache[iid] + except KeyError: + # interface not cached + caching = True + except AttributeError: + # not caching at present + caching = False + + try: + i = self.queryInterface(iid) + except Exception, e: + raise LookupError(e) + if i is None: + raise NotImplementedError + + # not needed according to ORBit2 spec, but makes Java queries work + # more reliably according to Orca experience + i._narrow(i.__class__) + if caching: + # cache the narrow'ed result, but only if we're caching for this object + self._cache[iid] = i + return i + + return _inner + +def _makeExceptionHandler(func): + ''' + Builds a function calling the one it wraps in try/except statements catching + CORBA exceptions. + + @return: Function calling the method being wrapped + @rtype: function + ''' + def _inner(self, *args, **kwargs): + try: + # try calling the original func + return func(self, *args, **kwargs) + except ORBit.CORBA.NO_IMPLEMENT, e: + # raise Python exception + raise NotImplementedError(e) + except ORBit.CORBA.Exception, e: + # raise Python exception + raise LookupError(e) + return _inner + +def _mixInterfaces(cls, interfaces): + ''' + Add methods for querying to interfaces other than the base accessible to + the given class. + + @param cls: Class to mix interface methods into + @type cls: class + @param interfaces: Classes representing AT-SPI interfaces + @type interfaces: list of class + ''' + # create functions in this module for all interfaces listed in constants + for interface in interfaces: + # build name of converter from the name of the interface + name = 'query%s' % utils.getInterfaceName(interface) + # build a function that queries to the given interface + func = _makeQuery(utils.getInterfaceIID(interface)) + # build a new method that is a clone of the original function + method = new.function(func.func_code, func.func_globals, name, + func.func_defaults, func.func_closure) + # add the method to the given class + setattr(cls, name, method) + +def _mixExceptions(cls): + ''' + Wraps all methods and properties in a class with handlers for CORBA + exceptions. + + @param cls: Class to mix interface methods into + @type cls: class + ''' + # loop over all names in the new class + for name in cls.__dict__.keys(): + obj = cls.__dict__[name] + # check if we're on a protected or private method + if name.startswith('_'): + continue + # check if we're on a method + elif isinstance(obj, types.FunctionType): + # wrap the function in an exception handler + method = _makeExceptionHandler(obj) + # add the wrpped function to the class + setattr(cls, name, method) + # check if we're on a property + elif isinstance(obj, property): + # wrap the getters and setters + if obj.fget: + getter = _makeExceptionHandler(getattr(cls, obj.fget.__name__)) + else: + getter = None + if obj.fset: + setter = _makeExceptionHandler(getattr(cls, obj.fset.__name__)) + else: + setter = None + setattr(cls, name, property(getter, setter)) + +def _mixClass(cls, new_cls): + ''' + Adds the methods in new_cls to cls. After mixing, all instances of cls will + have the new methods. If there is a method name clash, the method already in + cls will be prefixed with '_mix_' before the new method of the same name is + mixed in. + + @note: _ is not the prefix because if you wind up with __ in front of a + variable, it becomes private and mangled when an instance is created. + Difficult to invoke from the mixin class. + + @param cls: Existing class to mix features into + @type cls: class + @param new_cls: Class containing features to add + @type new_cls: class + ''' + # loop over all names in the new class + for name, func in new_cls.__dict__.items(): + # get only functions from the new_class + if (isinstance(func, types.FunctionType)): + # build a new function that is a clone of the one from new_cls + method = new.function(func.func_code, func.func_globals, name, + func.func_defaults, func.func_closure) + try: + # check if a method of the same name already exists in the target + old_method = getattr(cls, name) + except AttributeError: + pass + else: + # rename the old method so we can still call it if need be + setattr(cls, '_mix_'+name, old_method) + # add the clone to cls + setattr(cls, name, method) + elif isinstance(func, staticmethod): + try: + # check if a method of the same name already exists in the target + old_method = getattr(cls, name) + except AttributeError: + pass + else: + # rename the old method so we can still call it if need be + setattr(cls, '_mix_'+name, old_method) + setattr(cls, name, func) + +class _AccessibleMixin(object): + ''' + Defines methods to be added to the Accessibility.Accessible class. The + features defined here will be added to the Accessible class at run time so + that all instances of Accessible have them (i.e. there is no need to + explicitly wrap an Accessible in this class or derive a new class from it.) + + @cvar SLOTTED_CLASSES: Mapping from raw Accessibility class to a new class + having the slots defined by L{SLOTS} + @type SLOTTED_CLASSES: dictionary + @cvar SLOTS: All slots to create + @type SLOTS: tuple + ''' + SLOTTED_CLASSES = {} + SLOTS = ('_cache', '_cache_level') + + def __new__(cls): + ''' + Creates a new class mimicking the one requested, but with an extra _cache + attribute set in the __slots__ tuple. This field can be set to a dictionary + or other object to allow caching to occur. + + Note that we can't simply mix __slots__ into this class because __slots__ + has an effect only at class creation time. + + @param cls: Accessibility object class + @type cls: class + @return: Instance of the new class + @rtype: object + ''' + try: + # check if we've already created a new version of the class + new_cls = _AccessibleMixin.SLOTTED_CLASSES[cls] + except KeyError: + # create the new class if not + new_cls = type(cls.__name__, (cls,), + {'__module__' : cls.__module__, + '__slots__' : _AccessibleMixin.SLOTS}) + _AccessibleMixin.SLOTTED_CLASSES[cls] = new_cls + obj = cls._mix___new__(new_cls) + obj._cache_level = _CACHE_LEVEL + if obj._cache_level is not None: + # be sure to create the cache dictionary, if we're caching + obj._cache = {} + return obj + + def __del__(self): + ''' + Decrements the reference count on the accessible object when there are no + Python references to this object. This provides automatic reference + counting for AT-SPI objects. + ''' + try: + self.unref() + except Exception: + pass + + def __iter__(self): + ''' + Iterator that yields one accessible child per iteration. If an exception is + encountered, None is yielded instead. + + @return: A child accessible + @rtype: Accessibility.Accessible + ''' + for i in xrange(self.childCount): + try: + yield self.getChildAtIndex(i) + except constants.CORBAException: + yield None + + def __str__(self): + ''' + Gets a human readable representation of the accessible. + + @return: Role and name information for the accessible + @rtype: string + ''' + try: + return '[%s | %s]' % (self.getRoleName(), self.name) + except Exception: + return '[DEAD]' + + def __nonzero__(self): + ''' + @return: True, always + @rtype: boolean + ''' + return True + + def __getitem__(self, index): + ''' + Thin wrapper around getChildAtIndex. + + @param index: Index of desired child + @type index: integer + @return: Accessible child + @rtype: Accessibility.Accessible + ''' + return self.getChildAtIndex(index) + + def __len__(self): + ''' + Thin wrapper around childCount. + + @return: Number of child accessibles + @rtype: integer + ''' + return self.childCount + + def _get_name(self): + ''' + Gets the name of the accessible from the cache if it is available, + otherwise, fetches it remotely. + + @return: Name of the accessible + @rtype: string + ''' + if self._cache_level != constants.CACHE_PROPERTIES: + return self._mix__get_name() + + try: + return self._cache['name'] + except KeyError: + name = self._mix__get_name() + self._cache['name'] = name + return name + + def getRoleName(self): + ''' + Gets the unlocalized role name of the accessible from the cache if it is + available, otherwise, fetches it remotely. + + @return: Role name of the accessible + @rtype: string + ''' + if self._cache_level != constants.CACHE_PROPERTIES: + return self._mix_getRoleName() + + try: + return self._cache['rolename'] + except KeyError: + name = self._mix_getRoleName() + self._cache['rolename'] = name + return name + + def _get_description(self): + ''' + Gets the description of the accessible from the cache if it is available, + otherwise, fetches it remotely. + + @return: Description of the accessible + @rtype: string + ''' + if self._cache_level != constants.CACHE_PROPERTIES: + return self._mix__get_description() + + try: + return self._cache['description'] + except KeyError: + name = self._mix__get_description() + self._cache['description'] = name + return name + + def getIndexInParent(self): + ''' + Gets the index of this accessible in its parent. Uses the implementation of + this method provided by the Accessibility.Accessible object, but checks the + bound of the value to ensure it is not outside the range of childCount + reported by this accessible's parent. + + @return: Index of this accessible in its parent + @rtype: integer + ''' + i = self._mix_getIndexInParent() + try: + # correct for out-of-bounds index reporting + return min(self.parent.childCount-1, i) + except AttributeError: + # return sentinel if there is no parent + return -1 + + def getApplication(self): + ''' + Gets the most-parent accessible (the application) of this accessible. Tries + using the getApplication method introduced in AT-SPI 1.7.0 first before + resorting to traversing parent links. + + @warning: Cycles involving more than the previously traversed accessible + are not detected by this code. + @return: Application object + @rtype: Accessibility.Application + ''' + try: + return self._mix_getApplication() + except AttributeError: + pass + curr = self + try: + while curr.parent is not None and (not curr.parent == curr): + curr = curr.parent + return curr + except Exception: + pass + # return None if the application isn't reachable for any reason + return None + +# mix the new functions +_mixClass(Accessibility.Accessible, _AccessibleMixin) +# mix queryInterface convenience methods +_mixInterfaces(Accessibility.Accessible, constants.ALL_INTERFACES) +# mix the exception handlers into all queryable interfaces +map(_mixExceptions, constants.ALL_INTERFACES) +# mix the exception handlers into other Accessibility objects +map(_mixExceptions, [Accessibility.StateSet]) diff --git a/pyatspi/constants.py b/pyatspi/constants.py new file mode 100644 index 0000000..6643b06 --- /dev/null +++ b/pyatspi/constants.py @@ -0,0 +1,210 @@ +''' +Defines constants used throughout this wrapper. + +@author: Peter Parente +@author: Pete Brunet +@organization: IBM Corporation +@copyright: Copyright (c) 2005, 2007 IBM Corporation +@license: LGPL + +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. + +Portions of this code originally licensed and copyright (c) 2005, 2007 +IBM Corporation under the BSD license, available at +U{http://www.opensource.org/licenses/bsd-license.php} +''' +import ORBit +import Accessibility +import utils + +# pull dictionary of Accessibility module namespace into local var, temporarily +acc_dict = vars(Accessibility) + +# run through all objects in the Accessibility module +ALL_INTERFACES = [] +# list of classes that are not queriable interfaces +not_interfaces = ['RoleSet', 'StateSet', 'DeviceEventListener', 'LoginHelper', + 'ContentStream', 'DeviceEventController', 'Registry' + 'DeviceEventListener', 'EventListener', 'Relation', + 'CommandListener', 'Selector'] +for obj in acc_dict.values(): + try: + # see if the object has a typecode + kind = obj.__typecode__.kind + except AttributeError: + continue + # compare the typecode to the one for CORBA objects, and to our list of + # classes that are not queriable interfaces + if (kind == ORBit.CORBA.tk_objref and + utils.getInterfaceName(obj) not in not_interfaces): + # this is an interface class + ALL_INTERFACES.append(obj) +# get rid of our temporary list +del not_interfaces + +# constants used in the Component interface to get screen coordinates +DESKTOP_COORDS = 0 +WINDOW_COORDS = 1 + +# constants used to synthesize mouse events +MOUSE_B1P = 'b1p' +MOUSE_B1R = 'b1r' +MOUSE_B1C = 'b1c' +MOUSE_B1D = 'b1d' +MOUSE_B2P = 'b2p' +MOUSE_B2R = 'b2r' +MOUSE_B2C = 'b2c' +MOUSE_B2D = 'b2d' +MOUSE_B3P = 'b3p' +MOUSE_B3R = 'b3r' +MOUSE_B3C = 'b3c' +MOUSE_B3D = 'b3d' +MOUSE_ABS = 'abs' +MOUSE_REL = 'rel' + +# defines levels of caching where if x > y, x caches all of y plus more +CACHE_INTERFACES = 0 +CACHE_PROPERTIES = 1 + +# dictionary used to correct the bug of not being able to register for all the +# subevents given only an AT-SPI event class (i.e. first part of the event +# name) keys are event names having subevents and values are the subevents +# under the key event; handlers *can* be registered for events not in this tree +EVENT_TREE = { + 'terminal': + ['terminal:line-changed', + 'terminal:columncount-changed', + 'terminal:linecount-changed', + 'terminal:application-changed', + 'terminal:charwidth-changed' + ], + 'document': + ['document:load-complete', + 'document:reload', + 'document:load-stopped', + 'document:content-changed', + 'document:attributes-changed' + ], + 'object': + ['object:property-change', + 'object:bounds-changed', + 'object:link-selected', + 'object:state-changed', + 'object:children-changed', + 'object:visible-data-changed', + 'object:selection-changed', + 'object:model-changed', + 'object:active-descendant-changed', + 'object:row-inserted', + 'object:row-reordered', + 'object:row-deleted', + 'object:column-inserted', + 'object:column-reordered', + 'object:column-deleted', + 'object:text-bounds-changed', + 'object:text-selection-changed', + 'object:text-changed', + 'object:text-attributes-changed', + 'object:text-caret-moved', + 'object:attributes-changed'], + 'object:text-changed' : + ['object:text-changed:insert', + 'object:text-changed:delete'], + 'object:property-change' : + ['object:property-change:accessible-parent', + 'object:property-change:accessible-name', + 'object:property-change:accessible-description', + 'object:property-change:accessible-value', + 'object:property-change:accessible-role', + 'object:property-change:accessible-table-caption', + 'object:property-change:accessible-table-column-description', + 'object:property-change:accessible-table-column-header', + 'object:property-change:accessible-table-row-description', + 'object:property-change:accessible-table-row-header', + 'object:property-change:accessible-table-summary'], + 'object:children-changed' : + ['object:children-changed:add', + 'object:children-changed:remove'], + 'object:state-changed' : + ['object:state-changed:'], + 'mouse' : + ['mouse:abs', + 'mouse:rel', + 'mouse:button'], + 'mouse:button' : + ['mouse:button:1p', + 'mouse:button:1r', + 'mouse:button:2p', + 'mouse:button:2r', + 'mouse:button:3p', + 'mouse:button:3r'], + 'window' : + ['window:minimize', + 'window:maximize', + 'window:restore', + 'window:close', + 'window:create', + 'window:reparent', + 'window:desktop-create', + 'window:desktop-destroy', + 'window:activate', + 'window:deactivate', + 'window:raise', + 'window:lower', + 'window:move', + 'window:resize', + 'window:shade', + 'window:unshade', + 'window:restyle'], + 'focus' : + ['focus:'] +} + +# pull ROLE_*, STATE_*, TEXT_*, MODIFIER_*, LOCALE_*, and RELATION_*, etc. +# constants into the local namespace for convenient access +# grab all the variable names and their values from the Accessibility module + +# get the dictionary for the local namespace +loc_dict = locals() +# these are the prefixes for the variable names we want to pull out of the +# Accessibility module +prefixes = ['ROLE_', 'STATE_', 'TEXT_', 'MODIFIER_', 'LOCALE_', 'RELATION_', + 'KEY_', 'MATCH_', 'SORT_', 'LAYER_'] +# for each variable name in the Accessibility namespace, check if it starts +# with at least one of the prefixes above; if it does, add a 2-tuple of +# variable name and value to the values list +values = ((name, value) for name, value in acc_dict.items() + if len([p for p in prefixes if name.startswith(p)])) +# create a new dictionary from the list of tuples and then update the local +# namespace dictionary with that dictionary +loc_dict.update(dict(values)) + +# build a dictionary mapping state values to names based on the prefix of the +# constant name imported from Accessibility +STATE_VALUE_TO_NAME = dict(((value, name[6:].lower().replace('_', ' ')) + for name, value + in acc_dict.items() + if name.startswith('STATE_'))) + +# build a dictionary mapping relation values to names based on the prefix of +# the constant name imported from Accessibility +RELATION_VALUE_TO_NAME = dict(((value, name[9:].lower().replace('_', ' ')) + for name, value + in acc_dict.items() + if name.startswith('RELATION_'))) + +# throw away any temporary variables so they don't hang around in this module +del acc_dict, loc_dict, prefixes, values diff --git a/pyatspi/event.py b/pyatspi/event.py new file mode 100644 index 0000000..af6aa28 --- /dev/null +++ b/pyatspi/event.py @@ -0,0 +1,229 @@ +''' +Wrapper classes for AT-SPI events and device events. + +@author: Peter Parente +@organization: IBM Corporation +@copyright: Copyright (c) 2005, 2007 IBM Corporation +@license: LGPL + +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. + +Portions of this code originally licensed and copyright (c) 2005, 2007 +IBM Corporation under the BSD license, available at +U{http://www.opensource.org/licenses/bsd-license.php} +''' +import constants + +class DeviceEvent(object): + ''' + Wraps an AT-SPI device event with a more Pythonic interface. Primarily adds + a consume attribute which can be used to cease propagation of a device event. + + @ivar consume: Should this event be consumed and not allowed to pass on to + observers further down the dispatch chain in this process or possibly + system wide? + @type consume: boolean + @ivar type: Kind of event, KEY_PRESSED_EVENT or KEY_RELEASED_EVENT + @type type: Accessibility.EventType + @ivar id: Serial identifier for this key event + @type id: integer + @ivar hw_code: Hardware scan code for the key + @type hw_code: integer + @ivar modifiers: Modifiers held at the time of the key event + @type modifiers: integer + @ivar timestamp: Time at which the event occurred relative to some platform + dependent starting point (e.g. XWindows start time) + @type timestamp: integer + @ivar event_string: String describing the key pressed (e.g. keysym) + @type event_string: string + @ivar is_text: Is the event representative of text to be inserted (True), or + of a control key (False)? + @type is_text: boolean + ''' + def __init__(self, event): + ''' + Attaches event data to this object. + + @param event: Event object + @type event: Accessibility.DeviceEvent + ''' + self.consume = False + self.type = event.type + self.id = event.id + self.hw_code = event.hw_code + self.modifiers = event.modifiers + self.timestamp = event.timestamp + self.event_string = event.event_string + self.is_text = event.is_text + + def __str__(self): + ''' + Builds a human readable representation of the event. + + @return: Event description + @rtype: string + ''' + if self.type == constants.KEY_PRESSED_EVENT: + kind = 'pressed' + elif self.type == constants.KEY_RELEASED_EVENT: + kind = 'released' + return '''\ +%s +\thw_code: %d +\tevent_string: %s +\tmodifiers: %d +\tid: %d +\ttimestamp: %d +\tis_text: %s''' % (kind, self.hw_code, self.event_string, self.modifiers, + self.id, self.timestamp, self.is_text) + +class Event(object): + ''' + Wraps an AT-SPI event with a more Pythonic interface managing exceptions, + the differences in any_data across versions, and the reference counting of + accessibles provided with the event. + + @note: All unmarked attributes of this class should be considered public + readable and writable as the class is acting as a record object. + + @ivar consume: Should this event be consumed and not allowed to pass on to + observers further down the dispatch chain in this process? + @type consume: boolean + @ivar type: The type of the AT-SPI event + @type type: L{EventType} + @ivar detail1: First AT-SPI event parameter + @type detail1: integer + @ivar detail2: Second AT-SPI event parameter + @type detail2: integer + @ivar any_data: Extra AT-SPI data payload + @type any_data: object + @ivar host_application: Application owning the event source + @type host_application: Accessibility.Application + @ivar source_name: Name of the event source at the time of event dispatch + @type source_name: string + @ivar source_role: Role of the event source at the time of event dispatch + @type source_role: Accessibility.Role + @ivar source: Source of the event + @type source: Accessibility.Accessible + ''' + def __init__(self, event): + ''' + Extracts information from the provided event. If the event is a "normal" + event, pulls the detail1, detail2, any_data, and source values out of the + given object and stores it in this object. If the event is a device event, + key ID is stored in detail1, scan code is stored in detail2, key name, + key modifiers (e.g. ALT, CTRL, etc.), is text flag, and timestamp are + stored as a 4-tuple in any_data, and source is None (since key events are + global). + + @param event: Event from an AT-SPI callback + @type event: Accessibility.Event or Accessibility.DeviceEvent + ''' + # always start out assuming no consume + self.consume = False + self.type = EventType(event.type) + self.detail1 = event.detail1 + self.detail2 = event.detail2 + # store the event source and increase the reference count since event + # sources are borrowed references; the AccessibleMixin automatically + # decrements it later + self.source = event.source + self.source.ref() + + # process any_data in a at-spi version independent manner + details = event.any_data.value() + try: + # see if we have a "new" any_data object which is an EventDetails struct + self.any_data = details.any_data.value() + except Exception: + # any kind of error means we have an "old" any_data object and None of + # the extra data so set them to None + self.any_data = details + self.host_application = None + self.source_name = None + self.source_role = None + else: + # the rest of the data should be here, so retrieve it + self.host_application = details.host_application + self.source_name = details.source_name + self.source_role = details.source_role + + def __str__(self): + ''' + Builds a human readable representation of the event including event type, + parameters, and source info. + + @return: Event description + @rtype: string + ''' + return '%s(%s, %s, %s)\n\tsource: %s\n\thost_application: %s' % \ + (self.type, self.detail1, self.detail2, self.any_data, + self.source, self.host_application) + +class EventType(object): + ''' + Wraps the AT-SPI event type string so its components can be accessed + individually as klass (can't use the keyword class), major, minor, and detail + (klass:major:minor:detail). + + @note: All attributes of an instance of this class should be considered + public readable as it is acting a a struct. + @ivar klass: Most general event type identifier (object, window, mouse, etc.) + @type klass: string + @ivar major: Second level event type description + @type major: string + @ivar minor: Third level event type description + @type minor: string + @ivar detail: Lowest level event type description + @type detail: string + @ivar name: Full, unparsed event name as received from AT-SPI + @type name: string + @cvar format: Names of the event string components + @type format: 4-tuple of string + ''' + format = ('klass', 'major', 'minor', 'detail') + + def __init__(self, name): + ''' + Parses the full AT-SPI event name into its components + (klass:major:minor:detail). If the provided event name is an integer + instead of a string, then the event is really a device event. + + @param name: Full AT-SPI event name + @type name: string + @raise AttributeError: When the given event name is not a valid string + ''' + # get rid of any leading and trailing ':' separators + self.name = name.strip(':') + self.klass = None + self.major = None + self.minor = None + self.detail = None + + # split type according to delimiters + split = self.name.split(':') + # loop over all the components + for i in xrange(len(split)): + # store values of attributes in this object + setattr(self, self.format[i], split[i]) + + def __str__(self): + ''' + @return: Full event name as human readable representation of this event + type + @rtype: string + ''' + return self.name diff --git a/pyatspi/registry.py b/pyatspi/registry.py new file mode 100644 index 0000000..9f08c4b --- /dev/null +++ b/pyatspi/registry.py @@ -0,0 +1,734 @@ +''' +Registry that hides some of the details of registering for AT-SPI events and +starting and stopping the main program loop. + +@todo: PP: when to destroy device listener? + +@author: Peter Parente +@organization: IBM Corporation +@copyright: Copyright (c) 2005, 2007 IBM Corporation +@license: LGPL + +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. + +Portions of this code originally licensed and copyright (c) 2005, 2007 +IBM Corporation under the BSD license, available at +U{http://www.opensource.org/licenses/bsd-license.php} +''' +import signal +import time +import weakref +import Queue +import traceback +import ORBit +import bonobo +import gobject +import Accessibility +import Accessibility__POA +import utils +import constants +import event + +class _Observer(object): + ''' + Parent class for all event observers. Dispatches all received events to the + L{Registry} that created this L{Observer}. Provides basic reference counting + functionality needed by L{Registry} to determine when an L{Observer} can be + released for garbage collection. + + The reference counting provided by this class is independent of the reference + counting used by CORBA. Keeping the counts separate makes it easier for the + L{Registry} to detect when an L{Observer} can be freed in the + L{Registry._unregisterObserver} method. + + @ivar registry: Reference to the L{Registry} that created this L{Observer} + @type registry: weakref.proxy to L{Registry} + @ivar ref_count: Reference count on this L{Observer} + @type ref_count: integer + ''' + def __init__(self, registry): + ''' + Stores a reference to the creating L{Registry}. Intializes the reference + count on this object to zero. + + @param registry: The L{Registry} that created this observer + @type registry: weakref.proxy to L{Registry} + ''' + self.registry = weakref.proxy(registry) + self.ref_count = 0 + + def clientRef(self): + ''' + Increments the Python reference count on this L{_Observer} by one. This + method is called when a new client is registered in L{Registry} to receive + notification of an event type monitored by this L{_Observer}. + ''' + self.ref_count += 1 + + def clientUnref(self): + ''' + Decrements the L{pyLinAcc} reference count on this L{_Observer} by one. + This method is called when a client is unregistered in L{Registry} to stop + receiving notifications of an event type monitored by this L{_Observer}. + ''' + self.ref_count -= 1 + + def getClientRefCount(self): + ''' + @return: Current Python reference count on this L{_Observer} + @rtype: integer + ''' + return self.ref_count + + def ref(self): + '''Required by CORBA. Does nothing.''' + pass + + def unref(self): + '''Required by CORBA. Does nothing.''' + pass + +class _DeviceObserver(_Observer, Accessibility__POA.DeviceEventListener): + ''' + Observes keyboard press and release events. + + @ivar registry: The L{Registry} that created this observer + @type registry: L{Registry} + @ivar key_set: Set of keys to monitor + @type key_set: list of integer + @ivar mask: Watch for key events while these modifiers are held + @type mask: integer + @ivar kind: Kind of events to monitor + @type kind: integer + @ivar mode: Keyboard event mode + @type mode: Accessibility.EventListenerMode + ''' + def __init__(self, registry, synchronous, preemptive, global_): + ''' + Creates a mode object that defines when key events will be received from + the system. Stores all other information for later registration. + + @param registry: The L{Registry} that created this observer + @type registry: L{Registry} + @param synchronous: Handle the key event synchronously? + @type synchronous: boolean + @param preemptive: Allow event to be consumed? + @type preemptive: boolean + @param global_: Watch for events on inaccessible applications too? + @type global_: boolean + ''' + _Observer.__init__(self, registry) + self.mode = Accessibility.EventListenerMode() + self.mode.preemptive = preemptive + self.mode.synchronous = synchronous + self.mode._global = global_ + + def register(self, dc, key_set, mask, kind): + ''' + Starts keyboard event monitoring. + + @param reg: Reference to the raw registry object + @type reg: Accessibility.Registry + @param key_set: Set of keys to monitor + @type key_set: list of integer + @param mask: Integer modifier mask or an iterable over multiple masks to + unapply all at once + @type mask: integer or iterable + @param kind: Kind of events to monitor + @type kind: integer + ''' + try: + # check if the mask is iterable + iter(mask) + except TypeError: + # register a single integer if not + dc.registerKeystrokeListener(self._this(), key_set, mask, kind, + self.mode) + else: + for m in mask: + dc.registerKeystrokeListener(self._this(), key_set, m, kind, self.mode) + + def unregister(self, dc, key_set, mask, kind): + ''' + Stops keyboard event monitoring. + + @param reg: Reference to the raw registry object + @type reg: Accessibility.Registry + @param key_set: Set of keys to monitor + @type key_set: list of integer + @param mask: Integer modifier mask or an iterable over multiple masks to + unapply all at once + @type mask: integer or iterable + @param kind: Kind of events to monitor + @type kind: integer + ''' + try: + # check if the mask is iterable + iter(mask) + except TypeError: + # unregister a single integer if not + dc.deregisterKeystrokeListener(self._this(), key_set, mask, kind, + self.mode) + else: + for m in mask: + dc.deregisterKeystrokeListener(self._this(), key_set, m, kind, + self.mode) + + def queryInterface(self, repo_id): + ''' + Reports that this class only implements the AT-SPI DeviceEventListener + interface. Required by AT-SPI. + + @param repo_id: Request for an interface + @type repo_id: string + @return: The underlying CORBA object for the device event listener + @rtype: Accessibility.EventListener + ''' + if repo_id == utils.getInterfaceIID(Accessibility.DeviceEventListener): + return self._this() + else: + return None + + def notifyEvent(self, ev): + ''' + Notifies the L{Registry} that an event has occurred. Wraps the raw event + object in our L{Event} class to support automatic ref and unref calls. An + observer can set the L{Event} consume flag to True to indicate this event + should not be allowed to pass to other AT-SPI observers or the underlying + application. + + @param ev: Keyboard event + @type ev: Accessibility.DeviceEvent + @return: Should the event be consumed (True) or allowed to pass on to other + AT-SPI observers (False)? + @rtype: boolean + ''' + # wrap the device event + ev = event.DeviceEvent(ev) + self.registry.handleDeviceEvent(ev, self) + return ev.consume + +class _EventObserver(_Observer, Accessibility__POA.EventListener): + ''' + Observes all non-keyboard AT-SPI events. Can be reused across event types. + ''' + def register(self, reg, name): + ''' + Starts monitoring for the given event. + + @param name: Name of the event to start monitoring + @type name: string + @param reg: Reference to the raw registry object + @type reg: Accessibility.Registry + ''' + reg.registerGlobalEventListener(self._this(), name) + + def unregister(self, reg, name): + ''' + Stops monitoring for the given event. + + @param name: Name of the event to stop monitoring + @type name: string + @param reg: Reference to the raw registry object + @type reg: Accessibility.Registry + ''' + reg.deregisterGlobalEventListener(self._this(), name) + + def queryInterface(self, repo_id): + ''' + Reports that this class only implements the AT-SPI DeviceEventListener + interface. Required by AT-SPI. + + @param repo_id: Request for an interface + @type repo_id: string + @return: The underlying CORBA object for the device event listener + @rtype: Accessibility.EventListener + ''' + if repo_id == utils.getInterfaceIID(Accessibility.EventListener): + return self._this() + else: + return None + + def notifyEvent(self, ev): + ''' + Notifies the L{Registry} that an event has occurred. Wraps the raw event + object in our L{Event} class to support automatic ref and unref calls. + Aborts on any exception indicating the event could not be wrapped. + + @param ev: AT-SPI event signal (anything but keyboard) + @type ev: Accessibility.Event + ''' + # wrap raw event so ref counts are correct before queueing + ev = event.Event(ev) + self.registry.handleEvent(ev) + +class Registry(object): + ''' + Wraps the Accessibility.Registry to provide more Pythonic registration for + events. + + This object should be treated as a singleton, but such treatment is not + enforced. You can construct another instance of this object and give it a + reference to the Accessibility.Registry singleton. Doing so is harmless and + has no point. + + @ivar async + @type async: boolean + @ivar reg: + @type reg: Accessibility.Registry + @ivar dev: + @type dev: Accessibility.DeviceEventController + @ivar queue: + @type queue: Queue.Queue + @ivar clients: + @type clients: dictionary + @ivar observers: + @type observers: dictionary + ''' + def __init__(self, reg): + ''' + Stores a reference to the AT-SPI registry. Gets and stores a reference + to the DeviceEventController. + + @param reg: Reference to the AT-SPI registry daemon + @type reg: Accessibility.Registry + ''' + self.async = None + self.reg = reg + self.dev = self.reg.getDeviceEventController() + self.queue = Queue.Queue() + self.clients = {} + self.observers = {} + + def __call__(self): + ''' + @return: This instance of the registry + @rtype: L{Registry} + ''' + return self + + def start(self, async=False, gil=True): + ''' + Enter the main loop to start receiving and dispatching events. + + @param async: Should event dispatch be asynchronous (decoupled) from + event receiving from the AT-SPI registry? + @type async: boolean + @param gil: Add an idle callback which releases the Python GIL for a few + milliseconds to allow other threads to run? Necessary if other threads + will be used in this process. + @type gil: boolean + ''' + self.async = async + + # register a signal handler for gracefully killing the loop + signal.signal(signal.SIGINT, self.stop) + signal.signal(signal.SIGTERM, self.stop) + + if gil: + def releaseGIL(): + time.sleep(1e-5) + return True + i = gobject.idle_add(releaseGIL) + + # enter the main loop + bonobo.main() + + if gil: + gobject.source_remove(i) + + def stop(self, *args): + '''Quits the main loop.''' + try: + bonobo.main_quit() + except RuntimeError: + # ignore errors when quitting (probably already quitting) + pass + + def getDesktopCount(self): + ''' + Gets the number of available desktops. + + @return: Number of desktops + @rtype: integer + @raise LookupError: When the count cannot be retrieved + ''' + try: + return self.reg.getDesktopCount() + except Exception: + raise LookupError + + def getDesktop(self, i): + ''' + Gets a reference to the i-th desktop. + + @param i: Which desktop to get + @type i: integer + @return: Desktop reference + @rtype: Accessibility.Desktop + @raise LookupError: When the i-th desktop cannot be retrieved + ''' + try: + return self.reg.getDesktop(i) + except Exception, e: + raise LookupError(e) + + def registerEventListener(self, client, *names): + ''' + Registers a new client callback for the given event names. Supports + registration for all subevents if only partial event name is specified. + Do not include a trailing colon. + + For example, 'object' will register for all object events, + 'object:property-change' will register for all property change events, + and 'object:property-change:accessible-parent' will register only for the + parent property change event. + + Registered clients will not be automatically removed when the client dies. + To ensure the client is properly garbage collected, call + L{Manager.removeClient}. + + @param client: Callable to be invoked when the event occurs + @type client: callable + @param names: List of full or partial event names + @type names: list of string + ''' + for name in names: + # store the callback for each specific event name + self._registerClients(client, name) + + def deregisterEventListener(self, client, *names): + ''' + Unregisters an existing client callback for the given event names. Supports + unregistration for all subevents if only partial event name is specified. + Do not include a trailing colon. + + This method must be called to ensure a client registered by + L{Manager.addClient} is properly garbage collected. + + @param client: Client callback to remove + @type client: callable + @param names: List of full or partial event names + @type names: list of string + @return: Were event names specified for which the given client was not + registered? + @rtype: boolean + ''' + missed = False + for name in names: + # remove the callback for each specific event name + missed |= self._unregisterClients(client, name) + return missed + + def registerKeystrokeListener(self, client, key_set=[], mask=0, + kind=(constants.KEY_PRESSED_EVENT, + constants.KEY_RELEASED_EVENT), + synchronous=True, preemptive=True, + global_=False): + ''' + Registers a listener for key stroke events. + + @param client: Callable to be invoked when the event occurs + @type client: callable + @param key_set: Set of hardware key codes to stop monitoring. Leave empty + to indicate all keys. + @type key_set: list of integer + @param mask: When the mask is None, the codes in the key_set will be + monitored only when no modifier is held. When the mask is an + integer, keys in the key_set will be monitored only when the modifiers in + the mask are held. When the mask is an iterable over more than one + integer, keys in the key_set will be monitored when any of the modifier + combinations in the set are held. + @type mask: integer + @param kind: Kind of events to watch, KEY_PRESSED_EVENT or + KEY_RELEASED_EVENT. + @type kind: list + @param synchronous: Should the callback notification be synchronous, giving + the client the chance to consume the event? + @type synchronous: boolean + @param preemptive: Should the callback be allowed to preempt / consume the + event? + @type preemptive: boolean + @param global_: Should callback occur even if an application not supporting + AT-SPI is in the foreground? (requires xevie) + @type global_: boolean + ''' + try: + # see if we already have an observer for this client + ob = self.clients[client] + except KeyError: + # create a new device observer for this client + ob = _DeviceObserver(self, synchronous, preemptive, global_) + # store the observer to client mapping, and the inverse + self.clients[ob] = client + self.clients[client] = ob + # register for new keystrokes on the observer + ob.register(self.dev, key_set, mask, kind) + + def deregisterKeystrokeListener(self, client, key_set=[], mask=0, + kind=(constants.KEY_PRESSED_EVENT, + constants.KEY_RELEASED_EVENT)): + ''' + Deregisters a listener for key stroke events. + + @param client: Callable to be invoked when the event occurs + @type client: callable + @param key_set: Set of hardware key codes to stop monitoring. Leave empty + to indicate all keys. + @type key_set: list of integer + @param mask: When the mask is None, the codes in the key_set will be + monitored only when no modifier is held. When the mask is an + integer, keys in the key_set will be monitored only when the modifiers in + the mask are held. When the mask is an iterable over more than one + integer, keys in the key_set will be monitored when any of the modifier + combinations in the set are held. + @type mask: integer + @param kind: Kind of events to stop watching, KEY_PRESSED_EVENT or + KEY_RELEASED_EVENT. + @type kind: list + @raise KeyError: When the client isn't already registered for events + ''' + # see if we already have an observer for this client + ob = self.clients[client] + # register for new keystrokes on the observer + ob.unregister(self.dev, key_set, mask, kind) + + def generateKeyboardEvent(self, keycode, keysym, kind): + ''' + Generates a keyboard event. One of the keycode or the keysym parameters + should be specified and the other should be None. The kind parameter is + required and should be one of the KEY_PRESS, KEY_RELEASE, KEY_PRESSRELEASE, + KEY_SYM, or KEY_STRING. + + @param keycode: Hardware keycode or None + @type keycode: integer + @param keysym: Symbolic key string or None + @type keysym: string + @param kind: Kind of event to synthesize + @type kind: integer + ''' + if keysym is None: + self.dev.generateKeyboardEvent(keycode, '', kind) + else: + self.dev.generateKeyboardEvent(None, keysym, kind) + + def generateMouseEvent(self, x, y, name): + ''' + Generates a mouse event at the given absolute x and y coordinate. The kind + of event generated is specified by the name. For example, MOUSE_B1P + (button 1 press), MOUSE_REL (relative motion), MOUSE_B3D (butten 3 + double-click). + + @param x: Horizontal coordinate, usually left-hand oriented + @type x: integer + @param y: Vertical coordinate, usually left-hand oriented + @type y: integer + @param name: Name of the event to generate + @type name: string + ''' + self.dev.generateMouseEvent(x, y, name) + + def handleDeviceEvent(self, event, ob): + ''' + Dispatches L{event.DeviceEvent}s to registered clients. Clients are called + in the order they were registered for the given AT-SPI event. If any + client sets the L{event.DeviceEvent.consume} flag to True, callbacks cease + for the event for clients of this registry instance. Clients of other + registry instances and clients in other processes may be affected + depending on the values of synchronous and preemptive used when invoking + L{registerKeystrokeListener}. + + @note: Asynchronous dispatch of device events is not supported. + + @param event: AT-SPI device event + @type event: L{event.DeviceEvent} + @param ob: Observer that received the event + @type ob: L{_DeviceObserver} + ''' + try: + # try to get the client registered for this event type + client = self.clients[ob] + except KeyError: + # client may have unregistered recently, ignore event + return + # make the call to the client + try: + client(event) + except Exception: + # print the exception, but don't let it stop notification + traceback.print_exc() + + def handleEvent(self, event): + ''' + Handles an AT-SPI event by either queuing it for later dispatch when the + L{async} flag is set, or dispatching it immediately. + + @param event: AT-SPI event + @type event: L{event.Event} + ''' + if self.async: + # queue for now + self.queue.put_nowait(event) + else: + # dispatch immediately + self._dispatchEvent(event) + + def _dispatchEvent(self, event): + ''' + Dispatches L{event.Event}s to registered clients. Clients are called in + the order they were registered for the given AT-SPI event. If any client + sets the L{Event} consume flag to True, callbacks cease for the event for + clients of this registry instance. Clients of other registry instances and + clients in other processes are unaffected. + + @param event: AT-SPI event + @type event: L{event.Event} + ''' + try: + # try to get the client registered for this event type + clients = self.clients[event.type.name] + except KeyError: + # client may have unregistered recently, ignore event + return + # make the call to each client + for client in clients: + try: + client(event) + except Exception: + # print the exception, but don't let it stop notification + traceback.print_exc() + if event.consume: + # don't allow further processing if the consume flag is set + break + + def _registerClients(self, client, name): + ''' + Internal method that recursively associates a client with AT-SPI event + names. Allows a client to incompletely specify an event name in order to + register for subevents without specifying their full names manually. + + @param client: Client callback to receive event notifications + @type client: callable + @param name: Partial or full event name + @type name: string + ''' + try: + # look for an event name in our event tree dictionary + events = constants.EVENT_TREE[name] + except KeyError: + # if the event name doesn't exist, it's a leaf event meaning there are + # no subtypes for that event + # add this client to the list of clients already in the dictionary + # using the event name as the key; if there are no clients yet for this + # event, insert an empty list into the dictionary before appending + # the client + et = event.EventType(name) + clients = self.clients.setdefault(et.name, []) + try: + # if this succeeds, this client is already registered for the given + # event type, so ignore the request + clients.index(client) + except ValueError: + # else register the client + clients.append(client) + self._registerObserver(name) + else: + # if the event name does exist in the tree, there are subevents for + # this event; loop through them calling this method again to get to + # the leaf events + for e in events: + self._registerClients(client, e) + + def _unregisterClients(self, client, name): + ''' + Internal method that recursively unassociates a client with AT-SPI event + names. Allows a client to incompletely specify an event name in order to + unregister for subevents without specifying their full names manually. + + @param client: Client callback to receive event notifications + @type client: callable + @param name: Partial or full event name + @type name: string + ''' + missed = False + try: + # look for an event name in our event tree dictionary + events = constants.EVENT_TREE[name] + except KeyError: + try: + # if the event name doesn't exist, it's a leaf event meaning there are + # no subtypes for that event + # get the list of registered clients and try to remove the one provided + et = event.EventType(name) + clients = self.clients[et.name] + clients.remove(client) + self._unregisterObserver(name) + except (ValueError, KeyError): + # ignore any exceptions indicating the client is not registered + missed = True + return missed + # if the event name does exist in the tree, there are subevents for this + # event; loop through them calling this method again to get to the leaf + # events + for e in events: + missed |= self._unregisterClients(client, e) + return missed + + def _registerObserver(self, name): + ''' + Creates a new L{_Observer} to watch for events of the given type or + returns the existing observer if one is already registered. One + L{_Observer} is created for each leaf in the L{constants.EVENT_TREE} or + any event name not found in the tree. + + @param name: Raw name of the event to observe + @type name: string + @return: L{_Observer} object that is monitoring the event + @rtype: L{_Observer} + ''' + et = event.EventType(name) + try: + # see if an observer already exists for this event + ob = self.observers[et.name] + except KeyError: + # build a new observer if one does not exist + ob = _EventObserver(self) + # we have to register for the raw name because it may be different from + # the parsed name determined by EventType (e.g. trailing ':' might be + # missing) + ob.register(self.reg, name) + self.observers[et.name] = ob + # increase our client ref count so we know someone new is watching for the + # event + ob.clientRef() + return ob + + def _unregisterObserver(self, name): + ''' + Destroys an existing L{Observer} for the given event type only if no clients + are registered for the events it is monitoring. + + @param name: Name of the event to observe + @type name: string + @raise KeyError: When an observer for the given event is not regist + ''' + et = event.EventType(name) + # see if an observer already exists for this event + ob = self.observers[et.name] + ob.clientUnref() + if ob.getClientRefCount() == 0: + ob.unregister(self.registry, name) + del self.observers[et.name] diff --git a/pyatspi/utils.py b/pyatspi/utils.py new file mode 100644 index 0000000..3f0e850 --- /dev/null +++ b/pyatspi/utils.py @@ -0,0 +1,285 @@ +''' +Utility functions for AT-SPI for querying interfaces, searching the hierarchy, +converting constants to strings, and so forth. + +@author: Peter Parente +@organization: IBM Corporation +@copyright: Copyright (c) 2005, 2007 IBM Corporation +@license: LGPL + +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. + +Portions of this code originally licensed and copyright (c) 2005, 2007 +IBM Corporation under the BSD license, available at +U{http://www.opensource.org/licenses/bsd-license.php} +''' +def getInterfaceIID(cls): + ''' + Gets the ID of an interface class in string format for use in queryInterface. + + @param cls: Class representing an AT-SPI interface + @type cls: class + @return: IID for the interface + @rtype: string + @raise AttributeError: When the parameter does not provide typecode info + ''' + return cls.__typecode__.repo_id + +def getInterfaceName(cls): + ''' + Gets the human readable name of an interface class in string format. + + @param cls: Class representing an AT-SPI interface + @type cls: class + @return: Name of the interface + @rtype: string + @raise AttributeError: When the parameter does not provide typecode info + ''' + return cls.__typecode__.name + +# we're importing here to avoid cyclic importants; constants relies on the +# two functions above +import constants + +def stringToConst(prefix, suffix): + ''' + Maps a string name to an AT-SPI constant. The rules for the mapping are as + follows: + - The prefix is captalized and has an _ appended to it. + - All spaces in the suffix are mapped to the _ character. + - All alpha characters in the suffix are mapped to their uppercase. + + The resulting name is used with getattr to look up a constant with that name + in the L{pyLinAcc.Constants} module. If such a constant does not exist, the + string suffix is returned instead. + + This method allows strings to be used to refer to roles, relations, etc. + without direct access to the constants. It also supports the future expansion + of roles, relations, etc. by allowing arbitrary strings which may or may not + map to the current standard set of roles, relations, etc., but may still match + some non-standard role, relation, etc. being reported by an application. + + @param prefix: Prefix of the constant name such as role, relation, state, + text, modifier, key + @type prefix: string + @param suffix: Name of the role, relation, etc. to use to lookup the constant + @type suffix: string + @return: The matching constant value + @rtype: object + ''' + name = prefix.upper()+'_'+suffix.upper().replace(' ', '_') + return getattr(constants, name, suffix) + +def stateToString(value): + ''' + Converts a state value to a string based on the name of the state constant in + the L{Constants} module that has the given value. + + @param value: An AT-SPI state + @type value: Accessibility.StateType + @return: Human readable, untranslated name of the state + @rtype: string + ''' + return constants.STATE_VALUE_TO_NAME.get(value) + +def relationToString(value): + ''' + Converts a relation value to a string based on the name of the state constant + in the L{Constants} module that has the given value. + + @param value: An AT-SPI relation + @type value: Accessibility.RelationType + @return: Human readable, untranslated name of the relation + @rtype: string + ''' + return constants.RELATION_VALUE_TO_NAME.get(value) + +def allModifiers(): + ''' + Generates all possible keyboard modifiers for use with + L{Registry.Registry.registerKeystrokeListener}. + ''' + mask = 0 + while mask <= (1 << constants.MODIFIER_NUMLOCK): + yield mask + mask += 1 + +def findDescendant(acc, pred, breadth_first=False): + ''' + Searches for a descendant node satisfying the given predicate starting at + this node. The search is performed in depth-first order by default or + in breadth first order if breadth_first is True. For example, + + my_win = findDescendant(lambda x: x.name == 'My Window') + + will search all descendants of node until one is located with the name 'My + Window' or all nodes are exausted. Calls L{_findDescendantDepth} or + L{_findDescendantBreadth} to start the recursive search. + + @param acc: Root accessible of the search + @type acc: Accessibility.Accessible + @param pred: Search predicate returning True if accessible matches the + search criteria or False otherwise + @type pred: callable + @param breadth_first: Search breadth first (True) or depth first (False)? + @type breadth_first: boolean + @return: Accessible matching the criteria or None if not found + @rtype: Accessibility.Accessible or None + ''' + if breadth_first: + return _findDescendantBreadth(acc, pred) + + for child in acc: + try: + ret = _findDescendantDepth(acc, pred) + except Exception: + ret = None + if ret is not None: return ret + +def _findDescendantBreadth(acc, pred): + ''' + Internal function for locating one descendant. Called by + L{AccessibleMixin.findDescendant} to start the search. + + @param acc: Root accessible of the search + @type acc: Accessibility.Accessible + @param pred: Search predicate returning True if accessible matches the + search criteria or False otherwise + @type pred: callable + @return: Matching node or None to keep searching + @rtype: Accessibility.Accessible or None + ''' + for child in acc: + try: + if pred(child): return child + except Exception: + pass + for child in acc: + try: + ret = _findDescedantBreadth(child, pred) + except Exception: + ret = None + if ret is not None: return ret + +def _findDescendantDepth(acc, pred): + ''' + Internal function for locating one descendant. Called by + L{AccessibleMixin.findDescendant} to start the search. + + @param acc: Root accessible of the search + @type acc: Accessibility.Accessible + @param pred: Search predicate returning True if accessible matches the + search criteria or False otherwise + @type pred: callable + @return: Matching node or None to keep searching + @rtype: Accessibility.Accessible or None + ''' + try: + if pred(acc): return acc + except Exception: + pass + for child in acc: + try: + ret = _findDescendantDepth(child, pred) + except Exception: + ret = None + if ret is not None: return ret + +def findAllDescendants(acc, pred): + ''' + Searches for all descendant nodes satisfying the given predicate starting at + this node. Does an in-order traversal. For example, + + pred = lambda x: x.getRole() == pyatspi.ROLE_PUSH_BUTTON + buttons = pyatspi.findAllDescendants(node, pred) + + will locate all push button descendants of node. + + @param acc: Root accessible of the search + @type acc: Accessibility.Accessible + @param pred: Search predicate returning True if accessible matches the + search criteria or False otherwise + @type pred: callable + @return: All nodes matching the search criteria + @rtype: list + ''' + matches = [] + _findAllDescendants(acc, pred, matches) + return matches + +def _findAllDescendants(acc, pred, matches): + ''' + Internal method for collecting all descendants. Reuses the same matches + list so a new one does not need to be built on each recursive step. + ''' + for child in acc: + try: + if pred(child): matches.append(child) + except Exception: + pass + findAllDescendants(child, pred, matches) + +def findAncestor(acc, pred): + ''' + Searches for an ancestor satisfying the given predicate. Note that the + AT-SPI hierarchy is not always doubly linked. Node A may consider node B its + child, but B is not guaranteed to have node A as its parent (i.e. its parent + may be set to None). This means some searches may never make it all the way + up the hierarchy to the desktop level. + + @param acc: Starting accessible object + @type acc: Accessibility.Accessible + @param pred: Search predicate returning True if accessible matches the + search criteria or False otherwise + @type pred: callable + @return: Node matching the criteria or None if not found + @rtype: Accessibility.Accessible + ''' + if acc is None: + # guard against bad start condition + return None + while 1: + if acc.parent is None: + # stop if there is no parent and we haven't returned yet + return None + try: + if pred(acc.parent): return acc.parent + except Exception: + pass + # move to the parent + acc = acc.parent + +def getPath(acc): + ''' + Gets the path from the application ancestor to the given accessible in + terms of its child index at each level. + + @param acc: Target accessible + @type acc: Accessibility.Accessible + @return: Path to the target + @rtype: list of integer + @raise LookupError: When the application accessible cannot be reached + ''' + path = [] + while 1: + if acc.parent is None: + path.reverse() + return path + try: + path.append(acc.getIndexInParent()) + except Exception: + raise LookupError + acc = acc.parent -- 2.7.4