event_monitor.py 18.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
'''
Event monitor plugin.

@author: Eitan Isaacson
@organization: IBM Corporation
@copyright: Copyright (c) 2007 IBM Corporation
@license: BSD

All rights reserved. This program and the accompanying materials are made 
available under the terms of the BSD which accompanies this distribution, and 
is available at U{http://www.opensource.org/licenses/bsd-license.php}
'''
13 14 15 16 17 18 19
import gi

from gi.repository import Gtk as gtk
from gi.repository import Gdk as gdk
from gi.repository import GObject
from gi.repository import Pango

20
import pyatspi
21 22
import os.path
import gettext, os, sys, locale
23
from accerciser.plugin import ViewportPlugin
Eitan Isaacson's avatar
Eitan Isaacson committed
24
from accerciser.i18n import _, N_, DOMAIN
25

26 27
UI_FILE = os.path.join(os.path.dirname(__file__), 
                       'event_monitor.ui')
28

29
class EventMonitor(ViewportPlugin):
30 31
  '''
  Class for the monitor viewer.
32 33 34 35 36

  @ivar source_filter: Determines what events from what sources could be shown.
  Either source_app and source_acc for selected applications and accessibles 
  respectively. Or everything.
  @type source_filter: string
37 38
  @ivar main_xml: The main event monitor gtkbuilder file.
  @type main_xml: gtk.GtkBuilder
39 40 41 42 43 44 45 46 47 48 49
  @ivar monitor_toggle: Toggle button for turining monitoring on and off.
  @type monitor_toggle: gtk.ToggleButton
  @ivar listen_list: List of at-spi events the monitor is currently listening
  to.
  @type listen_list: list
  @ivar events_model: Data model of all at-spi event types.
  @type events_model: gtk.TreeStore
  @ivar textview_monitor: Text view of eent monitor.
  @type textview_monitor: gtk.TextView
  @ivar monitor_buffer: Text buffer for event monitor.
  @type monitor_buffer: gtk.TextBuffer
50
  '''
51
  plugin_name = N_('Event Monitor')
52
  plugin_name_localized = _(plugin_name)
53 54
  plugin_description = \
      N_('Shows events as they occur from selected types and sources')
55 56 57 58 59 60
  COL_NAME = 0
  COL_FULL_NAME = 1
  COL_TOGGLE = 2
  COL_INCONSISTENT = 3

  def init(self):
61 62 63
    '''
    Initialize the event monitor plugin.
    '''
64
    self.global_hotkeys = [(N_('Highlight last event entry'),
65
                            self._onHighlightEvent,
66 67
                            gdk.KEY_e, gdk.ModifierType.MOD1_MASK | \
                                       gdk.ModifierType.CONTROL_MASK),
68 69
                           (N_('Start/stop event recording'),
                            self._onStartStop,
70 71
                            gdk.KEY_r, gdk.ModifierType.MOD1_MASK | \
                                       gdk.ModifierType.CONTROL_MASK),
72 73
                           (N_('Clear event log'),
                            self._onClearlog,
74 75
                            gdk.KEY_t, gdk.ModifierType.MOD1_MASK | \
                                       gdk.ModifierType.CONTROL_MASK)]
76

77
    self.source_filter = None
78
    self.main_xml = gtk.Builder()
Eitan Isaacson's avatar
Eitan Isaacson committed
79
    self.main_xml.set_translation_domain(DOMAIN)
80 81
    self.main_xml.add_from_file(UI_FILE)
    vpaned = self.main_xml.get_object('monitor_vpaned')
82
    self.plugin_area.add(vpaned)
83
    self.events_model = self.main_xml.get_object('events_treestore')
84 85 86
    self._popEventsModel()
    self._initTextView()

87
    self.monitor_toggle = self.main_xml.get_object('monitor_toggle')
88

89 90 91 92 93 94 95
    self.source_filter = None
    self.sources_dict = { \
        self.main_xml.get_object('source_everthing') : 'source_everthing', \
        self.main_xml.get_object('source_app') : 'source_app', \
        self.main_xml.get_object('source_acc') : 'source_acc' \
    }

96 97
    self.listen_list = []

98 99
    self.node.connect('accessible-changed', self._onNodeUpdated)

