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