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