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