2008-05-28 Mark Doffman <mark.doffman@codethink.co.uk>
[platform/core/uifw/at-spi2-atk.git] / pyatspi / utils.py
1 '''
2 Utility functions for AT-SPI for querying interfaces, searching the hierarchy,
3 converting constants to strings, and so forth.
4
5 @author: Peter Parente
6 @organization: IBM Corporation
7 @copyright: Copyright (c) 2005, 2007 IBM Corporation
8 @license: LGPL
9
10 This library is free software; you can redistribute it and/or
11 modify it under the terms of the GNU Library General Public
12 License as published by the Free Software Foundation; either
13 version 2 of the License, or (at your option) any later version.
14
15 This library is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 Library General Public License for more details.
19
20 You should have received a copy of the GNU Library General Public
21 License along with this library; if not, write to the
22 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 Boston, MA 02111-1307, USA.
24
25 Portions of this code originally licensed and copyright (c) 2005, 2007
26 IBM Corporation under the BSD license, available at
27 U{http://www.opensource.org/licenses/bsd-license.php}
28 '''
29 import ORBit
30 import Accessibility__POA
31
32 def getInterfaceIID(obj):
33   '''
34   Gets the ID of an interface class or object in string format for use in
35   queryInterface.
36   
37   @param obj: Class representing an AT-SPI interface or instance
38   @type obj: object
39   @return: IID for the interface
40   @rtype: string
41   @raise AttributeError: When the parameter does not provide typecode info
42   '''
43   return obj.__typecode__.repo_id
44
45 def getInterfaceName(obj):
46   '''
47   Gets the human readable name of an interface class or object in string
48   format.
49   
50   @param obj: Class representing an AT-SPI interface or instance
51   @type obj: class
52   @return: Name of the interface
53   @rtype: string
54   @raise AttributeError: When the parameter does not provide typecode info
55   '''
56   return obj.__typecode__.name
57
58 # we're importing here to avoid cyclic importants; constants relies on the
59 # two functions above
60 import constants
61
62 def listInterfaces(obj):
63   '''
64   Gets a list of the names of all interfaces supported by this object. The
65   names are the short-hand interface names like "Accessible" and "Component",
66   not the full interface identifiers.
67
68   @param obj: Arbitrary object to query for all accessibility related
69   interfaces. Must provide a queryInterface method.
70   @type obj: object
71   @return: Set of supported interface names
72   @rtype: set
73   @raise AttributeError: If the object provide does not implement
74   queryInterface
75   '''
76   names = set()
77   for ic in constants.ALL_INTERFACES:
78     io = obj.queryInterface(getInterfaceIID(ic))
79     if io is None:
80       continue
81     names.add(getInterfaceName(ic))
82   return names
83
84 def stringToConst(prefix, suffix):
85   '''
86   Maps a string name to an AT-SPI constant. The rules for the mapping are as 
87   follows:
88     - The prefix is captalized and has an _ appended to it.
89     - All spaces in the suffix are mapped to the _ character. 
90     - All alpha characters in the suffix are mapped to their uppercase.
91     
92   The resulting name is used with getattr to look up a constant with that name
93   in the L{constants} module. If such a constant does not exist, the string
94   suffix is returned instead.
95
96   This method allows strings to be used to refer to roles, relations, etc.
97   without direct access to the constants. It also supports the future expansion
98   of roles, relations, etc. by allowing arbitrary strings which may or may not
99   map to the current standard set of roles, relations, etc., but may still
100   match some non-standard role, relation, etc. being reported by an
101   application.
102   
103   @param prefix: Prefix of the constant name such as role, relation, state, 
104     text, modifier, key
105   @type prefix: string
106   @param suffix: Name of the role, relation, etc. to use to lookup the constant
107   @type suffix: string
108   @return: The matching constant value
109   @rtype: object
110   '''
111   name = prefix.upper()+'_'+suffix.upper().replace(' ', '_')
112   return getattr(constants, name, suffix)
113
114 def stateToString(value):
115   '''
116   Converts a state value to a string based on the name of the state constant in
117   the L{constants} module that has the given value.
118   
119   @param value: An AT-SPI state
120   @type value: Accessibility.StateType
121   @return: Human readable, untranslated name of the state
122   @rtype: string
123   '''
124   return constants.STATE_VALUE_TO_NAME.get(value)
125
126 def relationToString(value):
127   '''
128   Converts a relation value to a string based on the name of the state constant
129   in the L{constants} module that has the given value.
130   
131   @param value: An AT-SPI relation
132   @type value: Accessibility.RelationType
133   @return: Human readable, untranslated name of the relation
134   @rtype: string
135   '''
136   return constants.RELATION_VALUE_TO_NAME.get(value)
137
138 def allModifiers():
139   '''
140   Generates all possible keyboard modifiers for use with 
141   L{registry.Registry.registerKeystrokeListener}.
142   '''
143   mask = 0
144   while mask <= (1 << constants.MODIFIER_NUMLOCK):
145     yield mask
146     mask += 1
147
148 def findDescendant(acc, pred, breadth_first=False):
149   '''
150   Searches for a descendant node satisfying the given predicate starting at 
151   this node. The search is performed in depth-first order by default or
152   in breadth first order if breadth_first is True. For example,
153   
154   my_win = findDescendant(lambda x: x.name == 'My Window')
155   
156   will search all descendants of x until one is located with the name 'My
157   Window' or all nodes are exausted. Calls L{_findDescendantDepth} or
158   L{_findDescendantBreadth} to start the recursive search.
159   
160   @param acc: Root accessible of the search
161   @type acc: Accessibility.Accessible
162   @param pred: Search predicate returning True if accessible matches the 
163       search criteria or False otherwise
164   @type pred: callable
165   @param breadth_first: Search breadth first (True) or depth first (False)?
166   @type breadth_first: boolean
167   @return: Accessible matching the criteria or None if not found
168   @rtype: Accessibility.Accessible or None
169   '''
170   if breadth_first:
171     return _findDescendantBreadth(acc, pred)
172
173   for child in acc:
174     try:
175       ret = _findDescendantDepth(acc, pred)
176     except Exception:
177       ret = None
178     if ret is not None: return ret
179
180 def _findDescendantBreadth(acc, pred):
181   '''    
182   Internal function for locating one descendant. Called by L{findDescendant} to
183   start the search.
184   
185   @param acc: Root accessible of the search
186   @type acc: Accessibility.Accessible
187   @param pred: Search predicate returning True if accessible matches the 
188       search criteria or False otherwise
189   @type pred: callable
190   @return: Matching node or None to keep searching
191   @rtype: Accessibility.Accessible or None
192   '''
193   for child in acc:
194     try:
195       if pred(child): return child
196     except Exception:
197       pass
198   for child in acc:
199     try:
200       ret = _findDescendantBreadth(child, pred)
201     except Exception:
202       ret = None
203     if ret is not None: return ret
204
205 def _findDescendantDepth(acc, pred):
206   '''
207   Internal function for locating one descendant. Called by L{findDescendant} to
208   start the search.
209
210   @param acc: Root accessible of the search
211   @type acc: Accessibility.Accessible
212   @param pred: Search predicate returning True if accessible matches the 
213     search criteria or False otherwise
214   @type pred: callable
215   @return: Matching node or None to keep searching
216   @rtype: Accessibility.Accessible or None
217   '''
218   try:
219     if pred(acc): return acc
220   except Exception:
221     pass
222   for child in acc:
223     try:
224       ret = _findDescendantDepth(child, pred)
225     except Exception:
226       ret = None
227     if ret is not None: return ret
228     
229 def findAllDescendants(acc, pred):
230   '''
231   Searches for all descendant nodes satisfying the given predicate starting at 
232   this node. Does an in-order traversal. For example,
233   
234   pred = lambda x: x.getRole() == pyatspi.ROLE_PUSH_BUTTON
235   buttons = pyatspi.findAllDescendants(node, pred)
236   
237   will locate all push button descendants of node.
238   
239   @param acc: Root accessible of the search
240   @type acc: Accessibility.Accessible
241   @param pred: Search predicate returning True if accessible matches the 
242       search criteria or False otherwise
243   @type pred: callable
244   @return: All nodes matching the search criteria
245   @rtype: list
246   '''
247   matches = []
248   _findAllDescendants(acc, pred, matches)
249   return matches
250
251 def _findAllDescendants(acc, pred, matches):
252   '''
253   Internal method for collecting all descendants. Reuses the same matches
254   list so a new one does not need to be built on each recursive step.
255   '''
256   for child in acc:
257     try:
258       if pred(child): matches.append(child)
259     except Exception:
260       pass
261     _findAllDescendants(child, pred, matches)
262   
263 def findAncestor(acc, pred):
264   '''
265   Searches for an ancestor satisfying the given predicate. Note that the
266   AT-SPI hierarchy is not always doubly linked. Node A may consider node B its
267   child, but B is not guaranteed to have node A as its parent (i.e. its parent
268   may be set to None). This means some searches may never make it all the way
269   up the hierarchy to the desktop level.
270   
271   @param acc: Starting accessible object
272   @type acc: Accessibility.Accessible
273   @param pred: Search predicate returning True if accessible matches the 
274     search criteria or False otherwise
275   @type pred: callable
276   @return: Node matching the criteria or None if not found
277   @rtype: Accessibility.Accessible
278   '''
279   if acc is None:
280     # guard against bad start condition
281     return None
282   while 1:
283     if acc.parent is None:
284       # stop if there is no parent and we haven't returned yet
285       return None
286     try:
287       if pred(acc.parent): return acc.parent
288     except Exception:
289       pass
290     # move to the parent
291     acc = acc.parent
292
293 def getPath(acc):
294   '''
295   Gets the path from the application ancestor to the given accessible in
296   terms of its child index at each level.
297   
298   @param acc: Target accessible
299   @type acc: Accessibility.Accessible
300   @return: Path to the target
301   @rtype: list of integer
302   @raise LookupError: When the application accessible cannot be reached
303   '''
304   path = []
305   while 1:
306     if acc.parent is None:
307       path.reverse()
308       return path
309     try:
310       path.append(acc.getIndexInParent())
311     except Exception:
312       raise LookupError
313     acc = acc.parent
314
315 class _StateSetImpl(Accessibility__POA.StateSet):
316   '''
317   Implementation of the StateSet interface. Clients should not use this class
318   directly, but rather the L{StateSet} proxy class.
319   
320   @param states: Set of states
321   @type states: set
322   '''
323   def __init__(self):
324     '''Initializes the state set.'''
325     self.states = set()
326     
327   def contains(self, state):
328     '''
329     Checks if this StateSet contains the given state.
330     
331     @param state: State to check
332     @type state: Accessibility.StateType
333     @return: True if the set contains the given state
334     @rtype: boolean
335     '''
336     return state in self.states
337   
338   def add(self, state):
339     '''
340     Adds a state to this set.
341     
342     @param state: State to add
343     @type state: Accessibility.StateType
344     '''
345     self.states.add(state)
346   
347   def remove(self, state):
348     '''
349     Removes a state from this set.
350     
351     @param state: State to remove
352     @type state: Accessibility.StateType
353     '''
354     self.states.remove(state)
355   
356   def equals(self, state_set):
357     '''
358     Checks if this StateSet contains exactly the same members as the given
359     StateSet.
360     
361     @param state_set: Another set
362     @type state_set: Accessibility.StateSet
363     @return: Are the sets equivalent in terms of their contents?
364     @rtype: boolean
365     '''
366     # don't check private members, object might be from another process
367     # or implementation
368     return set(state_set.getStates()) == self.states
369   
370   def compare(self, state_set):
371     '''
372     Computes the symmetric differences of this L{StateSet} and the given
373     L{StateSet}.
374
375     @note: This method is not currently implemented because of difficulties
376     with reference counting. This method needs to return a new
377     Accessibility.StateSet object, but the Python implementation for that
378     object needs to be kept alive. The problem is who will keep that
379     server implementation alive? As soon as it goes out of scope, it's
380     GC'ed. This object cannot keep it alive either as it may go out of
381     scope before the new object is ready to be finalized. With a global
382     cache of objects, we don't know when to invalidate.
383     
384     @param state_set: Another set
385     @type state_set: Accessibility.StateSet
386     @return: Elements in only one of the two sets
387     @rtype: Accessibility.StateSet
388     '''
389     raise ORBit.CORBA.NO_IMPLEMENT
390     
391     # don't check private members, object might be from another process
392     # or implementation
393     #states = set(state_set.getStates())
394     #diff = self.states.symmetric_difference(states)
395     #new_ss = _StateSetImpl()
396     #map(new_ss._this().add, diff)
397     #return new_ss._this()
398   
399   def isEmpty(self):
400     '''
401     Checks if this L{StateSet} is empty.
402     
403     @return: Is it empty?
404     @rtype: boolean
405     '''
406     return len(self.states) == 0
407
408   def getStates(self):
409     '''
410     Gets the sequence of all states in this set.
411     
412     @return: List of states
413     @rtype: list
414     '''
415     return list(self.states)
416
417 class StateSet(object):
418   '''
419   Python proxy for the L{_StateSetImpl} class. Use this to safely instantiate
420   new StateSet objects in Python.
421
422   @param impl: State set implementation
423   @type impl: L{_StateSetImpl}
424   '''
425   def __init__(self, *states):
426     '''
427     Initializes the state set with the given states.
428
429     @param states: States to add immediately
430     @type states: list
431     '''
432     self.impl = _StateSetImpl()
433     map(self.impl._this().add, states)
434     
435   def contains(self, state):
436     '''
437     Checks if this StateSet contains the given state.
438     
439     @param state: State to check
440     @type state: Accessibility.StateType
441     @return: True if the set contains the given state
442     @rtype: boolean
443     '''
444     return self.impl._this().contains(state)
445   
446   def add(self, *states):
447     '''
448     Adds states to this set.
449     
450     @param states: State(s) to add
451     @type states: Accessibility.StateType
452     '''
453     map(self.impl._this().add, states)
454     
455   def remove(self, state):
456     '''
457     Removes states from this set.
458     
459     @param states: State(s) to remove
460     @type states: Accessibility.StateType
461     '''
462     map(self.impl._this().remove, state)
463   
464   def equals(self, state_set):
465     '''
466     Checks if this StateSet contains exactly the same members as the given
467     StateSet.
468     
469     @param state_set: Another set
470     @type state_set: Accessibility.StateSet
471     @return: Are the sets equivalent in terms of their contents?
472     @rtype: boolean
473     '''
474     if isinstance(state_set, self.__class__):
475       # convenience if we're given a proxy
476       state_set = state_set.raw()
477     return self.impl._this().equals(state_set)
478   
479   def compare(self, state_set):
480     '''
481     Finds the symmetric difference between this state set andthe one provided,
482     and returns it as a new StateSet.
483
484     @note: This does not use L{_StateSetImpl.compare} which cannot be
485     implemented at this time
486     @param state_set: Set to compare against
487     @type state_set: Accessibility.StateSet
488     @return: Proxy for the new set
489     @rtype: L{StateSet}
490     '''
491     if isinstance(state_set, self.__class__):
492       # shortcut if it's another one of our proxies
493       state_set = state_set.raw()
494     a = set(self.impl._this().getStates())
495     b = set(state_set.getStates())
496     diff = a.symmetric_difference(b)
497     return StateSet(*diff)
498   
499   def isEmpty(self):
500     '''
501     Checks if this StateSet is empty.
502     
503     @return: Is it empty?
504     @rtype: boolean
505     '''
506     return self.impl._this().isEmpty()
507
508   def getStates(self):
509     '''
510     Gets the sequence of all states in this set.
511     
512     @return: List of states
513     @rtype: list
514     '''
515     return self.impl._this().getStates()
516
517   def raw(self):
518     '''
519     Gets the Accessibility.StateSet object proxied for use in a remote
520     call.
521
522     @return: State set
523     @rtype: Accessibility.StateSet
524     '''
525     return self.impl._this()