1''' 2AT-SPI interface viewer plugin. 3 4@author: Eitan Isaacson 5@organization: Mozilla Foundation 6@copyright: Copyright (c) 2007 Mozilla Foundation 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''' 13 14import gi 15 16from gi.repository import Gtk as gtk 17from gi.repository import GdkPixbuf 18from gi.repository import Pango 19from gi.repository.GLib import markup_escape_text 20 21import pyatspi 22import os.path 23 24from accerciser.plugin import ViewportPlugin 25from accerciser.icons import getIcon 26from accerciser import node 27from accerciser.i18n import _, N_, DOMAIN 28from xml.dom import minidom 29 30UI_FILE = os.path.join(os.path.dirname(__file__), 31 'interface_view.ui') 32 33class InterfaceViewer(ViewportPlugin): 34 ''' 35 Interface Viewer plugin class. 36 37 @ivar label_role: Top plugin label that displays the role name and 38 the accessible name. 39 @type label_role: gtk.Label 40 @ivar sections: List of L{_InterfaceSection} instances. 41 @type sections: list 42 ''' 43 # Translators: this is a plugin name 44 plugin_name = N_('Interface Viewer') 45 plugin_name_localized = _(plugin_name) 46 # Translators: this is a plugin description 47 plugin_description = N_('Allows viewing of various interface properties') 48 49 def init(self): 50 ''' 51 Intialize plugin. 52 ''' 53 # HACK: Put all the callbacks in this class. 54 dom = minidom.parse(UI_FILE) 55 callbacks= set([signal.getAttribute('handler') \ 56 for signal in dom.getElementsByTagName('signal')]) 57 del dom 58 59 ui_xml = gtk.Builder() 60 ui_xml.set_translation_domain(DOMAIN) 61 ui_xml.add_from_file(UI_FILE) 62 frame = ui_xml.get_object('iface_view_frame') 63 self.label_role = ui_xml.get_object('label_role') 64 self.plugin_area.add(frame) 65 self.sections = [ 66 _SectionAccessible(ui_xml, self.node), 67 _SectionAction(ui_xml, self.node), 68 _SectionApplication(ui_xml, self.node), 69 _SectionComponent(ui_xml, self.node), 70 _SectionDocument(ui_xml, self.node), 71 _SectionHyperlink(ui_xml, self.node), 72 _SectionHypertext(ui_xml, self.node), 73 _SectionImage(ui_xml, self.node), 74 _SectionSelection(ui_xml, self.node), 75 _SectionStreamableContent(ui_xml, self.node), 76 _SectionTable(ui_xml, self.node), 77 _SectionText(ui_xml, self.node), 78 _SectionValue(ui_xml, self.node), 79 _SectionCollection(ui_xml, self.node), 80 _SectionDesktop(ui_xml, self.node), 81 _SectionLoginHelper(ui_xml, self.node)] 82 83 # HACK: Add callbacks to this class. 84 for cb in callbacks: 85 for section in self.sections: 86 method = getattr(section, cb, None) 87 if not method: continue 88 setattr(self, cb, method) 89 90 ui_xml.connect_signals(self) 91 92 # Mark all expanders with no associated section classes as unimplemented 93 implemented_expanders = [s.expander for s in self.sections] 94 vbox_ifaces = ui_xml.get_object('vbox_ifaces') 95 96 for expander in vbox_ifaces.get_children(): 97 if expander not in implemented_expanders: 98 iface_name = \ 99 expander.get_label().lower().replace(' ', '').replace('_', '') 100 section = _InterfaceSection(ui_xml, self.node, iface_name) 101 section.disable() 102 103 pyatspi.Registry.registerEventListener( 104 self.onAccNameChanged, 'object:property-change:accessible-name') 105 106 def onAccNameChanged(self, event): 107 ''' 108 Listener for accessible name changes, if it is ours, change the name. 109 110 @param event: 'object:property-change:accessible-name' event. 111 @type acc: Accessibility.Event 112 ''' 113 if event.source != self.node.acc: 114 return 115 116 role = self.node.acc.getRoleName() 117 name = self.node.acc.name 118 if name: 119 role_name = '%s: %s' % (role, name) 120 else: 121 role_name = role 122 self.label_role.set_markup('<b>%s</b>' % markup_escape_text(role_name)) 123 124 def onAccChanged(self, acc): 125 ''' 126 Method that is invoked when the main accessible selection s changed. 127 128 @param acc: New accessible 129 @type acc: Accessibility.Accessible 130 ''' 131 role = acc.getRoleName() 132 name = acc.name 133 if name: 134 role_name = '%s: %s' % (role, name) 135 else: 136 role_name = role 137 self.label_role.set_markup('<b>%s</b>' % markup_escape_text(role_name)) 138 interfaces = pyatspi.listInterfaces(acc) 139 for section_obj in self.sections: 140 section_obj.disable() 141 if section_obj.interface_name in interfaces: 142 section_obj.enable(acc) 143 144class _InterfaceSection(object): 145 ''' 146 An abstract class that defines the interface for interface sections. 147 148 @cvar interface_name: Name of interface this section is for. 149 @type interface_name: string 150 151 @ivar node: Application-wide L{Node}. 152 @type node: L{Node} 153 @ivar expander: The section expander widget. 154 @type expander: gtk.Expander 155 @ivar event_listeners: List of client and event pairs that are 156 registered by this section. They are typically registered an population time, 157 and alwas deregistered on L{clearUI}. 158 @type event_listeners: list 159 ''' 160 interface_name = None 161 def __init__(self, ui_xml, node, interface_name=None): 162 ''' 163 Initialize section object. and call init() for derived classes. 164 165 @param ui_xml: Interface viewer glade xml. 166 @type ui_xml: gtk.glade.XML 167 @param node: Application-wide node of selected accessible. 168 @type node: L{Node} 169 @param interface_name: Override default interface name. 170 @type interface_name: string 171 ''' 172 self.interface_name = interface_name or self.interface_name 173 self.node = node 174 self.expander = \ 175 ui_xml.get_object('expander_%s' % self.interface_name.lower()) 176 self._setExpanderChildrenSensitive(False) 177 self.event_listeners = [] 178 self.init(ui_xml) 179 180 def init(self, ui_xml): 181 ''' 182 Abtract method for initializing section-specific code. 183 184 @param ui_xml: Interface viewer glade xml. 185 @type ui_xml: gtk.glade.XML 186 ''' 187 pass 188 189 def enable(self, acc): 190 ''' 191 Make section sensitive and populate it. 192 193 @param acc: Accessible to use for information when populating section. 194 @type acc: Accessibility.Accessible 195 ''' 196 self._setExpanderChildrenSensitive(True) 197 self.populateUI(acc) 198 199 def populateUI(self, acc): 200 ''' 201 Abstract method for section specific populating code. 202 203 @param acc: Accessible to use for information when populating section. 204 @type acc: Accessibility.Accessible 205 ''' 206 pass 207 208 def disable(self): 209 ''' 210 Disable section, make insensitive. 211 ''' 212 self._setExpanderChildrenSensitive(False) 213 for client, event_names in self.event_listeners: 214 pyatspi.Registry.deregisterEventListener(client, *event_names) 215 self.clearUI() 216 217 def clearUI(self): 218 ''' 219 Abstract method for section-specific cleanup. 220 ''' 221 pass 222 223 def _setExpanderChildrenSensitive(self, sensitive, expander=None): 224 ''' 225 Convinience method for making the expander's children insensitive. 226 We don't want tomake the expander itself insensitive because the user might 227 still want to keep it open or close it when it is disabled. 228 229 @param sensitive: True for sensitive. 230 @type sensitive: boolean 231 @param expander: Expander widget. Uses instances expander by default. 232 @type expander: gtk.Expander 233 ''' 234 expander = expander or self.expander 235 label = expander.get_label_widget() 236 label_text = label.get_label() 237 not_implemented_str = _('(not implemented)') 238 239 if sensitive: 240 label_text = label_text.replace(not_implemented_str, '') 241 label_text = label_text.strip(' ') 242 elif not not_implemented_str in label_text: 243 label_text = label_text + ' ' + not_implemented_str 244 label.set_label(label_text) 245 for child in expander.get_children(): 246 child.set_sensitive(sensitive) 247 248 def _isSelectedInView(self, selection): 249 ''' 250 Convinience method for determining if a given treeview selection has any 251 selected nodes. 252 253 @param selection: Selection to check. 254 @type selection: gtk.TreeSelection 255 256 @return: True is something is selected 257 @rtype: boolean 258 ''' 259 model, rows = selection.get_selected_rows() 260 something_is_selected = bool(rows) 261 return something_is_selected 262 263 def _onViewSelectionChanged(self, selection, *widgets): 264 ''' 265 Convinience callback for selection changes. Useful for setting given 266 widgets to be sensitive only when something is selected, for example 267 action buttons. 268 269 @param selection: The selection object that triggered the callback. 270 @type selection: gtk.TreeSelection 271 @param widgets: list of widgets that should be made sensitive/insensitive 272 on selection changes. 273 @type widgets: list of gtk.Widget 274 ''' 275 for widget in widgets: 276 widget.set_sensitive(self._isSelectedInView(selection)) 277 278 def registerEventListener(self, client, *event_names): 279 pyatspi.Registry.registerEventListener(client, *event_names) 280 self.event_listeners.append((client, event_names)) 281 282class _SectionAccessible(_InterfaceSection): 283 ''' 284 A class that populates an Accessible interface section. 285 286 @ivar states_model: Model for accessible states. 287 @type states_model: gtk.ListStore 288 @ivar relations_view: Tree view for accessible relations. 289 @type relations_view: gtk.TreeView 290 @ivar relations_model: Model for accessible relations. 291 @type relations_view: gtk.TreeStore 292 @ivar header_bg: Background color for relation header. 293 @type header_bg: gtk.gdk.Color 294 @ivar relation_bg: Background color for relation row. 295 @type relation_bg: gtk.gdk.Color 296 @ivar attr_model: Model for accessible attributes. 297 @type attr_model: gtk.ListStore 298 ''' 299 300 interface_name = 'Accessible' 301 302 def init(self, ui_xml): 303 ''' 304 Initialization that is specific to the Accessible interface 305 (construct data models, connect signals to callbacks, etc.) 306 307 @param ui_xml: Interface viewer glade xml. 308 @type ui_xml: gtk.glade.XML 309 ''' 310 # Child count and description labels 311 self.child_count_label = ui_xml.get_object('label_acc_child count') 312 self.desc_label = ui_xml.get_object('label_acc_desc') 313 self.id_label = ui_xml.get_object('label_acc_id') 314 315 # configure states tree view 316 self.states_model = ui_xml.get_object('states_liststore') 317 318 # configure relations tree view 319 self.relations_view = ui_xml.get_object('relations_view') 320 self.relations_model = ui_xml.get_object('relations_treestore') 321 # preset the different bg colors 322 style = self.relations_view.get_style_context() 323 self.header_bg = style.get_background_color(gtk.StateFlags.NORMAL).to_string() 324 self.relation_bg = style.get_background_color(gtk.StateFlags.NORMAL).to_string() 325 326 selection = self.relations_view.get_selection() 327 show_button = ui_xml.get_object('button_relation_show') 328 show_button.set_sensitive(self._isSelectedInView(selection)) 329 selection.connect('changed', self._onViewSelectionChanged, show_button) 330 331 # configure accessible attributes tree view 332 self.attr_model = ui_xml.get_object('accattrib_liststore') 333 334 def populateUI(self, acc): 335 ''' 336 Populate the Accessible section with relevant data of the 337 currently selected accessible. 338 339 @param acc: The currently selected accessible. 340 @type acc: Accessibility.Accessible 341 ''' 342 343 self.child_count_label.set_text(str(acc.childCount)) 344 self.desc_label.set_label(acc.description or _('(no description)')) 345 try: 346 self.id_label.set_label(acc.accessibleId) 347 except: 348 self.id_label.set_label(_('(no ID)')) 349 350 states = [pyatspi.stateToString(s) for s in acc.getState().getStates()] 351 states.sort() 352 list(map(self.states_model.append, [[state] for state in states])) 353 354 try: 355 attribs = acc.getAttributes() 356 except: 357 pass 358 else: 359 for attr in attribs: 360 name, value = attr.split(':', 1) 361 self.attr_model.append([name, value]) 362 363 relations = acc.getRelationSet() 364 for relation in relations: 365 r_type_name = repr(relation.getRelationType()).replace('RELATION_', '') 366 r_type_name = r_type_name.replace('_', ' ').lower().capitalize() 367 iter = self.relations_model.append( 368 None, [None, 369 markup_escape_text(r_type_name), -1, 370 self.header_bg, False]) 371 for i in range(relation.getNTargets()): 372 acc = relation.getTarget(i) 373 self.relations_model.append( 374 iter, [getIcon(acc), 375 markup_escape_text(acc.name), i, 376 self.relation_bg, True]) 377 self.relations_view.expand_all() 378 379 self.registerEventListener(self._accEventState, 'object:state-changed') 380 381 def clearUI(self): 382 ''' 383 Clear all section-specific data. 384 ''' 385 self.relations_model.clear() 386 self.states_model.clear() 387 self.attr_model.clear() 388 389 def _relationSelectFunc(self, path): 390 ''' 391 Make relation-type headers unselectable. 392 393 @param path: The path about to be selected 394 @type path: tuple 395 396 @return: True if selectable 397 @rtype: boolean 398 ''' 399 return not len(path) == 1 400 401 def _onRelationShow(self, relations_view, *more_args): 402 ''' 403 Callback for row activation or button press. Selects the related 404 accessible in the main application. 405 406 @param relations_view: The relations treeview. 407 @type relations_view: gtk.TreeView 408 @param *more_args: More arguments that are provided by variuos types of 409 signals, but we discard them all. 410 @type *more_args: list 411 ''' 412 selection = relations_view.get_selection() 413 model, iter = selection.get_selected() 414 if iter and model[iter][2] >= 0: 415 path = model.get_path(iter) 416 relations = self.node.acc.getRelationSet() 417 acc = relations[path[0]].getTarget(model[iter][2]) 418 if acc: 419 self.node.update(acc) 420 421 def _accEventState(self, event): 422 ''' 423 Callback for accessible state changes. Repopulates the states model. 424 425 @param event: Event that triggered this callback. 426 @type event: Accessibility.Event 427 ''' 428 if self.node.acc == event.source: 429 self.states_model.clear() 430 try: 431 states = [pyatspi.stateToString(s) for s in \ 432 self.node.acc.getState().getStates()] 433 except LookupError: 434 # Maybe we got a defunct state, in which case the object is diseased. 435 states = [] 436 states.sort() 437 list(map(self.states_model.append, [[state] for state in states])) 438 439 440class _SectionAction(_InterfaceSection): 441 ''' 442 A class that populates an Action interface section. 443 444 @ivar actions_model: Model for accessible states. 445 @type actions_model: gtk.ListStore 446 @ivar action_selection: Current selection of actions tree view. 447 @type action_selection: gtk.TreeSelection 448 ''' 449 interface_name = 'Action' 450 def init(self, ui_xml): 451 ''' 452 Initialization that is specific to the Action interface 453 (construct data models, connect signals to callbacks, etc.) 454 455 @param ui_xml: Interface viewer glade xml. 456 @type ui_xml: gtk.glade.XML 457 ''' 458 # configure actions tree view 459 treeview = ui_xml.get_object('treeview_action') 460 self.actions_model = treeview.get_model() 461 self.action_selection = treeview.get_selection() 462 show_button = ui_xml.get_object('button_action_do') 463 show_button.set_sensitive(self._isSelectedInView(self.action_selection)) 464 self.action_selection.connect('changed', 465 self._onViewSelectionChanged, show_button) 466 467 def populateUI(self, acc): 468 ''' 469 Populate the Action section with relevant data of the 470 currently selected accessible. 471 472 @param acc: The currently selected accessible. 473 @type acc: Accessibility.Accessible 474 ''' 475 ai = acc.queryAction() 476 for i in range(ai.nActions): 477 self.actions_model.append([i, ai.getName(i), 478 ai.getDescription(i), 479 ai.getKeyBinding(i)]) 480 481 def clearUI(self): 482 ''' 483 Clear all section-specific data. 484 ''' 485 self.actions_model.clear() 486 487 def _onActionRowActivated(self, treeview, path, view_column): 488 ''' 489 Callback for row activation in action treeview. Performs actions. 490 491 @param treeview: Actions tree view. 492 @type treeview: gtk.TreeView 493 @param path: Path of activated role. 494 @type path: tuple 495 @param view_column: The column that was clicked 496 @type view_column: integer 497 ''' 498 action_num = self.actions_model[path][0] 499 ai = self.node.acc.queryAction() 500 ai.doAction(action_num) 501 502 def _onActionClicked(self, button): 503 ''' 504 Callback for "do action" button. Performs action of currently selected row. 505 506 @param button: The button that was pressed. 507 @type button: gtk.Button 508 ''' 509 actions_model, iter = self.action_selection.get_selected() 510 action_num = actions_model[iter][0] 511 ai = self.node.acc.queryAction() 512 ai.doAction(action_num) 513 514class _SectionApplication(_InterfaceSection): 515 ''' 516 A class that populates an Application interface section. 517 518 @ivar label_id: Label that displays application id info. 519 @type label_id: gtk.Label 520 @ivar label_tk: Label for toolkit name. 521 @type label_tk: gtk.Label 522 @ivar label_version: Label for toolkit version. 523 @type label_version: gtk.Label 524 ''' 525 interface_name = 'Application' 526 def init(self, ui_xml): 527 ''' 528 Initialization that is specific to the Application interface 529 (construct data models, connect signals to callbacks, etc.) 530 531 @param ui_xml: Interface viewer glade xml. 532 @type ui_xml: gtk.glade.XML 533 ''' 534 self.label_id = ui_xml.get_object('label_app_id') 535 self.label_tk = ui_xml.get_object('label_app_tk') 536 self.label_version = ui_xml.get_object('label_app_version') 537 538 def populateUI(self, acc): 539 ''' 540 Populate the Application section with relevant data of the 541 currently selected accessible. 542 543 @param acc: The currently selected accessible. 544 @type acc: Accessibility.Accessible 545 ''' 546 ai = acc.queryApplication() 547 self.label_id.set_text(repr(ai.id)) 548 self.label_tk.set_text(ai.toolkitName) 549 self.label_version.set_text(ai.version) 550 551 def clearUI(self): 552 ''' 553 Clear all section-specific data. 554 ''' 555 self.label_id.set_text('') 556 self.label_tk.set_text('') 557 self.label_version.set_text('') 558 559class _SectionComponent(_InterfaceSection): 560 ''' 561 A class that populates a Component interface section. 562 563 @ivar label_posrel: Relative position label 564 @type label_posrel: gtk.Label 565 @ivar label_posabs: Absolute position label 566 @type label_posabs: gtk.Label 567 @ivar label_layer: Layer label 568 @type label_layer: gtk.Label 569 @ivar label_zorder: Z-order label 570 @type label_zorder: gtk.Label 571 @ivar label_alpha: Alpha label 572 @type label_alpha: gtk.Label 573 ''' 574 interface_name = 'Component' 575 def init(self, ui_xml): 576 ''' 577 Initialization that is specific to the Component interface 578 (construct data models, connect signals to callbacks, etc.) 579 580 @param ui_xml: Interface viewer glade xml. 581 @type ui_xml: gtk.glade.XML 582 ''' 583 self.label_posabs = ui_xml.get_object('absolute_position_label') 584 self.label_posrel = ui_xml.get_object('relative_position_label') 585 self.label_size = ui_xml.get_object('size_label') 586 self.label_layer = ui_xml.get_object('layer_label') 587 self.label_zorder = ui_xml.get_object('zorder_label') 588 self.label_alpha = ui_xml.get_object('alpha_label') 589 590 def populateUI(self, acc): 591 ''' 592 Populate the Component section with relevant data of the 593 currently selected accessible. 594 595 @param acc: The currently selected accessible. 596 @type acc: Accessibility.Accessible 597 ''' 598 ci = acc.queryComponent() 599 bbox = ci.getExtents(pyatspi.DESKTOP_COORDS) 600 self.label_posabs.set_text('%d, %d' % (bbox.x, bbox.y)) 601 self.label_size.set_text('%dx%d' % (bbox.width, bbox.height)) 602 bbox = ci.getExtents(pyatspi.WINDOW_COORDS) 603 self.label_posrel.set_text('%d, %d' % (bbox.x, bbox.y)) 604 layer = ci.getLayer() 605 self.label_layer.set_text(repr(ci.getLayer()).replace('LAYER_', '')) 606 self.label_zorder.set_text(repr(ci.getMDIZOrder())) 607 self.label_alpha.set_text(repr(ci.getAlpha())) 608 self.registerEventListener(self._accEventComponent, 609 'object:bounds-changed', 610 'object:visible-data-changed') 611 def clearUI(self): 612 ''' 613 Clear all section-specific data. 614 ''' 615 self.label_posrel.set_text('') 616 self.label_posabs.set_text('') 617 self.label_size.set_text('') 618 self.label_layer.set_text('') 619 self.label_zorder.set_text('') 620 self.label_alpha.set_text('') 621 622 def _accEventComponent(self, event): 623 ''' 624 Callback for whenever any of the component attributes change. 625 626 @param event: Evnt that triggered this callback. 627 @type event: Accessibility.Event 628 ''' 629 if event.source == self.node.acc: 630 self.populateUI(event.source) 631 632class _SectionDocument(_InterfaceSection): 633 interface_name = 'Document' 634 ''' 635 A class that populates an Component interface section. 636 637 @ivar attr_model: Attribute data model 638 @type attr_model: gtk.ListStore 639 @ivar label_locale: Locale label 640 @type label_locale: gtk.Label 641 ''' 642 643 def init(self, ui_xml): 644 ''' 645 Initialization that is specific to the Document interface 646 (construct data models, connect signals to callbacks, etc.) 647 648 @param ui_xml: Interface viewer glade xml. 649 @type ui_xml: gtk.glade.XML 650 ''' 651 # configure document attributes tree view 652 self.attr_model = ui_xml.get_object('docattrib_liststore') 653 self.label_locale = ui_xml.get_object('label_doc_locale') 654 655 def populateUI(self, acc): 656 ''' 657 Populate the Document section with relevant data of the 658 currently selected accessible. 659 660 @param acc: The currently selected accessible. 661 @type acc: Accessibility.Accessible 662 ''' 663 di = acc.queryDocument() 664 665 self.label_locale.set_text(di.getLocale()) 666 667 try: 668 attribs = di.getAttributes() 669 except: 670 attribs = None 671 if attribs: 672 for attr in attribs: 673 name, value = attr.split(':', 1) 674 self.attr_model.append([name, value]) 675 676 def clearUI(self): 677 ''' 678 Clear all section-specific data. 679 ''' 680 self.attr_model.clear() 681 self.label_locale.set_text('') 682 683class _SectionHyperlink(_InterfaceSection): 684 interface_name = 'Hyperlink' 685 ''' 686 A placeholder class for Hyperlink interface section. 687 ''' 688 689class _SectionCollection(_InterfaceSection): 690 interface_name = 'Collection' 691 ''' 692 A placeholder class for Collection interface section. 693 ''' 694 695class _SectionDesktop(_InterfaceSection): 696 interface_name = 'Desktop' 697 ''' 698 A placeholder class for Desktop interface section. 699 ''' 700 701class _SectionLoginHelper(_InterfaceSection): 702 interface_name = 'LoginHelper' 703 ''' 704 A placeholder class for LoginHelper interface section. 705 ''' 706 707class _SectionHypertext(_InterfaceSection): 708 ''' 709 A class that populates an Hypertext interface section. 710 711 @ivar links_model: Data model for available links. 712 @type links_model: gtk.ListStore 713 ''' 714 interface_name = 'Hypertext' 715 def init(self, ui_xml): 716 ''' 717 Initialization that is specific to the Hypertext interface 718 (construct data models, connect signals to callbacks, etc.) 719 720 @param ui_xml: Interface viewer glade xml. 721 @type ui_xml: gtk.glade.XML 722 ''' 723 # configure links tree view 724 treeview = ui_xml.get_object('treeview_links') 725 # It's a treestore because of potential multiple anchors 726 self.links_model = gtk.TreeStore(int, # Link index 727 str, # Name 728 str, # Description 729 str, # URI 730 int, # Start offset 731 int, # End offset 732 object) # Anchor object 733 treeview.set_model(self.links_model) 734 735 crt = gtk.CellRendererText() 736 tvc = gtk.TreeViewColumn(_('Name')) 737 tvc.set_sizing(gtk.TreeViewColumnSizing.AUTOSIZE) 738 tvc.set_resizable(True) 739 tvc.pack_start(crt, True) 740 tvc.add_attribute(crt, 'text', 1) 741 treeview.append_column(tvc) 742 743 crt = gtk.CellRendererText() 744 tvc = gtk.TreeViewColumn(_('URI')) 745 tvc.set_sizing(gtk.TreeViewColumnSizing.AUTOSIZE) 746 tvc.set_resizable(True) 747 tvc.pack_start(crt, True) 748 tvc.add_attribute(crt, 'text', 3) 749 treeview.append_column(tvc) 750 751 crt = gtk.CellRendererText() 752 tvc = gtk.TreeViewColumn(_('Start')) 753 tvc.set_sizing(gtk.TreeViewColumnSizing.AUTOSIZE) 754 tvc.set_resizable(True) 755 tvc.pack_start(crt, True) 756 tvc.add_attribute(crt, 'text', 4) 757 treeview.append_column(tvc) 758 759 crt = gtk.CellRendererText() 760 tvc = gtk.TreeViewColumn(_('End')) 761 tvc.set_sizing(gtk.TreeViewColumnSizing.AUTOSIZE) 762 tvc.set_resizable(True) 763 tvc.pack_start(crt, True) 764 tvc.add_attribute(crt, 'text', 5) 765 treeview.append_column(tvc) 766 767 selection = treeview.get_selection() 768 show_button = ui_xml.get_object('button_hypertext_show') 769 show_button.set_sensitive(self._isSelectedInView(selection)) 770 selection.connect('changed', self._onViewSelectionChanged, show_button) 771 772 773 def populateUI(self, acc): 774 ''' 775 Populate the Hypertext section with relevant data of the 776 currently selected accessible. 777 778 @param acc: The currently selected accessible. 779 @type acc: Accessibility.Accessible 780 ''' 781 hti = acc.queryHypertext() 782 783 for link_index in range(hti.getNLinks()): 784 link = hti.getLink(link_index) 785 iter = self.links_model.append(None, 786 [link_index, 787 '', '', '', 788 link.startIndex, 789 link.endIndex, None]) 790 for anchor_index in range(link.nAnchors): 791 acc_obj = link.getObject(anchor_index) 792 self.links_model.append(iter, 793 [link_index, acc_obj.name, acc_obj.description, 794 link.getURI(anchor_index), 795 link.startIndex, link.endIndex, acc_obj]) 796 if anchor_index == 0: 797 self.links_model[iter][1] = \ 798 acc_obj.name # Otherwise the link is nameless. 799 800 801 def clearUI(self): 802 ''' 803 Clear all section-specific data. 804 ''' 805 self.links_model.clear() 806 807 def _onLinkShow(self, link_view, *more_args): 808 ''' 809 Callback for row activation or button press. Selects the related 810 link accessible in the main application. 811 812 @param link_view: The links tree view. 813 @type link_view: gtk.TreeView 814 @param *more_args: More arguments that are provided by variuos types of 815 signals, but we discard them all. 816 @type *more_args: list 817 ''' 818 selection = link_view.get_selection() 819 model, iter = selection.get_selected() 820 if iter: 821 acc = model[iter][6] 822 if acc: 823 self.node.update(acc) 824 825 826class _SectionImage(_InterfaceSection): 827 ''' 828 A class that populates an Image interface section. 829 830 @ivar label_pos: Position label 831 @type label_pos: gtk.Label 832 @ivar label_size: Size label 833 @type label_size: gtk.Label 834 ''' 835 interface_name = 'Image' 836 837 def init(self, ui_xml): 838 ''' 839 Initialization that is specific to the Image interface 840 (construct data models, connect signals to callbacks, etc.) 841 842 @param ui_xml: Interface viewer glade xml. 843 @type ui_xml: gtk.glade.XML 844 ''' 845 self.label_pos = ui_xml.get_object('img_position_label') 846 self.label_size = ui_xml.get_object('img_size_label') 847 self.label_locale = ui_xml.get_object('img_locale_label') 848 self.label_desc = ui_xml.get_object('img_locale_label') 849 850 def populateUI(self, acc): 851 ''' 852 Populate the Image section with relevant data of the 853 currently selected accessible. 854 855 @param acc: The currently selected accessible. 856 @type acc: Accessibility.Accessible 857 ''' 858 ii = acc.queryImage() 859 860 bbox = ii.getImageExtents(pyatspi.DESKTOP_COORDS) 861 self.label_pos.set_text('%d, %d' % (bbox.x, bbox.y)) 862 self.label_size.set_text('%dx%d' % (bbox.width, bbox.height)) 863 self.label_desc.set_label(ii.imageDescription or \ 864 _('(no description)')) 865 self.label_locale.set_text(ii.imageLocale) 866 def clearUI(self): 867 ''' 868 Clear all section-specific data. 869 ''' 870 self.label_pos.set_text('') 871 self.label_size.set_text('') 872 873class _SectionSelection(_InterfaceSection): 874 ''' 875 A class that populates a Selection interface section. 876 877 @ivar sel_model: Data model for child selection options. 878 @type sel_model: gtk.ListStore 879 @ivar sel_selection: Selection in selection treeview. 880 @type sel_selection: gtk.TreeSelection 881 @ivar button_select_all: Button for selecting all of the selection nodes. 882 @type button_select_all: gtk.Button 883 ''' 884 interface_name = 'Selection' 885 886 def init(self, ui_xml): 887 ''' 888 Initialization that is specific to the Selection interface 889 (construct data models, connect signals to callbacks, etc.) 890 891 @param ui_xml: Interface viewer glade xml. 892 @type ui_xml: gtk.glade.XML 893 ''' 894 # configure selection tree view 895 treeview = ui_xml.get_object('treeview_selection') 896 self.sel_model = gtk.ListStore(GdkPixbuf.Pixbuf, str, object) 897 treeview.set_model(self.sel_model) 898 # connect selection changed signal 899 self.sel_selection = treeview.get_selection() 900 show_button = ui_xml.get_object('button_select_clear') 901 show_button.set_sensitive(self._isSelectedInView(self.sel_selection)) 902 self.sel_selection.connect('changed', 903 self._onViewSelectionChanged, 904 show_button) 905 self.sel_selection.connect('changed', 906 self._onSelectionSelected) 907 self.button_select_all = ui_xml.get_object('button_select_all') 908 909 def populateUI(self, acc): 910 ''' 911 Populate the Selection section with relevant data of the 912 currently selected accessible. 913 914 @param acc: The currently selected accessible. 915 @type acc: Accessibility.Accessible 916 ''' 917 if acc.childCount > 50: 918 theme = gtk.IconTheme.get_default() 919 self.sel_model.append( 920 [theme.load_icon('gtk-dialog-warning', 24, 921 gtk.IconLookupFlags.USE_BUILTIN), 922 _('Too many selectable children'), None]) 923 # Set section as insensitive, but leave expander label sensitive. 924 section_widgets = self.expander.get_children() 925 section_widgets.remove(self.expander.get_label_widget()) 926 for child in section_widgets: 927 child.set_sensitive(False) 928 return 929 930 for child in acc: 931 if child is not None: 932 state = child.getState() 933 if state.contains(pyatspi.STATE_SELECTABLE): 934 self.sel_model.append([getIcon(child), child.name, child]) 935 936 state = acc.getState() 937 multiple_selections = state.contains(pyatspi.STATE_MULTISELECTABLE) 938 939 self.button_select_all.set_sensitive(multiple_selections) 940 941 if multiple_selections: 942 self.sel_selection.set_mode = gtk.SelectionMode.MULTIPLE 943 else: 944 self.sel_selection.set_mode = gtk.SelectionMode.SINGLE 945 946 def _onSelectionSelected(self, selection): 947 ''' 948 Callback for selection change in the selection treeview. Confusing? 949 950 @param selection: The treeview's selection object. 951 @type selection: gtk.TreeSelection 952 ''' 953 try: 954 si = self.node.acc.querySelection() 955 except NotImplementedError: 956 return 957 958 model, paths = selection.get_selected_rows() 959 selected_children = [model.get_value(model.get_iter(path), 2).getIndexInParent() for path in paths] 960 961 for child_index in range(len(self.node.acc)): 962 if child_index in selected_children: 963 si.selectChild(child_index) 964 else: 965 si.deselectChild(child_index) 966 967 def clearUI(self): 968 ''' 969 Clear all section-specific data. 970 ''' 971 self.sel_model.clear() 972 973 def _onSelectionClear(self, widget): 974 ''' 975 Callback for selection clear button. 976 977 @param widget: Widget that triggered callback. 978 @type widget: gtk.Widget 979 ''' 980 si = self.node.acc.querySelection() 981 982 self.sel_selection.unselect_all() 983 si.clearSelection() 984 985 def _onSelectAll(self, widget): 986 ''' 987 Callback for selection select all button. 988 989 @param widget: Widget that triggered callback. 990 @type widget: gtk.Widget 991 ''' 992 si = self.node.acc.querySelection() 993 si.selectAll() 994 995class _SectionStreamableContent(_InterfaceSection): 996 ''' 997 A class that populates a StreamableContent interface section. 998 999 @ivar streams_model: Data model for available streams. 1000 @type streams_model: gtk.ListStore 1001 ''' 1002 interface_name = 'StreamableContent' 1003 1004 def init(self, ui_xml): 1005 ''' 1006 Initialization that is specific to the StreamableContent interface 1007 (construct data models, connect signals to callbacks, etc.) 1008 1009 @param ui_xml: Interface viewer glade xml. 1010 @type ui_xml: gtk.glade.XML 1011 ''' 1012 # configure streamable content tree view 1013 self.streams_model = ui_xml.get_object('streams_liststore') 1014 1015 def populateUI(self, acc): 1016 ''' 1017 Populate the StreamableContent section with relevant data of the 1018 currently selected accessible. 1019 1020 @param acc: The currently selected accessible. 1021 @type acc: Accessibility.Accessible 1022 ''' 1023 sci = acc.queryStreamableContent() 1024 1025 for content_type in sci.getContentTypes(): 1026 self.streams_model.append([content_type, 1027 sci.getURI(content_type)]) 1028 1029 def clearUI(self): 1030 ''' 1031 Clear all section-specific data. 1032 ''' 1033 self.streams_model.clear() 1034 1035class _SectionTable(_InterfaceSection): 1036 ''' 1037 A class that populates a Selection interface section. 1038 1039 @ivar selected_frame: Container frame for selected cell info. 1040 @type selected_frame: gtk.Frame 1041 @ivar caption_label: Caption label. 1042 @type caption_label: gtk.Label 1043 @ivar summary_label: Summary label. 1044 @type summary_label: gtk.Label 1045 @ivar rows_label: Rows label. 1046 @type rows_label: gtk.Label 1047 @ivar columns_label: Columns label. 1048 @type columns_label: gtk.Label 1049 @ivar srows_label: Selected rows label. 1050 @type srows_label: gtk.Label 1051 @ivar scolumns_label: Scolumns label. 1052 @type scolumns_label: gtk.Label 1053 @ivar row_ext_label: Row extents label. 1054 @type row_ext_label: gtk.Label 1055 @ivar col_ext_label: Column extents label. 1056 @type col_ext_label: gtk.Label 1057 @ivar hrow_button: Row header button. 1058 @type hrow_button: gtk.Button 1059 @ivar hcol_button: Column header button. 1060 @type hcol_button: gtk.Button 1061 @ivar cell_button: Cell button. 1062 @type cell_button: gtk.Button 1063 ''' 1064 interface_name = 'Table' 1065 def init(self, ui_xml): 1066 ''' 1067 Initialization that is specific to the Table interface 1068 (construct data models, connect signals to callbacks, etc.) 1069 1070 @param ui_xml: Interface viewer glade xml. 1071 @type ui_xml: gtk.glade.XML 1072 ''' 1073 self.selected_frame = ui_xml.get_object('selected_cell_frame') 1074 self.caption_label = ui_xml.get_object('table_caption_label') 1075 self.summary_label = ui_xml.get_object('table_summary_label') 1076 self.rows_label = ui_xml.get_object('table_rows_label') 1077 self.columns_label = ui_xml.get_object('table_columns_label') 1078 self.srows_label = ui_xml.get_object('table_srows_label') 1079 self.scolumns_label = ui_xml.get_object('table_scolumns_label') 1080 self.row_ext_label = ui_xml.get_object('table_row_extents') 1081 self.col_ext_label = ui_xml.get_object('table_column_extents') 1082 self.col_ext_label = ui_xml.get_object('table_column_extents') 1083 self.hrow_button = ui_xml.get_object('table_hrow_button') 1084 self.hcol_button = ui_xml.get_object('table_hcol_button') 1085 self.cell_button = ui_xml.get_object('table_cell_button') 1086 1087 def populateUI(self, acc): 1088 ''' 1089 Populate the Table section with relevant data of the 1090 currently selected accessible. 1091 1092 @param acc: The currently selected accessible. 1093 @type acc: Accessibility.Accessible 1094 ''' 1095 ti = acc.queryTable() 1096 self.selected_frame.set_sensitive(False) 1097 for attr, label in [(ti.caption, self.caption_label), 1098 (ti.summary, self.summary_label), 1099 (ti.nRows, self.rows_label), 1100 (ti.nColumns, self.columns_label), 1101 (ti.nSelectedRows, self.srows_label), 1102 (ti.nSelectedColumns, self.scolumns_label)]: 1103 label.set_text(str(attr)) 1104 self.registerEventListener(self._accEventTable, 1105 'object:active-descendant-changed') 1106 1107 def clearUI(self): 1108 ''' 1109 Clear all section-specific data. 1110 ''' 1111 self.caption_label.set_text('') 1112 self.summary_label.set_text('') 1113 self.rows_label.set_text('') 1114 self.columns_label.set_text('') 1115 self.srows_label.set_text('') 1116 self.scolumns_label.set_text('') 1117 self.row_ext_label.set_text('') 1118 self.col_ext_label.set_text('') 1119 self.col_ext_label.set_text('') 1120 self.hrow_button.set_label('') 1121 self.hcol_button.set_label('') 1122 self.cell_button.set_label('') 1123 1124 1125 def _accEventTable(self, event): 1126 ''' 1127 Callback for 'object:active-descendant-changed' to detect selected cell 1128 changes. 1129 1130 @param event: The event that triggered the callback. 1131 @type event: Accessibility.Event 1132 ''' 1133 if self.node.acc != event.source: 1134 return 1135 1136 acc = event.source 1137 ti = acc.queryTable() 1138 self.selected_frame.set_sensitive(True) 1139 is_cell, row, column, rextents, cextents, selected = \ 1140 ti.getRowColumnExtentsAtIndex(event.any_data.getIndexInParent()) 1141 1142 for attr, label in [(rextents, self.row_ext_label), 1143 (cextents, self.col_ext_label), 1144 (ti.nSelectedRows, self.srows_label), 1145 (ti.nSelectedColumns, self.scolumns_label)]: 1146 label.set_text(str(attr)) 1147 1148 for desc, acc, button in [(ti.getRowDescription(row), 1149 ti.getRowHeader(row), 1150 self.hrow_button), 1151 (ti.getColumnDescription(column), 1152 ti.getColumnHeader(column), 1153 self.hcol_button), 1154 ('%s (%s, %s)' % (event.any_data, row, column), 1155 event.any_data, 1156 self.cell_button)]: 1157 button.set_label(str(desc or '<no description>')) 1158 button.set_sensitive(bool(acc)) 1159 setattr(button, 'acc', acc) 1160 1161 def _onTableButtonClicked(self, button): 1162 ''' 1163 Callback for buttons that represent headers. 1164 Will make the header the main application's selection. 1165 1166 @param button: Button that triggered event. 1167 @type button: gtk.Button 1168 ''' 1169 self.node.update(getattr(button, 'acc')) 1170 1171class _SectionText(_InterfaceSection): 1172 ''' 1173 A class that populates a Text interface section. 1174 1175 @ivar attr_model: Data model for text attributes. 1176 @type attr_model: gtk.ListStore 1177 @ivar offset_spin: Offset spinner. 1178 @type offset_spin: gtk.SpinButton 1179 @ivar text_view: Text view of provided text. 1180 @type text_view: gtk.TextView 1181 @ivar text_buffer: Text buffer of provided text. 1182 @type text_buffer: gtk.TextBuffer 1183 @ivar toggle_defaults: Toggle button for viewing default text attributes. 1184 @type toggle_defaults: gtk.CheckButton 1185 @ivar label_start: Label for current attributes start offset. 1186 @type label_start: gtk.Label 1187 @ivar label_end: Label for current attributes end offset. 1188 @type label_end: gtk.Label 1189 @ivar _text_insert_handler: Handler ID for text insert events. 1190 @type _text_insert_handler: integer 1191 @ivar _text_delete_handler: Handler ID for text delete events. 1192 @type _text_delete_handler: integer 1193 @ivar outgoing_calls: Cached outgoing calls to avoid circular event 1194 invocation. 1195 @type outgoing_calls: dictionary 1196 ''' 1197 interface_name = 'Text' 1198 def init(self, ui_xml): 1199 ''' 1200 Initialization that is specific to the Text interface 1201 (construct data models, connect signals to callbacks, etc.) 1202 1203 @param ui_xml: Interface viewer glade xml. 1204 @type ui_xml: gtk.glade.XML 1205 ''' 1206 # configure text attribute tree view 1207 self.attr_model = ui_xml.get_object('textattrib_liststore') 1208 1209 self.offset_spin = ui_xml.get_object('spinbutton_text_offset') 1210 self.text_view = ui_xml.get_object('textview_text') 1211 pango_ctx = self.text_view.get_pango_context() 1212 for f in pango_ctx.list_families(): 1213 name = f.get_name() 1214 # These are known to show e.g U+FFFC 1215 if name in [ "Courier New", "Liberation Sans" ]: 1216 self.text_view.modify_font(Pango.FontDescription(name)) 1217 break 1218 1219 self.text_buffer = self.text_view.get_buffer() 1220 self.toggle_defaults = ui_xml.get_object('checkbutton_text_defaults') 1221 self.label_start = ui_xml.get_object('label_text_attr_start') 1222 self.label_end = ui_xml.get_object('label_text_attr_end') 1223 1224 self._text_insert_handler = 0 1225 self._text_delete_handler = 0 1226 1227 mark = self.text_buffer.create_mark('attr_mark', 1228 self.text_buffer.get_start_iter(), True) 1229 self.text_buffer.create_tag('attr_region', foreground='red') 1230 1231 self.text_buffer.connect('mark-set', self._onTextMarkSet) 1232 1233 mark.set_visible(True) 1234 1235 1236 self.text_buffer.connect('modified-changed', 1237 self._onTextModified) 1238 self.text_buffer.connect('notify::cursor-position', 1239 self._onTextCursorMove) 1240 1241 self.text_buffer.set_modified(False) 1242 1243 # Initialize fifos to help eliminate the viscous cycle of signals. 1244 # It would be nice if we could just block/unblock it like in gtk, but 1245 # since it is IPC, asynchronous and not atomic, we are forced to do this. 1246 self.outgoing_calls = {'itext_insert': self.CallCache(), 1247 'itext_delete': self.CallCache()} 1248 1249 def populateUI(self, acc): 1250 ''' 1251 Populate the Text section with relevant data of the 1252 currently selected accessible. 1253 1254 @param acc: The currently selected accessible. 1255 @type acc: Accessibility.Accessible 1256 ''' 1257 self.offset_spin.set_value(0) 1258 1259 ti = acc.queryText() 1260 1261 text = ti.getText(0, ti.characterCount) 1262 self.text_buffer.set_text(text) 1263 1264 self.offset_spin.get_adjustment().upper = ti.characterCount 1265 1266 self.popTextAttr(offset=0) 1267 1268 try: 1269 eti = acc.queryEditableText() 1270 except: 1271 eti = None 1272 1273 expander_label = self.expander.get_label_widget() 1274 label_text = expander_label.get_label() 1275 label_text = label_text.replace(_('(Editable)'), '') 1276 label_text = label_text.strip(' ') 1277 if eti and acc.getState().contains(pyatspi.STATE_EDITABLE): 1278 label_text += ' ' + _('(Editable)') 1279 self.text_view.set_editable(True) 1280 else: 1281 self.text_view.set_editable(False) 1282 expander_label.set_label(label_text) 1283 1284 self._text_insert_handler = self.text_buffer.connect('insert-text', 1285 self._onITextInsert) 1286 self._text_delete_handler = self.text_buffer.connect('delete-range', 1287 self._onITextDelete) 1288 1289 self.registerEventListener(self._accEventText, 1290 'object:text-changed') 1291 1292 def clearUI(self): 1293 ''' 1294 Clear all section-specific data. 1295 ''' 1296 if self._text_insert_handler: 1297 self.text_buffer.disconnect(self._text_insert_handler) 1298 self._text_insert_handler = 0 1299 if self._text_delete_handler: 1300 self.text_buffer.disconnect(self._text_delete_handler) 1301 self._text_delete_handler = 0 1302 1303 self.offset_spin.set_value(0) 1304 self.label_start.set_text('') 1305 self.label_end.set_text('') 1306 self.text_buffer.set_text('') 1307 self.attr_model.clear() 1308 1309 def _attrStringToDict(self, attr_string): 1310 ''' 1311 Convinience method for converting attribute strings to dictionaries. 1312 1313 @param attr_string: "key:value" pairs seperated by semi-colons. 1314 @type attr_string: string 1315 1316 @return: Dictionary of attributes 1317 @rtype: dictionary 1318 ''' 1319 if not attr_string: 1320 return {} 1321 attr_dict = {} 1322 for attr_pair in attr_string.split(';'): 1323 key, value = attr_pair.split(':', 1) 1324 if ((key[0]==' ') and (len(key) > 0)): #at-spi 1 1325 key = key[1:] 1326 attr_dict[key] = value 1327 return attr_dict 1328 1329 def _onTextModified(self, text_buffer): 1330 ''' 1331 Callback that is triggered when the main text buffer is modified. 1332 Allows to adjust the spinners bounds accordingly. 1333 1334 @param text_buffer: The section's main text buffer. 1335 @type text_buffer: gtk.TextBuffer 1336 ''' 1337 self.offset_spin.get_adjustment().upper = text_buffer.get_char_count() 1338 self.offset_spin.set_range(0, text_buffer.get_char_count()) 1339 text_buffer.set_modified(False) 1340 1341 def _onTextMarkSet(self, text_buffer, iter, text_mark): 1342 ''' 1343 Callback that is triggered when the attribute mark is moved to 1344 allow repopulation of the attributes view. 1345 1346 @param text_buffer: The section's main text buffer. 1347 @type text_buffer: gtk.TextBuffer 1348 @param iter: Text iter of the new mark position. 1349 @type iter: gtk.TextIter 1350 @param text_mark: The text mark that was moved. 1351 @type text_mark: gtk.TextMark 1352 ''' 1353 self.popTextAttr() 1354 1355 def _onTextSpinnerChanged(self, spinner): 1356 ''' 1357 Callback for hen the spinner's value changes. 1358 Moves attribute mark accordingly. 1359 1360 @param spinner: The marker offset spinner. 1361 @type spinner: gtk.SpinButton 1362 ''' 1363 iter = self.text_buffer.get_iter_at_offset(int(self.offset_spin.get_value())) 1364 self.text_buffer.move_mark_by_name('attr_mark', iter) 1365 1366 def _onDefaultsToggled(self, toggle_button): 1367 ''' 1368 Callback for when the "defaults" checkbutton is toggled. Re-populates 1369 attributes view. 1370 1371 @param toggle_button: The defaults checkbutton 1372 @type toggle_button: gtk.CheckButton 1373 ''' 1374 self.popTextAttr() 1375 1376 def popTextAttr(self, offset=None): 1377 ''' 1378 Populate the attributes view with attributes at the given offset, or at 1379 the attribute mark. 1380 1381 @param offset: Offset of wanted attributes. If none is given, 1382 use attribute mark's offset. 1383 @type offset: integer 1384 ''' 1385 try: 1386 ti = self.node.acc.queryText() 1387 except: 1388 return 1389 1390 if offset is None: 1391 mark = self.text_buffer.get_mark('attr_mark') 1392 iter = self.text_buffer.get_iter_at_mark(mark) 1393 offset = iter.get_offset() 1394 1395 show_default = self.toggle_defaults.get_active() 1396 attr, start, end = ti.getAttributes(offset) 1397 if show_default: 1398 def_attr = ti.getDefaultAttributes() 1399 attr_dict = self._attrStringToDict(def_attr) 1400 attr_dict.update(self._attrStringToDict(attr)) 1401 else: 1402 attr_dict = self._attrStringToDict(attr) 1403 1404 attr_list = list(attr_dict.keys()) 1405 attr_list.sort() 1406 1407 self.attr_model.clear() 1408 for attr in attr_list: 1409 self.attr_model.append([attr, attr_dict[attr]]) 1410 1411 self.text_buffer.remove_tag_by_name( 1412 'attr_region', 1413 self.text_buffer.get_start_iter(), 1414 self.text_buffer.get_end_iter()) 1415 self.text_buffer.apply_tag_by_name( 1416 'attr_region', 1417 self.text_buffer.get_iter_at_offset(start), 1418 self.text_buffer.get_iter_at_offset(end)) 1419 1420 # Translators: This string appears in Accerciser's Interface Viewer 1421 # and refers to a range of characters which has a particular format. 1422 # "Start" is the character offset where the formatting begins. If 1423 # the first four letters of some text is bold, the start offset of 1424 # that bold formatting is 0. 1425 self.label_start.set_markup(_('Start: %d') % start) 1426 # Translators: This string appears in Accerciser's Interface Viewer 1427 # and refers to a range of characters which has a particular format. 1428 # "End" is the character offset where the formatting ends. If the 1429 # first four letters of some text is bold, the end offset of that 1430 # bold formatting is 4. 1431 self.label_end.set_markup(_('End: %d') % end) 1432 1433 def _onTextViewPressed(self, widget, event): 1434 ''' 1435 Update spin button in the case that the textview was clicked. 1436 Once the spin button's value changes, it's own callback fires which 1437 re-populates the attribute view. 1438 1439 @param widget: The widget that recived the click event, 1440 typically the text view. 1441 @type widget: gtk.Widget 1442 @param event: The click event. 1443 @type event: gtk.gdk.Event 1444 ''' 1445 if event.button != 1: 1446 return 1447 1448 x, y = event.get_coords() 1449 x, y = self.text_view.window_to_buffer_coords(gtk.TextWindowType.WIDGET, 1450 int(x), int(y)) 1451 iter = self.text_view.get_iter_at_location(x, y) 1452 1453 self.offset_spin.set_value(iter.get_offset()) 1454 1455 def _onTextCursorMove(self, text_buffer, param_spec): 1456 ''' 1457 Update spinner when input cursor moves. 1458 1459 @param text_buffer: The section's main text buffer. 1460 @type text_buffer: gtk.TextBuffer 1461 @param param_spec: Some gobject crud 1462 @type param_spec: object 1463 ''' 1464 1465 self.offset_spin.set_value(text_buffer.get_property('cursor-position')) 1466 1467 s = text_buffer.get_selection_bounds() 1468 if s != (): 1469 # Highlight selected text 1470 try: 1471 start,end = s 1472 startOffset = start.get_offset() 1473 endOffset = end.get_offset() 1474 text = self.node.acc.queryText() 1475 (x, y, width, height) = text.getRangeExtents(startOffset, endOffset, pyatspi.DESKTOP_COORDS) 1476 ah = node._HighLight(x, y, width, height, 1477 node.FILL_COLOR, node.FILL_ALPHA, 1478 node.BORDER_COLOR, node.BORDER_ALPHA, 1479 2.0, 0) 1480 ah.highlight(node.HL_DURATION) 1481 except: 1482 pass 1483 1484 def _accEventText(self, event): 1485 ''' 1486 Callback for accessible text changes. Updates the text buffer accordingly. 1487 1488 @param event: Event that triggered thi callback. 1489 @type event: Accessibility.Event 1490 ''' 1491 if self.node.acc != event.source: 1492 return 1493 1494 if event.type.major == 'text-changed': 1495 text_iter = self.text_buffer.get_iter_at_offset(event.detail1) 1496 if event.type.minor == 'insert': 1497 call = (event.detail1, event.any_data, event.detail2) 1498 if self.outgoing_calls['itext_insert'].isCached(call): 1499 return 1500 self.text_buffer.handler_block(self._text_insert_handler) 1501 self.text_buffer.insert(text_iter, event.any_data) 1502 self.text_buffer.handler_unblock(self._text_insert_handler) 1503 1504 elif event.type.minor == 'delete': 1505 call = (event.detail1, event.detail1 + event.detail2) 1506 if self.outgoing_calls['itext_delete'].isCached(call): 1507 return 1508 text_iter_end = \ 1509 self.text_buffer.get_iter_at_offset(event.detail1 + event.detail2) 1510 self.text_buffer.handler_block(self._text_delete_handler) 1511 self.text_buffer.delete(text_iter, text_iter_end) 1512 self.text_buffer.handler_unblock(self._text_delete_handler) 1513 1514 def _onITextInsert(self, text_buffer, iter, text, length): 1515 ''' 1516 Callback for text inserts in the text buffer. Sends changes to examined 1517 accessible. 1518 1519 @param text_buffer: The section's main text buffer. 1520 @type text_buffer: gtk.TextBuffer 1521 @param iter: Text iter in which the insert occured. 1522 @type iter: gtk.TextIter 1523 @param text: The text that was inserted 1524 @type text: string 1525 @param length: The length of theinserted text. 1526 @type length: integer 1527 ''' 1528 try: 1529 eti = self.node.acc.queryEditableText() 1530 except: 1531 return 1532 1533 call = (iter.get_offset(), text, length) 1534 1535 self.outgoing_calls['itext_insert'].append(call) 1536 eti.insertText(*call) 1537 1538 def _onITextDelete(self, text_buffer, start, end): 1539 ''' 1540 Callback for text deletes in the text buffer. Sends changes to examined 1541 accessible. 1542 1543 @param text_buffer: The section's main text buffer. 1544 @type text_buffer: gtk.TextBuffer 1545 @param start: The start offset of the delete action. 1546 @type start: integer 1547 @param end: The end offset of the delete action. 1548 @type end: integer 1549 ''' 1550 try: 1551 eti = self.node.acc.queryEditableText() 1552 except: 1553 return 1554 1555 call = (start.get_offset(), end.get_offset()) 1556 1557 self.outgoing_calls['itext_delete'].append(call) 1558 eti.deleteText(*call) 1559 1560 def _onTextFocusChanged(self, text_view, event): 1561 ''' 1562 Callback for leaving and entering focus from the textview, 1563 it hides/shows the attribute marker. When the textview has focus 1564 there is no need for the marker to be visible because of th input 1565 cursor. 1566 1567 @param text_view: The text view that is being entered or leaved. 1568 @type text_view: gtk.TextView 1569 @param event: The focus event. 1570 @type event: gtk.gdk.Event 1571 ''' 1572 mark = self.text_buffer.get_mark('attr_mark') 1573 mark.set_visible(not event.in_) 1574 1575 class CallCache(list): 1576 ''' 1577 A list derivative that provides a method for checking if something 1578 is in the list and removing it at the same time. 1579 ''' 1580 def isCached(self, obj): 1581 ''' 1582 Checks if a certain object is in this list instance. If it is, return 1583 True and remove it. 1584 1585 @param obj: Object to check for. 1586 @type obj: object 1587 1588 @return: True if it is in the list. 1589 @rtype: boolean 1590 ''' 1591 if obj in self: 1592 self.remove(obj) 1593 return True 1594 else: 1595 return False 1596 1597class _SectionValue(_InterfaceSection): 1598 ''' 1599 A class that populates a Value interface section. 1600 1601 @ivar spinbutton: Value spinner. 1602 @type spinbutton: gtk.SpinButton 1603 @ivar label_max: Label of maximal value. 1604 @type label_max: gtk.Label 1605 @ivar label_min: Label of minimal value. 1606 @type label_min: gtk.Label 1607 @ivar label_inc: Label of minimal value increment. 1608 @type label_inc: gtk.Label 1609 ''' 1610 interface_name = 'Value' 1611 def init(self, ui_xml): 1612 ''' 1613 Initialization that is specific to the Value interface 1614 (construct data models, connect signals to callbacks, etc.) 1615 1616 @param ui_xml: Interface viewer glade xml. 1617 @type ui_xml: gtk.glade.XML 1618 ''' 1619 self.spinbutton = ui_xml.get_object('spinbutton_value') 1620 self.label_max = ui_xml.get_object('label_value_max') 1621 self.label_min = ui_xml.get_object('label_value_min') 1622 self.label_inc = ui_xml.get_object('label_value_inc') 1623 self.registerEventListener(self._accEventValue, 1624 'object:value-changed') 1625 1626 def populateUI(self, acc): 1627 ''' 1628 Populate the Value section with relevant data of the 1629 currently selected accessible. 1630 1631 @param acc: The currently selected accessible. 1632 @type acc: Accessibility.Accessible 1633 ''' 1634 vi = acc.queryValue() 1635 self.label_max.set_text(str(vi.maximumValue)) 1636 self.label_min.set_text(str(vi.minimumValue)) 1637 self.label_inc.set_text(str(vi.minimumIncrement)) 1638 1639 minimumIncrement = vi.minimumIncrement 1640 digits = 0 1641 1642 while minimumIncrement - int(minimumIncrement) != 0: 1643 digits += 1 1644 minimumIncrement *= 10 1645 1646 # Calling set_range will clamp the value of spinbutton to the allowable 1647 # range, causing us to try to set the value of the accessible when we 1648 # really shouldn't. 1649 self.ignore_value_changes = True 1650 self.spinbutton.set_range(vi.minimumValue, vi.maximumValue) 1651 self.ignore_value_changes = False 1652 self.spinbutton.set_value(vi.currentValue) 1653 self.spinbutton.set_digits(digits) 1654 1655 def _onValueSpinnerChange(self, spinner): 1656 ''' 1657 Callback for spinner changes. Updates accessible. 1658 1659 @param spinner: The Value spinner 1660 @type spinner: gtk.SpinButton 1661 ''' 1662 if self.ignore_value_changes: return 1663 vi = self.node.acc.queryValue() 1664 vi.currentValue = spinner.get_value() 1665 1666 def _accEventValue(self, event): 1667 ''' 1668 Callback for value changes from the accessible. Update spin button. 1669 1670 @param event: The event that triggered the callback. 1671 @type event: Accessibility.Event 1672 ''' 1673 if self.node.acc != event.source: 1674 return 1675 vi = self.node.acc.queryValue() 1676 if self.spinbutton.get_value() != vi.currentValue: 1677 self.spinbutton.set_value(vi.currentValue) 1678