1<?xml version="1.0" encoding="utf-8"?>
2<page xmlns="http://projectmallard.org/1.0/" type="topic" id="howto_write_a_plugin" xml:lang="ca">
3  <info>
4    <link type="guide" xref="index#advanced"/>
5    <title type="sort">3. Escriure connectors nous</title>
6    <link type="next" xref="preferences"/>
7    <desc>
8      Extending <app>Accerciser</app> with your desired functionalities
9    </desc>
10    <credit type="author">
11      <name>Eitan Isaacson</name>
12      <email>eitan@ascender.com</email>
13    </credit>
14    <credit type="author">
15      <name>Peter Parente</name>
16      <email>pparent@us.ibm.com</email>
17    </credit>
18    <credit type="author">
19      <name>Aline Bessa</name>
20      <email>alibezz@gmail.com</email>
21    </credit>
22    <license>
23      <p>Creative Commons Compartir Igual 3.0</p>
24    </license>
25
26    <mal:credit xmlns:mal="http://projectmallard.org/1.0/" type="translator copyright">
27      <mal:name>Joan Duran</mal:name>
28      <mal:email>jodufi@gmail.com</mal:email>
29      <mal:years>2007-2009</mal:years>
30    </mal:credit>
31  </info>
32  <title>Escriure connectors per l'<app>Accerciser</app></title>
33  <p>
34    Extending <app>Accerciser</app> with new plugins became much simpler
35    since <link href="https://wiki.gnome.org/Apps/Accerciser/PluginTutorial">this tutorial</link>
36    was written. Given that it provides a very detailed explanation about the topic,
37    this section consists basically of its original content - only simple editions
38    and updates were done.
39  </p>
40  <p>
41    <app>Accerciser</app> supports three basic types of plugins:
42  </p>
43  <list>
44    <item>
45      <p>
46        Base plugins - These plugins are derived from the Plugin base class. They do not provide
47        a visible interface, but could provide additional functionality to <app>Accerciser</app>.
48      </p>
49    </item>
50    <item>
51      <p>
52        Console plugins - These plugins provide simple console output into a text area in a plugin
53        tab. Not to be confused with the packaged <link xref="ipython_plugin">IPython Console Plugin</link>.
54      </p>
55    </item>
56    <item>
57      <p>
58        Viewport plugins - The majority of <app>Accerciser</app> default plugins.
59        They provide a custom graphical interface in a tab.
60      </p>
61    </item>
62  </list>
63
64  <section id="base_plugin">
65    <title>Creating a Base Plugin</title>
66    <p>
67      We will create a simplified version of the <link xref="quick_select_plugin">Quick Select Plugin</link>.
68      This plugin will select the last focused accessible when pressing <keyseq><key>ctrl</key><key>alt</key><key>e</key></keyseq>.
69    </p>
70    <p>
71      First off, the import lines we will use are:
72    </p>
73    <code>
74      from accerciser.plugin import Plugin
75      import gtk
76      import pyatspi
77    </code>
78    <p>
79      Next we will derive a new class from the Plugin base class,
80      and assign some mandatory class attributes:
81    </p>
82    <code>
83      class FocusSelect(Plugin):
84        plugin_name = 'Focus Select'
85        plugin_description = 'Allows selecting last focused accessible.'
86    </code>
87    <p>
88      We will now override the init method, in which we will set a global key
89      action for selecting the last focused accessible item, register an event
90      listener for the "focus" event, and set the <cmd>last_focused</cmd> instance
91      variable to <cmd>None</cmd>.
92    </p>
93    <code>
94      def init(self):
95        pyatspi.Registry.registerEventListener(self.accEventFocusChanged, 'focus')
96        self.global_hotkeys = [('Inspect last focused accessible',
97                                self.inspectLastFocused,
98                                gtk.keysyms.e,
99                                gtk.gdk.CONTROL_MASK | gtk.gdk.MOD1_MASK)]
100        self.last_focused = None
101    </code>
102    <p>
103      Notice that the <cmd>global_hotkeys</cmd> instance variable is a list of
104      tuples. Each tuple is a global hotkey action, composed by an action description,
105      a desired method to call, a key symbol of keypress, and a key modifier mask.
106    </p>
107    <p>
108      In the "focus" event callback, we assign the <cmd>last_focused</cmd> instance
109      variable with the accessible item that has just emitted the "focus" event.
110    </p>
111    <code>
112      def accEventFocusChanged(self, event):
113        if not self.isMyApp(event.source):
114          self.last_focused = event.source
115    </code>
116    <p>
117       In the hotkey action callback, we update the application wide node with the
118       last focused accessible item, if we have recorded it:
119    </p>
120    <code>
121      def inspectLastFocused(self):
122        if self.last_focused:
123          self.node.update(self.last_focused)
124    </code>
125  </section>
126  <section id="console_plugin">
127    <title>Creating a Console Plugin</title>
128    <p>
129      We will create a console plugin to display focus changes emitted by an accessible
130      item with a "push button" role - remember that it is easy to check what is the role
131      of any item with <app>Accerciser</app>; you can verify it in the <link xref="desktop_tree_view">Application Tree View</link>,
132      for example.
133    </p>
134    <p>
135      The needed import lines are:
136    </p>
137    <code>
138      from accerciser.plugin import ConsolePlugin
139      import pyatspi
140    </code>
141    <p>
142      Then we add a class definition, with a plugin name and description:
143    </p>
144    <code>
145      class PushButtonFocus(ConsolePlugin):
146        plugin_name = 'Push Button Focus'
147        plugin_description = 'Print event when pushbutton get\'s focus.'
148    </code>
149    <p>
150      We override the init method adding a register listener:
151    </p>
152    <code>
153       def init(self):
154         pyatspi.Registry.registerEventListener(self.accEventFocusChanged, 'focus')
155    </code>
156    <p>
157      In the callback method, all push button events are printed.
158    </p>
159    <code>
160      def accEventFocusChanged(self, event):
161        if event.source.getRole() == pyatspi.ROLE_PUSH_BUTTON:
162          self.appendText(str(event)+'\n')
163    </code>
164  </section>
165  <section id="viewport_plugin">
166    <title>Creating a Viewport Plugin</title>
167    <p>
168      We will create a viewport plugin that allows quick testing of the
169      "click" action in accessible items that support the AT-SPI Action interface
170      and have an action named "click". It will be a simple button that, once
171      clicked, does the "click" action in the accessible.
172    </p>
173    <p>
174      First off, some mandatory import lines:
175    </p>
176    <code>
177      import gtk
178      from accerciser.plugin import ViewportPlugin
179    </code>
180    <p>
181      Next, a class definition, with a name and description:
182    </p>
183    <code>
184      class Clicker(ViewportPlugin):
185        plugin_name = 'Clicker'
186        plugin_description = 'Test the "click" action in relevant accessibles.'
187    </code>
188    <p>
189      We override the init method with some UI building, and connecting a callback
190      to a signal for the button. We use the alignment container to allow the button
191      to be centered in the plugin tab, and not monstrously take up the entire plugin
192      space. Notice that the <cmd>plugin_area</cmd> instance variable contains a gtk.Frame
193      that could be populated with all the plugin's widgets.
194    </p>
195    <code>
196       def init(self):
197         alignment = gtk.Alignment(0.5,0.5,0,0)
198         self.click_button = gtk.Button('Click me!')
199         alignment.add(self.click_button)
200         self.plugin_area.add(alignment)
201
202         self.click_button.connect('clicked', self.onClick)
203
204         self.show_all()
205    </code>
206    <p>
207       We also created a convenience method that returns a list of supported actions
208       of the currently selected accessible item - if it does not support the Action
209       interface, it returns an empty list:
210    </p>
211    <code>
212       def accSupportedActions(self):
213       try:
214         ai = self.node.acc.queryAction()
215       except NotImplementedError:
216         action_names = []
217       else:
218         action_names = [ai.getName(i) for i in xrange(ai.nActions)]
219       return action_names
220    </code>
221    <p>
222      The base plugin class has a method call onAccChanged that is called everytime the
223      target application's selected accessible item changes. We will override it setting
224      the button to be sensitive only when the current accessible item has the "click" action:
225    </p>
226    <code>
227       def onAccChanged(self, acc):
228         has_click = 'click' in self.accSupportedActions()
229         self.click_button.set_sensitive(has_click)
230    </code>
231    <p>
232      The callback method for button "clicked" performs the "click" action on the accessible item.
233      Since this callback could only be called when the button is sensitive, we don't need to worry
234      about checking if the current accessible has the "click" action?
235    </p>
236    <code>
237      def onClick(self, button):
238        ai = self.node.acc.queryAction()
239        action_names = [ai.getName(i) for i in xrange(ai.nActions)]
240        ai.doAction(action_names.index('click'))
241    </code>
242   </section>
243</page>
244