1''' 2Defines the manager for plugin layout and loading. 3 4@author: Eitan Isaacson 5@organization: IBM Corporation 6@copyright: Copyright (c) 2006, 2007 IBM Corporation 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 GLib 17from gi.repository import Gtk as gtk 18from gi.repository.Gio import Settings as GSettings 19 20from .base_plugin import Plugin 21from .view import ViewManager 22from accerciser.tools import ToolsAccessor, getTreePathBoundingBox 23from .message import MessageManager 24import os 25import sys 26import imp 27import traceback 28from accerciser.i18n import _, N_, C_ 29 30GSCHEMA = 'org.a11y.Accerciser' 31 32class PluginManager(gtk.ListStore, ToolsAccessor): 33 ''' 34 35 @cvar COL_INSTANCE: Instance column ID. 36 @type COL_INSTANCE: integer 37 @cvar COL_CLASS: Class column ID. 38 @type COL_CLASS: integer 39 @cvar COL_PATH: Module path column ID. 40 @type COL_PATH: integer 41 42 @ivar node: Application's selected accessible node. 43 @type node: L{Node} 44 @ivar hotkey_manager: Application's hotkey manager. 45 @type hotkey_manager: L{HotkeyManager} 46 @ivar view_manager: Plugin view manager. 47 @type view_manager: L{ViewManager} 48 @ivar message_manager: Plugin message manager. 49 @type message_manager: L{MessageManager} 50 51 ''' 52 COL_INSTANCE = 0 53 COL_CLASS = 1 54 COL_PATH = 2 55 def __init__(self, node, hotkey_manager, *main_views): 56 ''' 57 Initialize the plugin manager. 58 59 @param node: The application's main node. 60 @type node: L{Node} 61 @param hotkey_manager: Application's hot key manager. 62 @type hotkey_manager: L{HotkeyManager} 63 @param main_views: List of permanent plugin views. 64 @type main_views: list of {PluginView} 65 ''' 66 gtk.ListStore.__init__(self, 67 object, # Plugin instance 68 object, # Plugin class 69 str) # Plugin path 70 self.node = node 71 self.hotkey_manager = hotkey_manager 72 self.gsettings = GSettings.new(GSCHEMA) 73 self.view_manager = ViewManager(*main_views) 74 self.message_manager = MessageManager() 75 self.message_manager.connect('plugin-reload-request', 76 self._onPluginReloadRequest) 77 self.message_manager.connect('module-reload-request', 78 self._onModuleReloadRequest) 79 message_tab = self.message_manager.getMessageTab() 80 self.view_manager.addElement(message_tab) 81 self._row_changed_handler = \ 82 self.connect('row_changed', self._onPluginRowChanged) 83 self._loadPlugins() 84 85 def close(self): 86 ''' 87 Close view manager and plugins. 88 ''' 89 self.view_manager.close() 90 for row in self: 91 plugin = row[self.COL_INSTANCE] 92 if plugin: 93 plugin._close() 94 95 def _loadPlugins(self): 96 ''' 97 Load all plugins in global and local plugin paths. 98 ''' 99 # AQUI PETAA 100 for plugin_dir, plugin_fn in self._getPluginFiles(): 101 self._loadPluginFile(plugin_dir, plugin_fn) 102 self.view_manager.initialView() 103 104 def _getPluginFiles(self): 105 ''' 106 Get list of all modules in plugin paths. 107 108 @return: List of plugin files with their paths. 109 @rtype: tuple 110 ''' 111 plugin_file_list = [] 112 plugin_dir_local = os.path.join(GLib.get_user_data_dir(), 113 'accerciser', 'plugins') 114 plugin_dir_global = os.path.join(sys.prefix, 'share', 115 'accerciser', 'plugins') 116 for plugin_dir in (plugin_dir_local, plugin_dir_global): 117 if not os.path.isdir(plugin_dir): 118 continue 119 for fn in os.listdir(plugin_dir): 120 if fn.endswith('.py') and not fn.startswith('.'): 121 plugin_file_list.append((plugin_dir, fn[:-3])) 122 123 return plugin_file_list 124 125 def _getPluginLocals(self, plugin_dir, plugin_fn): 126 ''' 127 Get namespace of given module 128 129 @param plugin_dir: Path. 130 @type plugin_dir: string 131 @param plugin_fn: Module. 132 @type plugin_fn: string 133 134 @return: Dictionary of modules symbols. 135 @rtype: dictionary 136 ''' 137 sys.path.insert(0, plugin_dir) 138 try: 139 params = imp.find_module(plugin_fn, [plugin_dir]) 140 plugin = imp.load_module(plugin_fn, *params) 141 plugin_locals = plugin.__dict__ 142 except Exception as e: 143 self.message_manager.newModuleError(plugin_fn, plugin_dir, 144 traceback.format_exception_only(e.__class__, e)[0].strip(), 145 traceback.format_exc()) 146 return {} 147 sys.path.pop(0) 148 return plugin_locals 149 150 def _loadPluginFile(self, plugin_dir, plugin_fn): 151 ''' 152 Find plugin implementations in the given module, and store them. 153 154 @param plugin_dir: Path. 155 @type plugin_dir: string 156 @param plugin_fn: Module. 157 @type plugin_fn: string 158 ''' 159 plugin_locals = self._getPluginLocals(plugin_dir, plugin_fn) 160 # use keys list to avoid size changes during iteration 161 for symbol in list(plugin_locals.keys()): 162 try: 163 is_plugin = \ 164 issubclass(plugin_locals[symbol], Plugin) and \ 165 getattr(plugin_locals[symbol], 'plugin_name', None) 166 except TypeError: 167 continue 168 if is_plugin: 169 self.handler_block(self._row_changed_handler) 170 171 iter_id = self.append([None, plugin_locals[symbol], plugin_dir]) 172 self.handler_unblock(self._row_changed_handler) 173 # if a plugin class is found, initialize 174 disabled_list = self.gsettings.get_strv('disabled-plugins') 175 enabled = plugin_locals[symbol].plugin_name not in \ 176 disabled_list 177 if enabled: 178 self._enablePlugin(iter_id) 179 self.row_changed(self.get_path(iter_id), iter_id) 180 181 def _enablePlugin(self, iter): 182 ''' 183 Instantiate a plugin class pointed to by the given iter. 184 185 @param iter: Iter of plugin class we should instantiate. 186 @type iter: gtk.TreeIter 187 ''' 188 plugin_class = self[iter][self.COL_CLASS] 189 plugin_instance = None 190 try: 191 plugin_instance = plugin_class(self.node, self.message_manager) 192 plugin_instance.init() 193 for key_combo in plugin_instance.global_hotkeys: 194 self.hotkey_manager.addKeyCombo( 195 plugin_class.plugin_name, 196 plugin_class.plugin_name_localized or plugin_class.plugin_name 197 , *key_combo) 198 except Exception as e: 199 self.message_manager.newPluginError( 200 plugin_instance, plugin_class, 201 traceback.format_exception_only(e.__class__, e)[0].strip(), 202 traceback.format_exc()) 203 try: 204 plugin_instance._close() 205 except: 206 pass 207 return 208 self[iter][self.COL_INSTANCE] = plugin_instance 209 if isinstance(plugin_instance, gtk.Widget): 210 self.view_manager.addElement(plugin_instance) 211 plugin_instance.onAccChanged(plugin_instance.node.acc) 212 disabled_list = self.gsettings.get_strv('disabled-plugins') 213 if plugin_instance.plugin_name in disabled_list: 214 disabled_list.remove(plugin_instance.plugin_name) 215 self.gsettings.set_strv('disabled-plugins', disabled_list) 216 217 def _disablePlugin(self, iter): 218 ''' 219 Disable plugin pointed to by the given iter. 220 221 @param iter: Iter of plugin instance to be disabled. 222 @type iter: gtk.TreeIter 223 ''' 224 plugin_instance = self[iter][self.COL_INSTANCE] 225 if not plugin_instance: return 226 for key_combo in plugin_instance.global_hotkeys: 227 self.hotkey_manager.removeKeyCombo( 228 plugin_instance.plugin_name, *key_combo) 229 if isinstance(plugin_instance, gtk.Widget): 230 plugin_instance.destroy() 231 plugin_instance._close() 232 233 disabled_list = self.gsettings.get_strv('disabled-plugins') 234 if not plugin_instance.plugin_name in disabled_list: 235 disabled_list.append(plugin_instance.plugin_name) 236 self.gsettings.set_strv('disabled-plugins', disabled_list) 237 238 self[iter][self.COL_INSTANCE] = False 239 240 def _reloadPlugin(self, iter): 241 ''' 242 Reload plugin pointed to by the given iter. 243 244 @param iter: Iter of plugin to be reloaded. 245 @type iter: gtk.TreeIter 246 247 @return: New instance of plugin 248 @rtype: L{Plugin} 249 ''' 250 old_class = self[iter][self.COL_CLASS] 251 plugin_fn = old_class.__module__ 252 plugin_dir = self[iter][self.COL_PATH] 253 plugin_locals = self._getPluginLocals(plugin_dir, plugin_fn) 254 self[iter][self.COL_CLASS] = plugin_locals.get(old_class.__name__) 255 self._enablePlugin(iter) 256 return self[iter][self.COL_INSTANCE] 257 258 def _getIterWithClass(self, plugin_class): 259 ''' 260 Get iter with given plugin class. 261 262 @param plugin_class: The plugin class to search for. 263 @type plugin_class: type 264 265 @return: The first iter with the given class. 266 @rtype: gtk.TreeIter 267 ''' 268 for row in self: 269 if row[self.COL_CLASS] == plugin_class: 270 return row.iter 271 return None 272 273 def _onPluginReloadRequest(self, message_manager, message, plugin_class): 274 ''' 275 Callback for a plugin reload request from the message manager. 276 277 @param message_manager: The message manager that emitted the signal. 278 @type message_manager: L{MessageManager} 279 @param message: The message widget. 280 @type message: L{PluginMessage} 281 @param plugin_class: The plugin class that should be reloaded. 282 @type plugin_class: type 283 ''' 284 message.destroy() 285 iter = self._getIterWithClass(plugin_class) 286 if not iter: return 287 self._disablePlugin(iter) 288 plugin = self._reloadPlugin(iter) 289 if plugin: 290 self.view_manager.giveElementFocus(plugin) 291 292 def _onModuleReloadRequest(self, message_manager, message, module, path): 293 ''' 294 Callback for a module reload request from the message manager. 295 296 @param message_manager: The message manager that emitted the signal. 297 @type message_manager: L{MessageManager} 298 @param message: The message widget. 299 @type message: L{PluginMessage} 300 @param module: The module to be reloaded. 301 @type module: string 302 @param path: The path of the module. 303 @type path: string 304 ''' 305 message.destroy() 306 self._loadPluginFile(path, module) 307 308 def togglePlugin(self, path): 309 ''' 310 Toggle the plugin, either enable or disable depending on current state. 311 312 @param path: Tree path to plugin. 313 @type path: tuple 314 ''' 315 iter = self.get_iter(path) 316 if self[iter][self.COL_INSTANCE]: 317 self._disablePlugin(iter) 318 else: 319 self._reloadPlugin(iter) 320 321 def _onPluginRowChanged(self, model, path, iter): 322 ''' 323 Callback for model row changes. Persists plugins state (enabled/disabled) 324 in gsettings. 325 326 @param model: Current model, actually self. 327 @type model: gtk.ListStore 328 @param path: Tree path of changed row. 329 @type path: tuple 330 @param iter: Iter of changed row. 331 @type iter: gtk.TreeIter 332 ''' 333 plugin_class = model[iter][self.COL_CLASS] 334 if plugin_class is None: 335 return 336 plugin_instance = model[iter][self.COL_INSTANCE] 337 disabled_list = self.gsettings.get_strv('disabled-plugins') 338 if plugin_instance is None: 339 if plugin_class.plugin_name not in disabled_list: 340 disabled_list.append(plugin_class.plugin_name) 341 else: 342 if plugin_class.plugin_name in disabled_list: 343 disabled_list.remove(plugin_class.plugin_name) 344 345 def View(self): 346 ''' 347 Helps emulate a non-static inner class. These don't exist in python, 348 I think. 349 350 @return: An inner view class. 351 @rtype: L{PluginManager._View} 352 ''' 353 return self._View(self) 354 355 class _View(gtk.TreeView): 356 ''' 357 Implements a treeview of a {PluginManager} 358 359 @ivar plugin_manager: Plugin manager to use as data model. 360 @type plugin_manager: L{PluginManager} 361 @ivar view_manager: View manager to use for plugin view data. 362 @type view_manager: L{ViewManager} 363 ''' 364 def __init__(self, plugin_manager): 365 ''' 366 Initialize view. 367 368 @param plugin_manager: Plugin manager to use as data model. 369 @type plugin_manager: L{PluginManager} 370 ''' 371 gtk.TreeView.__init__(self) 372 self.plugin_manager = plugin_manager 373 self.view_manager = plugin_manager.view_manager 374 self.set_model(plugin_manager) 375 self.connect('button-press-event', self._onButtonPress) 376 self.connect('popup-menu', self._onPopupMenu) 377 378 crc = gtk.CellRendererToggle() 379 tvc = gtk.TreeViewColumn() 380 tvc.pack_start(crc, True) 381 tvc.set_cell_data_func(crc, self._pluginStateDataFunc) 382 crc.connect('toggled', self._onPluginToggled) 383 self.append_column(tvc) 384 385 crt = gtk.CellRendererText() 386 tvc = gtk.TreeViewColumn(_('Name')) 387 tvc.pack_start(crt, True) 388 tvc.set_cell_data_func(crt, self._pluginNameDataFunc) 389 self.append_column(tvc) 390 391 crc = gtk.CellRendererText() 392 # Translators: This is the viewport in which the plugin appears, 393 # it is a noun. 394 # 395 tvc = gtk.TreeViewColumn(C_('viewport', 'View')) 396 tvc.pack_start(crc, False) 397 tvc.set_cell_data_func(crc, self._viewNameDataFunc) 398 crc.set_property('editable', True) 399 crc.connect('edited', self._onViewChanged) 400 self.append_column(tvc) 401 402 def _onButtonPress(self, widget, event): 403 ''' 404 Callback for plugin view context menus. 405 406 @param widget: Widget that emitted signal. 407 @type widget: gtk.Widget 408 @param event: Event object. 409 @type event: gtk.gdk.Event 410 ''' 411 if event.button == 3: 412 path = self.get_path_at_pos(int(event.x), int(event.y))[0] 413 self._showPopup(event.button, event.time, path) 414 415 def _onPopupMenu(self, widget): 416 ''' 417 Callback for popup request event. Usually happens when keyboard 418 context menu os pressed. 419 420 @param widget: Widget that emitted signal. 421 @type widget: gtk.Widget 422 423 @return: Return true to stop event trickling. 424 @rtype: boolean 425 ''' 426 path, col = self.get_cursor() 427 rect = getTreePathBoundingBox(self, path, col) 428 self._showPopup(0, gtk.get_current_event_time(), 429 path, lambda m, r: (r.x, r.y, True), rect) 430 return True 431 432 433 def _showPopup(self, button, time, path, pos_func=None, data=None): 434 ''' 435 Convinience function for showing the view manager's popup menu. 436 437 @param button: Mouse button that was clicked. 438 @type button: integer 439 @param time: Time of event. 440 @type time: float 441 @param path: Tree path of context menu. 442 @type path: tuple 443 @param pos_func: Function to use for determining menu placement. 444 @type pos_func: callable 445 @param data: Additional data. 446 @type data: object 447 ''' 448 plugin = \ 449 self.plugin_manager[path][self.plugin_manager.COL_INSTANCE] 450 menu = self.view_manager.Menu(plugin, self.get_toplevel()) 451 menu.popup(None, None, pos_func, data, button, time) 452 453 def _viewNameDataFunc(self, column, cell, model, iter, foo=None): 454 ''' 455 Function for determining the displayed data in the tree's view column. 456 457 @param column: Column number. 458 @type column: integer 459 @param cell: Cellrender. 460 @type cell: gtk.CellRendererText 461 @param model: Tree's model 462 @type model: gtk.ListStore 463 @param iter: Tree iter of current row, 464 @type iter: gtk.TreeIter 465 ''' 466 plugin_class = model[iter][self.plugin_manager.COL_CLASS] 467 if issubclass(plugin_class, gtk.Widget): 468 view_name = \ 469 self.view_manager.getViewNameForPlugin(plugin_class.plugin_name) 470 cell.set_property('sensitive', True) 471 else: 472 view_name = N_('No view') 473 cell.set_property('sensitive', False) 474 cell.set_property('text', _(view_name)) 475 476 def _pluginNameDataFunc(self, column, cell, model, iter, foo=None): 477 ''' 478 Function for determining the displayed data in the tree's plugin column. 479 480 @param column: Column number. 481 @type column: integer 482 @param cell: Cellrender. 483 @type cell: gtk.CellRendererText 484 @param model: Tree's model 485 @type model: gtk.ListStore 486 @param iter: Tree iter of current row, 487 @type iter: gtk.TreeIter 488 ''' 489 plugin_class = model[iter][self.plugin_manager.COL_CLASS] 490 cell.set_property('text', plugin_class.plugin_name_localized or \ 491 plugin_class.plugin_name) 492 493 def _pluginStateDataFunc(self, column, cell, model, iter, foo=None): 494 ''' 495 Function for determining the displayed state of the plugin's checkbox. 496 497 @param column: Column number. 498 @type column: integer 499 @param cell: Cellrender. 500 @type cell: gtk.CellRendererText 501 @param model: Tree's model 502 @type model: gtk.ListStore 503 @param iter: Tree iter of current row, 504 @type iter: gtk.TreeIter 505 ''' 506 cell.set_property('active', 507 bool(model[iter][self.plugin_manager.COL_INSTANCE])) 508 509 def _onPluginToggled(self, renderer_toggle, path): 510 ''' 511 Callback for a "toggled" signal from a L{gtk.CellRendererToggle} in the 512 plugin dialog. Passes along the toggle request to the L{PluginManager}. 513 514 @param renderer_toggle: The toggle cellrenderer that emitted the signal. 515 @type renderer_toggle: L{gtk.CellRendererToggle} 516 @param path: The path that has been toggled. 517 @type path: tuple 518 ''' 519 self.plugin_manager.togglePlugin(path) 520 521 def _onViewChanged(self, cellrenderertext, path, new_text): 522 ''' 523 Callback for an "edited" signal from a L{gtk.CellRendererCombo} in the 524 plugin dialog. Passes along the new requested view name to the 525 L{PluginManager}. 526 527 @param cellrenderertext: The combo cellrenderer that emitted the signal. 528 @type renderer_toggle: L{gtk.CellRendererCombo} 529 @param path: The path that has been touched. 530 @type path: tuple 531 @param new_text: The new text that has been entered in to the combo entry. 532 @type new_text: string 533 ''' 534 plugin = \ 535 self.plugin_manager[path][self.plugin_manager.COL_INSTANCE] 536 self.view_manager.changeView(plugin, new_text) 537