~jaap.karssenberg/zim/pyzim-gtk3

« back to all changes in this revision

Viewing changes to zim/plugins/__init__.py

  • Committer: Jaap Karssenberg
  • Date: 2014-03-08 11:47:43 UTC
  • mfrom: (668.1.49 pyzim-refactor)
  • Revision ID: jaap.karssenberg@gmail.com-20140308114743-fero6uvy9zirbb4o
Merge branch with refactoring

Show diffs side-by-side

added added

removed removed

Lines of Context:
13
13
Also see the HACKING notebook in the source distribution for some
14
14
notes on writing new plugins.
15
15
 
16
 
@note: sub-modules T should contain one and exactly one subclass of
 
16
@note: sub-modules should contain one and exactly one subclass of
17
17
L{PluginClass}. This is because this class is detected automatically
18
18
when loading the plugin. This means you can also not import classes of
19
19
other plugins directly into the module.
20
20
'''
21
21
 
 
22
from __future__ import with_statement
 
23
 
22
24
 
23
25
import gobject
24
26
import types
25
27
import os
26
28
import sys
27
 
import weakref
28
29
import logging
29
30
import inspect
 
31
import collections
30
32
 
31
33
import zim.fs
32
34
from zim.fs import Dir
33
 
from zim.config import ListDict, get_environ
34
 
 
35
 
from zim.signals import ConnectorMixin, SIGNAL_AFTER
36
 
from zim.actions import action, toggle_action, get_actiongroup
 
35
 
 
36
from zim.signals import SignalEmitter, ConnectorMixin, SIGNAL_AFTER, SignalHandler
 
37
from zim.actions import action, toggle_action, get_gtk_actiongroup
 
38
from zim.utils import classproperty, get_module, lookup_subclass, WeakSet
 
39
 
 
40
from zim.config import VirtualConfigManager
37
41
 
38
42
 
39
43
logger = logging.getLogger('zim.plugins')
51
55
        This directoy is part of the search path for plugin modules, so users
52
56
        can install plugins in locally.
53
57
        '''
 
58
        from zim.environ import environ
54
59
        if os.name == 'nt':
55
 
                appdata = get_environ('APPDATA')
 
60
                appdata = environ.get('APPDATA')
56
61
                if appdata:
57
62
                        dir = Dir((appdata, 'Python/Python25/site-packages'))
58
63
                        return dir.path
106
111
set_plugin_search_path()
107
112
 
108
113
 
109
 
def get_module(name):
110
 
        '''Import a module
111
 
 
112
 
        @param name: the module name
113
 
        @returns: module object
114
 
        @raises ImportError: if the given name does not exist
115
 
 
116
 
        @note: don't actually use this method to get plugin modules, see
117
 
        L{get_plugin_module()} instead.
118
 
        '''
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)
123
 
        return mod
124
 
 
125
 
 
126
 
def lookup_subclass(module, klass):
127
 
        '''Look for a subclass of klass in the module
128
 
 
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).
133
 
 
134
 
        @param module: module object
135
 
        @param klass: base class
136
 
 
137
 
        @note: don't actually use this method to get plugin classes, see
138
 
        L{get_plugin()} instead.
139
 
        '''
140
 
        subclasses = lookup_subclasses(module, klass)
141
 
        if len(subclasses) > 1:
142
 
                raise AssertionError, 'BUG: Multiple subclasses found of type: %s' % klass
143
 
        elif subclasses:
144
 
                return subclasses[0]
145
 
        else:
146
 
                return None
147
 
 
148
 
 
149
 
def lookup_subclasses(module, klass):
150
 
        '''Look for all subclasses of klass in the module
151
 
 
152
 
        @param module: module object
153
 
        @param klass: base class
154
 
        '''
155
 
        subclasses = []
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)
160
 
 
161
 
        return subclasses
162
 
 
163
 
 
164
 
def get_plugin_module(name):
165
 
        '''Get the plugin module for a given name
166
 
 
167
 
        @param name: the plugin module name (e.g. "calendar")
168
 
        @returns: the plugin module object
169
 
        '''
170
 
        return get_module('zim.plugins.' + name.lower())
171
 
 
172
 
 
173
 
def get_plugin(name):
 
114
def get_plugin_class(name):
174
115
        '''Get the plugin class for a given name
175
116
 
176
117
        @param name: the plugin module name (e.g. "calendar")
177
118
        @returns: the plugin class object
178
119
        '''
