Writing Plugins for Accerciser

Extending Accerciser with new plugins became much simpler since this tutorial was written. Given that it provides a very detailed explanation about the topic, this section consists basically of its original content - only simple editions and updates were done.

Accerciser supports three basic types of plugins:

  • Base plugins - These plugins are derived from the Plugin base class. They do not provide a visible interface, but could provide additional functionality to Accerciser.

  • Console plugins - These plugins provide simple console output into a text area in a plugin tab. Not to be confused with the packaged IPython Console Plugin.

  • Viewport plugins - The majority of Accerciser default plugins. They provide a custom graphical interface in a tab.

Creating a Base Plugin

We will create a simplified version of the Quick Select Plugin. This plugin will select the last focused accessible when pressing ctrl+alt+e.

First off, the import lines we will use are:

      from accerciser.plugin import Plugin
      import gtk
      import pyatspi
    

Next we will derive a new class from the Plugin base class, and assign some mandatory class attributes:

      class FocusSelect(Plugin):
        plugin_name = 'Focus Select'
        plugin_description = 'Allows selecting last focused accessible.'
    

We will now override the init method, in which we will set a global key action for selecting the last focused accessible item, register an event listener for the "focus" event, and set the last_focused instance variable to None.

      def init(self):
        pyatspi.Registry.registerEventListener(self.accEventFocusChanged, 'focus')
        self.global_hotkeys = [('Inspect last focused accessible',
                                self.inspectLastFocused,
                                gtk.keysyms.e,
                                gtk.gdk.CONTROL_MASK | gtk.gdk.MOD1_MASK)]
        self.last_focused = None
    

Notice that the global_hotkeys instance variable is a list of tuples. Each tuple is a global hotkey action, composed by an action description, a desired method to call, a key symbol of keypress, and a key modifier mask.

In the "focus" event callback, we assign the last_focused instance variable with the accessible item that has just emitted the "focus" event.

      def accEventFocusChanged(self, event):
        if not self.isMyApp(event.source):
          self.last_focused = event.source
    

In the hotkey action callback, we update the application wide node with the last focused accessible item, if we have recorded it:

      def inspectLastFocused(self):
        if self.last_focused:
          self.node.update(self.last_focused)
    

Creating a Console Plugin

We will create a console plugin to display focus changes emitted by an accessible item with a "push button" role - remember that it is easy to check what is the role of any item with Accerciser; you can verify it in the Application Tree View, for example.

The needed import lines are:

      from accerciser.plugin import ConsolePlugin
      import pyatspi
    

Then we add a class definition, with a plugin name and description:

      class PushButtonFocus(ConsolePlugin):
        plugin_name = 'Push Button Focus'
        plugin_description = 'Print event when pushbutton get\'s focus.'
    

We override the init method adding a register listener:

       def init(self):
         pyatspi.Registry.registerEventListener(self.accEventFocusChanged, 'focus')
    

In the callback method, all push button events are printed.

      def accEventFocusChanged(self, event):
        if event.source.getRole() == pyatspi.ROLE_PUSH_BUTTON:
          self.appendText(str(event)+'\n')
    

Creating a Viewport Plugin

We will create a viewport plugin that allows quick testing of the "click" action in accessible items that support the AT-SPI Action interface and have an action named "click". It will be a simple button that, once clicked, does the "click" action in the accessible.

First off, some mandatory import lines:

      import gtk
      from accerciser.plugin import ViewportPlugin
    

Next, a class definition, with a name and description:

      class Clicker(ViewportPlugin):
        plugin_name = 'Clicker'
        plugin_description = 'Test the "click" action in relevant accessibles.'
    

We override the init method with some UI building, and connecting a callback to a signal for the button. We use the alignment container to allow the button to be centered in the plugin tab, and not monstrously take up the entire plugin space. Notice that the plugin_area instance variable contains a gtk.Frame that could be populated with all the plugin's widgets.

       def init(self):
         alignment = gtk.Alignment(0.5,0.5,0,0)
         self.click_button = gtk.Button('Click me!')
         alignment.add(self.click_button)
         self.plugin_area.add(alignment)

         self.click_button.connect('clicked', self.onClick)

         self.show_all()
    

We also created a convenience method that returns a list of supported actions of the currently selected accessible item - if it does not support the Action interface, it returns an empty list:

       def accSupportedActions(self):
       try:
         ai = self.node.acc.queryAction()
       except NotImplementedError:
         action_names = []
       else:
         action_names = [ai.getName(i) for i in xrange(ai.nActions)]
       return action_names
    

The base plugin class has a method call onAccChanged that is called everytime the target application's selected accessible item changes. We will override it setting the button to be sensitive only when the current accessible item has the "click" action:

       def onAccChanged(self, acc):
         has_click = 'click' in self.accSupportedActions()
         self.click_button.set_sensitive(has_click)
    

The callback method for button "clicked" performs the "click" action on the accessible item. Since this callback could only be called when the button is sensitive, we don't need to worry about checking if the current accessible has the "click" action?

      def onClick(self, button):
        ai = self.node.acc.queryAction()
        action_names = [ai.getName(i) for i in xrange(ai.nActions)]
        ai.doAction(action_names.index('click'))