def __init__(self, ui): gobject.GObject.__init__(self) self.ui = ui assert 'name' in self.plugin_info, 'Plugins should provide a name in the info dict' assert 'description' in self.plugin_info, 'Plugins should provide a description in the info dict' assert 'author' in self.plugin_info, 'Plugins should provide a author in the info dict' if self.plugin_preferences: assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tupels' section = self.__class__.__name__ self.preferences = self.ui.preferences[section] for key, type, label, default in self.plugin_preferences: self.preferences.setdefault(key, default) self.uistate = ListDict() self._is_image_generator_plugin = False self.ui.connect_after('open-notebook', self._merge_uistate)
def __init__(self, ui): '''Constructor @param ui: a L{NotebookInterface} object @implementation: sub-classes may override this constructor, but it is advised instead to do the work of initializing the plugin in the methods L{initialize_ui()}, L{initialize_notebook()}, L{finalize_ui()} and L{finalize_notebook()} where apropriate. ''' # NOTE: this method is decorated by the meta class gobject.GObject.__init__(self) self.ui = ui assert 'name' in self.plugin_info, 'Plugins should provide a name in the info dict' assert 'description' in self.plugin_info, 'Plugins should provide a description in the info dict' assert 'author' in self.plugin_info, 'Plugins should provide a author in the info dict' if self.plugin_preferences: assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' section = self.__class__.__name__ self.preferences = self.ui.preferences[section] for pref in self.plugin_preferences: if len(pref) == 4: key, type, label, default = pref self.preferences.setdefault(key, default) else: key, type, label, default, check = pref self.preferences.setdefault(key, default, check=check) self._is_image_generator_plugin = False if self.ui.notebook: section = self.__class__.__name__ self.uistate = self.ui.uistate[section] else: self.uistate = ListDict() # Find related extension classes in same module # any class with the "__extends__" field will be added # (Being subclass of Extension is optional) self.extension_classes = {} self._extensions = [] module = get_module(self.__class__.__module__) for name, klass in inspect.getmembers(module, inspect.isclass): if hasattr(klass, '__extends__') and klass.__extends__: assert klass.__extends__ not in self.extension_classes, \ 'Extension point %s used multiple times in %s' % (klass.__extends__, module.__name__) self.extension_classes[klass.__extends__] = klass
class PluginClass(ConnectorMixin, gobject.GObject): '''Base class for plugins. Every module containing a plugin should have exactly one class derived from this base class. That class will be initialized when the plugin is loaded. Plugin classes should define two class attributes: L{plugin_info} and L{plugin_preferences}. Optionally, they can also define the class attribute L{is_profile_independent}. This class inherits from L{ConnectorMixin} and calls L{ConnectorMixin.disconnect_all()} when the plugin is destroyed. Therefore it is highly recommended to use the L{ConnectorMixin} methods in sub-classes. @cvar plugin_info: A dict with basic information about the plugin, it should contain at least the following keys: - C{name}: short name - C{description}: one paragraph description - C{author}: name of the author - C{help}: page name in the manual (optional) This info will be used e.g. in the plugin tab of the preferences dialog. @cvar plugin_preferences: A tuple or list defining the global preferences for this plugin. Each preference is defined by a 4-tuple containing the following items: 1. the key in the config file 2. an option type (see InputForm.add_inputs for more details) 3. a label to show in the dialog 4. a default value These preferences will be initialized to their default value if not configured by the user and the values can be found in the L{preferences} dict. The type and label will be used to render a default configure dialog when triggered from the preferences dialog. Changes to these preferences will be stored in a config file so they are persistent. @cvar is_profile_independent: A boolean indicating that the plugin configuration is global and not meant to change between notebooks. The default value (if undefined) is False. Plugins that set L{is_profile_independent} to True will be initialized before opening the notebook. All other plugins will only be loaded after the notebook is initialized. @ivar ui: the main application object, e.g. an instance of L{zim.gui.GtkInterface} or L{zim.www.WWWInterface} @ivar preferences: a C{ListDict()} with plugin preferences Preferences are the global configuration of the plugin, they are stored in the X{preferences.conf} config file. @ivar uistate: a C{ListDict()} with plugin ui state The "uistate" is the per notebook state of the interface, it is intended for stuff like the last folder opened by the user or the size of a dialog after resizing. It is stored in the X{state.conf} file in the notebook cache folder. @signal: C{preferences-changed ()}: emitted after the preferences were changed, triggers the L{do_preferences_changed} handler ''' __metaclass__ = PluginClassMeta # define signals we want to use - (closure type, return type and arg types) __gsignals__ = { 'preferences-changed': (gobject.SIGNAL_RUN_LAST, None, ()), } plugin_info = {} plugin_preferences = () is_profile_independent = False @classmethod def check_dependencies_ok(klass): '''Checks minimum dependencies are met @returns: C{True} if this plugin can be loaded ''' check, dependencies = klass.check_dependencies() return check @classmethod def check_dependencies(klass): '''Checks what dependencies are met and gives details @returns: a boolean telling overall dependencies are met, followed by a list with details. This list consists of 3-tuples consisting of a (short) description of the dependency, a boolean for dependency being met, and a boolean for this dependency being optional or not. @implementation: must be implemented in sub-classes that have one or more (external) dependencies. ''' return (True, []) def __init__(self, ui): '''Constructor @param ui: a L{NotebookInterface} object @implementation: sub-classes may override this constructor, but it is advised instead to do the work of initializing the plugin in the methods L{initialize_ui()}, L{initialize_notebook()}, L{finalize_ui()} and L{finalize_notebook()} where apropriate. ''' # NOTE: this method is decorated by the meta class gobject.GObject.__init__(self) self.ui = ui assert 'name' in self.plugin_info, 'Plugins should provide a name in the info dict' assert 'description' in self.plugin_info, 'Plugins should provide a description in the info dict' assert 'author' in self.plugin_info, 'Plugins should provide a author in the info dict' if self.plugin_preferences: assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' section = self.__class__.__name__ self.preferences = self.ui.preferences[section] for pref in self.plugin_preferences: if len(pref) == 4: key, type, label, default = pref self.preferences.setdefault(key, default) else: key, type, label, default, check = pref self.preferences.setdefault(key, default, check=check) self._is_image_generator_plugin = False if self.ui.notebook: section = self.__class__.__name__ self.uistate = self.ui.uistate[section] else: self.uistate = ListDict() # Find related extension classes in same module # any class with the "__extends__" field will be added # (Being subclass of Extension is optional) self.extension_classes = {} self._extensions = [] module = get_module(self.__class__.__module__) for name, klass in inspect.getmembers(module, inspect.isclass): if hasattr(klass, '__extends__') and klass.__extends__: assert klass.__extends__ not in self.extension_classes, \ 'Extension point %s used multiple times in %s' % (klass.__extends__, module.__name__) self.extension_classes[klass.__extends__] = klass def _merge_uistate(self): # As a convenience we provide a uistate dict directly after # initialization of the plugin. However, in reality this # config file is only available after the notebook is opened. # Therefore we need to link the actual file and merge back # any defaults that were set during plugin intialization etc. if self.ui.uistate: section = self.__class__.__name__ defaults = self.uistate self.uistate = self.ui.uistate[section] for key, value in defaults.items(): self.uistate.setdefault(key, value) def _extension_point(self, obj): # TODO also check parent classes name = obj.__class__.__name__ if name in self.extension_classes: ext = self.extension_classes[name](self, obj) ref = weakref.ref(obj, self._del_extension) self._extensions.append(ref) def _del_extension(self, ref): if ref in self._extensions: self._extensions.remove(ref) @property def extensions(self): extensions = [ref() for ref in self._extensions] return [e for e in extensions if e] # Filter out None values def initialize_ui(self, ui): '''Callback called during construction of the ui. Called after construction of the plugin when the application object is available. At this point the construction of the the interface itself does not yet need to be complete. Typically used to initialize any interface components of the plugin. @note: the plugin should check the C{ui_type} attribute of the application object to distinguish the Gtk from the WWW interface and only do something for the correct interface. @param ui: a L{NotebookInterface} object, e.g. L{zim.gui.GtkInterface} @implementation: optional, may be implemented by subclasses. ''' pass def initialize_notebook(self, notebookuri): '''Callback called before construction of the notebook This callback is called before constructing the notebook object. It is intended for a fairly specific type of plugins that may want to do some manipulation of the notebook location before actually loading the notebook, e.g. auto-mounting a filesystem. Not called when plugin is constructed while notebook already exists. @param notebookuri: the URI of the notebook location @implementation: optional, may be implemented by subclasses. ''' pass def finalize_notebook(self, notebook): '''Callback called once the notebook object is created This callback is called once the notebook object is constructed and loaded in the application object. This is a logical point to do any intialization that requires the notebook the be available. @param notebook: the L{Notebook} object @implementation: optional, may be implemented by subclasses. ''' self._extension_point(notebook) def finalize_ui(self, ui): '''Callback called just before entering the main loop Called after the interface is fully initialized and has a notebook object loaded. Typically used for any initialization that needs the full application to be ready. @note: the plugin should check the C{ui_type} attribute of the application object to distinguish the Gtk from the WWW interface and only do something for the correct interface. @param ui: a L{NotebookInterface} object, e.g. L{zim.gui.GtkInterface} @implementation: optional, may be implemented by subclasses. ''' # NOTE: this method is decorated by the meta class pass def do_decorate_window(self, window): '''Callback which is called for each window and dialog that opens in zim. May be overloaded by sub classes ''' self._extension_point(window) # HACK if hasattr(window, 'pageview'): self._extension_point(window.pageview) def do_preferences_changed(self): '''Handler called when preferences are changed by the user @implementation: optional, may be implemented by subclasses. to apply relevant changes. ''' pass def destroy(self): '''Destroy the plugin object and all extensions It is only called when a user actually disables the plugin, not when the application exits. Destroys all active extensions and disconnects all signals. This should revert any changes the plugin made to the application (although preferences etc. can be left in place). ''' ### TODO clean up this section when all plugins are ported if self.ui.ui_type == 'gtk': try: self.ui.remove_ui(self) self.ui.remove_actiongroup(self) except: logger.exception('Exception while disconnecting %s', self) if self._is_image_generator_plugin: try: self.ui.mainpage.pageview.unregister_image_generator_plugin(self) except: logger.exception('Exception while disconnecting %s', self) ### while self._extensions: ref = self._extensions.pop() obj = ref() if obj: obj.destroy() try: self.disconnect_all() except: logger.exception('Exception while disconnecting %s', self) def toggle_action(self, action, active=None): '''Trigger a toggle action. This is a convenience method to help defining toggle actions in the menu or toolbar. It helps to keep the menu item or toolbar item in sync with your internal state. A typical usage to define a handler for a toggle action called 'show_foo' would be:: def show_foo(self, show=None): self.toggle_action('show_foo', active=show) def do_show_foo(self, show=None): if show is None: show = self.actiongroup.get_action('show_foo').get_active() # ... the actual logic for toggling on / off 'foo' This way you have a public method C{show_foo()} that can be called by anybody and a handler C{do_show_foo()} that is called when the user clicks the menu item. The trick is that when C{show_foo()} is called, the menu item is also updates. @param action: the name of the action item @param active: when C{None} the item is toggled with respect to it's current state, when C{True} or C{False} forces a state ''' name = action action = self.actiongroup.get_action(name) if active is None or active != action.get_active(): action.activate() else: method = getattr(self, 'do_'+name) method(active) #~ def remember_decorated_window(self, window): #~ import weakref #~ if not hasattr(self, '_decorated_windows'): #~ self._decorated_windows = [] #~ ref = weakref.ref(window, self._clean_decorated_windows_list) #~ self._decorated_windows.append(ref) #~ def _clean_decorated_windows_list(self, *a): #~ self._decorated_windows = [ #~ ref for ref in self._decorated_windows #~ if not ref() is None ] #~ def get_decorated_windows(self): #~ if not hasattr(self, '_decorated_windows'): #~ return [] #~ else: #~ self._clean_decorated_windows_list() #~ return [ref() for ref in self._decorated_windows] def register_image_generator_plugin(self, type): '''Convenience method to register a plugin that adds a type of image objects @param type: the type of the objects (e.g. "equation") @todo: document image geneartor plugins ''' self.ui.mainwindow.pageview.register_image_generator_plugin(self, type) self._is_image_generator_pluging = True
class PluginClass(gobject.GObject): '''Base class for plugins. Every module containing a plugin should have exactly one class derived from this base class. That class will be initialized when the plugin is loaded. Plugins should define two class attributes. The first is a dict called 'plugin_info'. It can contain the following keys: * name - short name * description - one paragraph description * author - name of the author * help - page name in the manual (optional) This info will be used e.g. in the plugin tab of the preferences dialog. Secondly a tuple can be defined called 'plugin_preferences'. Each item in this list should in turn be tuple containing four items: * the key in the config file * an option type (see Dialog.add_fields() for more details) * a label to show in the dialog * a default value These preferences will be initialized if not set and the actual values can be found in the 'preferences' attribute. The type and label will be used to render a default configure dialog when triggered from the preferences dialog. ''' plugin_info = {} plugin_preferences = () # define signals we want to use - (closure type, return type and arg types) __gsignals__ = { 'preferences-changed': (gobject.SIGNAL_RUN_LAST, None, ()), } def __init__(self, ui): gobject.GObject.__init__(self) self.ui = ui assert 'name' in self.plugin_info, 'Plugins should provide a name in the info dict' assert 'description' in self.plugin_info, 'Plugins should provide a description in the info dict' assert 'author' in self.plugin_info, 'Plugins should provide a author in the info dict' if self.plugin_preferences: assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tupels' section = self.__class__.__name__ self.preferences = self.ui.preferences[section] for key, type, label, default in self.plugin_preferences: self.preferences.setdefault(key, default) self.uistate = ListDict() self._is_image_generator_plugin = False self.ui.connect_after('open-notebook', self._merge_uistate) def _merge_uistate(self, *a): # As a convenience we provide a uistate dict directly after # initialization of the plugin. However, in reality this # config file is only available after the notebook is openend. # Therefore we need to link the actual file and merge back # any defaults that were set during plugin intialization etc. if self.ui.uistate: section = self.__class__.__name__ defaults = self.uistate self.uistate = self.ui.uistate[section] for key, value in defaults.items(): self.uistate.setdefault(key, value) def do_preferences_changed(self): '''Handler called when preferences are changed by the user. Can be overloaded by sub classes to apply relevant changes. ''' pass def disconnect(self): '''Disconnect the plugin object from the ui, should revert any changes it made to the application. Default handler removes any GUI actions and menu items that were defined. ''' if self.ui.ui_type == 'gtk': self.ui.remove_ui(self) self.ui.remove_actiongroup(self) if self._is_image_generator_plugin: self.ui.mainpage.pageview.unregister_image_generator_plugin(self) def toggle_action(self, action, active=None): '''Trigger a toggle action. If 'active' is None it is toggled, else it is forced to state of 'active'. This method helps to keep the menu item or toolbar item asociated with the action in sync with your internal state. A typical usage to define a handler for a toggle action called 'show_foo' would be: def show_foo(self, show=None): self.toggle_action('show_foo', active=show) def do_show_foo(self, show=None): if show is None: show = self.actiongroup.get_action('show_foo').get_active() # ... the actual logic for toggling on / off 'foo' ''' name = action action = self.actiongroup.get_action(name) if active is None or active != action.get_active(): action.activate() else: method = getattr(self, 'do_'+name) method(active) def register_image_generator_plugin(self, type): self.ui.mainwindow.pageview.register_image_generator_plugin(self, type) self._is_image_generator_pluging = True