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