179
 
        mod = get_plugin_module(name)
180
 
        obj = lookup_subclass(mod, PluginClass)
181
 
        obj.plugin_key = name
182
 
        return obj
 
120
        mod = get_module('zim.plugins.' + name)
 
121
        return lookup_subclass(mod, PluginClass)
183
122
 
184
123
 
185
124
def list_plugins():
186
125
        '''List available plugin module names
187
126
 
188
127
        @returns: a set of available plugin names that can be loaded
189
 
        using L{get_plugin()}.
 
128
        using L{get_plugin_class()}.
190
129
        '''
191
130
        # Only listing folders in __path__ because this parameter determines
192
131
        # what folders will considered when importing sub-modules of the
197
136
        for dir in __path__:
198
137
                dir = Dir(dir)
199
138
                for candidate in dir.list(): # returns [] if dir does not exist
200
 
                        if candidate.startswith('_'):
 
139
                        if candidate.startswith('_') or candidate == 'base':
201
140
                                continue
202
141
                        elif candidate.endswith('.py'):
203
142
                                #~ print '>> FOUND %s.py in %s' % (candidate, dir.path)
209
148
                        else:
210
149
                                pass
211
150
 
212
 
        return plugins
213
 
 
214
 
 
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
218
 
        methods.
 
151
        return sorted(plugins)
 
152
 
 
153
 
 
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.
 
158
 
 
159
        This object behaves as a dictionary with plugin object names as
 
160
        keys and plugin objects as value
219
161
        '''
220
162
 
221
 
        def __init__(klass, name, bases, dictionary):
222
 
                originit = klass.__init__
223
 
 
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
231
 
 
232
 
                        if self.ui.notebook:
233
 
                                self.initialize_ui(ui)
234
 
                                self.finalize_notebook(self.ui.notebook)
235
 
                        else:
236
 
                                self.initialize_ui(ui)
237
 
 
238
 
                                def after_open_notebook(*a):
239
 
                                        self._merge_uistate()
240
 
                                        self.finalize_notebook(self.ui.notebook)
241
 
 
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
245
 
 
246
 
                klass.__init__ = decoratedinit
247
 
 
248
 
 
249
 
                origfinalize = klass.finalize_ui
250
 
 
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))
259
 
 
260
 
                klass.finalize_ui = decoratedfinalize
261
 
 
262
 
 
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', [])
 
169
 
 
170
                self._plugins = {}
 
171
                self._extendables = WeakSet()
 
172
 
 
173
                self._load_plugins()
 
174
 
 
175
                self.connectto(self._preferences, 'changed',
 
176
                        self.on_preferences_changed)
 
177
 
 
178
        def __getitem__(self, name):
 
179
                return self._plugins[name]
 
180
 
 
181
        def __iter__(self):
 
182
                return iter(sorted(self._plugins.keys()))
 
183
                        # sort to make operation predictable - easier debugging
 
184
 
 
185
        def __len__(self):
 
186
                return len(self._plugins)
 
187
 
 
188
        def _load_plugins(self):
 
189
                '''Load plugins based on config'''
 
190
                for name in sorted(self.general_preferences['plugins']):
 
191
                        try:
 
192
                                self.load_plugin(name)
 
193
                        except:
 
194
                                logger.exception('Exception while loading plugin: %s', name)
 
195
                                self.general_preferences['plugins'].remove(name)
 
196
 
 
197
        @SignalHandler
 
198
        def on_preferences_changed(self, o):
 
199
                current = set(self._plugins.keys())
 
200
                new = set(self.general_preferences['plugins'])
 
201
 
 
202
                for name in current - new:
 
203
                        try:
 
204
                                self.remove_plugin(name)
 
205
                        except:
 
206
                                logger.exception('Exception while loading plugin: %s', name)
 
207
 
 
208
                for name in new - current:
 
209
                        try:
 
210
                                self.load_plugin(name)
 
211
                        except:
 
212
                                logger.exception('Exception while loading plugin: %s', name)
 
213
                                self.general_preferences['plugins'].remove(name)
 
214
 
 
215
        def load_plugin(self, name):
 
216
                '''Load a single plugin by name
 
217
 
 
218
                When the plugin was loaded already the existing object
 
