1'''
2Event monitor plugin.
3
4@author: Eitan Isaacson
5@organization: IBM Corporation
6@copyright: Copyright (c) 2007 IBM Corporation
7@license: BSD
8
9All rights reserved. This program and the accompanying materials are made
10available under the terms of the BSD which accompanies this distribution, and
11is available at U{http://www.opensource.org/licenses/bsd-license.php}
12'''
13import gi
14
15from gi.repository import Gtk as gtk
16from gi.repository import Gdk as gdk
17from gi.repository import GLib
18from gi.repository import Pango
19
20import pyatspi
21import os.path
22import gettext, os, sys, locale
23from accerciser.plugin import ViewportPlugin
24from accerciser.i18n import _, N_, DOMAIN
25from accerciser import node
26
27UI_FILE = os.path.join(os.path.dirname(__file__),
28                       'event_monitor.ui')
29
30class EventMonitor(ViewportPlugin):
31  '''
32  Class for the monitor viewer.
33
34  @ivar source_filter: Determines what events from what sources could be shown.
35  Either source_app and source_acc for selected applications and accessibles
36  respectively. Or everything.
37  @type source_filter: string
38  @ivar main_xml: The main event monitor gtkbuilder file.
39  @type main_xml: gtk.GtkBuilder
40  @ivar monitor_toggle: Toggle button for turining monitoring on and off.
41  @type monitor_toggle: gtk.ToggleButton
42  @ivar listen_list: List of at-spi events the monitor is currently listening
43  to.
44  @type listen_list: list
45  @ivar events_model: Data model of all at-spi event types.
46  @type events_model: gtk.TreeStore
47  @ivar textview_monitor: Text view of eent monitor.
48  @type textview_monitor: gtk.TextView
49  @ivar monitor_buffer: Text buffer for event monitor.
50  @type monitor_buffer: gtk.TextBuffer
51  '''
52  plugin_name = N_('Event Monitor')
53  plugin_name_localized = _(plugin_name)
54  plugin_description = \
55      N_('Shows events as they occur from selected types and sources')
56  COL_NAME = 0
57  COL_FULL_NAME = 1
58  COL_TOGGLE = 2
59  COL_INCONSISTENT = 3
60
61  def init(self):
62    '''
63    Initialize the event monitor plugin.
64    '''
65    self.global_hotkeys = [(N_('Highlight last event entry'),
66                            self._onHighlightEvent,
67                            gdk.KEY_e, gdk.ModifierType.MOD1_MASK | \
68                                       gdk.ModifierType.CONTROL_MASK),
69                           (N_('Start/stop event recording'),
70                            self._onStartStop,
71                            gdk.KEY_r, gdk.ModifierType.MOD1_MASK | \
72                                       gdk.ModifierType.CONTROL_MASK),
73                           (N_('Clear event log'),
74                            self._onClearlog,
75                            gdk.KEY_t, gdk.ModifierType.MOD1_MASK | \
76                                       gdk.ModifierType.CONTROL_MASK)]
77
78    self.source_filter = None
79    self.main_xml = gtk.Builder()
80    self.main_xml.set_translation_domain(DOMAIN)
81    self.main_xml.add_from_file(UI_FILE)
82    vpaned = self.main_xml.get_object('monitor_vpaned')
83    self.plugin_area.add(vpaned)
84    self.events_model = self.main_xml.get_object('events_treestore')
85    self._popEventsModel()
86    self._initTextView()
87
88    self.monitor_toggle = self.main_xml.get_object('monitor_toggle')
89
90    self.source_filter = None
91    self.sources_dict = { \
92        self.main_xml.get_object('source_everthing') : 'source_everthing', \
93        self.main_xml.get_object('source_app') : 'source_app', \
94        self.main_xml.get_object('source_acc') : 'source_acc' \
95    }
96
97    self.listen_list = []
98
99    self.node.connect('accessible-changed', self._onNodeUpdated)
100
101    self.main_xml.connect_signals(self)
102    self.show_all()
103
104  def _onStartStop(self):
105    active = self.monitor_toggle.get_active()
106    self.monitor_toggle.set_active(not active)
107
108  def _onClearlog(self):
109    self.monitor_buffer.set_text('')
110
111  def _onNodeUpdated(self, node, acc):
112    if acc == node.desktop and \
113          self.source_filter in ('source_app', 'source_acc'):
114      self.monitor_toggle.set_active(False)
115
116  def _popEventsModel(self):
117    '''
118    Populate the model for the event types tree view. Uses a constant
119    from pyatspi for the listing of all event types.
120    '''
121    events = list(pyatspi.EVENT_TREE.keys())
122    for sub_events in pyatspi.EVENT_TREE.values():
123      events.extend(sub_events)
124    events = list(set([event.strip(':') for event in events]))
125    events.sort()
126    GLib.idle_add(self._appendChildren, None, '', 0, events)
127
128  def _initTextView(self):
129    '''
130    Initialize text view in monitor plugin.
131    '''
132    self.textview_monitor = self.main_xml.get_object('textview_monitor')
133
134    self.monitor_buffer = self.textview_monitor.get_buffer()
135    self.monitor_mark = \
136        self.monitor_buffer.create_mark('scroll_mark',
137                                        self.monitor_buffer.get_end_iter(),
138                                        False)
139    self.monitor_buffer.create_mark('mark_last_log',
140                                    self.monitor_buffer.get_end_iter(),
141                                    True)
142    self.monitor_buffer.create_tag('last_log', weight=700)
143
144  def _appendChildren(self, parent_iter, parent, level, events):
145    '''
146    Append child events to a parent event's iter.
147
148    @param parent_iter: The tree iter of the parent.
149    @type parent_iter: gtk.TreeIter
150    @param parent: The parent event.
151    @type parent: string
152    @param level: The generation of the children.
153    @type level: integer
154    @param events: A list of children
155    @type events: list
156
157    @return: Return false to that this won't be called again in the mainloop.
158    @rtype: boolean
159    '''
160    for event in events:
161      if event.count(':') == level and event.startswith(parent):
162        iter = self.events_model.append(parent_iter,
163                                        [event.split(':')[-1],
164                                         event, False, False])
165        GLib.idle_add(self._appendChildren, iter, event, level + 1, events)
166    return False
167
168  def _onToggled(self, renderer_toggle, path):
169    '''
170    Callback for toggled events in the treeview.
171
172    @param renderer_toggle: The toggle cell renderer.
173    @type renderer_toggle: gtk.CellRendererToggle
174    @param path: The path of the toggled node.
175    @type path: tuple
176    '''
177    iter = self.events_model.get_iter(path)
178    val = not self.events_model.get_value(iter, self.COL_TOGGLE)
179    self._iterToggle(iter, val)
180    self._resetClient()
181
182  def _resetClient(self):
183    '''
184    De-registers the client from the currently monitored events.
185    If the monitor is still enabled it get's a list of enabled events
186    and re-registers the client.
187    '''
188    pyatspi.Registry.deregisterEventListener(self._handleAccEvent,
189                                             *self.listen_list)
190    self.listen_list = self._getEnabledEvents(self.events_model.get_iter_first())
191    if self.monitor_toggle.get_active():
192      pyatspi.Registry.registerEventListener(self._handleAccEvent,
193                                             *self.listen_list)
194
195  def _getEnabledEvents(self, iter):
196    '''
197    Recursively walks through the events model and collects all enabled
198    events in a list.
199
200    @param iter: Iter of root node to check under.
201    @type iter: gtk.TreeIter
202
203    @return: A list of enabled events.
204    @rtype: list
205    '''
206    listen_for = []
207    while iter:
208      toggled = self.events_model.get_value(iter, self.COL_TOGGLE)
209      inconsistent = self.events_model.get_value(iter, self.COL_INCONSISTENT)
210      if toggled and not inconsistent:
211        listen_for.append(self.events_model.get_value(iter, self.COL_FULL_NAME))
212      elif inconsistent:
213        listen_for_child = self._getEnabledEvents(self.events_model.iter_children(iter))
214        listen_for.extend(listen_for_child)
215      iter = self.events_model.iter_next(iter)
216    return listen_for
217
218  def _iterToggle(self, iter, val):
219    '''
220    Toggle the given node. If the node has children toggle them accordingly
221    too. Toggle all anchester nodes too, either true, false or inconsistent,
222    sepending on the value of their children.
223
224    @param iter: Iter of node to toggle.
225    @type iter: gtk.TreeIter
226    @param val: Toggle value.
227    @type val: boolean
228    '''
229    self.events_model.set_value(iter, self.COL_INCONSISTENT, False)
230    self.events_model.set_value(iter, self.COL_TOGGLE, val)
231    self._setAllDescendants(iter, val)
232    parent = self.events_model.iter_parent(iter)
233    while parent:
234      is_consistent = self._descendantsConsistent(parent)
235      self.events_model.set_value(parent,
236                                  self.COL_INCONSISTENT,
237                                  not is_consistent)
238      self.events_model.set_value(parent, self.COL_TOGGLE, val)
239      parent = self.events_model.iter_parent(parent)
240
241  def _setAllDescendants(self, iter, val):
242    '''
243    Set all descendants of a given node to a certain toggle value.
244
245    @param iter: Parent node's iter.
246    @type iter: gtk.TreeIter
247    @param val: Toggle value.
248    @type val: boolean
249    '''
250    child = self.events_model.iter_children(iter)
251    while child:
252      self.events_model.set_value(child, self.COL_TOGGLE, val)
253      self._setAllDescendants(child, val)
254      child = self.events_model.iter_next(child)
255
256  def _descendantsConsistent(self, iter):
257    '''
258    Determine if all of a node's descendants are consistently toggled.
259
260    @param iter: Parent node's iter.
261    @type iter: gtk.TreeIter
262
263    @return: True if descendants nodes are consistent.
264    @rtype: boolean
265    '''
266    child = self.events_model.iter_children(iter)
267    if child:
268      first_val = self.events_model.get_value(child, self.COL_TOGGLE)
269    while child:
270      child_val = self.events_model.get_value(child, self.COL_TOGGLE)
271      is_consistent = self._descendantsConsistent(child)
272      if (first_val != child_val or not is_consistent):
273        return False
274      child = self.events_model.iter_next(child)
275    return True
276
277  def _onSelectAll(self, button):
278    '''
279    Callback for "select all" button. Select all event types.
280
281    @param button: Button that was clicked
282    @type button: gtk.Button
283    '''
284    iter = self.events_model.get_iter_first()
285    while iter:
286      self._iterToggle(iter, True)
287      iter = self.events_model.iter_next(iter)
288    self._resetClient()
289
290  def _onClearSelection(self, button):
291    '''
292    Callback for "clear selection" button. Clear all selected events.
293
294    @param button: Button that was clicked.
295    @type button: gtk.Button
296    '''
297    iter = self.events_model.get_iter_first()
298    while iter:
299      self._iterToggle(iter, False)
300      iter = self.events_model.iter_next(iter)
301    self._resetClient()
302
303  def _logEvent(self, event):
304    '''
305    Log the given event.
306
307    @param event: The event to log.
308    @type event: Accessibility.Event
309    '''
310    iter = self.monitor_buffer.get_iter_at_mark(self.monitor_mark)
311    self.monitor_buffer.move_mark_by_name(
312      'mark_last_log',
313      self.monitor_buffer.get_iter_at_mark(self.monitor_mark))
314    self._insertEventIntoBuffer(event)
315    self.textview_monitor.scroll_mark_onscreen(self.monitor_mark)
316
317  def _insertEventIntoBuffer(self, event):
318    '''
319    Inserts given event in to text buffer. Creates hyperlinks for
320    the events context accessibles.
321
322    @param event: The at-spi event we are inserting.
323    @type event: Accessibility.Event
324    '''
325    self._writeText('%s(%s, %s, %s)\n\tsource: ' % \
326                      (event.type, event.detail1,
327                       event.detail2, event.any_data))
328    hyperlink = self._createHyperlink(event.source)
329    self._writeText(str(event.source), hyperlink)
330    self._writeText('\n\tapplication: ')
331    hyperlink = self._createHyperlink(event.host_application)
332    self._writeText(str(event.host_application), hyperlink)
333    if hasattr(event, "sender") and event.sender != event.host_application:
334        self._writeText('\n\tsender: ')
335        hyperlink = self._createHyperlink(event.sender)
336        self._writeText(str(event.sender), hyperlink)
337    self._writeText('\n')
338    if event.type == "screen-reader:region-changed":
339        try:
340            text = event.source.queryText()
341            (x, y, width, height) = text.getRangeExtents(event.detail1, event.detail2, pyatspi.DESKTOP_COORDS)
342            if width > 0 and height > 0:
343                ah = node._HighLight(x, y, width, height,
344                                     node.FILL_COLOR, node.FILL_ALPHA,
345                                     node.BORDER_COLOR, node.BORDER_ALPHA,
346                                     2.0, 0)
347                ah.highlight(node.HL_DURATION)
348        except:
349            pass
350
351  def _writeText(self, text, *tags):
352    '''
353    Convinience function for inserting text in to the text buffer.
354    If tags are provided they are applied to the inserted text.
355
356    @param text: Text to insert
357    @type text: string
358    @param *tags: List of optional tags to insert with text
359    @type *tags: list of gtk.TextTag
360    '''
361    if tags:
362      self.monitor_buffer.insert_with_tags(
363        self.monitor_buffer.get_iter_at_mark(self.monitor_mark),
364        text, *tags)
365    else:
366      self.monitor_buffer.insert(
367        self.monitor_buffer.get_iter_at_mark(self.monitor_mark),
368        text)
369
370  def _createHyperlink(self, acc):
371    '''
372    Create a hyperlink tag for a given accessible. When the link is clicked
373    the accessible is selected in the main program.
374
375    @param acc: The accessible to create the tag for.
376    @type acc: Accessibility.Accessible
377
378    @return: The new hyperlink tag
379    @rtype: gtk.TextTag
380    '''
381    hyperlink = self.monitor_buffer.create_tag(
382      None,
383      foreground='blue',
384      underline=Pango.Underline.SINGLE)
385    hyperlink.connect('event', self._onLinkClicked)
386    setattr(hyperlink, 'acc', acc)
387    setattr(hyperlink, 'islink', True)
388    return hyperlink
389
390  def _onLinkClicked(self, tag, widget, event, iter):
391    '''
392    Callback for clicked link. Select links accessible in main application.
393
394    @param tag: Tag that was clicked.
395    @type tag: gtk.TextTag
396    @param widget: The widget that received event.
397    @type widget: gtk.Widget
398    @param event: The event object.
399    @type event: gdk.Event
400    @param iter: The text iter that was clicked.
401    @type iter: gtk.TextIter
402    '''
403    if event.type == gdk.EventType.BUTTON_RELEASE and \
404           event.button == 1 and not self.monitor_buffer.get_has_selection():
405      self.node.update(getattr(tag, 'acc'))
406
407  def _onLinkKeyPress(self, textview, event):
408    '''
409    Callback for a keypress in the text view. If the keypress is enter or
410    space, and the cursor is above a link, follow it.
411
412    @param textview: Textview that was pressed.
413    @type textview: gtk.TextView
414    @param event: Event object.
415    @type event: gdk.Event
416    '''
417    if event.keyval in (gdk.KEY_Return,
418                        gdk.KEY_KP_Enter,
419                        gdk.KEY_space):
420      buffer = textview.get_buffer()
421      iter = buffer.get_iter_at_mark(buffer.get_insert())
422      acc = None
423      for tag in iter.get_tags():
424        acc = getattr(tag, 'acc')
425        if acc:
426          self.node.update(acc)
427          break
428
429  def _onLinkMotion(self, textview, event):
430    '''
431    Change mouse cursor shape when hovering over a link.
432
433    @param textview: Monitors text view.
434    @type textview: gtk.TextView
435    @param event: Event object
436    @type event: gdk.Event
437
438    @return: Return False so event continues in callback chain.
439    @rtype: boolean
440    '''
441    x, y = textview.window_to_buffer_coords(gtk.TextWindowType.WIDGET,
442                                             int(event.x), int(event.y))
443    isText = True
444    iter = textview.get_iter_at_location(x, y)
445    if isinstance(iter, tuple):
446        (isText, iter) = iter
447    cursor = gdk.Cursor(gdk.CursorType.XTERM)
448    if isText:
449      for tag in iter.get_tags():
450        if getattr(tag, 'islink'):
451          cursor = gdk.Cursor(gdk.CursorType.HAND2)
452          break
453    window = textview.get_window(gtk.TextWindowType.TEXT)
454    window.set_cursor(cursor)
455    window.get_pointer()
456    return False
457
458  def _handleAccEvent(self, event):
459    '''
460    Main at-spi event client. If event passes filtering requirements, log it.
461
462    @param event: The at-spi event recieved.
463    @type event: Accessibility.Event
464    '''
465    if self.isMyApp(event.source) or not self._eventFilter(event):
466      return
467    self._logEvent(event)
468
469  def _onSave(self, button):
470    '''
471    Callback for 'save' button clicked. Saves the buffer in to the given
472    filename.
473
474    @param button: Button that was clicked.
475    @type button: gtk.Button
476    '''
477    save_dialog = gtk.FileChooserDialog(
478      'Save monitor output',
479      action=gtk.FileChooserAction.SAVE,
480      buttons=(gtk.ButtonsType.CANCEL, gtk.ResponseType.CANCEL,
481               gtk.ButtonsType.OK, gtk.ResponseType.OK))
482    save_dialog.set_do_overwrite_confirmation(True)
483    save_dialog.set_default_response(gtk.ResponseType.OK)
484    response = save_dialog.run()
485    save_dialog.show_all()
486    if response == gtk.ResponseType.OK:
487      save_to = open(save_dialog.get_filename(), 'w')
488      save_to.write(
489        self.monitor_buffer.get_text(self.monitor_buffer.get_start_iter(),
490                                     self.monitor_buffer.get_end_iter()))
491      save_to.close()
492    save_dialog.destroy()
493
494  def _onClear(self, button):
495    '''
496    Callback for 'clear' button. Clears monitor's text buffer.
497
498    @param button: Button that was clicked.
499    @type button: gtk.Button
500    '''
501    self.monitor_buffer.set_text('')
502
503
504  def _onMonitorToggled(self, monitor_toggle):
505    '''
506    Callback for monitor toggle button. Activates or deactivates monitoring.
507
508    @param monitor_toggle: The toggle button that was toggled.
509    @type monitor_toggle: gtk.ToggleButton
510    '''
511    if monitor_toggle.get_active():
512      pyatspi.Registry.registerEventListener(self._handleAccEvent,
513                                             *self.listen_list)
514    else:
515      pyatspi.Registry.deregisterEventListener(self._handleAccEvent,
516                                               *self.listen_list)
517
518  def _onSourceToggled(self, radio_button):
519    '''
520    Callback for radio button selection for choosing source filters.
521
522    @param radio_button: Radio button that was selected.
523    @type radio_button: gtk.RadioButton
524    '''
525    self.source_filter = self.sources_dict[radio_button]
526
527  def _eventFilter(self, event):
528    '''
529    Determine if an event's source should make the event filtered.
530
531    @param event: The given at-spi event.
532    @type event: Accessibility.Event
533
534    @return: False if the event should be filtered.
535    @rtype: boolean
536    '''
537    if self.source_filter == 'source_app':
538      try:
539        return event.source.getApplication() == self.acc.getApplication()
540      except:
541        return False
542    elif self.source_filter == 'source_acc':
543      return event.source == self.acc
544    else:
545      return True
546
547  def _onHighlightEvent(self):
548    '''
549    A callback fom a global key binding. Makes the last event in the textview
550    bold.
551    '''
552    start_iter = self.monitor_buffer.get_iter_at_mark(
553      self.monitor_buffer.get_mark('mark_last_log'))
554    end_iter = self.monitor_buffer.get_end_iter()
555    self.monitor_buffer.apply_tag_by_name('last_log', start_iter, end_iter)
556