100
    self.main_xml.connect_signals(self)
101 102
    self.show_all()

103 104 105 106 107 108 109
  def _onStartStop(self):
    active = self.monitor_toggle.get_active()
    self.monitor_toggle.set_active(not active)

  def _onClearlog(self):
    self.monitor_buffer.set_text('')

110 111 112 113 114
  def _onNodeUpdated(self, node, acc):
    if acc == node.desktop and \
          self.source_filter in ('source_app', 'source_acc'):
      self.monitor_toggle.set_active(False)

115
  def _popEventsModel(self):
116 117 118 119
    '''
    Populate the model for the event types tree view. Uses a constant
    from pyatspi for the listing of all event types.
    '''
Joanmarie Diggs's avatar
Joanmarie Diggs committed
120 121
    events = list(pyatspi.EVENT_TREE.keys())
    for sub_events in pyatspi.EVENT_TREE.values():
122 123 124
      events.extend(sub_events)
    events = list(set([event.strip(':') for event in events]))
    events.sort()
125
    GObject.idle_add(self._appendChildren, None, '', 0, events)
126 127

  def _initTextView(self):
128 129 130
    '''
    Initialize text view in monitor plugin.
    '''
131
    self.textview_monitor = self.main_xml.get_object('textview_monitor')
132 133 134 135 136 137 138 139 140 141 142 143
    
    self.monitor_buffer = self.textview_monitor.get_buffer()
    self.monitor_mark = \
        self.monitor_buffer.create_mark('scroll_mark', 
                                        self.monitor_buffer.get_end_iter(),
                                        False)
    self.monitor_buffer.create_mark('mark_last_log', 
                                    self.monitor_buffer.get_end_iter(),
                                    True)
    self.monitor_buffer.create_tag('last_log', weight=700)

  def _appendChildren(self, parent_iter, parent, level, events):
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
    '''
    Append child events to a parent event's iter.
    
    @param parent_iter: The tree iter of the parent.
    @type parent_iter: gtk.TreeIter
    @param parent: The parent event.
    @type parent: string
    @param level: The generation of the children.
    @type level: integer
    @param events: A list of children
    @type events: list
    
    @return: Return false to that this won't be called again in the mainloop.
    @rtype: boolean
    '''
159 160 161 162 163
    for event in events:
      if event.count(':') == level and event.startswith(parent):
        iter = self.events_model.append(parent_iter, 
                                        [event.split(':')[-1],
                                         event, False, False])
164
        GObject.idle_add(self._appendChildren, iter, event, level + 1, events)
165
    return False
166 167
        
  def _onToggled(self, renderer_toggle, path):
168 169 170 171 172 173 174 175
    '''
    Callback for toggled events in the treeview.
    
    @param renderer_toggle: The toggle cell renderer.
    @type renderer_toggle: gtk.CellRendererToggle
    @param path: The path of the toggled node.
    @type path: tuple
    '''
176 177 178 179 180 181
    iter = self.events_model.get_iter(path)
    val = not self.events_model.get_value(iter, self.COL_TOGGLE)
    self._iterToggle(iter, val)
    self._resetClient()

  def _resetClient(self):
182 183 184 185 186
    '''
    De-registers the client from the currently monitored events. 
    If the monitor is still enabled it get's a list of enabled events 
    and re-registers the client.
    '''
187
    pyatspi.Registry.deregisterEventListener(self._handleAccEvent, 
188
                                             *self.listen_list)
189
    self.listen_list = self._getEnabledEvents(self.events_model.get_iter_first())
190
    if self.monitor_toggle.get_active():
191 192
      pyatspi.Registry.registerEventListener(self._handleAccEvent, 
                                             *self.listen_list)
193 194

  def _getEnabledEvents(self, iter):
195 196 197 198 199 200 201 202 203 204
    '''
    Recursively walks through the events model and collects all enabled 
    events in a list.
    
    @param iter: Iter of root node to check under.
    @type iter: gtk.TreeIter
    
    @return: A list of enabled events.
    @rtype: list
    '''
