class SourceViewObjectType(InsertedObjectTypeExtension): name = 'code' label = _('Code Block') # T: menu item object_attr = { 'lang': String(None), 'linenumbers': Boolean(True), } def __init__(self, plugin, objmap): self._widgets = WeakSet() self.preferences = plugin.preferences InsertedObjectTypeExtension.__init__(self, plugin, objmap) self.connectto(self.preferences, 'changed', self.on_preferences_changed) def new_model_interactive(self, parent, notebook, page): lang = InsertCodeBlockDialog(parent).run() if lang is None: raise ValueError # dialog cancelled else: attrib = self.parse_attrib({'lang': lang}) return SourceViewBuffer(attrib, '') def model_from_data(self, notebook, page, attrib, text): return SourceViewBuffer(attrib, text) def data_from_model(self, buffer): return buffer.get_object_data() def create_widget(self, buffer): widget = SourceViewWidget(buffer) widget.set_preferences(self.preferences) self._widgets.add(widget) return widget def on_preferences_changed(self, preferences): for widget in self._widgets: widget.set_preferences(preferences) def format_html(self, dumper, attrib, data): # to use highlight.js add the following to your template: #<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/styles/default.min.css"> #<script src="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/highlight.min.js"></script> #<script>hljs.initHighlightingOnLoad();</script> #Map GtkSourceView language ids match with Highlight.js language ids. #http://packages.ubuntu.com/precise/all/libGtkSource.0-common/filelist #http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html sh_map = {'dosbatch': 'dos'} sh_lang = sh_map[attrib['lang']] if attrib['lang'] in sh_map else attrib['lang'] # TODO: some template instruction to be able to use other highlighters as well? output = ['<pre><code class="%s">' % html_encode(sh_lang)] # for syntaxhigligther #class="brush: language;" works with SyntaxHighlighter 2.0.278, 3 & 4 #output = ['<pre class="brush: %s;">' % html_encode(sh_lang)] # for syntaxhigligther output.append(html_encode(data)) output.append('</code></pre>\n') return output
def __init__(self, config=None): '''Constructor @param config: a L{ConfigManager} object that is used to load plugin preferences. Defaults to a L{VirtualConfigManager} for testing. ''' assert 'name' in self.plugin_info, 'Missing "name" in plugin_info' assert 'description' in self.plugin_info, 'Missing "description" in plugin_info' assert 'author' in self.plugin_info, 'Missing "author" in plugin_info' self.extensions = WeakSet() if self.plugin_preferences: assert isinstance( self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' self.config = config or VirtualConfigManager() self.preferences = self.config.get_config_dict( '<profile>/preferences.conf')[self.config_key] for pref in self.plugin_preferences: if len(pref) == 4: key, type, label, default = pref self.preferences.setdefault(key, default) #~ print ">>>>", key, default, '--', self.preferences[key] else: key, type, label, default, check = pref self.preferences.setdefault(key, default, check=check) #~ print ">>>>", key, default, check, '--', self.preferences[key] self.load_extensions_classes()
def __init__(self, config=None): '''Constructor @param config: a L{ConfigManager} object that is used to load plugin preferences. Defaults to a L{VirtualConfigManager} for testing. ''' assert 'name' in self.plugin_info, 'Missing "name" in plugin_info' assert 'description' in self.plugin_info, 'Missing "description" in plugin_info' assert 'author' in self.plugin_info, 'Missing "author" in plugin_info' self.extensions = WeakSet() if self.plugin_preferences: assert isinstance( self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' self.config = config or VirtualConfigManager() self.preferences = self.config.get_config_dict( '<profile>/preferences.conf')[self.config_key] self._init_config(self.preferences, self.plugin_preferences) self._init_config(self.preferences, self.plugin_notebook_properties ) # defaults for the properties are preferences self.load_insertedobject_types() self.load_extensions_classes()
def __init__(self, plugin, objmap): # InsertSymbolDialog(self.plugi).run() self._widgets = WeakSet() self.preferences = plugin.preferences InsertedObjectTypeExtension.__init__(self, plugin, objmap) self.connectto(self.preferences, 'changed', self.on_preferences_changed)
def _reset(self): self._preferences = ConfigManager.preferences['General'] self._preferences.setdefault('plugins', []) self._plugins = {} self._extendables = WeakSet() self.failed = set() self.insertedobjects = InsertedObjectTypeMap()
def __init__(self, attrib, data, preferences): if data.endswith('\n'): data = data[:-1] # If we have trailing \n it looks like an extra empty line # in the buffer, so we default remove one CustomObjectClass.__init__(self, attrib, data) self.preferences = preferences self.buffer = None self._widgets = WeakSet()
def __init__(self, config=None): self.config = config or VirtualConfigManager() self._preferences = \ self.config.get_config_dict('<profile>/preferences.conf') self.general_preferences = self._preferences['General'] self.general_preferences.setdefault('plugins', []) self._plugins = {} self._extendables = WeakSet() self._load_plugins() self.connectto(self._preferences, 'changed', self.on_preferences_changed)
class TableViewObjectType(InsertedObjectTypeExtension): name = 'runnable' label = _('Runnable') object_attr = { 'program': String('grep'), 'arguments': String('-r "hello!"') } def __init__(self, plugin, objmap): # InsertSymbolDialog(self.plugi).run() self._widgets = WeakSet() self.preferences = plugin.preferences InsertedObjectTypeExtension.__init__(self, plugin, objmap) self.connectto(self.preferences, 'changed', self.on_preferences_changed) def data_from_model(self, buffer): return buffer.get_object_data() def model_from_data(self, notebook, page, attrib, data): return TableModel(attrib) def model_from_element(self, attrib, element): assert ElementTree.iselement(element) attrib = self.parse_attrib(attrib) return TableModel(attrib) def create_widget(self, model): widget = TableViewWidget(model) self._widgets.add(widget) return widget def on_preferences_changed(self, preferences): for widget in self._widgets: widget.set_preferences(preferences) def dump(self, builder): builder.start("start") builder.data("data") builder.end("end")
def __init__(self): assert 'name' in self.plugin_info, 'Missing "name" in plugin_info' assert 'description' in self.plugin_info, 'Missing "description" in plugin_info' assert 'author' in self.plugin_info, 'Missing "author" in plugin_info' self.extensions = WeakSet() if self.plugin_preferences: assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' self.preferences = ConfigManager.preferences[self.config_key] self._init_config(self.preferences, self.plugin_preferences) self._init_config(self.preferences, self.plugin_notebook_properties) # defaults for the properties are preferences self.extension_classes = list(self.discover_classes(ExtensionBase))
def __init__(self, config=None): '''Constructor Constructor will directly load a list of default plugins based on the preferences in the config. Failures while loading these plugins will be logged but not raise errors. @param config: a L{ConfigManager} object that is passed along to the plugins and is used to load plugin preferences. Defaults to a L{VirtualConfigManager} for testing. ''' self.config = config or VirtualConfigManager() self._preferences = \ self.config.get_config_dict('<profile>/preferences.conf') self.general_preferences = self._preferences['General'] self.general_preferences.setdefault('plugins', []) self._plugins = {} self._extendables = WeakSet() self._load_plugins() self.connectto(self._preferences, 'changed', self.on_preferences_changed)
def __init__(self, attrib, header, rows, preferences): ''' Creates a new object which can displayed within the page :param attrib: aligns, wraps :param header: titles of the table as list :param rows: body-rows of the table as list of lists :param preferences: optionally some preferences ''' _attrib = {} for k, v in attrib.iteritems(): if isinstance(v, list): v = ','.join(map(str, v)) _attrib[k] = v CustomObjectClass.__init__(self, _attrib, [header] + rows) self.attrib = {'type': OBJECT_TYPE} # just to be sure self._tableattrib = attrib self._header = header self._rows = rows self._widgets = WeakSet() self._liststore = None # shared model between widgets self.preferences = preferences
def __init__(self, config=None): assert 'name' in self.plugin_info assert 'description' in self.plugin_info assert 'author' in self.plugin_info self.extensions = WeakSet() if self.plugin_preferences: assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' self.config = config or VirtualConfigManager() self.preferences = self.config.get_config_dict('<profile>/preferences.conf')[self.config_key] for pref in self.plugin_preferences: if len(pref) == 4: key, type, label, default = pref self.preferences.setdefault(key, default) #~ print ">>>>", key, default, '--', self.preferences[key] else: key, type, label, default, check = pref self.preferences.setdefault(key, default, check=check) #~ print ">>>>", key, default, check, '--', self.preferences[key] self.load_extensions_classes()
def register_object(self, type, factory): '''Register a factory method or class for a specific object type. @param type: the object type as string (unique name) @param factory: can be either an object class or a method, should callable and return objects. When constructing objects this factory will be called as:: factory(attrib, text) Where: - C{attrib} is a dict with attributes - C{text} is the main text source of the object @returns: a previously set factory for C{type} or C{None} ''' logger.debug('Registered object %s', type) type = type.lower() old = self.factories.get(type) self.factories[type] = factory self.objects[type] = WeakSet() return old
class SourceViewObject(CustomObjectClass): OBJECT_ATTR = { 'type': String('code'), 'lang': String(None), 'linenumbers': Boolean(True), } def __init__(self, attrib, data, preferences): if data.endswith('\n'): data = data[:-1] # If we have trailing \n it looks like an extra empty line # in the buffer, so we default remove one CustomObjectClass.__init__(self, attrib, data) self.preferences = preferences self.buffer = None self._widgets = WeakSet() def get_widget(self): if not self.buffer: self.buffer = gtksourceview2.Buffer() self.buffer.set_text(self._data) self.buffer.connect('modified-changed', self.on_modified_changed) self.buffer.set_highlight_matching_brackets(True) self.buffer.set_modified(False) self._data = None try: if self._attrib['lang']: self.buffer.set_language(lm.get_language(self._attrib['lang'])) except: logger.exception('Could not set language for sourceview: %s', lang) widget = SourceViewWidget(self, self.buffer) self._widgets.add(widget) widget.view.set_show_line_numbers(self._attrib['linenumbers']) widget.set_preferences(self.preferences) return widget def preferences_changed(self): for widget in self._widgets: widget.set_preferences(self.preferences) def on_modified_changed(self, buffer): # Sourceview changed, set change on oject, reset state of # sourceview buffer so we get a new signal with next change if buffer.get_modified(): self.set_modified(True) buffer.set_modified(False) def get_data(self): '''Returns data as text.''' if self.buffer: bounds = self.buffer.get_bounds() text = self.buffer.get_text(bounds[0], bounds[1]) text += '\n' # Make sure we always have a trailing \n return text else: return self._data def dump(self, format, dumper, linker=None): if format == "html": if self._attrib['lang']: ''' to use highlight.js add the following to your template: <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/styles/default.min.css"> <script src="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/highlight.min.js"></script> <script>hljs.initHighlightingOnLoad();</script> Map GtkSourceView language ids match with Highlight.js language ids. http://packages.ubuntu.com/precise/all/libgtksourceview2.0-common/filelist http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html ''' sh_map = {'dosbatch': 'dos'} sh_lang = sh_map[self._attrib['lang']] if self._attrib['lang'] in sh_map else self._attrib['lang'] # TODO: some template instruction to be able to use other highlighters as well? output = ['<pre><code class="%s">' % html_encode(sh_lang)] # for syntaxhigligther '''' class="brush: language;" works with SyntaxHighlighter 2.0.278, 3 & 4 output = ['<pre class="brush: %s;">' % html_encode(sh_lang)] # for syntaxhigligther ''' else: output = ['<pre>\n'] data = self.get_data() data = html_encode(data) # XXX currently dumper gives encoded lines - NOK #if self._attrib['linenumbers']: # for i, l in enumerate(data.splitlines(1)): # output.append('%i ' % (i+1) + l) #else: output.append(data) # ignoring numbering for html - syntaxhighlighter takes care of that if self._attrib['lang']: output.append('</code></pre>\n') else: output.append('</pre>\n') return output return CustomObjectClass.dump(self, format, dumper, linker) def set_language(self, lang): '''Set language in SourceView.''' self._attrib['lang'] = lang self.set_modified(True) if self.buffer: if lang is None: self.buffer.set_language(None) else: self.buffer.set_language(lm.get_language(lang)) def show_line_numbers(self, show): '''Toggles line numbers in SourceView.''' self._attrib['linenumbers'] = show self.set_modified(True) for widget in self._widgets: widget.view.set_show_line_numbers(show)
class PluginManager(ConnectorMixin, collections.Mapping): '''Manager that maintains a set of active plugins This class is the interface towards the rest of the application to load/unload plugins and to let plugins extend specific application objects. All object that want to instantiate new objects that are extendable need a reference to the plugin manager object that is instantiated when the application starts. When you instatiate a new object and want to present it for plugin extension, call the L{extend()} method. This object behaves as a dictionary with plugin object names as keys and plugin objects as value ''' def __init__(self, config=None): '''Constructor Constructor will directly load a list of default plugins based on the preferences in the config. Failures while loading these plugins will be logged but not raise errors. @param config: a L{ConfigManager} object that is passed along to the plugins and is used to load plugin preferences. Defaults to a L{VirtualConfigManager} for testing. ''' self.config = config or VirtualConfigManager() self._preferences = \ self.config.get_config_dict('<profile>/preferences.conf') self.general_preferences = self._preferences['General'] self.general_preferences.setdefault('plugins', []) self._plugins = {} self._extendables = WeakSet() self._load_plugins() self.connectto(self._preferences, 'changed', self.on_preferences_changed) def __getitem__(self, name): return self._plugins[name] def __iter__(self): return iter(sorted(self._plugins.keys())) # sort to make operation predictable - easier debugging def __len__(self): return len(self._plugins) def _load_plugins(self): '''Load plugins based on config''' for name in sorted(self.general_preferences['plugins']): try: self.load_plugin(name) except: logger.exception('Exception while loading plugin: %s', name) self.general_preferences['plugins'].remove(name) @classmethod def list_installed_plugins(klass): '''Lists plugin names for all installed plugins @returns: a set of plugin names ''' # List "zim.plugins" sub modules based on __path__ because this # parameter determines what folders will considered when importing # sub-modules of the this package once this module is loaded. plugins = set() for dir in __path__: dir = Dir(dir) for candidate in dir.list(): # returns [] if dir does not exist if candidate.startswith('_') or candidate == 'base': continue elif candidate.endswith('.py'): plugins.add(candidate[:-3]) elif zim.fs.isdir(dir.path+'/'+candidate) \ and os.path.exists(dir.path+'/'+candidate+'/__init__.py'): plugins.add(candidate) else: pass return plugins @classmethod def get_plugin_class(klass, name): '''Get the plugin class for a given name @param name: the plugin name (e.g. "calendar") @returns: the plugin class object ''' modname = 'zim.plugins.' + name mod = get_module(modname) return lookup_subclass(mod, PluginClass) @SignalHandler def on_preferences_changed(self, o): current = set(self._plugins.keys()) new = set(self.general_preferences['plugins']) for name in current - new: try: self.remove_plugin(name) except: logger.exception('Exception while loading plugin: %s', name) for name in new - current: try: self.load_plugin(name) except: logger.exception('Exception while loading plugin: %s', name) self.general_preferences['plugins'].remove(name) def load_plugin(self, name): '''Load a single plugin by name When the plugin was loaded already the existing object will be returned. Thus for each plugin only one instance can be active. @param name: the plugin module name @returns: the plugin object @raises Exception: when loading the plugin failed ''' assert isinstance(name, basestring) if name in self._plugins: return self._plugins[name] logger.debug('Loading plugin: %s', name) klass = self.get_plugin_class(name) if not klass.check_dependencies_ok(): raise AssertionError, 'Dependencies failed for plugin %s' % name plugin = klass(self.config) self.connectto(plugin, 'extension-point-changed') self._plugins[name] = plugin for obj in self._extendables: try: plugin.extend(obj) except: logger.exception('Exception in plugin: %s', name) if not name in self.general_preferences['plugins']: with self.on_preferences_changed.blocked(): self.general_preferences['plugins'].append(name) self.general_preferences.changed() return plugin def remove_plugin(self, name): '''Remove a plugin and it's extensions Fails silently if the plugin is not loaded. @param name: the plugin module name ''' if name in self.general_preferences['plugins']: # Do this first regardless of exceptions etc. with self.on_preferences_changed.blocked(): self.general_preferences['plugins'].remove(name) self.general_preferences.changed() try: plugin = self._plugins.pop(name) self.disconnect_from(plugin) except KeyError: pass else: logger.debug('Unloading plugin %s', name) plugin.destroy() def _foreach(self, func): # sort to make operation predictable - easier debugging for name, plugin in sorted(self._plugins.items()): try: func(plugin) except: logger.exception('Exception in plugin: %s', name) def extend(self, obj): '''Let any plugin extend the object instance C{obj} Will also remember the object (by a weak reference) such that plugins loaded after this call will also be called to extend C{obj} on their construction @param obj: arbitrary object that can be extended by plugins ''' if not obj in self._extendables: self._foreach(lambda p: p.extend(obj)) self._extendables.add(obj) def on_extension_point_changed(self, plugin, name): for obj in self._extendables: if obj.__class__.__name__ == name: try: plugin.extend(obj) except: logger.exception('Exception in plugin: %s', name)
class SourceViewObject(CustomObjectClass): OBJECT_ATTR = { 'type': String('code'), 'lang': String(None), 'linenumbers': Boolean(True), } def __init__(self, attrib, data, preferences): if data.endswith('\n'): data = data[:-1] # If we have trailing \n it looks like an extra empty line # in the buffer, so we default remove one CustomObjectClass.__init__(self, attrib, data) self.preferences = preferences self.buffer = None self._widgets = WeakSet() def get_widget(self): if not self.buffer: self.buffer = gtksourceview2.Buffer() self.buffer.set_text(self._data) self.buffer.connect('modified-changed', self.on_modified_changed) self.buffer.set_highlight_matching_brackets(True) self.buffer.set_modified(False) self._data = None try: if self._attrib['lang']: self.buffer.set_language( lm.get_language(self._attrib['lang'])) except: logger.exception('Could not set language for sourceview: %s', lang) widget = SourceViewWidget(self, self.buffer) self._widgets.add(widget) widget.view.set_show_line_numbers(self._attrib['linenumbers']) widget.set_preferences(self.preferences) return widget def preferences_changed(self): for widget in self._widgets: widget.set_preferences(self.preferences) def on_modified_changed(self, buffer): # Sourceview changed, set change on oject, reset state of # sourceview buffer so we get a new signal with next change if buffer.get_modified(): self.set_modified(True) buffer.set_modified(False) def get_data(self): '''Returns data as text.''' if self.buffer: bounds = self.buffer.get_bounds() text = self.buffer.get_text(bounds[0], bounds[1]) text += '\n' # Make sure we always have a trailing \n return text else: return self._data def dump(self, format, dumper, linker=None): if format == "html": if self._attrib['lang']: # class="brush: language;" works with SyntaxHighlighter 2.0.278 # by Alex Gorbatchev <http://alexgorbatchev.com/SyntaxHighlighter/> # TODO: not all GtkSourceView language ids match with SyntaxHighlighter # language ids. # TODO: some template instruction to be able to use other highlighters as well? output = [ '<pre class="brush: %s;">\n' % html_encode(self._attrib['lang']) ] else: output = ['<pre>\n'] data = self.get_data() data = html_encode( data) # XXX currently dumper gives encoded lines - NOK if self._attrib['linenumbers']: for i, l in enumerate(data.splitlines(1)): output.append('%i ' % (i + 1) + l) else: output.append(data) output.append('</pre>\n') return output return CustomObjectClass.dump(self, format, dumper, linker) def set_language(self, lang): '''Set language in SourceView.''' self._attrib['lang'] = lang self.set_modified(True) if self.buffer: if lang is None: self.buffer.set_language(None) else: self.buffer.set_language(lm.get_language(lang)) def show_line_numbers(self, show): '''Toggles line numbers in SourceView.''' self._attrib['linenumbers'] = show self.set_modified(True) for widget in self._widgets: widget.view.set_show_line_numbers(show)
class TableViewObjectType(InsertedObjectTypeExtension): name = 'table' label = _('Table') # T: menu item verb_icon = 'zim-insert-table' object_attr = { 'aligns': String(''), # i.e. String(left,right,center) 'wraps': String('') # i.e. String(0,1,0) } def __init__(self, plugin, objmap): self._widgets = WeakSet() self.preferences = plugin.preferences InsertedObjectTypeExtension.__init__(self, plugin, objmap) self.connectto(self.preferences, 'changed', self.on_preferences_changed) def new_model_interactive(self, parent, notebook, page): definition = EditTableDialog(parent).run() if definition is None: raise ValueError # dialog cancelled ids, headers, wraps, aligns = definition attrib = self.parse_attrib({ 'aligns': ','.join(map(str, aligns)), 'wraps': ','.join(map(str, wraps)) }) rows = [''] * len(headers) return TableModel(attrib, headers, rows) def model_from_data(self, notebook, page, attrib, data): tree = WikiParser().parse(data) element = tree._etree.getroot().find( 'table') # XXX - should use token interface instead if element is not None: return self.model_from_element(element.attrib, element) else: return TableModel(attrib, [data.strip()], ['']) def model_from_element(self, attrib, element): assert ElementTree.iselement(element) attrib = self.parse_attrib(attrib) headers, rows = self._tabledom_to_list(element) return TableModel(attrib, headers, rows) def _tabledom_to_list(self, tabledata): ''' Extracts necessary data out of a xml-table into a list structure :param tabledata: XML - formated as a zim-tree table-object :return: tuple of header-list and list of row lists - ([h1,h2],[[r11,r12],[r21,r22]) ''' headers = [head.text for head in tabledata.findall('thead/th')] headers = list(map(CellFormatReplacer.zim_to_cell, headers)) rows = [] for trow in tabledata.findall('trow'): row = trow.findall('td') row = [ ElementTree.tostring(r, 'unicode').replace('<td>', '').replace( '</td>', '') for r in row ] row = list(map(CellFormatReplacer.zim_to_cell, row)) rows.append(row) return headers, rows def create_widget(self, model): widget = TableViewWidget(model) widget.set_preferences(self.preferences) self._widgets.add(widget) return widget def on_preferences_changed(self, preferences): for widget in self._widgets: widget.set_preferences(preferences) def dump(self, builder, model): headers, attrib, rows = model.get_object_data() def append(tag, text): builder.start(tag, {}) builder.data(text) builder.end(tag) builder.start(TABLE, dict(attrib)) builder.start(HEADROW) for header in headers: append(HEADDATA, header) builder.end(HEADROW) for row in rows: builder.start(TABLEROW, {}) for cell in row: append(TABLEDATA, cell) builder.end(TABLEROW) builder.end(TABLE)
def __init__(self): self.factories = {} self.objects = {'fallback': WeakSet()} self.window_extensions = {}
class SourceViewObject(CustomObjectClass): OBJECT_ATTR = { 'type': String('code'), 'lang': String(None), 'linenumbers': Boolean(True), } def __init__(self, attrib, data, preferences): if data.endswith('\n'): data = data[:-1] # If we have trailing \n it looks like an extra empty line # in the buffer, so we default remove one CustomObjectClass.__init__(self, attrib, data) self.preferences = preferences self.buffer = None self._widgets = WeakSet() def get_widget(self): if not self.buffer: self.buffer = gtksourceview2.Buffer() self.buffer.set_text(self._data) self.buffer.connect('modified-changed', self.on_modified_changed) self.buffer.set_highlight_matching_brackets(True) self.buffer.set_modified(False) self._data = None try: if self._attrib['lang']: self.buffer.set_language(lm.get_language(self._attrib['lang'])) except: logger.exception('Could not set language for sourceview: %s', lang) widget = SourceViewWidget(self, self.buffer) self._widgets.add(widget) widget.view.set_show_line_numbers(self._attrib['linenumbers']) widget.set_preferences(self.preferences) return widget def preferences_changed(self): for widget in self._widgets: widget.set_preferences(self.preferences) def on_modified_changed(self, buffer): # Sourceview changed, set change on oject, reset state of # sourceview buffer so we get a new signal with next change if buffer.get_modified(): self.set_modified(True) buffer.set_modified(False) def get_data(self): '''Returns data as text.''' if self.buffer: bounds = self.buffer.get_bounds() text = self.buffer.get_text(bounds[0], bounds[1]) text += '\n' # Make sure we always have a trailing \n return text else: return self._data def dump(self, format, dumper, linker=None): if format == "html": if self._attrib['lang']: # class="brush: language;" works with SyntaxHighlighter 2.0.278 # by Alex Gorbatchev <http://alexgorbatchev.com/SyntaxHighlighter/> # TODO: not all GtkSourceView language ids match with SyntaxHighlighter # language ids. # TODO: some template instruction to be able to use other highlighters as well? output = ['<pre class="brush: %s;">\n' % html_encode(self._attrib['lang'])] else: output = ['<pre>\n'] data = self.get_data() data = html_encode(data) # XXX currently dumper gives encoded lines - NOK if self._attrib['linenumbers']: for i, l in enumerate(data.splitlines(1)): output.append('%i ' % (i+1) + l) else: output.append(data) output.append('</pre>\n') return output return CustomObjectClass.dump(self, format, dumper, linker) def set_language(self, lang): '''Set language in SourceView.''' self._attrib['lang'] = lang self.set_modified(True) if self.buffer: if lang is None: self.buffer.set_language(None) else: self.buffer.set_language(lm.get_language(lang)) def show_line_numbers(self, show): '''Toggles line numbers in SourceView.''' self._attrib['linenumbers'] = show self.set_modified(True) for widget in self._widgets: widget.view.set_show_line_numbers(show)
def __init__(self, plugin): InsertedObjectType.__init__(self, plugin) self._widgets = WeakSet() self.preferences = plugin.preferences self.connectto(self.preferences, 'changed', self.on_preferences_changed)
class TableViewObject(CustomObjectClass): '''data presenter of an inserted table within a page''' OBJECT_ATTR = { 'type': String('table'), 'aligns': String(''), # i.e. String(left,right,center) 'wraps': String('') # i.e. String(0,1,0) } def __init__(self, attrib, header, rows, preferences): ''' Creates a new object which can displayed within the page :param attrib: aligns, wraps :param header: titles of the table as list :param rows: body-rows of the table as list of lists :param preferences: optionally some preferences ''' _attrib = {} for k, v in attrib.iteritems(): if isinstance(v, list): v = ','.join(map(str, v)) _attrib[k] = v CustomObjectClass.__init__(self, _attrib, [header] + rows) self.attrib = {'type': OBJECT_TYPE} # just to be sure self._tableattrib = attrib self._header = header self._rows = rows self._widgets = WeakSet() self._liststore = None # shared model between widgets self.preferences = preferences # getters and setters for attributes def get_aligns(self): ''' get the list of align-attributes ''' return self._attrib['aligns'].split(',') def set_aligns(self, data): ''' Set list of align attributes for the current table. Each item belongs to a column.''' assert (isinstance(data, list)) self._attrib['aligns'] = ','.join(data) def get_wraps(self): ''' get the list of wrap-attributes ''' return map(int, self._attrib['wraps'].split(',')) def set_wraps(self, data): ''' Set list of wrap attributes for the current table. Each item belongs to a column.''' assert (isinstance(data, list)) self._attrib['wraps'] = ','.join(str(item) for item in data) def _get_liststore(self, reset=False): if reset or not self._liststore: cols = [str] * len(self._header) self._liststore = gtk.ListStore(*cols) for trow in self._rows: self._liststore.append(trow) self._liststore.connect('row-changed', self.on_modified_changed) return self._liststore def get_widget(self): ''' Creates a new table-widget which can displayed on the wiki-page ''' liststore = self._get_liststore() attrib = {'aligns': self.get_aligns(), 'wraps': self.get_wraps()} widget = TableViewWidget(self, liststore, self._header, attrib) self._widgets.add(widget) widget.set_preferences(self.preferences) return widget def preferences_changed(self): ''' Updates all created table-widgets, if preferences have changed ''' for widget in self._widgets: widget.set_preferences(self.preferences) def on_sort_column_changed(self, liststore): ''' Trigger after a column-header is clicked and therefore its sort order has changed ''' self.set_modified(True) def on_modified_changed(self, liststore, path, treeiter): ''' Trigger after a table cell content is changed by the user ''' self.set_modified(True) def get_data(self): '''Returns table-object into textual data, for saving it as text.''' headers = self._header attrs = { 'aligns': self._attrib['aligns'], 'wraps': self._attrib['wraps'] } if not self._liststore: rows = self._rows else: rows = [] for treerow in self._liststore: rows.append( map( lambda cell: CellFormatReplacer.cell_to_input( cell, True), treerow)) return headers, rows, attrs def change_model(self, new_model): ''' Replace liststore with new model and notify widgets to update their treeview. :param new_model: tuple of lists for ([id], [header], [warps], [aligns]) ''' # prepare results out of dialog-window id_mapping, headers, aligns, wraps = ({}, [], [], []) for i, model in enumerate(new_model): if model[0] != -1: id_mapping[i] = model[0] header = model[1] if model[1] else ' ' headers.append(header) aligns.append(model[3]) wraps.append(model[2]) # update data if self._liststore: liststore = self._get_liststore() self._rows = self._update_rows(liststore, id_mapping, len(headers)) liststore = self._get_liststore(reset=True) else: liststore = None self._rows = self._update_rows(self._rows, id_mapping, len(headers)) self.set_aligns(aligns) self.set_wraps(wraps) self.set_modified(True) # notify widgets for widget in self._widgets: assert liststore is not None, 'Huh?' attrib = {'aligns': self.get_aligns(), 'wraps': self.get_wraps()} widget.on_model_changed(liststore, headers, attrib) self.preferences_changed() # reset prefs on widgets def _update_rows(self, old_rows, id_mapping, nr_cols): ''' Old value of cells are used in the new table, but only if its column is not deleted ''' new_rows = [] for oldrow in old_rows: newrow = [' '] * nr_cols for v, k in id_mapping.iteritems(): newrow[v] = oldrow[k] new_rows.append(newrow) return new_rows def build_parsetree_of_table(self, builder, iter): logger.debug("Anchor with TableObject: %s", self) # inserts a newline before and after table-object bound = iter.copy() bound.backward_char() char_before_table = bound.get_slice(iter) need_newline_infront = char_before_table.decode( 'utf-8') != "\n".decode('utf-8') bound = iter.copy() bound.forward_char() iter2 = bound.copy() bound.forward_char() char_after_table = iter2.get_slice(bound) need_newline_behind = char_after_table.decode('utf-8') != "\n".decode( 'utf-8') # headers, rows, attrib = self.get_data() #~ print "Table data:", headers, rows, attrib if need_newline_infront: builder.data('\n') builder.start(TABLE, attrib) builder.start(HEADROW) for header in headers: builder.append(HEADDATA, header) builder.end(HEADROW) for row in rows: builder.start(TABLEROW) for cell in row: builder.append(TABLEDATA, cell) builder.end(TABLEROW) builder.end(TABLE) if need_newline_behind: builder.data('\n')
class TableViewObjectType(InsertedObjectType): name = 'table' label = _('Table') # T: menu item verb_icon = 'zim-insert-table' object_attr = { 'aligns': String(''), # i.e. String(left,right,center) 'wraps': String('') # i.e. String(0,1,0) } def __init__(self, plugin): InsertedObjectType.__init__(self, plugin) self._widgets = WeakSet() self.preferences = plugin.preferences self.connectto(self.preferences, 'changed', self.on_preferences_changed) def new_object_interactive(self, parent): definition = EditTableDialog(parent).run() if definition is None: raise ValueError # dialog cancelled ids, headers, wraps, aligns = definition attrib = self.parse_attrib({ 'aligns': ','.join(map(str, aligns)), 'wraps': ','.join(map(str, wraps)) }) rows = [''] * len(headers) data = ' | '.join(headers) + '\n' + ' | '.join(rows) + '\n' return attrib, data def model_from_data(self, attrib, data): rows = [line.split(' | ') for line in data.splitlines()] headers = rows.pop(0) if rows else [] if not headers: headers = ['Column1', 'Column2'] return TableModel(attrib, headers, rows) def model_from_element(self, attrib, element): assert ElementTree.iselement(element) attrib = self.parse_attrib(attrib) headers, rows = self._tabledom_to_list(element) return TableModel(attrib, headers, rows) def _tabledom_to_list(self, tabledata): ''' Extracts necessary data out of a xml-table into a list structure :param tabledata: XML - formated as a zim-tree table-object :return: tuple of header-list and list of row lists - ([h1,h2],[[r11,r12],[r21,r22]) ''' headers = [head.text for head in tabledata.findall('thead/th')] headers = list(map(CellFormatReplacer.zim_to_cell, headers)) rows = [] for trow in tabledata.findall('trow'): row = trow.findall('td') row = [ ElementTree.tostring(r, 'unicode').replace('<td>', '').replace( '</td>', '') for r in row ] row = list(map(CellFormatReplacer.zim_to_cell, row)) rows.append(row) return headers, rows def create_widget(self, model): widget = TableViewWidget(model) widget.set_preferences(self.preferences) self._widgets.add(widget) return widget def on_preferences_changed(self, preferences): for widget in self._widgets: widget.set_preferences(preferences) def dump(self, builder, model): headers, attrib, rows = model.get_object_data() builder.start(TABLE, dict(attrib)) builder.start(HEADROW) for header in headers: builder.append(HEADDATA, header) builder.end(HEADROW) for row in rows: builder.start(TABLEROW) for cell in row: builder.append(TABLEDATA, cell) builder.end(TABLEROW) builder.end(TABLE)
class PluginManager(ConnectorMixin, collections.Mapping): '''Manager that maintains a set of active plugins Handles loading and destroying plugins and is the entry point for extending application components. This object behaves as a dictionary with plugin object names as keys and plugin objects as value ''' def __init__(self, config=None): self.config = config or VirtualConfigManager() self._preferences = \ self.config.get_config_dict('<profile>/preferences.conf') self.general_preferences = self._preferences['General'] self.general_preferences.setdefault('plugins', []) self._plugins = {} self._extendables = WeakSet() self._load_plugins() self.connectto(self._preferences, 'changed', self.on_preferences_changed) def __getitem__(self, name): return self._plugins[name] def __iter__(self): return iter(sorted(self._plugins.keys())) # sort to make operation predictable - easier debugging def __len__(self): return len(self._plugins) def _load_plugins(self): '''Load plugins based on config''' for name in sorted(self.general_preferences['plugins']): try: self.load_plugin(name) except: logger.exception('Exception while loading plugin: %s', name) self.general_preferences['plugins'].remove(name) @SignalHandler def on_preferences_changed(self, o): current = set(self._plugins.keys()) new = set(self.general_preferences['plugins']) for name in current - new: try: self.remove_plugin(name) except: logger.exception('Exception while loading plugin: %s', name) for name in new - current: try: self.load_plugin(name) except: logger.exception('Exception while loading plugin: %s', name) self.general_preferences['plugins'].remove(name) def load_plugin(self, name): '''Load a single plugin by name When the plugin was loaded already the existing object will be returned. Thus for each plugin only one instance can be active. @param name: the plugin module name @returns: the plugin object @raises Exception: when loading the plugin failed ''' assert isinstance(name, basestring) if name in self._plugins: return self._plugins[name] logger.debug('Loading plugin: %s', name) klass = get_plugin_class(name) if not klass.check_dependencies_ok(): raise AssertionError, 'Dependencies failed for plugin %s' % name plugin = klass(self.config) self.connectto(plugin, 'extension-point-changed') self._plugins[name] = plugin for obj in self._extendables: try: plugin.extend(obj) except: logger.exception('Exception in plugin: %s', name) if not name in self.general_preferences['plugins']: with self.on_preferences_changed.blocked(): self.general_preferences['plugins'].append(name) self.general_preferences.changed() return plugin def remove_plugin(self, name): '''Remove a plugin and it's extensions Fails silently if the plugin is not loaded. @param name: the plugin module name ''' if name in self.general_preferences['plugins']: # Do this first regardless of exceptions etc. with self.on_preferences_changed.blocked(): self.general_preferences['plugins'].remove(name) self.general_preferences.changed() try: plugin = self._plugins.pop(name) self.disconnect_from(plugin) except KeyError: pass else: logger.debug('Unloading plugin %s', name) plugin.destroy() def _foreach(self, func): # sort to make operation predictable - easier debugging for name, plugin in sorted(self._plugins.items()): try: func(plugin) except: logger.exception('Exception in plugin: %s', name) def extend(self, obj): '''Let any plugin extend the object instance C{obj} Will also remember object (by a weak reference) such that plugins loaded after this call will also be called to extend C{obj} on their construction @param obj: arbitrary object that can be extended by plugins ''' if not obj in self._extendables: self._foreach(lambda p: p.extend(obj)) self._extendables.add(obj) def on_extension_point_changed(self, plugin, name): for obj in self._extendables: if obj.__class__.__name__ == name: try: plugin.extend(obj) except: logger.exception('Exception in plugin: %s', name)
def __init__(self): self.factories = {} self.objects = {'fallback': WeakSet()}
class PluginClass(ConnectorMixin, SignalEmitter): '''Base class for plugins objects. To be recognized as a plugin, a submodule of "zim.plugins" needs to define one (and only one) sub-class of L{PluginClass}. This class will define the main plugin object and contains meta data about the plugin and e.g. plugin preferences. The plugin object itself doesn't directly interact with the rest of the zim application. To actually add functionality to zim, the plugin module will also need to define one or more "extension" classes. These classes act as decorators for specific objects that appear in the application. All extension classes defined in the same module file as the plugin object are automatically linked to the plugin. 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. Plugin classes should at minimum define two class attributes: C{plugin_info} and C{plugin_preferences}. When these are defined no other code is needed to have a basic plugin up and running. @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 (if any). Each preference is defined by a 4-tuple containing the following items: 1. the dict key of the option (used in the config file and in the preferences dict) 2. an option type (see L{InputForm.add_inputs(){} for more details) 3. a (translatable) label to show in the preferences dialog for this option 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 of the plugin object. The type and label will be used to render a default config dialog when triggered from the preferences dialog. Changes to these preferences will be stored in a config file so they are persistent. @ivar preferences: a L{ConfigDict} with plugin preferences Preferences are the global configuration of the plugin, they are stored in the X{preferences.conf} config file. @ivar config: a L{ConfigManager} object that can be used to lookup additional config files for the plugin @ivar extension_classes: a dictionary with extension classes found in the plugin module @ivar extensions: a set with extension objects loaded by this plugin. The lookup extensions objects it is usually better to use the methods L{get_extension()} or L{get_extensions()} rather than using this set directly. @signal: C{extension-point-changed (name)}: emitted when extension point C{name} changes ''' # define signals we want to use - (closure type, return type and arg types) __signals__ = {'extension-point-changed': (None, None, (basestring, ))} plugin_info = {} plugin_preferences = () @classproperty def config_key(klass): '''The name of section used in the config files to store the preferences for this plugin. ''' return klass.__name__ @classmethod def check_dependencies_ok(klass): '''Checks minimum dependencies are met @returns: C{True} if this plugin can be loaded based on L{check_dependencies()} ''' check, dependencies = klass.check_dependencies() return check @classmethod def check_dependencies(klass): '''Checks what dependencies are met and gives details for display in the preferences dialog @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. Default always returns C{True} with an empty list. ''' return (True, []) def __init__(self, config=None): '''Constructor @param config: a L{ConfigManager} object that is used to load plugin preferences. Defaults to a L{VirtualConfigManager} for testing. ''' assert 'name' in self.plugin_info, 'Missing "name" in plugin_info' assert 'description' in self.plugin_info, 'Missing "description" in plugin_info' assert 'author' in self.plugin_info, 'Missing "author" in plugin_info' self.extensions = WeakSet() if self.plugin_preferences: assert isinstance( self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' self.config = config or VirtualConfigManager() self.preferences = self.config.get_config_dict( '<profile>/preferences.conf')[self.config_key] for pref in self.plugin_preferences: if len(pref) == 4: key, type, label, default = pref self.preferences.setdefault(key, default) #~ print ">>>>", key, default, '--', self.preferences[key] else: key, type, label, default, check = pref self.preferences.setdefault(key, default, check=check) #~ print ">>>>", key, default, check, '--', self.preferences[key] self.load_extensions_classes() @classmethod def lookup_subclass(pluginklass, klass): '''Returns first subclass of C{klass} found in the module of this plugin. (Similar to L{zim.utils.lookup_subclass}). @param pluginklass: plugin class @param klass: base class of the wanted class ''' module = get_module(pluginklass.__module__) return lookup_subclass(module, klass) def load_extensions_classes(self): '''Instantiates the C{extension_classes} dictionary with classes found in the same module as the plugin object. Called directly by the constructor. ''' self.extension_classes = {} for extends, klass in self.discover_extensions_classes(): self.add_extension_class(extends, klass) @classmethod def discover_extensions_classes(pluginklass): '''Find extension classes in same module as the plugin object class. @returns: yields 2-tuple of the name of the object class to be extended (as set by the L{extends} decorator) and the extension class object ''' # Any class with the "__extends__" field will be added # (Being subclass of ObjectExtension is optional) module = get_module(pluginklass.__module__) for n, klass in inspect.getmembers(module, inspect.isclass): if hasattr(klass, '__extends__') and klass.__extends__: yield klass.__extends__, klass def set_extension_class(self, extends, klass): '''Set the extension class for a specific target object class This method can be used to dynamically set extension classes on run time. E.g. of the extension class depends on a preference. If another extension class was already defined for the same target object, it is removed. When the plugin is managed by a L{PluginManager} and that manager is aware of objects of the target class, extensions will immediatly be instantiated for those objects. @param extends: class name of the to-be-extended object @param klass: the extension class @emits: extension-point-changed ''' if extends in self.extension_classes: if self.extension_classes[extends] == klass: pass else: self.remove_extension_class(extends) self.add_extension_class(extends, klass) else: self.add_extension_class(extends, klass) def add_extension_class(self, extends, klass): '''Add an extension class for a specific target object class When the plugin is managed by a L{PluginManager} and that manager is aware of objects of the target class, extensions will immediatly be instantiated for those objects. @param extends: class name of the to-be-extended object @param klass: the extension class @emits: extension-point-changed ''' if extends in self.extension_classes: raise AssertionError, 'Extension point %s already in use' % name self.extension_classes[extends] = klass self.emit('extension-point-changed', extends) def remove_extension_class(self, extends): '''Remove the extension class for a specific target object class Will result in all extension objects for this object class to be destroyed. @param extends: class name of the to-be-extended object ''' klass = self.extension_classes.pop(extends) for obj in self.get_extensions(klass): obj.destroy() def extend(self, obj, _name=None): '''This method will look through the extensions defined for this plugin and construct a new extension object if a match is found for C{obj}. @param obj: the obejct to be extended @param _name: lookup name to use when extending the object. To be used for testing only. Normally the class name of C{obj} is used. ''' name = _name or obj.__class__.__name__ if name in self.extension_classes: ext = self.extension_classes[name](self, obj) self.extensions.add(ext) def get_extension(self, klass, **attr): '''Look up an extension object instatiation @param klass: the class of the extention object (_not_ the to-be-extended klass) @param attr: any object attributes that should match @returns: a single extension object or C{None} ''' ext = self.get_extensions(klass) for key, value in attr.items(): ext = filter(lambda e: getattr(e, key) == value, ext) if len(ext) > 1: raise AssertionError, 'BUG: multiple extensions of class %s found' % klass elif ext: return ext[0] else: return None def get_extensions(self, klass): '''Look up extension object instatiations @param klass: the class of the extention object (_not_ the to-be-extended klass) @returns: a list of extension objects (if any) ''' return [e for e in self.extensions if isinstance(e, klass)] 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). ''' for obj in self.extensions: obj.destroy() try: self.disconnect_all() except: logger.exception('Exception while disconnecting %s', self)
class PluginClass(ConnectorMixin, SignalEmitter): '''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}. 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. @ivar ui: the main application object, e.g. an instance of L{zim.gui.GtkInterface} or L{zim.www.WWWInterface} @ivar preferences: a C{ConfigDict()} 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{ConfigDict()} 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{extension-point-changed (name)}: emitted when extension point C{name} changes ''' # define signals we want to use - (closure type, return type and arg types) __signals__ = { 'extension-point-changed': (None, None, (basestring,)) } plugin_info = {} plugin_preferences = () @classproperty def config_key(klass): return klass.__name__ @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, config=None): assert 'name' in self.plugin_info assert 'description' in self.plugin_info assert 'author' in self.plugin_info self.extensions = WeakSet() if self.plugin_preferences: assert isinstance(self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' self.config = config or VirtualConfigManager() self.preferences = self.config.get_config_dict('<profile>/preferences.conf')[self.config_key] for pref in self.plugin_preferences: if len(pref) == 4: key, type, label, default = pref self.preferences.setdefault(key, default) #~ print ">>>>", key, default, '--', self.preferences[key] else: key, type, label, default, check = pref self.preferences.setdefault(key, default, check=check) #~ print ">>>>", key, default, check, '--', self.preferences[key] self.load_extensions_classes() @classmethod def lookup_subclass(pluginklass, klass): '''Returns first subclass of C{klass} found in the module of this plugin. (Similar to L{zim.utils.lookup_subclass}) @param pluginklass: plugin class @param klass: base class of the wanted class ''' module = get_module(pluginklass.__module__) return lookup_subclass(module, klass) def load_extensions_classes(self): self.extension_classes = {} for name, klass in self.discover_extensions_classes(): self.add_extension_class(name, klass) @classmethod def discover_extensions_classes(pluginklass): # Find related extension classes in same module # any class with the "__extends__" field will be added # (Being subclass of ObjectExtension is optional) module = get_module(pluginklass.__module__) for n, klass in inspect.getmembers(module, inspect.isclass): if hasattr(klass, '__extends__') and klass.__extends__: yield klass.__extends__, klass def set_extension_class(self, name, klass): if name in self.extension_classes: if self.extension_classes[name] == klass: pass else: self.remove_extension_class(name) self.add_extension_class(name, klass) else: self.add_extension_class(name, klass) def add_extension_class(self, name, klass): if name in self.extension_classes: raise AssertionError, 'Extension point %s already in use' % name self.extension_classes[name] = klass self.emit('extension-point-changed', name) def remove_extension_class(self, name): klass = self.extension_classes.pop(name) for obj in self.get_extensions(klass): obj.destroy() def extend(self, obj, name=None): # TODO also check parent classes # name should only be used for testing name = name or obj.__class__.__name__ if name in self.extension_classes: ext = self.extension_classes[name](self, obj) self.extensions.add(ext) def get_extension(self, klass, **attr): ext = self.get_extensions(klass) for key, value in attr.items(): ext = filter(lambda e: getattr(e, key) == value, ext) if len(ext) > 1: raise AssertionError, 'BUG: multiple extensions of class %s found' % klass elif ext: return ext[0] else: return None def get_extensions(self, klass): return [e for e in self.extensions if isinstance(e, klass)] 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). ''' for obj in self.extensions: obj.destroy() try: self.disconnect_all() except: logger.exception('Exception while disconnecting %s', self)
class PluginManagerClass(ConnectorMixin, collections.Mapping): '''Manager that maintains a set of active plugins This class is the interface towards the rest of the application to load/unload plugins. It behaves as a dictionary with plugin object names as keys and plugin objects as value ''' def __init__(self): '''Constructor Constructor will directly load a list of default plugins based on the preferences in the config. Failures while loading these plugins will be logged but not raise errors. @param config: a L{ConfigManager} object that is passed along to the plugins and is used to load plugin preferences. Defaults to a L{VirtualConfigManager} for testing. ''' self._reset() def _reset(self): self._preferences = ConfigManager.preferences['General'] self._preferences.setdefault('plugins', []) self._plugins = {} self._extendables = WeakSet() self.failed = set() self.insertedobjects = InsertedObjectTypeMap() def load_plugins_from_preferences(self, names): '''Calls L{load_plugin()} for each plugin in C{names} but does not raise an exception when loading fails. ''' for name in names: try: self.load_plugin(name) except Exception as exc: if isinstance(exc, ImportError): logger.info('No such plugin: %s', name) else: logger.exception('Exception while loading plugin: %s', name) if name in self._preferences['plugins']: self._preferences['plugins'].remove(name) self.failed.add(name) def __call__(self): return self # singleton behavior if called as class def __getitem__(self, name): return self._plugins[name] def __iter__(self): return iter(sorted(self._plugins.keys())) # sort to make operation predictable - easier debugging def __len__(self): return len(self._plugins) @classmethod def list_installed_plugins(klass): '''Lists plugin names for all installed plugins @returns: a set of plugin names ''' # List "zim.plugins" sub modules based on __path__ because this # parameter determines what folders will considered when importing # sub-modules of the this package once this module is loaded. plugins = set() # THIS LINE IS REPLACED BY SETUP.PY - DON'T CHANGE IT for folder in [f for f in map(LocalFolder, __path__) if f.exists()]: for child in folder: name = child.basename if name.startswith('_') or name == 'base': continue elif isinstance(child, LocalFile) and name.endswith('.py'): plugins.add(name[:-3]) elif isinstance(child, LocalFolder) \ and child.file('__init__.py').exists(): plugins.add(name) else: pass return plugins @classmethod def get_plugin_class(klass, name): '''Get the plugin class for a given name @param name: the plugin module name @returns: the plugin class object ''' modname = 'zim.plugins.' + name mod = get_module(modname) return lookup_subclass(mod, PluginClass) def _new_extendable(self, obj): '''Let any plugin extend the object instance C{obj} Will also remember the object (by a weak reference) such that plugins loaded after this call will also be called to extend C{obj} on their construction @param obj: arbitrary object that can be extended by plugins ''' logger.debug("New extendable: %s", obj) assert not obj in self._extendables for name, plugin in sorted(self._plugins.items()): # sort to make operation predictable self._extend(plugin, obj) self._extendables.add(obj) def _extend(self, plugin, obj): for ext_class in plugin.extension_classes: if issubclass(ext_class, obj.__zim_extension_bases__): logger.debug("Load extension: %s", ext_class) try: ext = ext_class(plugin, obj) except: logger.exception( 'Failed loading extension %s for plugin %s', ext_class, plugin) else: plugin.extensions.add(ext) def load_plugin(self, name): '''Load a single plugin by name When the plugin was loaded already the existing object will be returned. Thus for each plugin only one instance can be active. @param name: the plugin module name @returns: the plugin object @raises Exception: when loading the plugin failed ''' assert isinstance(name, str) if name in self._plugins: return self._plugins[name] logger.debug('Loading plugin: %s', name) klass = self.get_plugin_class(name) if not klass.check_dependencies_ok(): raise AssertionError('Dependencies failed for plugin %s' % name) plugin = klass() self._plugins[name] = plugin for obj in self._extendables: self._extend(plugin, obj) if not name in self._preferences['plugins']: self._preferences['plugins'].append(name) self._preferences.changed() return plugin def remove_plugin(self, name): '''Remove a plugin and it's extensions Fails silently if the plugin is not loaded. @param name: the plugin module name ''' if name in self._preferences['plugins']: # Do this first regardless of exceptions etc. self._preferences['plugins'].remove(name) self._preferences.changed() try: plugin = self._plugins.pop(name) self.disconnect_from(plugin) except KeyError: pass else: logger.debug('Unloading plugin %s', name) plugin.destroy()
class PluginClass(ConnectorMixin): '''Base class for plugins objects. To be recognized as a plugin, a submodule of "zim.plugins" needs to define one (and only one) sub-class of L{PluginClass}. This class will define the main plugin object and contains meta data about the plugin and e.g. plugin preferences. The plugin object itself doesn't directly interact with the rest of the zim application. To actually add functionality to zim, the plugin module will also need to define one or more "extension" classes. These classes act as decorators for specific objects that appear in the application. All extension classes defined in the same module file as the plugin object are automatically linked to the plugin. 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. Plugin classes should at minimum define two class attributes: C{plugin_info} and C{plugin_preferences}. When these are defined no other code is needed to have a basic plugin up and running. @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 (if any). Each preference is defined by a 4-tuple containing the following items: 1. the dict key of the option (used in the config file and in the preferences dict) 2. an option type (see L{InputForm.add_inputs(){} for more details) 3. a (translatable) label to show in the preferences dialog for this option 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 of the plugin object. The type and label will be used to render a default config dialog when triggered from the preferences dialog. Changes to these preferences will be stored in a config file so they are persistent. @ivar preferences: a L{ConfigDict} with plugin preferences Preferences are the global configuration of the plugin, they are stored in the X{preferences.conf} config file. @ivar config: a L{ConfigManager} object that can be used to lookup additional config files for the plugin @ivar extension_classes: a dictionary with extension classes found in the plugin module @ivar extensions: a set with extension objects loaded by this plugin. ''' # define signals we want to use - (closure type, return type and arg types) plugin_info = {} plugin_preferences = () plugin_notebook_properties = () @classproperty def config_key(klass): '''The name of section used in the config files to store the preferences for this plugin. ''' return klass.__name__ @classmethod def check_dependencies_ok(klass): '''Checks minimum dependencies are met @returns: C{True} if this plugin can be loaded based on L{check_dependencies()} ''' check, dependencies = klass.check_dependencies() return check @classmethod def check_dependencies(klass): '''Checks what dependencies are met and gives details for display in the preferences dialog @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. Default always returns C{True} with an empty list. ''' return (True, []) def __init__(self, config=None): '''Constructor @param config: a L{ConfigManager} object that is used to load plugin preferences. Defaults to a L{VirtualConfigManager} for testing. ''' assert 'name' in self.plugin_info, 'Missing "name" in plugin_info' assert 'description' in self.plugin_info, 'Missing "description" in plugin_info' assert 'author' in self.plugin_info, 'Missing "author" in plugin_info' self.extensions = WeakSet() if self.plugin_preferences: assert isinstance( self.plugin_preferences[0], tuple), 'BUG: preferences should be defined as tuples' self.config = config or VirtualConfigManager() self.preferences = self.config.get_config_dict( '<profile>/preferences.conf')[self.config_key] self._init_config(self.preferences, self.plugin_preferences) self._init_config(self.preferences, self.plugin_notebook_properties ) # defaults for the properties are preferences self.load_insertedobject_types() self.load_extensions_classes() @staticmethod def _init_config(config, definitions): for pref in definitions: if len(pref) == 4: key, type, label, default = pref config.setdefault(key, default) else: key, type, label, default, check = pref config.setdefault(key, default, check=check) @staticmethod def form_fields(definitions): fields = [] for pref in definitions: if len(pref) == 4: key, type, label, default = pref else: key, type, label, default, check = pref if type in ('int', 'choice'): fields.append((key, type, label, check)) else: fields.append((key, type, label)) return fields def notebook_properties(self, notebook): properties = notebook.config[self.config_key] if not properties: self._init_config(properties, self.plugin_notebook_properties) # update defaults based on preference for key, definition in properties.definitions.items(): try: definition.default = definition.check( self.preferences[key]) except ValueError: pass return properties @classmethod def lookup_subclass(pluginklass, klass): '''Returns first subclass of C{klass} found in the module of this plugin. (Similar to L{zim.utils.lookup_subclass}). @param pluginklass: plugin class @param klass: base class of the wanted class ''' module = get_module(pluginklass.__module__) return lookup_subclass(module, klass) def load_insertedobject_types(self): '''Loads L{InsertedObjectType} classes defined in the same modul as the plugin. ''' from zim.objectmanager import ObjectManager self._objecttypes = [ objtype(self) for objtype in self.discover_classes(InsertedObjectType) ] for obj in self._objecttypes: ObjectManager.register_object(obj) def load_extensions_classes(self): '''Instantiates the C{extension_classes} dictionary with classes found in the same module as the plugin object. Called directly by the constructor. ''' self.extension_classes = {} for klass in self.discover_classes(ExtensionBase): extends = klass.__extends__ if extends in self.extension_classes: raise AssertionError('Extension point %s already in use' % name) self.extension_classes[extends] = klass @classmethod def discover_classes(pluginklass, baseclass): '''Yields a list of classes derived from C{baseclass} and defined in the same module as the plugin ''' module = get_module(pluginklass.__module__) for klass in lookup_subclasses(module, baseclass): yield klass def extend(self, obj): '''This method will look through the extensions defined for this plugin and construct a new extension object if a match is found for C{obj}. @param obj: the object to be extended ''' name = obj.__class__.__name__ if name in self.extension_classes: try: ext = self.extension_classes[name](self, obj) except ExtensionNotApplicable: pass except: logger.exception('Failed loading extension %s for plugin %s', self.extension_classes[name], self) else: self.extensions.add(ext) 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). ''' from zim.objectmanager import ObjectManager for obj in self.extensions: obj.destroy() for obj in self._objecttypes: ObjectManager.unregister_object(obj) try: self.disconnect_all() self.teardown() except: logger.exception('Exception while disconnecting %s', self) def teardown(self): '''Cleanup method called by C{destroy()}. Can be implemented by sub-classes. ''' pass