2007-04-18 Li Yuan <li.yuan@sun.com>
authorliyuan <liyuan@e2bd861d-eb25-0410-b326-f6ed22b6b98c>
Wed, 18 Apr 2007 08:42:33 +0000 (08:42 +0000)
committerliyuan <liyuan@e2bd861d-eb25-0410-b326-f6ed22b6b98c>
Wed, 18 Apr 2007 08:42:33 +0000 (08:42 +0000)
        * 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
pyatspi/ChangeLog [new file with mode: 0644]
pyatspi/__init__.py [new file with mode: 0644]
pyatspi/accessible.py [new file with mode: 0644]
pyatspi/constants.py [new file with mode: 0644]
pyatspi/event.py [new file with mode: 0644]
pyatspi/registry.py [new file with mode: 0644]
pyatspi/utils.py [new file with mode: 0644]

index e966804..26789d6 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,14 @@
+2007-04-18  Li Yuan <li.yuan@sun.com>
+
+       * 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 <li.yuan@sun.com>
 
        * atk-bridge/bridge.c: (spi_atk_bridge_get_registry),
diff --git a/pyatspi/ChangeLog b/pyatspi/ChangeLog
new file mode 100644 (file)
index 0000000..51dddb9
--- /dev/null
@@ -0,0 +1,10 @@
+2007-04-18  Li Yuan <li.yuan@sun.com>
+
+       * 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 (file)
index 0000000..727e652
--- /dev/null
@@ -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 (file)
index 0000000..ff1391b
--- /dev/null
@@ -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 (file)
index 0000000..6643b06
--- /dev/null
@@ -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 (file)
index 0000000..af6aa28
--- /dev/null
@@ -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 (file)
index 0000000..9f08c4b
--- /dev/null
@@ -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 (file)
index 0000000..3f0e850
--- /dev/null
@@ -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