205 206 207 208 209 210 211 212 213 214 215 216 217
    listen_for = []
    while iter:
      toggled = self.events_model.get_value(iter, self.COL_TOGGLE)
      inconsistent = self.events_model.get_value(iter, self.COL_INCONSISTENT)
      if toggled and not inconsistent:
        listen_for.append(self.events_model.get_value(iter, self.COL_FULL_NAME))
      elif inconsistent:
        listen_for_child = self._getEnabledEvents(self.events_model.iter_children(iter))
        listen_for.extend(listen_for_child)
      iter = self.events_model.iter_next(iter)
    return listen_for

  def _iterToggle(self, iter, val):
218 219 220 221 222 223 224 225 226 227
    '''
    Toggle the given node. If the node has children toggle them accordingly 
    too. Toggle all anchester nodes too, either true, false or inconsistent,
    sepending on the value of their children.
    
    @param iter: Iter of node to toggle.
    @type iter: gtk.TreeIter
    @param val: Toggle value.
    @type val: boolean
    '''
228 229 230 231 232 233 234 235 236 237 238 239 240
    self.events_model.set_value(iter, self.COL_INCONSISTENT, False)
    self.events_model.set_value(iter, self.COL_TOGGLE, val)
    self._setAllDescendants(iter, val)
    parent = self.events_model.iter_parent(iter)
    while parent:
      is_consistent = self._descendantsConsistent(parent)
      self.events_model.set_value(parent, 
                                  self.COL_INCONSISTENT,
                                  not is_consistent)
      self.events_model.set_value(parent, self.COL_TOGGLE, val)
      parent = self.events_model.iter_parent(parent)
  
  def _setAllDescendants(self, iter, val):
241 242 243 244 245 246 247 248
    '''
    Set all descendants of a given node to a certain toggle value.
    
    @param iter: Parent node's iter.
    @type iter: gtk.TreeIter
    @param val: Toggle value.
    @type val: boolean
    '''
249 250 251 252 253 254 255
    child = self.events_model.iter_children(iter)
    while child:
      self.events_model.set_value(child, self.COL_TOGGLE, val)
      self._setAllDescendants(child, val)
      child = self.events_model.iter_next(child)
  
  def _descendantsConsistent(self, iter):
256 257 258 259 260 261 262 263 264
    '''
    Determine if all of a node's descendants are consistently toggled.
    
    @param iter: Parent node's iter.
    @type iter: gtk.TreeIter
    
    @return: True if descendants nodes are consistent.
    @rtype: boolean
    '''
265 266 267 268 269 270 271 272 273 274 275
    child = self.events_model.iter_children(iter)
    if child:
      first_val = self.events_model.get_value(child, self.COL_TOGGLE)
    while child:
      child_val = self.events_model.get_value(child, self.COL_TOGGLE)
      is_consistent = self._descendantsConsistent(child)
      if (first_val != child_val or not is_consistent):
        return False
      child = self.events_model.iter_next(child)
    return True

276 277 278 279 280 281 282
  def _onSelectAll(self, button):
    '''
    Callback for "select all" button. Select all event types.
    
    @param button: Button that was clicked
    @type button: gtk.Button
    '''
283
    iter = self.events_model.get_iter_first()
284 285 286 287 288
    while iter:
      self._iterToggle(iter, True)
      iter = self.events_model.iter_next(iter)
    self._resetClient()

289 290 291 292 293 294 295
  def _onClearSelection(self, button):
    '''
    Callback for "clear selection" button. Clear all selected events.
    
    @param button: Button that was clicked.
    @type button: gtk.Button
    '''
296
    iter = self.events_model.get_iter_first()
297 298 299 300 301
    while iter:
      self._iterToggle(iter, False)
      iter = self.events_model.iter_next(iter)
    self._resetClient()

302
  def _logEvent(self, event):
303
    '''
304
    Log the given event.
305
    
306 307
    @param event: The event to log.
    @type event: Accessibility.Event
308
    '''
309 310 311 312 313 314
    iter = self.monitor_buffer.get_iter_at_mark(self.monitor_mark)
    self.monitor_buffer.move_mark_by_name(
      'mark_last_log', 
      self.monitor_buffer.get_iter_at_mark(self.monitor_mark))
    self._insertEventIntoBuffer(event)
    self.textview_monitor.scroll_mark_onscreen(self.monitor_mark)
315 316

  def _insertEventIntoBuffer(self, event):