219
                will be returned. Thus for each plugin only one instance can be
 
220
                active.
 
221
 
 
222
                @param name: the plugin module name
 
223
                @returns: the plugin object
 
224
                @raises Exception: when loading the plugin failed
 
225
                '''
 
226
                assert isinstance(name, basestring)
 
227
                if name in self._plugins:
 
228
                        return self._plugins[name]
 
229
 
 
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
 
234
 
 
235
                plugin = klass(self.config)
 
236
                self.connectto(plugin, 'extension-point-changed')
 
237
                self._plugins[name] = plugin
 
238
 
 
239
                for obj in self._extendables:
 
240
                        try:
 
241
                                plugin.extend(obj)
 
242
                        except:
 
243
                                logger.exception('Exception in plugin: %s', name)
 
244
 
 
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()
 
249
 
 
250
                return plugin
 
251
 
 
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
 
256
                '''
 
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()
 
262
 
 
263
                try:
 
264
                        plugin = self._plugins.pop(name)
 
265
                        self.disconnect_from(plugin)
 
266
                except KeyError:
 
267
                        pass
 
268
                else:
 
269
                        logger.debug('Unloading plugin %s', name)
 
270
                        plugin.destroy()
 
271
 
 
272
        def _foreach(self, func):
 
273
                # sort to make operation predictable - easier debugging
 
274
                for name, plugin in sorted(self._plugins.items()):
 
275
                        try:
 
276
                                func(plugin)
 
277
                        except:
 
278
                                logger.exception('Exception in plugin: %s', name)
 
279
 
 
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
 
286
                '''
 
287
                if not obj in self._extendables:
 
288
                        self._foreach(lambda p: p.extend(obj))
 
289
                        self._extendables.add(obj)
 
290
 
 
291
        def on_extension_point_changed(self, plugin, name):
 
292
                for obj in self._extendables:
 
293
                        if obj.__class__.__name__ == name:
 
294
                                try:
 
295
                                        plugin.extend(obj)
 
296
                                except:
 
297
                                        logger.exception('Exception in plugin: %s', name)
 
298
 
 
299
 
 
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.
267
304
 
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}.
271
307
 
272
308
        This class inherits from L{ConnectorMixin} and calls
273
309
        L{ConnectorMixin.disconnect_all()} when the plugin is destroyed.
301
337
        Changes to these preferences will be stored in a config file so
302
338
        they are persistent.
303
339
 
304
 
        @cvar is_profile_independent: A boolean indicating that the plugin
305
 
        configuration is global and not meant to change between notebooks.
306
 
        The default value (if undefined) is False. Plugins that set
307
 
        L{is_profile_independent} to True will be initialized before
308
 
        opening the notebook. All other plugins will only be loaded after
309
 
        the notebook is initialized.
310
 
 
311
340
        @ivar ui: the main application object, e.g. an instance of
312
341
        L{zim.gui.GtkInterface} or L{zim.www.WWWInterface}
313
 
        @ivar preferences: a C{ListDict()} with plugin preferences
 
342
        @ivar preferences: a C{ConfigDict()} with plugin preferences
314
343
 
315
344
        Preferences are the global configuration of the plugin, they are
316
345
        stored in the X{preferences.conf} config file.
317
346
 
318
 
        @ivar uistate: a C{ListDict()} with plugin ui state
 
347
        @ivar uistate: a C{ConfigDict()} with plugin ui state
319
348
 
320
349
        The "uistate" is the per notebook state of the interface, it is
321
350
        intended for stuff like the last folder opened by the user or the
322
351
        size of a dialog after resizing. It is stored in the X{state.conf}
323
352
        file in the notebook cache folder.
324
353
 
325
 
        @signal: C{preferences-changed ()}: emitted after the preferences
326
 
        were changed, triggers the L{do_preferences_changed} handler
 
354
        @signal: C{extension-point-changed (name)}: emitted when extension
 
355
        point C{name} changes
327
356
        '''
328
357
 
329
 
        __metaclass__ = PluginClassMeta
330
 
 
331
358
        # define signals we want to use - (closure type, return type and arg types)
