1import gi
2
3from gi.repository import Gtk as gtk
4from gi.repository import Gdk as gdk
5from gi.repository import Wnck as wnck
6
7from accerciser.plugin import Plugin
8from accerciser.i18n import N_, _
9
10import pyatspi
11
12class QuickSelect(Plugin):
13  '''
14  Plugin class for quick select.
15  '''
16  plugin_name = N_('Quick Select')
17  plugin_name_localized = _(plugin_name)
18  plugin_description = \
19      N_('Plugin with various methods of selecting accessibles quickly.')
20
21  def init(self):
22    '''
23    Initialize plugin.
24    '''
25    self.global_hotkeys = [(N_('Inspect last focused accessible'),
26                            self._inspectLastFocused,
27                            gdk.KEY_a, gdk.ModifierType.CONTROL_MASK | \
28                                       gdk.ModifierType.MOD1_MASK),
29                           (N_('Inspect accessible under mouse'),
30                            self._inspectUnderMouse,
31                            gdk.KEY_question, gdk.ModifierType.CONTROL_MASK | \
32                                              gdk.ModifierType.MOD1_MASK)]
33
34    pyatspi.Registry.registerEventListener(self._accEventFocusChanged,
35                               'object:state-changed')
36
37    pyatspi.Registry.registerEventListener(self._accEventSelectionChanged,
38                               'object:selection-changed')
39
40    self.last_focused = None
41    self.last_selected = None
42
43  def _accEventFocusChanged(self, event):
44    '''
45    Hold a reference for the last focused accessible. This is used when a certain
46    global hotkey is pressed to select this accessible.
47
48    @param event: The event that is being handled.
49    @type event: L{pyatspi.event.Event}
50    '''
51    if event.type != "object:state-changed:focused" and \
52       event.type != "object:state-changed:selected":
53      return
54
55    if event.detail1 != 1:
56      return
57
58    if not self.isMyApp(event.source):
59      self.last_focused = event.source
60
61  def _accEventSelectionChanged(self, event):
62    '''
63    Hold a reference for the last parent of a selected accessible.
64    This will be useful if we want to find an accessible at certain coords.
65
66    @param event: The event that is being handled.
67    @type event: L{pyatspi.event.Event}
68    '''
69    if not self.isMyApp(event.source):
70      self.last_selected = event.source
71
72  def _inspectLastFocused(self):
73    '''
74    Inspect the last focused widget's accessible.
75    '''
76    if self.last_focused:
77      self.node.update(self.last_focused)
78
79  def _inspectUnderMouse(self):
80    '''
81    Inspect accessible of widget under mouse.
82    '''
83    display = gdk.Display.get_default()
84    screen, x, y, flags =  display.get_pointer()
85    del screen # A workaround http://bugzilla.gnome.org/show_bug.cgi?id=593732
86
87    # First check if the currently selected accessible has the pointer over it.
88    # This is an optimization: Instead of searching for
89    # STATE_SELECTED and ROLE_MENU and LAYER_POPUP in the entire tree.
90    item = self._getPopupItem(x, y)
91    if item:
92      self.node.update(item)
93      return
94
95    # Inspect accessible under mouse
96    desktop = pyatspi.Registry.getDesktop(0)
97    wnck_screen = wnck.Screen.get_default()
98    window_order = [w.get_name() for w in wnck_screen.get_windows_stacked()]
99    top_window = (None, -1)
100    for app in desktop:
101      if not app or self.isMyApp(app):
102        continue
103      for frame in app:
104        if not frame:
105          continue
106        acc = self._getComponentAtCoords(frame, x, y)
107        if acc:
108          try:
109            z_order = window_order.index(frame.name)
110          except ValueError:
111            # It's possibly a popup menu, so it would not be in our frame name
112            # list. And if it is, it is probably the top-most component.
113            try:
114              if acc.queryComponent().getLayer() == pyatspi.LAYER_POPUP:
115                self.node.update(acc)
116                return
117            except:
118              pass
119          else:
120            if z_order > top_window[1]:
121              top_window = (acc, z_order)
122
123    if top_window[0]:
124      self.node.update(top_window[0])
125
126  def _getPopupItem(self, x, y):
127    suspect_children = []
128    # First check if the currently selected accessible has the pointer over it.
129    # This is an optimization: Instead of searching for
130    # STATE_SELECTED and ROLE_MENU and LAYER_POPUP in the entire tree.
131    if self.last_selected and \
132          self.last_selected.getRole() == pyatspi.ROLE_MENU and \
133          self.last_selected.getState().contains(pyatspi.STATE_SELECTED):
134      try:
135        si = self.last_selected.querySelection()
136      except NotImplementedError:
137        return None
138
139      if si.nSelectedChildren > 0:
140        suspect_children = [si.getSelectedChild(0)]
141      else:
142        suspect_children = self.last_selected
143
144      for child in suspect_children:
145        try:
146          ci = child.queryComponent()
147        except NotImplementedError:
148          continue
149
150        if ci.contains(x, y, pyatspi.DESKTOP_COORDS) and \
151              ci.getLayer() == pyatspi.LAYER_POPUP:
152          return child
153
154      return None
155
156  def _getComponentAtCoords(self, parent, x, y):
157    '''
158    Gets any child accessible that resides under given desktop coordinates.
159
160    @param parent: Top-level accessible.
161    @type parent: L{Accessibility.Accessible}
162    @param x: X coordinate.
163    @type x: integer
164    @param y: Y coordinate.
165    @type y: integer
166
167    @return: Child accessible at given coordinates, or None.
168    @rtype: L{Accessibility.Accessible}
169    '''
170    container = parent
171    while True:
172      container_role = container.getRole()
173      if container_role == pyatspi.ROLE_PAGE_TAB_LIST:
174        try:
175          si = container.querySelection()
176          container = si.getSelectedChild(0)[0]
177        except NotImplementedError:
178          pass
179      try:
180        ci = container.queryComponent()
181      except:
182        break
183      else:
184        inner_container = container
185      container =  ci.getAccessibleAtPoint(x, y, pyatspi.DESKTOP_COORDS)
186      if not container or container.queryComponent() == ci:
187        # The gecko bridge simply has getAccessibleAtPoint return itself
188        # if there are no further children
189        break
190    if inner_container == parent:
191      return None
192    else:
193      return inner_container
194
195