317 318
    '''
    Inserts given event in to text buffer. Creates hyperlinks for
319
    the events context accessibles.
320 321 322 323
    
    @param event: The at-spi event we are inserting.
    @type event: Accessibility.Event
    '''
324 325 326 327 328 329 330 331 332
    self._writeText('%s(%s, %s, %s)\n\tsource: ' % \
                      (event.type, event.detail1, 
                       event.detail2, event.any_data))
    hyperlink = self._createHyperlink(event.source)
    self._writeText(str(event.source), hyperlink)
    self._writeText('\n\tapplication: ')
    hyperlink = self._createHyperlink(event.host_application)
    self._writeText(str(event.host_application), hyperlink)
    self._writeText('\n')
333 334

  def _writeText(self, text, *tags):
335 336 337 338 339 340 341 342 343
    '''
    Convinience function for inserting text in to the text buffer.
    If tags are provided they are applied to the inserted text.
    
    @param text: Text to insert
    @type text: string
    @param *tags: List of optional tags to insert with text
    @type *tags: list of gtk.TextTag
    '''
344
    if tags:
345 346
      self.monitor_buffer.insert_with_tags(
        self.monitor_buffer.get_iter_at_mark(self.monitor_mark),
347
        text, *tags)
348 349 350
    else:
      self.monitor_buffer.insert(
        self.monitor_buffer.get_iter_at_mark(self.monitor_mark),
351
        text)
352

353
  def _createHyperlink(self, acc):
354
    '''
355 356
    Create a hyperlink tag for a given accessible. When the link is clicked
    the accessible is selected in the main program.
357 358 359 360 361 362 363
    
    @param acc: The accessible to create the tag for.
    @type acc: Accessibility.Accessible
    
    @return: The new hyperlink tag
    @rtype: gtk.TextTag
    '''
364 365 366
    hyperlink = self.monitor_buffer.create_tag(
      None, 
      foreground='blue',
367
      underline=Pango.Underline.SINGLE)
368
    hyperlink.connect('event', self._onLinkClicked)
369 370
    setattr(hyperlink, 'acc', acc)
    setattr(hyperlink, 'islink', True)
371 372 373
    return hyperlink

  def _onLinkClicked(self, tag, widget, event, iter):
374
    '''
375
    Callback for clicked link. Select links accessible in main application.
376
    
377
    @param tag: Tag that was clicked.
378 379 380 381
    @type tag: gtk.TextTag
    @param widget: The widget that received event.
    @type widget: gtk.Widget
    @param event: The event object.
382
    @type event: gdk.Event
383 384 385
    @param iter: The text iter that was clicked.
    @type iter: gtk.TextIter
    '''
386
    if event.type == gdk.EventType.BUTTON_RELEASE and \
387
           event.button == 1 and not self.monitor_buffer.get_has_selection():
388
      self.node.update(getattr(tag, 'acc'))
389

390
  def _onLinkKeyPress(self, textview, event):
391 392 393 394 395 396 397
    '''
    Callback for a keypress in the text view. If the keypress is enter or 
    space, and the cursor is above a link, follow it.
    
    @param textview: Textview that was pressed.
    @type textview: gtk.TextView
    @param event: Event object.
398
    @type event: gdk.Event
399
    '''
400 401 402
    if event.keyval in (gdk.KEY_Return, 
                        gdk.KEY_KP_Enter,
                        gdk.KEY_space):
403 404 405 406
      buffer = textview.get_buffer()
      iter = buffer.get_iter_at_mark(buffer.get_insert())
      acc = None
      for tag in iter.get_tags():
407
        acc = getattr(tag, 'acc')
408 409 410 411 412
        if acc:
          self.node.update(acc)
          break

  def _onLinkMotion(self, textview, event):
413 414 415
    '''
    Change mouse cursor shape when hovering over a link.
    
416
    @param textview: Monitors text view.
417 418
    @type textview: gtk.TextView
    @param event: Event object
419
    @type event: gdk.Event
420 421 422 423
    
    @return: Return False so event continues in callback chain.
    @rtype: boolean
    '''
424
    x, y = textview.window_to_buffer_coords(gtk.TextWindowType.WIDGET,
425 426
                                             int(event.x), int(event.y))
    iter = textview.get_iter_at_location(x, y)