332
 
        __gsignals__ = {
333
 
                'preferences-changed': (gobject.SIGNAL_RUN_LAST, None, ()),
 
359
        __signals__ = {
 
360
                'extension-point-changed': (None, None, (basestring,))
334
361
        }
335
362
 
336
363
        plugin_info = {}
337
364
 
338
365
        plugin_preferences = ()
339
366
 
340
 
        is_profile_independent = False
 
367
        @classproperty
 
368
        def config_key(klass):
 
369
                return klass.__name__
341
370
 
342
371
        @classmethod
343
372
        def check_dependencies_ok(klass):
364
393
                '''
365
394
                return (True, [])
366
395
 
367
 
        def __init__(self, ui):
368
 
                '''Constructor
369
 
 
370
 
                @param ui: a L{NotebookInterface} object
371
 
 
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.
376
 
                '''
377
 
                # NOTE: this method is decorated by the meta class
378
 
                gobject.GObject.__init__(self)
379
 
                self.ui = ui
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()
 
401
 
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]
 
404
 
 
405
                self.config = config or VirtualConfigManager()
 
406
                self.preferences = self.config.get_config_dict('<profile>/preferences.conf')[self.config_key]
 
407
 
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]
391
413
                                else:
392
414
                                        key, type, label, default, check = pref
393
415
                                        self.preferences.setdefault(key, default, check=check)
394
 
 
395
 
                self._is_image_generator_plugin = False
396
 
 
397
 
                if self.ui.notebook:
398
 
                        section = self.__class__.__name__
399
 
                        self.uistate = self.ui.uistate[section]
400
 
                else:
401
 
                        self.uistate = ListDict()
402
 
 
 
416
                                        #~ print ">>>>", key, default, check, '--', self.preferences[key]
 
417
 
 
418
                self.load_extensions_classes()
 
419
 
 
420
        @classmethod
 
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
 
426
                '''
 
427
                module = get_module(pluginklass.__module__)
 
428
                return lookup_subclass(module, klass)
 
429
 
 
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)
 
434
 
 
435
        @classmethod
 
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
414
 
 
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.
421
 
                if self.ui.uistate:
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)
427
 
 
428
 
        def _extension_point(self, obj):
 
443
                                yield klass.__extends__, klass
 
444
 
 
445
        def set_extension_class(self, name, klass):
 
446
                if name in self.extension_classes:
 
447
                        if self.extension_classes[name] == klass:
 
448
                                pass
 
449
                        else:
 
450
                                self.remove_extension_class(name)
 
451
                                self.add_extension_class(name, klass)
 
452
                else:
 
453
                        self.add_extension_class(name, klass)
 
454
 
 
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)
 
460
 
 
461
        def remove_extension_class(self, name):
 
462
                klass = self.extension_classes.pop(name)
 
463
                for obj in self.get_extensions(klass):
 
464
                        obj.destroy()
 
465
 
 
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)
435
 
 
436
 
        def _del_extension(self, ref):
437
 
                if ref in self._extensions:
438
 
                        self._extensions.remove(ref)
439
 
 
440
 
        @property
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
444
 
 
445
 
        def initialize_ui(self, ui):
446
 
                '''Callback called during construction of the ui.
447
 
 
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.
452
 
 
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.
456
 
 
457
 
                @param ui: a L{NotebookInterface} object, e.g.
458
 
                L{zim.gui.GtkInterface}
459
 
 
460
 
                @implementation: optional, may be implemented by subclasses.
461
 
                '''
462
 
                pass
463
 
 
464
 
        def initialize_notebook(self, notebookuri):
465
 
                '''Callback called before construction of the notebook
466
 
 
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
471
 
                a filesystem.
472
 
 
473
 
                Not called when plugin is constructed while notebook already
474
 
                exists.
475
 
 
476
 
                @param notebookuri: the URI of the notebook location
477
 
 
478
 
                @implementation: optional, may be implemented by subclasses.
479
 
                '''
480
 
                pass
481
 
 
482
 
        def finalize_notebook(self, notebook):
483
 
                '''Callback called once the notebook object is created
484
 
 
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
488
 
                available.
489
 
 
490
 
                @param notebook: the L{Notebook} object
491
 
 
492
 
                @implementation: optional, may be implemented by subclasses.
493
 
                '''
494
 
                self._extension_point(notebook)
495
 
 
496
 
        def finalize_ui(self, ui):
497
 
                '''Callback called just before entering the main loop
498
 
 
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.
502
 
 
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.
506
 
 
507
 
                @param ui: a L{NotebookInterface} object, e.g.
508
 
                L{zim.gui.GtkInterface}
509
 
 
510
 
                @implementation: optional, may be implemented by subclasses.
511
 
                '''
