106
111
set_plugin_search_path()
109
def get_module(name):
112
@param name: the module name
113
@returns: module object
114
@raises ImportError: if the given name does not exist
116
@note: don't actually use this method to get plugin modules, see
117
L{get_plugin_module()} instead.
119
# __import__ has some quirks, see the reference manual
120
mod = __import__(name)
121
for part in name.split('.')[1:]:
122
mod = getattr(mod, part)
126
def lookup_subclass(module, klass):
127
'''Look for a subclass of klass in the module
129
This function is used in several places in zim to get extension
130
classes. Typically L{get_module()} is used first to get the module
131
object, then this lookup function is used to locate a class that
132
derives of a base class (e.g. PluginClass).
134
@param module: module object
135
@param klass: base class
137
@note: don't actually use this method to get plugin classes, see
138
L{get_plugin()} instead.
140
subclasses = lookup_subclasses(module, klass)
141
if len(subclasses) > 1:
142
raise AssertionError, 'BUG: Multiple subclasses found of type: %s' % klass
149
def lookup_subclasses(module, klass):
150
'''Look for all subclasses of klass in the module
152
@param module: module object
153
@param klass: base class
156
for name, obj in inspect.getmembers(module, inspect.isclass):
157
if issubclass(obj, klass) \
158
and obj.__module__.startswith(module.__name__):
159
subclasses.append(obj)
164
def get_plugin_module(name):
165
'''Get the plugin module for a given name
167
@param name: the plugin module name (e.g. "calendar")
168
@returns: the plugin module object
170
return get_module('zim.plugins.' + name.lower())
173
def get_plugin(name):
114
def get_plugin_class(name):
174
115
'''Get the plugin class for a given name
176
117
@param name: the plugin module name (e.g. "calendar")
177
118
@returns: the plugin class object
179
mod = get_plugin_module(name)
180
obj = lookup_subclass(mod, PluginClass)
181
obj.plugin_key = name
120
mod = get_module('zim.plugins.' + name)
121
return lookup_subclass(mod, PluginClass)
185
124
def list_plugins():
186
125
'''List available plugin module names
188
127
@returns: a set of available plugin names that can be loaded
189
using L{get_plugin()}.
128
using L{get_plugin_class()}.
191
130
# Only listing folders in __path__ because this parameter determines
192
131
# what folders will considered when importing sub-modules of the
215
class PluginClassMeta(gobject.GObjectMeta):
216
'''Meta class for objects inheriting from PluginClass.
217
It adds a wrapper to the constructor to call secondairy initialization
151
return sorted(plugins)
154
class PluginManager(ConnectorMixin, collections.Mapping):
155
'''Manager that maintains a set of active plugins
156
Handles loading and destroying plugins and is the entry point
157
for extending application components.
159
This object behaves as a dictionary with plugin object names as
160
keys and plugin objects as value
221
def __init__(klass, name, bases, dictionary):
222
originit = klass.__init__
224
#~ print 'DECORATE INIT', klass
225
def decoratedinit(self, ui, *arg, **kwarg):
226
# Calls initialize_ui and finalize_notebook *after* __init__
227
#~ print 'INIT', self
228
originit(self, ui, *arg, **kwarg)
229
if not self.__class__ is klass:
230
return # Avoid wrapping both base class and sub classes
233
self.initialize_ui(ui)
234
self.finalize_notebook(self.ui.notebook)
236
self.initialize_ui(ui)
238
def after_open_notebook(*a):
239
self._merge_uistate()
240
self.finalize_notebook(self.ui.notebook)
242
self.connectto(self.ui, 'open-notebook',
243
after_open_notebook, order=SIGNAL_AFTER)
244
# FIXME with new plugin API should not need this merging
246
klass.__init__ = decoratedinit
249
origfinalize = klass.finalize_ui
251
def decoratedfinalize(self, ui, *arg, **kwarg):
252
origfinalize(self, ui, *arg, **kwarg)
253
if not self.__class__ is klass:
254
return # Avoid wrapping both base class and sub classes
255
#~ print 'FINALIZE UI', self
256
for window in ui.windows:
257
self.do_decorate_window(window)
258
self.connectto(ui, 'new-window', lambda u,w: self.do_decorate_window(w))
260
klass.finalize_ui = decoratedfinalize
263
class PluginClass(ConnectorMixin, gobject.GObject):
163
def __init__(self, config=None):
164
self.config = config or VirtualConfigManager()
165
self._preferences = \
166
self.config.get_config_dict('<profile>/preferences.conf')
167
self.general_preferences = self._preferences['General']
168
self.general_preferences.setdefault('plugins', [])
171
self._extendables = WeakSet()
175
self.connectto(self._preferences, 'changed',
176
self.on_preferences_changed)
178
def __getitem__(self, name):
179
return self._plugins[name]
182
return iter(sorted(self._plugins.keys()))
183
# sort to make operation predictable - easier debugging
186
return len(self._plugins)
188
def _load_plugins(self):
189
'''Load plugins based on config'''
190
for name in sorted(self.general_preferences['plugins']):
192
self.load_plugin(name)
194
logger.exception('Exception while loading plugin: %s', name)
195
self.general_preferences['plugins'].remove(name)
198
def on_preferences_changed(self, o):
199
current = set(self._plugins.keys())
200
new = set(self.general_preferences['plugins'])
202
for name in current - new:
204
self.remove_plugin(name)
206
logger.exception('Exception while loading plugin: %s', name)
208
for name in new - current:
210
self.load_plugin(name)
212
logger.exception('Exception while loading plugin: %s', name)
213
self.general_preferences['plugins'].remove(name)
215
def load_plugin(self, name):
216
'''Load a single plugin by name
218
When the plugin was loaded already the existing object
219
will be returned. Thus for each plugin only one instance can be
222
@param name: the plugin module name
223
@returns: the plugin object
224
@raises Exception: when loading the plugin failed
226
assert isinstance(name, basestring)
227
if name in self._plugins:
228
return self._plugins[name]
230
logger.debug('Loading plugin: %s', name)
231
klass = get_plugin_class(name)
232
if not klass.check_dependencies_ok():
233
raise AssertionError, 'Dependencies failed for plugin %s' % name
235
plugin = klass(self.config)
236
self.connectto(plugin, 'extension-point-changed')
237
self._plugins[name] = plugin
239
for obj in self._extendables:
243
logger.exception('Exception in plugin: %s', name)
245
if not name in self.general_preferences['plugins']:
246
with self.on_preferences_changed.blocked():
247
self.general_preferences['plugins'].append(name)
248
self.general_preferences.changed()
252
def remove_plugin(self, name):
253
'''Remove a plugin and it's extensions
254
Fails silently if the plugin is not loaded.
255
@param name: the plugin module name
257
if name in self.general_preferences['plugins']:
258
# Do this first regardless of exceptions etc.
259
with self.on_preferences_changed.blocked():
260
self.general_preferences['plugins'].remove(name)
261
self.general_preferences.changed()
264
plugin = self._plugins.pop(name)
265
self.disconnect_from(plugin)
269
logger.debug('Unloading plugin %s', name)
272
def _foreach(self, func):
273
# sort to make operation predictable - easier debugging
274
for name, plugin in sorted(self._plugins.items()):
278
logger.exception('Exception in plugin: %s', name)
280
def extend(self, obj):
281
'''Let any plugin extend the object instance C{obj}
282
Will also remember object (by a weak reference) such that
283
plugins loaded after this call will also be called to extend
284
C{obj} on their construction
285
@param obj: arbitrary object that can be extended by plugins
287
if not obj in self._extendables:
288
self._foreach(lambda p: p.extend(obj))
289
self._extendables.add(obj)
291
def on_extension_point_changed(self, plugin, name):
292
for obj in self._extendables:
293
if obj.__class__.__name__ == name:
297
logger.exception('Exception in plugin: %s', name)
300
class PluginClass(ConnectorMixin, SignalEmitter):
264
301
'''Base class for plugins. Every module containing a plugin should
265
302
have exactly one class derived from this base class. That class
266
303
will be initialized when the plugin is loaded.
268
305
Plugin classes should define two class attributes: L{plugin_info} and
269
L{plugin_preferences}. Optionally, they can also define the class
270
attribute L{is_profile_independent}.
306
L{plugin_preferences}.
272
308
This class inherits from L{ConnectorMixin} and calls
273
309
L{ConnectorMixin.disconnect_all()} when the plugin is destroyed.
365
394
return (True, [])
367
def __init__(self, ui):
370
@param ui: a L{NotebookInterface} object
372
@implementation: sub-classes may override this constructor,
373
but it is advised instead to do the work of initializing the
374
plugin in the methods L{initialize_ui()}, L{initialize_notebook()},
375
L{finalize_ui()} and L{finalize_notebook()} where apropriate.
377
# NOTE: this method is decorated by the meta class
378
gobject.GObject.__init__(self)
380
assert 'name' in self.plugin_info, 'Plugins should provide a name in the info dict'
381
assert 'description' in self.plugin_info, 'Plugins should provide a description in the info dict'
382
assert 'author' in self.plugin_info, 'Plugins should provide a author in the info dict'
396
def __init__(self, config=None):
397
assert 'name' in self.plugin_info
398
assert 'description' in self.plugin_info
399
assert 'author' in self.plugin_info
400
self.extensions = WeakSet()
383
402
if self.plugin_preferences:
384
403
assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples'
385
section = self.__class__.__name__
386
self.preferences = self.ui.preferences[section]
405
self.config = config or VirtualConfigManager()
406
self.preferences = self.config.get_config_dict('<profile>/preferences.conf')[self.config_key]
387
408
for pref in self.plugin_preferences:
388
409
if len(pref) == 4:
389
410
key, type, label, default = pref
390
411
self.preferences.setdefault(key, default)
412
#~ print ">>>>", key, default, '--', self.preferences[key]
392
414
key, type, label, default, check = pref
393
415
self.preferences.setdefault(key, default, check=check)
395
self._is_image_generator_plugin = False
398
section = self.__class__.__name__
399
self.uistate = self.ui.uistate[section]
401
self.uistate = ListDict()
416
#~ print ">>>>", key, default, check, '--', self.preferences[key]
418
self.load_extensions_classes()
421
def lookup_subclass(pluginklass, klass):
422
'''Returns first subclass of C{klass} found in the module of
423
this plugin. (Similar to L{zim.utils.lookup_subclass})
424
@param pluginklass: plugin class
425
@param klass: base class of the wanted class
427
module = get_module(pluginklass.__module__)
428
return lookup_subclass(module, klass)
430
def load_extensions_classes(self):
431
self.extension_classes = {}
432
for name, klass in self.discover_extensions_classes():
433
self.add_extension_class(name, klass)
436
def discover_extensions_classes(pluginklass):
403
437
# Find related extension classes in same module
404
438
# any class with the "__extends__" field will be added
405
# (Being subclass of Extension is optional)
406
self.extension_classes = {}
407
self._extensions = []
408
module = get_module(self.__class__.__module__)
409
for name, klass in inspect.getmembers(module, inspect.isclass):
439
# (Being subclass of ObjectExtension is optional)
440
module = get_module(pluginklass.__module__)
441
for n, klass in inspect.getmembers(module, inspect.isclass):
410
442
if hasattr(klass, '__extends__') and klass.__extends__:
411
assert klass.__extends__ not in self.extension_classes, \
412
'Extension point %s used multiple times in %s' % (klass.__extends__, module.__name__)
413
self.extension_classes[klass.__extends__] = klass
415
def _merge_uistate(self):
416
# As a convenience we provide a uistate dict directly after
417
# initialization of the plugin. However, in reality this
418
# config file is only available after the notebook is opened.
419
# Therefore we need to link the actual file and merge back
420
# any defaults that were set during plugin intialization etc.
422
section = self.__class__.__name__
423
defaults = self.uistate
424
self.uistate = self.ui.uistate[section]
425
for key, value in defaults.items():
426
self.uistate.setdefault(key, value)
428
def _extension_point(self, obj):
443
yield klass.__extends__, klass
445
def set_extension_class(self, name, klass):
446
if name in self.extension_classes:
447
if self.extension_classes[name] == klass:
450
self.remove_extension_class(name)
451
self.add_extension_class(name, klass)
453
self.add_extension_class(name, klass)
455
def add_extension_class(self, name, klass):
456
if name in self.extension_classes:
457
raise AssertionError, 'Extension point %s already in use' % name
458
self.extension_classes[name] = klass
459
self.emit('extension-point-changed', name)
461
def remove_extension_class(self, name):
462
klass = self.extension_classes.pop(name)
463
for obj in self.get_extensions(klass):
466
def extend(self, obj, name=None):
429
467
# TODO also check parent classes
430
name = obj.__class__.__name__
468
# name should only be used for testing
469
name = name or obj.__class__.__name__
431
470
if name in self.extension_classes:
432
471
ext = self.extension_classes[name](self, obj)
433
ref = weakref.ref(obj, self._del_extension)
434
self._extensions.append(ref)
436
def _del_extension(self, ref):
437
if ref in self._extensions:
438
self._extensions.remove(ref)
441
def extensions(self):
442
extensions = [ref() for ref in self._extensions]
443
return [e for e in extensions if e] # Filter out None values
445
def initialize_ui(self, ui):
446
'''Callback called during construction of the ui.
448
Called after construction of the plugin when the application
449
object is available. At this point the construction of the the
450
interface itself does not yet need to be complete. Typically
451
used to initialize any interface components of the plugin.
453
@note: the plugin should check the C{ui_type} attribute of the
454
application object to distinguish the Gtk from the WWW
455
interface and only do something for the correct interface.
457
@param ui: a L{NotebookInterface} object, e.g.
458
L{zim.gui.GtkInterface}
460
@implementation: optional, may be implemented by subclasses.
464
def initialize_notebook(self, notebookuri):
465
'''Callback called before construction of the notebook
467
This callback is called before constructing the notebook object.
468
It is intended for a fairly specific type of plugins that
469
may want to do some manipulation of the notebook location
470
before actually loading the notebook, e.g. auto-mounting
473
Not called when plugin is constructed while notebook already
476
@param notebookuri: the URI of the notebook location
478
@implementation: optional, may be implemented by subclasses.
482
def finalize_notebook(self, notebook):
483
'''Callback called once the notebook object is created
485
This callback is called once the notebook object is constructed
486
and loaded in the application object. This is a logical point
487
to do any intialization that requires the notebook the be
490
@param notebook: the L{Notebook} object
492
@implementation: optional, may be implemented by subclasses.
494
self._extension_point(notebook)
496
def finalize_ui(self, ui):
497
'''Callback called just before entering the main loop
499
Called after the interface is fully initialized and has a
500
notebook object loaded. Typically used for any initialization
501
that needs the full application to be ready.
503
@note: the plugin should check the C{ui_type} attribute of the
504
application object to distinguish the Gtk from the WWW
505
interface and only do something for the correct interface.
507
@param ui: a L{NotebookInterface} object, e.g.
508
L{zim.gui.GtkInterface}
510
@implementation: optional, may be implemented by subclasses.
512
# NOTE: this method is decorated by the meta class
515
def do_decorate_window(self, window):
516
'''Callback which is called for each window and dialog that
518
May be overloaded by sub classes
520
self._extension_point(window)
523
if hasattr(window, 'pageview'):
524
self._extension_point(window.pageview)
526
def do_preferences_changed(self):
527
'''Handler called when preferences are changed by the user
529
@implementation: optional, may be implemented by subclasses.
530
to apply relevant changes.
472
self.extensions.add(ext)
474
def get_extension(self, klass, **attr):
475
ext = self.get_extensions(klass)
476
for key, value in attr.items():
477
ext = filter(lambda e: getattr(e, key) == value, ext)
480
raise AssertionError, 'BUG: multiple extensions of class %s found' % klass
486
def get_extensions(self, klass):
487
return [e for e in self.extensions if isinstance(e, klass)]
534
489
def destroy(self):
535
490
'''Destroy the plugin object and all extensions
540
495
This should revert any changes the plugin made to the
541
496
application (although preferences etc. can be left in place).
543
### TODO clean up this section when all plugins are ported
544
if self.ui.ui_type == 'gtk':
546
self.ui.remove_ui(self)
547
self.ui.remove_actiongroup(self)
549
logger.exception('Exception while disconnecting %s', self)
551
if self._is_image_generator_plugin:
553
self.ui.mainpage.pageview.unregister_image_generator_plugin(self)
555
logger.exception('Exception while disconnecting %s', self)
558
while self._extensions:
559
ref = self._extensions.pop()
498
for obj in self.extensions:
565
502
self.disconnect_all()
567
504
logger.exception('Exception while disconnecting %s', self)
569
def toggle_action(self, action, active=None):
570
'''Trigger a toggle action.
572
This is a convenience method to help defining toggle actions
573
in the menu or toolbar. It helps to keep the menu item
574
or toolbar item in sync with your internal state.
575
A typical usage to define a handler for a toggle action called
576
'show_foo' would be::
578
def show_foo(self, show=None):
579
self.toggle_action('show_foo', active=show)
581
def do_show_foo(self, show=None):
583
show = self.actiongroup.get_action('show_foo').get_active()
585
# ... the actual logic for toggling on / off 'foo'
587
This way you have a public method C{show_foo()} that can be
588
called by anybody and a handler C{do_show_foo()} that is
589
called when the user clicks the menu item. The trick is that
590
when C{show_foo()} is called, the menu item is also updates.
592
@param action: the name of the action item
593
@param active: when C{None} the item is toggled with respect
594
to it's current state, when C{True} or C{False} forces a state
597
action = self.actiongroup.get_action(name)
598
if active is None or active != action.get_active():
601
method = getattr(self, 'do_'+name)
604
#~ def remember_decorated_window(self, window):
606
#~ if not hasattr(self, '_decorated_windows'):
607
#~ self._decorated_windows = []
608
#~ ref = weakref.ref(window, self._clean_decorated_windows_list)
609
#~ self._decorated_windows.append(ref)
611
#~ def _clean_decorated_windows_list(self, *a):
612
#~ self._decorated_windows = [
613
#~ ref for ref in self._decorated_windows
614
#~ if not ref() is None ]
616
#~ def get_decorated_windows(self):
617
#~ if not hasattr(self, '_decorated_windows'):
620
#~ self._clean_decorated_windows_list()
621
#~ return [ref() for ref in self._decorated_windows]
623
def register_image_generator_plugin(self, type):
624
'''Convenience method to register a plugin that adds a type
627
@param type: the type of the objects (e.g. "equation")
629
@todo: document image geneartor plugins
631
self.ui.mainwindow.pageview.register_image_generator_plugin(self, type)
632
self._is_image_generator_pluging = True
635
# Need to register classes defining gobject signals
636
gobject.type_register(PluginClass)
507
def extends(klass, autoload=True):
640
508
'''Decorator for extension classes
641
509
Use this decorator to add extensions to the plugin.
642
510
Takes either a class or a class name for the class to be
649
517
name = klass.__name__
651
519
def inner(myklass):
652
myklass.__extends__ = name
521
myklass.__extends__ = name
522
# else: do nothing for now
658
class Extension(ConnectorMixin):
660
# TODO, maybe add try .. except wrapper for destroy in meta class ?
661
# have except always call super.destroy
528
class ObjectExtension(SignalEmitter, ConnectorMixin):
663
530
def __init__(self, plugin, obj):
664
531
self.plugin = plugin
534
# Make sure extension has same lifetime as object being extended
535
if not hasattr(obj, '__zim_extension_objects__'):
536
obj.__zim_extension_objects__ = []
537
obj.__zim_extension_objects__.append(self)
667
539
def destroy(self):
540
'''Called when the plugin is being destroyed
541
Calls L{teardown()} followed by the C{teardown()} methods of
546
for base in klass.__bases__:
547
if issubclass(base, ObjectExtension):
548
for k in walk(base): # recurs
551
for klass in walk(self.__class__):
555
logger.exception('Exception while disconnecting %s (%s)', self, klass)
556
# in case you are wondering: issubclass(Foo, Foo) evaluates True
669
self.disconnect_all()
671
logger.exception('Exception while disconnecting %s', self)
674
class WindowExtension(Extension):
559
self.obj.__zim_extension_objects__.remove(self)
560
except AttributeError:
565
self.plugin.extensions.discard(self)
566
# HACK avoid waiting for garbage collection to take place
569
'''Remove changes made by B{this} class from the extended object
570
To be overloaded by child classes
571
@note: do not call parent class C{remove()} here, that is
572
already taken care of by C{destroy()}
574
self.disconnect_all()
577
class WindowExtension(ObjectExtension):
676
579
def __init__(self, plugin, window):
580
ObjectExtension.__init__(self, plugin, window)
678
581
self.window = window
583
if hasattr(window, 'ui') and hasattr(window.ui, 'uistate') and window.ui.uistate: # XXX
584
self.uistate = window.ui.uistate[plugin.config_key]
680
588
if hasattr(self, 'uimanager_xml'):
681
# TODO move uimanager to window
682
actiongroup = get_actiongroup(self)
589
# XXX TODO move uimanager to window
590
actiongroup = get_gtk_actiongroup(self)
683
591
self.window.ui.uimanager.insert_action_group(actiongroup, 0)
684
592
self._uimanager_id = self.window.ui.uimanager.add_ui_from_string(self.uimanager_xml)
686
window.connect_object('destroy', self.__class__.destroy, self)
690
# TODO move uimanager to window
691
if hasattr(self, '_uimanager_id') \
692
and self._uimanager_id is not None:
693
self.window.ui.uimanager.remove_ui(self._uimanager_id)
694
self._uimanager_id = None
696
if hasattr(self, 'actiongroup'):
697
self.window.ui.uimanager.remove_action_group(self.actiongroup)
699
logger.exception('Exception while removing UI %s', self)
701
Extension.destroy(self)
594
self.connectto(window, 'destroy')
596
def on_destroy(self, window):
600
# TODO move uimanager to window
601
if hasattr(self, '_uimanager_id') \
602
and self._uimanager_id is not None:
603
self.window.ui.uimanager.remove_ui(self._uimanager_id)
604
self._uimanager_id = None
606
if hasattr(self, 'actiongroup') \
607
and self.actiongroup is not None:
608
self.window.ui.uimanager.remove_action_group(self.actiongroup)
704
611
class DialogExtension(WindowExtension):