427
    cursor = gdk.Cursor(gdk.CursorType.XTERM)
428
    for tag in iter.get_tags():
429
      if getattr(tag, 'islink'):
430
        cursor = gdk.Cursor(gdk.CursorType.HAND2)
431
        break
432 433 434
    window = textview.get_window(gtk.TextWindowType.TEXT)
    window.set_cursor(cursor)
    window.get_pointer()
435 436
    return False

437
  def _handleAccEvent(self, event):
438
    '''
439
    Main at-spi event client. If event passes filtering requirements, log it.
440 441 442 443
    
    @param event: The at-spi event recieved.
    @type event: Accessibility.Event
    '''
444 445
    if self.isMyApp(event.source) or not self._eventFilter(event):
      return
446
    self._logEvent(event)
447

448 449 450 451 452 453 454 455 456 457
  def _onSave(self, button):
    '''
    Callback for 'save' button clicked. Saves the buffer in to the given 
    filename.
    
    @param button: Button that was clicked.
    @type button: gtk.Button
    '''
    save_dialog = gtk.FileChooserDialog(
      'Save monitor output',
458 459 460
      action=gtk.FileChooserAction.SAVE,
      buttons=(gtk.ButtonsType.CANCEL, gtk.ResponseType.CANCEL,
               gtk.ButtonsType.OK, gtk.ResponseType.OK))
461
    save_dialog.set_do_overwrite_confirmation(True)
462
    save_dialog.set_default_response(gtk.ResponseType.OK)
463
    response = save_dialog.run()
464
    save_dialog.show_all()
465
    if response == gtk.ResponseType.OK:
466 467 468 469 470 471
      save_to = open(save_dialog.get_filename(), 'w')
      save_to.write(
        self.monitor_buffer.get_text(self.monitor_buffer.get_start_iter(),
                                     self.monitor_buffer.get_end_iter()))
      save_to.close()
    save_dialog.destroy()
472
  
473 474 475 476 477 478 479
  def _onClear(self, button):
    '''
    Callback for 'clear' button. Clears monitor's text buffer.
    
    @param button: Button that was clicked.
    @type button: gtk.Button
    '''
480 481 482 483
    self.monitor_buffer.set_text('')

  
  def _onMonitorToggled(self, monitor_toggle):
484 485 486 487 488 489
    '''
    Callback for monitor toggle button. Activates or deactivates monitoring.
    
    @param monitor_toggle: The toggle button that was toggled.
    @type monitor_toggle: gtk.ToggleButton
    '''
490
    if monitor_toggle.get_active():
491 492
      pyatspi.Registry.registerEventListener(self._handleAccEvent, 
                                             *self.listen_list)
493
    else:
494 495
      pyatspi.Registry.deregisterEventListener(self._handleAccEvent, 
                                               *self.listen_list)
496 497

  def _onSourceToggled(self, radio_button):
498 499 500 501 502 503
    '''
    Callback for radio button selection for choosing source filters.
    
    @param radio_button: Radio button that was selected.
    @type radio_button: gtk.RadioButton
    '''
504
    self.source_filter = self.sources_dict[radio_button]
505 506

  def _eventFilter(self, event):
507 508 509 510 511 512 513 514 515
    '''
    Determine if an event's source should make the event filtered.
    
    @param event: The given at-spi event.
    @type event: Accessibility.Event
    
    @return: False if the event should be filtered.
    @rtype: boolean
    '''
516
    if self.source_filter == 'source_app':
517
      try:
518
        return event.source.getApplication() == self.acc.getApplication()
519
      except:
520 521 522 523 524
        return False
    elif self.source_filter == 'source_acc':
      return event.source == self.acc
    else:
      return True
525 526
  
  def _onHighlightEvent(self):
527 528 529 530
    '''
    A callback fom a global key binding. Makes the last event in the textview
    bold.
    '''
531 532 533 534
    start_iter = self.monitor_buffer.get_iter_at_mark(
      self.monitor_buffer.get_mark('mark_last_log'))
    end_iter = self.monitor_buffer.get_end_iter()
    self.monitor_buffer.apply_tag_by_name('last_log', start_iter, end_iter)