512
 
                # NOTE: this method is decorated by the meta class
513
 
                pass
514
 
 
515
 
        def do_decorate_window(self, window):
516
 
                '''Callback which is called for each window and dialog that
517
 
                opens in zim.
518
 
                May be overloaded by sub classes
519
 
                '''
520
 
                self._extension_point(window)
521
 
 
522
 
                # HACK
523
 
                if hasattr(window, 'pageview'):
524
 
                        self._extension_point(window.pageview)
525
 
 
526
 
        def do_preferences_changed(self):
527
 
                '''Handler called when preferences are changed by the user
528
 
 
529
 
                @implementation: optional, may be implemented by subclasses.
530
 
                to apply relevant changes.
531
 
                '''
532
 
                pass
 
472
                        self.extensions.add(ext)
 
473
 
 
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)
 
478
 
 
479
                if len(ext) > 1:
 
480
                        raise AssertionError, 'BUG: multiple extensions of class %s found' % klass
 
481
                elif ext:
 
482
                        return ext[0]
 
483
                else:
 
484
                        return None
 
485
 
 
486
        def get_extensions(self, klass):
 
487
                return [e for e in self.extensions if isinstance(e, klass)]
533
488
 
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).
542
497
                '''
543
 
                ### TODO clean up this section when all plugins are ported
544
 
                if self.ui.ui_type == 'gtk':
545
 
                        try:
546
 
                                self.ui.remove_ui(self)
547
 
                                self.ui.remove_actiongroup(self)
548
 
                        except:
549
 
                                logger.exception('Exception while disconnecting %s', self)
550
 
 
551
 
                        if self._is_image_generator_plugin:
552
 
                                try:
553
 
                                        self.ui.mainpage.pageview.unregister_image_generator_plugin(self)
554
 
                                except:
555
 
                                        logger.exception('Exception while disconnecting %s', self)
556
 
                ###
557
 
 
558
 
                while self._extensions:
559
 
                        ref = self._extensions.pop()
560
 
                        obj = ref()
561
 
                        if obj:
562
 
                                obj.destroy()
 
498
                for obj in self.extensions:
 
499
                        obj.destroy()
563
500
 
564
501
                try:
565
502
                        self.disconnect_all()
566
503
                except:
567
504
                        logger.exception('Exception while disconnecting %s', self)
568
505
 
569
 
        def toggle_action(self, action, active=None):
570
 
                '''Trigger a toggle action.
571
 
 
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::
577
 
 
578
 
                        def show_foo(self, show=None):
579
 
                                self.toggle_action('show_foo', active=show)
580
 
 
581
 
                        def do_show_foo(self, show=None):
582
 
                                if show is None:
583
 
                                        show = self.actiongroup.get_action('show_foo').get_active()
584
 
 
585
 
                                # ... the actual logic for toggling on / off 'foo'
586
 
 
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.
591
 
 
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
595
 
                '''
596
 
                name = action
597
 
                action = self.actiongroup.get_action(name)
598
 
                if active is None or active != action.get_active():
599
 
                        action.activate()
600
 
                else:
601
 
                        method = getattr(self, 'do_'+name)
602
 
                        method(active)
603
 
 
604
 
        #~ def remember_decorated_window(self, window):
605
 
                #~ import weakref
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)
610
 
 
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 ]
615
 
 
616
 
        #~ def get_decorated_windows(self):
617
 
                #~ if not hasattr(self, '_decorated_windows'):
618
 
                        #~ return []
619
 
                #~ else:
620
 
                        #~ self._clean_decorated_windows_list()
621
 
                        #~ return [ref() for ref in self._decorated_windows]
622
 
 
623
 
        def register_image_generator_plugin(self, type):
624
 
                '''Convenience method to register a plugin that adds a type
625
 
                of image objects
626
 
 
627
 
                @param type: the type of the objects (e.g. "equation")
628
 
 
629
 
                @todo: document image geneartor plugins
