diff --git a/epydoc.conf b/epydoc.conf
new file mode 100644
index 000000000..092f7f1a5
--- /dev/null
+++ b/epydoc.conf
@@ -0,0 +1,28 @@
+[epydoc]
+
+# Information about the project.
+name: Gajim
+url: http://gajim.org
+
+verbosity: 3
+imports: yes
+redundant-details: yes
+docformat: restructuredtext
+# top: gajim
+
+# The list of modules to document. Modules can be named using
+# dotted names, module filenames, or package directory names.
+# This option may be repeated.
+modules: src/plugins/*.py
+
+# Write html output to the directory "apidocs"
+#output: pdf
+output: html
+target: apidocs/
+
+# Include all automatically generated graphs. These graphs are
+# generated using Graphviz dot.
+graph: all
+dotpath: /usr/bin/dot
+graph-font: Sans
+graph-font-size: 10
diff --git a/plugins/length_notifier.py b/plugins/length_notifier.py
new file mode 100644
index 000000000..bd0f9564f
--- /dev/null
+++ b/plugins/length_notifier.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+
+## This file is part of Gajim.
+##
+## Gajim is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## Gajim is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with Gajim. If not, see .
+##
+
+'''
+Message length notifier plugin.
+
+:author: Mateusz Biliński
+:since: 06/01/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+
+import gtk
+
+from plugins import GajimPlugin
+from plugins.helpers import log, log_calls
+
+class LengthNotifierPlugin(GajimPlugin):
+ name = 'Message Length Notifier'
+ short_name = 'length_notifier'
+ version = '0.1'
+ description = '''Highlights message entry field in chat window when given
+length of message is exceeded.'''
+ authors = ['Mateusz Biliński ']
+
+ @log_calls('LengthNotifierPlugin')
+ def __init__(self):
+ super(LengthNotifierPlugin, self).__init__()
+
+ self.__class__.gui_extension_points = {
+ 'chat_control' : (self.connect_with_chat_control,
+ self.disconnect_from_chat_control)
+ }
+
+ self.MESSAGE_WARNING_LENGTH = 140
+ self.WARNING_COLOR = gtk.gdk.color_parse('#F0DB3E')
+ self.JIDS = []
+
+ @log_calls('LengthNotifierPlugin')
+ def textview_length_warning(self, tb, chat_control):
+ tv = chat_control.msg_textview
+ d = chat_control.length_notifier_plugin_data
+ t = tb.get_text(tb.get_start_iter(), tb.get_end_iter())
+ if t:
+ len_t = len(t)
+ #print("len_t: %d"%(len_t))
+ if len_t>self.MESSAGE_WARNING_LENGTH:
+ if not d['prev_color']:
+ d['prev_color'] = tv.style.copy().base[gtk.STATE_NORMAL]
+ tv.modify_base(gtk.STATE_NORMAL, self.WARNING_COLOR)
+ elif d['prev_color']:
+ tv.modify_base(gtk.STATE_NORMAL, d['prev_color'])
+ d['prev_color'] = None
+
+ @log_calls('LengthNotifierPlugin')
+ def connect_with_chat_control(self, chat_control):
+ jid = chat_control.contact.jid
+ if self.jid_is_ok(jid):
+ d = {'prev_color' : None}
+ tv = chat_control.msg_textview
+ b = tv.get_buffer()
+ h_id = b.connect('changed', self.textview_length_warning, chat_control)
+ d['h_id'] = h_id
+ chat_control.length_notifier_plugin_data = d
+
+ return True
+
+ return False
+
+ @log_calls('LengthNotifierPlugin')
+ def disconnect_from_chat_control(self, chat_control):
+ d = chat_control.length_notifier_plugin_data
+ chat_control.msg_textview.get_buffer().disconnect(d['h_id'])
+ if d['prev_color']:
+ tv.modify_base(gtk.STATE_NORMAL, self.PREV_COLOR)
+
+ @log_calls('LengthNotifierPlugin')
+ def jid_is_ok(self, jid):
+ return True
\ No newline at end of file
diff --git a/src/chat_control.py b/src/chat_control.py
index cddf9cd35..ecc817bf6 100644
--- a/src/chat_control.py
+++ b/src/chat_control.py
@@ -1139,6 +1139,8 @@ class ChatControl(ChatControlBase):
self.update_ui()
# restore previous conversation
self.restore_conversation()
+
+ gajim.plugin_manager.gui_extension_point('chat_control', self)
def on_avatar_eventbox_enter_notify_event(self, widget, event):
'''we enter the eventbox area so we under conditions add a timeout
diff --git a/src/common/configpaths.py b/src/common/configpaths.py
index 516d849ef..e950b3e1a 100644
--- a/src/common/configpaths.py
+++ b/src/common/configpaths.py
@@ -92,6 +92,10 @@ class ConfigPaths:
self.add('DATA', os.path.join(u'..', windowsify(u'data')))
self.add('HOME', fse(os.path.expanduser('~')))
self.add('TMP', fse(tempfile.gettempdir()))
+
+ # dirs for plugins
+ self.add('PLUGINS_BASE', os.path.join(u'..', windowsify(u'plugins')))
+ self.add_from_root('PLUGINS_USER', u'plugins')
try:
import svn_config
diff --git a/src/common/gajim.py b/src/common/gajim.py
index eaa84ec4f..4cbdf609b 100644
--- a/src/common/gajim.py
+++ b/src/common/gajim.py
@@ -61,6 +61,7 @@ version = config.get('version')
connections = {} # 'account name': 'account (connection.Connection) instance'
verbose = False
ipython_window = None
+plugin_manager = None
h = logging.StreamHandler()
f = logging.Formatter('%(asctime)s %(name)s: %(message)s', '%d %b %Y %H:%M:%S')
@@ -85,6 +86,8 @@ MY_CACERTS = gajimpaths['MY_CACERTS']
TMP = gajimpaths['TMP']
DATA_DIR = gajimpaths['DATA']
HOME_DIR = gajimpaths['HOME']
+PLUGINS_DIRS = [gajimpaths['PLUGINS_BASE'],
+ gajimpaths['PLUGINS_USER']]
try:
LANG = locale.getdefaultlocale()[0] # en_US, fr_FR, el_GR etc..
diff --git a/src/common/sleepy.py b/src/common/sleepy.py
index bcc5f73c3..f56a7e044 100644
--- a/src/common/sleepy.py
+++ b/src/common/sleepy.py
@@ -35,22 +35,22 @@ STATE_AWAKE = 'awake'
SUPPORTED = True
try:
- if os.name == 'nt':
- import ctypes
+ if os.name == 'nt':
+ import ctypes
- GetTickCount = ctypes.windll.kernel32.GetTickCount
- GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo
+ GetTickCount = ctypes.windll.kernel32.GetTickCount
+ GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo
- class LASTINPUTINFO(ctypes.Structure):
- _fields_ = [('cbSize', ctypes.c_uint), ('dwTime', ctypes.c_uint)]
+ class LASTINPUTINFO(ctypes.Structure):
+ _fields_ = [('cbSize', ctypes.c_uint), ('dwTime', ctypes.c_uint)]
- lastInputInfo = LASTINPUTINFO()
- lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo)
+ lastInputInfo = LASTINPUTINFO()
+ lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo)
- elif sys.platform == 'darwin':
- import osx.idle as idle
- else: # unix
- import idle
+ elif sys.platform == 'darwin':
+ import osx.idle as idle
+ else: # unix
+ import idle
except:
gajim.log.debug('Unable to load idle module')
SUPPORTED = False
diff --git a/src/gajim.py b/src/gajim.py
index e34e5f81a..d1d746401 100755
--- a/src/gajim.py
+++ b/src/gajim.py
@@ -3461,6 +3461,10 @@ class Interface:
gobject.timeout_add_seconds(2, self.process_connections)
gobject.timeout_add_seconds(gajim.config.get(
'check_idle_every_foo_seconds'), self.read_sleepy)
+
+ # Creating plugin manager
+ import plugins
+ gajim.plugin_manager = plugins.PluginManager()
if __name__ == '__main__':
def sigint_cb(num, stack):
diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py
new file mode 100644
index 000000000..0d5d18fda
--- /dev/null
+++ b/src/plugins/__init__.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+
+## This file is part of Gajim.
+##
+## Gajim is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## Gajim is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with Gajim. If not, see .
+##
+
+'''
+Main file of plugins package.
+
+:author: Mateusz Biliński
+:since: 05/30/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+from pluginmanager import PluginManager
+from plugin import GajimPlugin
+
+__all__ = ['PluginManager', 'GajimPlugin']
diff --git a/src/plugins/helpers.py b/src/plugins/helpers.py
new file mode 100644
index 000000000..744f209ac
--- /dev/null
+++ b/src/plugins/helpers.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+
+## This file is part of Gajim.
+##
+## Gajim is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## Gajim is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with Gajim. If not, see .
+##
+
+'''
+Helper code related to plug-ins management system.
+
+:author: Mateusz Biliński
+:since: 05/30/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+__all__ = ['log', 'log_calls', 'Singleton']
+
+import logging
+log = logging.getLogger('gajim.plugin_system')
+'''
+Logger for code related to plug-in system.
+
+:type: logging.Logger
+'''
+
+consoleloghandler = logging.StreamHandler()
+consoleloghandler.setLevel(1)
+consoleloghandler.setFormatter(
+ logging.Formatter('%(levelname)s: %(message)s'))
+ #logging.Formatter('%(asctime)s %(name)s: %(levelname)s: %(message)s'))
+log.setLevel(logging.DEBUG)
+log.addHandler(consoleloghandler)
+log.propagate = False
+
+import functools
+
+class log_calls(object):
+ '''
+ Decorator class for functions to easily log when they are entered and left.
+ '''
+
+ def __init__(self, classname='', log=log):
+ '''
+ :Keywords:
+ classname : str
+ Name of class to prefix function name (if function is a method).
+ log : logging.Logger
+ Logger to use when outputing debug information on when function has
+ been entered and when left. By default: `plugins.helpers.log`
+ is used.
+ '''
+
+ self.full_func_name = ''
+ '''
+ Full name of function, with class name (as prefix) if given
+ to decorator.
+
+ Otherwise, it's only function name retrieved from function object
+ for which decorator was called.
+
+ :type: str
+ '''
+
+ if classname:
+ self.full_func_name = classname+'.'
+
+ def __call__(self, f):
+ '''
+ :param f: function to be wrapped with logging statements
+
+ :return: given function wrapped by *log.debug* statements
+ :rtype: function
+ '''
+ self.full_func_name += f.func_name
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ log.debug('%(funcname)s() '%{
+ 'funcname': self.full_func_name})
+ result = f(*args, **kwargs)
+ log.debug('%(funcname)s() '%{
+ 'funcname': self.full_func_name})
+ return result
+ return wrapper
+
+class Singleton(type):
+ '''
+ Singleton metaclass.
+ '''
+ def __init__(cls,name,bases,dic):
+ super(Singleton,cls).__init__(name,bases,dic)
+ cls.instance=None
+
+ def __call__(cls,*args,**kw):
+ if cls.instance is None:
+ cls.instance=super(Singleton,cls).__call__(*args,**kw)
+ log.debug('%(classname)s - new instance created'%{
+ 'classname' : cls.__name__})
+ else:
+ log.debug('%(classname)s - returning already existing instance'%{
+ 'classname' : cls.__name__})
+
+ return cls.instance
\ No newline at end of file
diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py
new file mode 100644
index 000000000..d3bfa62c9
--- /dev/null
+++ b/src/plugins/plugin.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+## This file is part of Gajim.
+##
+## Gajim is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## Gajim is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with Gajim. If not, see .
+##
+
+'''
+Base class for implementing plugin.
+
+:author: Mateusz Biliński
+:since: 06/01/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+from plugins.helpers import log, log_calls
+
+class GajimPlugin(object):
+ name = ''
+ short_name = ''
+ version = ''
+ description = ''
+ authors = []
+ gui_extension_points = {}
+
+ @log_calls('GajimPlugin')
+ def __init__(self):
+ pass
\ No newline at end of file
diff --git a/src/plugins/pluginmanager.py b/src/plugins/pluginmanager.py
new file mode 100644
index 000000000..59c3bad11
--- /dev/null
+++ b/src/plugins/pluginmanager.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+
+## This file is part of Gajim.
+##
+## Gajim is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## Gajim is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with Gajim. If not, see .
+##
+
+'''
+Helper code related to plug-ins management system.
+
+:author: Mateusz Biliński
+:since: 05/30/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+__all__ = ['PluginManager']
+
+import os
+import sys
+import fnmatch
+
+import common.gajim as gajim
+
+from helpers import log, log_calls, Singleton
+from plugin import GajimPlugin
+
+class PluginManager(object):
+ __metaclass__ = Singleton
+
+ @log_calls('PluginManager')
+ def __init__(self):
+ self.plugins = []
+ self.active = []
+ self.gui_extension_points = {}
+
+ for path in gajim.PLUGINS_DIRS:
+ self.plugins.extend(self._scan_dir_for_plugins(path))
+
+ log.debug('plugins: %s'%(self.plugins))
+
+ self._activate_all_plugins()
+
+ log.debug('active: %s'%(self.active))
+
+ @log_calls('PluginManager')
+ def gui_extension_point(self, gui_extpoint_name, *args):
+ if gui_extpoint_name in self.gui_extension_points:
+ for handlers in self.gui_extension_points[gui_extpoint_name]:
+ handlers[0](*args)
+
+ @log_calls('PluginManager')
+ def _activate_plugin(self, plugin):
+ '''
+ :param plugin: Plugin to be activated.
+ :type plugin: class object of GajimPlugin subclass
+ '''
+ p = plugin()
+
+ success = True
+
+ # :fix: what if only some handlers are successfully connected? we should
+ # revert all those connections that where successfully made. Maybe
+ # call 'self._deactivate_plugin()' or sth similar.
+ # Looking closer - we only rewrite tuples here. Real check should be
+ # made in method that invokes gui_extpoints handlers.
+ for gui_extpoint_name, gui_extpoint_handlers in \
+ p.gui_extension_points.iteritems():
+ self.gui_extension_points.setdefault(gui_extpoint_name,[]).append(
+ gui_extpoint_handlers)
+
+ if success:
+ self.active.append(p)
+
+ return success
+
+ @log_calls('PluginManager')
+ def _activate_all_plugins(self):
+ self.active = []
+ for plugin in self.plugins:
+ self._activate_plugin(plugin)
+
+ @log_calls('PluginManager')
+ def _scan_dir_for_plugins(self, path):
+ plugins_found = []
+ if os.path.isdir(path):
+ dir_list = os.listdir(path)
+ log.debug(dir_list)
+
+ sys.path.insert(0, path)
+ log.debug(sys.path)
+
+ for file in fnmatch.filter(dir_list, '*.py'):
+ log.debug('- "%s"'%(file))
+ file_path = os.path.join(path, file)
+ log.debug(' "%s"'%(file_path))
+ if os.path.isfile(file_path):
+ module_name = os.path.splitext(file)[0]
+ module = __import__(module_name)
+ filter_out_bad_names = \
+ lambda x: not (x.startswith('__') or
+ x.endswith('__'))
+ for module_attr_name in filter(filter_out_bad_names,
+ dir(module)):
+ module_attr = getattr(module, module_attr_name)
+ log.debug('%s : %s'%(module_attr_name, module_attr))
+
+ try:
+ if issubclass(module_attr, GajimPlugin) and \
+ not module_attr is GajimPlugin:
+ log.debug('is subclass of GajimPlugin')
+ plugins_found.append(module_attr)
+ except TypeError, e:
+ log.debug('module_attr: %s, error : %s'%(
+ module_name+'.'+module_attr_name,
+ e))
+
+ log.debug(module)
+
+ return plugins_found
\ No newline at end of file
diff --git a/test/test_pluginmanager.py b/test/test_pluginmanager.py
new file mode 100644
index 000000000..5c6723bcb
--- /dev/null
+++ b/test/test_pluginmanager.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+## This file is part of Gajim.
+##
+## Gajim is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## Gajim is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with Gajim. If not, see .
+##
+
+'''
+Testing PluginManager class.
+
+:author: Mateusz Biliński
+:since: 05/30/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+import os
+import unittest
+
+gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..')
+sys.path.append(gajim_root + '/src')
+
+from plugins import PluginManager
+
+class PluginManagerTestCase(unittest.TestCase):
+ def setUp(self):
+ self.pluginmanager = PluginManager()
+
+ def tearDown(self):
+ pass
+
+ def test_01_Singleton(self):
+ """ 1. Checking whether PluginManger class is singleton. """
+ self.pluginmanager.test_arg = 1
+ secondPluginManager = PluginManager()
+
+ self.failUnlessEqual(id(secondPluginManager), id(self.pluginmanager),
+ 'Different IDs in references to PluginManager objects (not a singleton)')
+ self.failUnlessEqual(secondPluginManager.test_arg, 1,
+ 'References point to different PluginManager objects (not a singleton')
+
+def suite():
+ suite = unittest.TestLoader().loadTestsFromTestCase(PluginManagerTestCase)
+ return suite
+
+if __name__=='__main__':
+ runner = unittest.TextTestRunner()
+ test_suite = suite()
+ runner.run(test_suite)
\ No newline at end of file