630
 
                '''
631
 
                self.ui.mainwindow.pageview.register_image_generator_plugin(self, type)
632
 
                self._is_image_generator_pluging = True
633
 
 
634
 
 
635
 
# Need to register classes defining gobject signals
636
 
gobject.type_register(PluginClass)
637
 
 
638
 
 
639
 
def extends(klass):
 
506
 
 
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__
650
518
 
651
519
        def inner(myklass):
652
 
                myklass.__extends__ = name
 
520
                if autoload:
 
521
                        myklass.__extends__ = name
 
522
                # else: do nothing for now
653
523
                return myklass
654
524
 
655
525
        return inner
656
526
 
657
527
 
658
 
class Extension(ConnectorMixin):
659
 
 
660
 
        # TODO, maybe add try .. except wrapper for destroy in meta class ?
661
 
        # have except always call super.destroy
 
528
class ObjectExtension(SignalEmitter, ConnectorMixin):
662
529
 
663
530
        def __init__(self, plugin, obj):
664
531
                self.plugin = plugin
665
532
                self.obj = obj
666
533
 
 
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)
 
538
 
667
539
        def destroy(self):
 
540
                '''Called when the plugin is being destroyed
 
541
                Calls L{teardown()} followed by the C{teardown()} methods of
 
542
                base classes.
 
543
                '''
 
544
                def walk(klass):
 
545
                        yield klass
 
546
                        for base in klass.__bases__:
 
547
                                if issubclass(base, ObjectExtension):
 
548
                                        for k in walk(base): # recurs
 
549
                                                yield k
 
550
 
 
551
                for klass in walk(self.__class__):
 
552
                        try:
 
553
                                klass.teardown(self)
 
554
                        except:
 
555
                                logger.exception('Exception while disconnecting %s (%s)', self, klass)
 
556
                        # in case you are wondering: issubclass(Foo, Foo) evaluates True
 
557
 
668
558
                try:
669
 
                        self.disconnect_all()
670
 
                except:
671
 
                        logger.exception('Exception while disconnecting %s', self)
672
 
 
673
 
 
674
 
class WindowExtension(Extension):
 
559
                        self.obj.__zim_extension_objects__.remove(self)
 
560
                except AttributeError:
 
561
                        pass
 
562
                except ValueError:
 
563
                        pass
 
564
 
 
565
                self.plugin.extensions.discard(self)
 
566
                        # HACK avoid waiting for garbage collection to take place
 
567
 
 
568
        def teardown(self):
 
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()}
 
573
                '''
 
574
                self.disconnect_all()
 
575
 
 
576
 
 
577
class WindowExtension(ObjectExtension):
675
578
 
676
579
        def __init__(self, plugin, window):
677
 
                self.plugin = plugin
 
580
                ObjectExtension.__init__(self, plugin, window)
678
581
                self.window = window
679
582
 
 
583
                if hasattr(window, 'ui') and hasattr(window.ui, 'uistate') and window.ui.uistate: # XXX
 
584
                        self.uistate = window.ui.uistate[plugin.config_key]
 
585
                else:
 
586
                        self.uistate = None
 
587
 
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)
685
593
 
686
 
                window.connect_object('destroy', self.__class__.destroy, self)
687
 
 
688
 
        def destroy(self):
689
 
                try:
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
695
 
 
696
 
                        if hasattr(self, 'actiongroup'):
697
 
                                self.window.ui.uimanager.remove_action_group(self.actiongroup)
698
 
                except:
699
 
                        logger.exception('Exception while removing UI %s', self)
700
 
 
701
 
                Extension.destroy(self)
 
594
                self.connectto(window, 'destroy')
 
595
 
 
596
        def on_destroy(self, window):
 
597
                self.destroy()
 
598
 
 
599
        def teardown(self):
 
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
 
605
 
 
606
                if hasattr(self, 'actiongroup') \
 
607
                and self.actiongroup is not None:
 
608
                        self.window.ui.uimanager.remove_action_group(self.actiongroup)
702
609
 
703
610
 
704
611
class DialogExtension(WindowExtension):
722
629
                        if b is not button:
723
630
                                self.window.action_area.reorder_child(b, -1) # reshuffle to the right
724
631
 
725
 
        def destroy(self):
726
 
                try:
727
 
                        for b in self._dialog_buttons:
728
 
                                self.window.action_area.remove(b)
729
 
                except:
730
 
                        logger.exception('Could not remove buttons')
731
 
 
732
 
                WindowExtension.destroy(self)
 
632
        def teardown(self):
 
633
                for b in self._dialog_buttons:
 
634
                        self.window.action_area.remove(b)