forget the past.
authorroot
Wed, 26 Apr 2006 10:48:09 +0000
changeset 0 7710b138d4eb
child 1 b9b031063093
forget the past. forget the past.
.hgignore
ChangeLog
DEPENDS
MANIFEST.in
README
RECOMMENDS
TODO
__init__.py
__pkginfo__.py
bin/oobrother
config_tools.py
debian/changelog
debian/control
debian/copyright
debian/oobrother-test.dirs
debian/python2.3-oobrother.dirs
debian/python2.3-oobrother.postinst
debian/python2.3-oobrother.prerm
debian/rules
doc/makefile
doc/manifest.fr.txt
doc/plugin_tutorial.fr.txt
doc/sample_plugin.py
editors.py
filelib/__init__.py
filelib/filters.py
i18n/en.po
i18n/en/LC_MESSAGES/oobrother.mo
i18n/fr.po
i18n/fr/LC_MESSAGES/oobrother.mo
i18n/messages.pot
i18n/oob_glade_strings.c
nodelib.py
oobrowser.glade
oobrowser.py
pixmaps/button_apply0.xpm
pixmaps/button_apply1.xpm
pixmaps/button_copy0.xpm
pixmaps/button_copy1.xpm
pixmaps/button_delete.xpm
pixmaps/class.png
pixmaps/close.xpm
pixmaps/cvs-add-16.png
pixmaps/cvs-commit-16.png
pixmaps/cvs-icon-small.png
pixmaps/cvs-icon.png
pixmaps/cvs-remove-16.png
pixmaps/cvs-update-16.png
pixmaps/icon.png
pixmaps/missing.jpeg
pixmaps/missing.png
pixmaps/oobrother_background.jpg
pixmaps/oobrother_background2.jpg
pixmaps/oobrother_background3.jpg
pixmaps/patch.png
pixmaps/patch2.png
pixmaps/svn-icon-small.png
pixmaps/svn-icon.png
pixmaps/tree-file-changed.png
pixmaps/tree-file-needspatch.png
pixmaps/tree-file-new.png
pixmaps/tree-file-newer.png
pixmaps/tree-file-normal.png
pixmaps/tree-folder-changed.png
pixmaps/tree-folder-new.png
pixmaps/tree-folder-normal.png
pixmaps/unknown.png
plugins/__init__.py
plugins/base.py
plugins/debian.py
plugins/devtools.py
plugins/pychecker.py
plugins/python.py
plugins/vcs.py
registry.py
setup.py
sysutils.py
test/data/bar.txt
test/data/dont_remove_me.txt
test/data/foo.txt
test/runtests.py
test/unittest_devtools.py
test/unittest_filters.py
test/unittest_pyplugin.py
test/unittest_registry.py
uiutils/__init__.py
uiutils/basemixins.py
uiutils/trees.py
uiutils/windows.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,4 @@
+(^|/)\.svn($|/)
+(^|/)\.hg($|/)
+(^|/)\.hgtags($|/)
+^log$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ChangeLog	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,9 @@
+ChangeLog for oobrother
+=======================
+
+2005/02/25  --  0.2.0
+    * debian distribution mangement
+    * filter box
+    * various bugfixes
+2004-11-10  --  0.1.0
+    * Initial release
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/DEPENDS	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,5 @@
+python-logilab-common (>= 0.5.0)
+devtools (python)
+python-gtk2
+python-pigg
+python-glade2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,9 @@
+recursive-include doc *.txt *.html *.py
+include oobrowser.glade
+include TODO
+include ChangeLog
+include DEPENDS RECOMMENDS
+include pixmaps/*.jpg
+include pixmaps/*.png
+include pixmaps/*.xpm
+recursive-include i18n *.po *.mo
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,49 @@
+oobrother
+=========
+
+
+What's this ?
+-------------
+OoBrother is a pure-python graphical filesystem browser,
+mainly designed to help python developers.
+Among other features, OoObrother :
+  - can be used as a speedbar (open each files with emacsclient)
+  - provides VCS facilities
+  - helps Debian package creations
+  - launches unittests
+  - checks modules and packages with pylint and pychecker
+.
+The most important thing is that you can easily customize OoBrother
+with your own plugins.
+
+
+
+Installation
+------------
+
+Extract the tarball, jump into the created directory and run ::
+
+	python setup.py install
+
+For installation options, see ::
+
+	python setup.py install --help
+
+License
+-------
+This program 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; either version 2 of the License, or (at your option) any later
+version.
+
+This program 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
+this program; if not, write to the Free Software Foundation, Inc.,
+59 Temple Place - Suite 330, Boston, MA  02111-1307
+USA.
+
+On Debian systems, the complete text of the GNU General Public License
+may be found in '/usr/share/common-licenses/GPL'.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/RECOMMENDS	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,3 @@
+pylint (python) (>= 0.5)
+cvs|svn
+vte (python)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,33 @@
+OOBrother's todo list
+=====================
+
+* fenêtre principale
+  * nouveau -> creation squelette paquet python 
+  * couper / copier / coller / supprimer fichiers et répertoires
+  * chargement / déchargement dynamique de plugins
+  * zone "état" (label, progress bar ?)
+  * gestion des fichiers modifiés ?
+  * raccourcis claviers configurables
+  
+* objet editeur de texte: 
+  * connaitre la position courante
+  * connaitre les buffers modifies
+* dimensionnement / placement par défaut des fenêtres
+* algo de placement des fenêtres
+ 
+* python plugin:
+  * object/refactoring browser
+    * insertion d'un nouveau module / package au bon endroit de l'arbre
+    * click sur classe parente envoie vers l'objet ref
+    * differentiation public / special / protected / prive
+    * afficher attributs d'instance, interfaces implémentées, objets
+      référencés, objets référençant
+    * supprimer package / module de l'arbre
+    * pouvoir cacher des noeuds sans les supprimer réellement de l'arbre
+    * de jolies icones :)
+
+* devtools plugin
+  * configuration/construction paquet debian
+  * add change log / todo / deb change log entry
+  * génération/maj MANIFEST.in
+  
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/__init__.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,145 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+
+"""A file system objects browser"""
+
+__revision__ = '$Id: __init__.py,v 1.17 2004-11-10 11:46:41 adim Exp $'
+
+import time
+import gtk, gtk.glade
+
+from logilab.common import logservice
+from logilab.common.logger import INDICATORS, AbstractLogger
+from os.path import join, dirname, normpath, exists
+
+__metatype__ = type
+
+COLORS = ['red', 'red', 'red', 'red', 'orange', 'blue', 'black', 'green']
+class OOBLogger(AbstractLogger):
+    """a logilab.common.logger compatible logger"""
+    def __init__(self, window):
+        AbstractLogger.__init__(self)
+        self.win = window
+        logservice.init_log(self.win.cfg['log-threshold'], logger = self)
+
+    def log(self, priority, message, substs=None):
+        """overriden from AbstractLogger to redirect message to the log
+        service window
+        """
+        if priority <= self.win.cfg['log-threshold'] :
+            if substs is not None:
+                message = message % substs
+        self.win.append_line(
+            (time.asctime(), INDICATORS[priority], message, COLORS[priority]),
+            scroll_to_line=True)
+
+
+import sys
+## FIXME: sys.prefix is not the right variable to use
+## It declares where Python is installed, not where the
+## program has been installed.
+## We should probably use __LOCAL_OOBBASE and guess from this
+if sys.prefix == '/usr':
+    __etc_prefix = "/etc"
+else:
+    __etc_prefix = join(sys.prefix, 'etc')
+    
+__LOCAL_OOBBASE = dirname(__file__)
+__LOCAL_PATHS = {'i18n': join(__LOCAL_OOBBASE, 'i18n'),
+                 'pixmaps' : join(__LOCAL_OOBBASE, 'pixmaps'),
+                 'glade': join(__LOCAL_OOBBASE,
+                               'oobrowser.glade'),
+                 'config': normpath(join(__LOCAL_OOBBASE, '..', 'config')),
+                 }
+
+__SYSTEM_PATHS = {'i18n': join(sys.prefix, 'share', 'locale'),
+                  'pixmaps' : join(sys.prefix, 'share', 'oobrother', 'pixmaps'),
+                  'glade': join(sys.prefix, 'share', 'oobrother',
+                                'oobrowser.glade'),
+                  'config': join(__etc_prefix, 'oobrother'),
+                 }
+
+__PREFIX_DIR = (__LOCAL_OOBBASE, ) + ('..', ) * 4
+__HOME_DIR = (__LOCAL_OOBBASE, ) + ('..', ) * 3
+__SHARE_PATH = ('share', 'oobrother')
+
+
+# --prefix install scheme
+__PREFIX_PATHS = {
+    'i18n'    : normpath(join(*(__PREFIX_DIR + ('share', 'locale', )))),
+    'pixmaps' : normpath(join(*(__PREFIX_DIR + __SHARE_PATH + ('pixmaps', )))),
+    'glade'   : normpath(join(*(__PREFIX_DIR + __SHARE_PATH + ('oobrowser.glade', )))),
+    # User should have changed the absolute '/etc/helicsation' path in
+    # a relative one in the setup.py script
+    'config'  : normpath(join(*(__PREFIX_DIR + ('etc', 'oobrother')))),
+    }
+
+# --home install scheme
+__HOME_PATHS = {
+    'i18n'    : normpath(join(*(__HOME_DIR + ('share', 'locale', )))),
+    'pixmaps' : normpath(join(*(__HOME_DIR + __SHARE_PATH + ('pixmaps', )))),
+    'glade'   : normpath(join(*(__HOME_DIR + __SHARE_PATH + ('oobrowser.glade',)))),
+    # User should have changed the absolute '/etc/helicsation' path in
+    # a relative one in the setup.py script
+    'config'  : normpath(join(*(__HOME_DIR + ('etc', 'oobrother')))),
+    }
+
+del __etc_prefix, __PREFIX_DIR, __HOME_DIR, __SHARE_PATH
+
+
+def get_path(key):
+    """pass a key and get the element from the local path or system path
+    """
+    lookup_chain = __LOCAL_PATHS, __PREFIX_PATHS, __HOME_PATHS, __SYSTEM_PATHS
+    tested_path = []
+    for path_scheme in lookup_chain:
+        filepath = path_scheme[key]
+        if exists(filepath):
+            return filepath
+        tested_path.append(filepath)
+    raise RuntimeError('Unable to find %s file. Tested paths : \n%s.'
+                       'This software is probably not installed correctly' %
+                       (key, '\n'.join(tested_path)))
+
+
+## def __load_config():
+##     """loads the oob_config module
+##     Returns the module object"""
+##     import imp
+##     file, pathname, desc = imp.find_module('oob_config',
+##                                            [get_path('config')])
+##     try:
+##         oob_config = imp.load_module('oob_config', file, pathname, desc)
+##     finally:
+##         file.close()
+##     return oob_config
+
+## oob_config = __load_config()
+## del __load_config
+# OOBROTHER_BASE = dirname(__file__)
+
+
+def localize_app():
+    """use gettext to localize application (glade + python strings)"""
+    import gettext
+    i18n_dir = get_path('i18n')
+    gettext.bindtextdomain('oobrother', i18n_dir)
+    gettext.textdomain('oobrother')
+    gettext.install('oobrother', i18n_dir, unicode = 1)
+    gtk.glade.bindtextdomain('oobrother', i18n_dir)
+    gtk.glade.textdomain('oobrother')
+
+localize_app()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/__pkginfo__.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,77 @@
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+# 
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+# Place - Suite 330, Boston, MA 02111-1307 USA.
+# 
+# On Debian systems, the complete text of the GNU General Public License may be
+# found in '/usr/share/common-licenses/GPL'.
+"""package information for oobrother"""
+
+__revision__ = '$Id: __pkginfo__.py,v 1.8 2005-02-24 17:30:14 adim Exp $'
+
+modname = 'oobrother'
+version = '0.2.0'
+license = 'GPL'
+copyright = 'Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).\nhttp://www.logilab.fr/ -- mailto:contact@logilab.fr'
+short_desc = 'a pluggable file system objects browser'
+long_desc = 'a **cool** pluggable file system objects browser'
+author = 'Logilab'
+author_email = 'devel@logilab.fr'
+form_complete = True
+pyversions = ('2.3',)
+web = 'http://www.logilab.org/projects/oobrother'
+numversion = version.split('.')
+
+short_desc = "customizable filesystem browser, " \
+             "designed to help python developers"
+
+long_desc = """OoBrother is a pure-python graphical filesystem browser,
+mainly designed to help python developers.
+Among other features, OoObrother :
+  - can be used as a speedbar (open each files with emacsclient)
+  - provides VCS facilities
+  - helps Debian package creations
+  - launches unittests
+  - checks modules and packages with pylint and pychecker
+.
+The most important thing is that you can easily customize OoBrother
+with your own plugins.
+"""
+
+from os import listdir
+from os.path import join, isdir, dirname, abspath
+from glob import glob
+
+doc_files = [join('doc', '*.txt'), join('doc', '*.py')]
+html_doc_files = [join('doc', '*.html')]
+
+scripts = [join('bin', 'oobrother')]
+debian_name = 'oobrother'
+
+HERE = abspath(dirname(__file__))
+
+data_files = [
+    (join('share', 'oobrother'),
+             glob(join('*.glade'))),
+    (join('share', 'oobrother', 'pixmaps'),
+             glob(join('pixmaps', '*.jpg')) +
+             glob(join('pixmaps', '*.png')) +
+             glob(join('pixmaps', '*.xpm'))),
+    (join('share', 'oobrother'),     
+             [join('i18n', fname) for fname in listdir(join(HERE, 'i18n'))
+              if fname != 'CVS' and not isdir(join('i18n', fname))]),
+    (join('share', 'locale', 'en', 'LC_MESSAGES'),
+             [join('i18n', 'en', 'LC_MESSAGES', 'oobrother.mo')]),
+    (join('share', 'locale', 'fr', 'LC_MESSAGES'),
+             [join('i18n', 'fr', 'LC_MESSAGES', 'oobrother.mo')]),
+    ]
+debian_maintainer = 'Adrien Di Mascio'
+debian_maintainer_email = 'Adrien.DiMascio@logilab.fr'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/oobrother	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+from oobrother import oobrowser
+oobrowser.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config_tools.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,202 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""This module contains some tools to handle main application and plugins
+configuration.
+"""
+
+__revision__ = '$Id: config_tools.py,v 1.12 2006-04-23 13:53:52 nico Exp $'
+
+from os.path import join
+
+import gtk, gobject
+from pigg.mvc import Controller
+from pigg.form import PyFormModel
+from pigg.wgenerator import notebook_generate
+
+from logilab.common.configuration import Configuration, OptionValueError
+
+from oobrother.uiutils.basemixins import WindowMixIn, PluggableFrameMixIn, \
+     MenuControlledWindowMixIn
+from oobrother.uiutils.trees import init_treeview_columns
+
+from oobrother.sysutils import OOBROTHER_HOME
+# from oobrother import get_path
+
+__metaclass__ = type
+
+
+def format_config_entry(opt_name, opt_dict):
+    """format a config entry to a schema line"""
+    opt_type = opt_dict['type']
+    if opt_type in ('choice', 'multiple_choice'):
+        extra_args = (opt_dict['choices'],)
+    else:
+        extra_args = ()
+    return (opt_name, opt_type, opt_name.replace('_', ' '),
+            extra_args, False, opt_dict.get('default'))
+
+def schema_from_config(config):
+    """return a schema as expected by `gtkmv.form.PyFormModel`, built
+    from a `logilab.common.configuration.Configuration` instance.
+    """
+    return [format_config_entry(opt_name, opt_dict)
+            for opt_name, opt_dict in config.options]
+            
+def notebook_from_config(config, model, ctrl):
+    """generates and returns a gtk note book according to the given
+    `logilab.common.configuration.Configuration` instance
+    """
+    nb_def = {}
+    for opt_name, opt_dict in config.options:
+        if opt_dict.get('type') is None:
+            continue                
+        section = opt_dict.get('group', config.name)
+        attr_def = format_config_entry(opt_name, opt_dict)
+        nb_def.setdefault(section, []).append(attr_def)
+    return notebook_generate(nb_def.items(), model, ctrl)
+
+
+def build_config_model(configurables):
+    """creates an returns a tree model from a configuration tree"""
+    store = gtk.TreeStore(gobject.TYPE_STRING,   # configuration section
+                          gobject.TYPE_PYOBJECT, # The configuration object
+                          )
+    for configurable in configurables:
+        fill_config_store(store, configurable, None)
+    return store
+
+def fill_config_store(store, configurable, tree_iter):
+    """recursive build of a tree model for a configuration tree"""
+    new_iter = store.append(tree_iter, [configurable.name, configurable])
+    for subconfig in configurable.subconfiguration():
+        fill_config_store(store, subconfig, new_iter)
+
+
+def configuration_models(configurables):
+    """return a list of configurable object (ie configuration node with
+    a 'cfg' attribute set)
+    """
+    result = []
+    for configurable in configurables:
+        if configurable.cfg:
+            result.append(configurable.cfg.model)
+        result += configuration_models(configurable.subconfiguration())
+    return result
+    
+
+class ConfigurableNode:
+    
+    def __init__(self, name, cfg=None, children=()):
+        self.name = name
+        self.cfg = cfg # optionel pluggable config object        
+        self._children = children
+        
+    def subconfiguration(self):
+        """return a list of configuration nodes for subconfiguration
+        """
+        return self._children
+
+    
+class PluggableConfig(Configuration):
+    """extends `logilab.common.configuration.Configuration` to handle
+    gui specific stuff (model, ctrl, pluggable widget
+    """
+    
+    def __init__(self, name, config_file, options):
+        if config_file:
+            config_file = join(OOBROTHER_HOME, config_file)
+        Configuration.__init__(self, config_file, options, name)
+        self.wdg = None
+        assert options
+        assert config_file
+        self.model = ConfigurationModel(self)
+        try:
+            self.load_file_configuration()
+        except OptionValueError, ex:
+            log(LOG_ERR, '%s: %s', (config_file, ex))
+        self.model.update()
+        self.ctrl = Controller(self.model)
+        self.wdg = notebook_from_config(self, self.model, self.ctrl).wdg
+        self.wdg.show_all()
+
+class ConfigurationModel(PyFormModel):
+    """a gtkmv model using a `logilab.common.configuration.Configuration`
+    instance as backend
+    """
+    
+    def __init__(self, config, schema=None):
+        self.config = config
+        if schema is None:
+            schema = schema_from_config(config)
+        self.schema = schema
+        PyFormModel.__init__(self, self.schema, {})
+
+    def update(self):
+        """update the model values from the backend values"""
+        for attr in self.schema:
+            attr = attr[0]
+            self.set_value(attr, self.config[attr])
+        self._orig_data = self._data.copy()
+                           
+    def _commit(self):
+        """write back the model's content (the form is completed at this point)
+        """
+        modifs = self.get_modifications()
+        if not modifs:
+            log(LOG_DEBUG, 'no modification to save to %s' %
+                self.config.config_file)
+            return
+        for key, value in modifs.items():
+            try:
+                self.config.global_set_option(key, value)
+            except KeyError:
+                self.config.set_option(key, value)
+        self.config.generate_config(open(self.config.config_file, 'w'))
+        log(LOG_INFO, '%s written' % self.config.config_file)
+        
+
+class GlobalConfigurationModel:
+    """the global configuration model is used to connect all the models
+    related to application's configuration and editable by the
+    configuration window.
+
+    It only defines necessary methods from the Model interface.
+    """
+    
+    def __init__(self, models):
+        self._models = models
+        
+    def commit(self):
+        """commit all models"""
+        for model in self._models:
+            model.commit()
+        
+    def rollback(self):
+        """rollback all models"""
+        for model in self._models:
+            model.rollback()
+            
+    def notify_all(self):
+        """ask each model to notify its observers"""
+        for model in self._models:
+            model.notify_all()
+            
+    def get_modifications(self):
+        """return the modification of each model merged alltogether"""
+        modifs = {}
+        for model in self._models:
+            modifs.update(model.get_modifications())
+        return modifs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/changelog	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,12 @@
+oobrother (0.2.0) unstable; urgency=low
+
+  * new upstream release
+
+ -- Adrien Di Mascio <Adrien.DiMascio@logilab.fr>  Fri, 25 Feb 2005 16:54:44 +0100
+
+oobrother (0.1.0-1) unstable; urgency=low
+
+  * initial release
+
+ -- Adrien Di Mascio <Adrien.DiMascio@logilab.fr>  Wed, 10 Nov 2004 11:47:33 +0100
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/control	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,78 @@
+Source: oobrother
+Section: python
+Priority: optional
+Maintainer: Adrien Di Mascio <Adrien.DiMascio@logilab.fr> 
+Build-Depends: debhelper (>= 4.0.0), python2.3-dev, python
+Standards-Version: 3.6.1
+
+Package: oobrother
+Architecture: all
+Recommends: pylint  (>= 0.5), cvs|svn, vte
+Depends: python (>= 2.3), python (<< 2.4), python2.3-oobrother (>= ${Source-Version}), oobrother-common
+Description: customizable filesystem browser, designed to help python developers [dummy package]
+ OoBrother is a pure-python graphical filesystem browser,
+ mainly designed to help python developers.
+ Among other features, OoObrother :
+ - can be used as a speedbar (open each files with emacsclient)
+ - provides VCS facilities
+ - helps Debian package creations
+ - launches unittests
+ - checks modules and packages with pylint and pychecker
+ .
+ The most important thing is that you can easily customize OoBrother
+ with your own plugins.
+ .
+ This package is an empty dummy package that always depends on a package built
+ for Debian's default Python version.
+ .
+ Homepage: http://www.logilab.org/projects/oobrother
+
+Package: oobrother-common
+Architecture: all
+Description: shared data for the oobrother package
+ OoBrother is a pure-python graphical filesystem browser,
+ mainly designed to help python developers.
+ Among other features, OoObrother :
+ - can be used as a speedbar (open each files with emacsclient)
+ - provides VCS facilities
+ - helps Debian package creations
+ - launches unittests
+ - checks modules and packages with pylint and pychecker
+ .
+ The most important thing is that you can easily customize OoBrother
+ with your own plugins.
+ .
+ This package provides files shared by oobrother across different python
+ versions:
+  * Shared data
+ .
+ Homepage: http://www.logilab.org/projects/oobrother
+
+Package: oobrother-test
+Architecture: all
+Depends: oobrother (= ${Source-Version}) | python2.3-oobrother (= ${Source-Version})
+Description: oobrother's test files
+ This package contains test files shared by the oobrother package. It
+ isn't necessary to install this package unless you want to execute or
+ look at the tests.
+
+Package: python2.3-oobrother
+Architecture: all
+Depends: python2.3, python2.3-logilab-common (>= 0.5.0), python2.3-devtools, python2.3-gtk2, python2.3-pigg, python2.3-glade2, oobrother-common (>= ${Source-Version})
+Recommends: python2.3-pylint  (>= 0.5), cvs|svn, python2.3-vte
+Description: customizable filesystem browser, designed to help python developers [built for python2.3]
+ OoBrother is a pure-python graphical filesystem browser,
+ mainly designed to help python developers.
+ Among other features, OoObrother :
+ - can be used as a speedbar (open each files with emacsclient)
+ - provides VCS facilities
+ - helps Debian package creations
+ - launches unittests
+ - checks modules and packages with pylint and pychecker
+ .
+ The most important thing is that you can easily customize OoBrother
+ with your own plugins.
+ .
+ This package is built with Python 2.3.
+ .
+ Homepage: http://www.logilab.org/projects/oobrother
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/copyright	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,29 @@
+This package was debianized by Adrien Di Mascio <Adrien.DiMascio@logilab.fr>  Sat, 13 Apr 2002 19:05:23 +0200.
+
+It was downloaded from http://www.logilab.org/projects/oobrother
+
+Upstream Author: 
+
+  Logilab <devel@logilab.fr>
+
+Copyright:
+
+Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+http://www.logilab.fr/ -- mailto:contact@logilab.fr
+
+This program 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; either version 2 of the License, or (at your option) any later
+version.
+
+This program 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
+this program; if not, write to the Free Software Foundation, Inc.,
+59 Temple Place - Suite 330, Boston, MA  02111-1307
+USA.
+
+On Debian systems, the complete text of the GNU General Public License
+may be found in '/usr/share/common-licenses/GPL'.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/oobrother-test.dirs	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,2 @@
+usr/share/doc/oobrother/
+usr/share/doc/oobrother/test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/python2.3-oobrother.dirs	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,4 @@
+usr/lib/python2.3/site-packages
+usr/lib/python2.3/site-packages/oobrother
+usr/share/doc/python2.3-oobrother
+usr/share/doc/python2.3-oobrother/html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/python2.3-oobrother.postinst	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,36 @@
+#! /bin/sh -e
+#
+# postinst script for Debian python packages.
+# Written 1998 by Gregor Hoffleit <flight@debian.org>.
+#
+VERSION=2.3
+DIRLIST="/usr/lib/python$VERSION/site-packages/oobrother"
+
+
+
+case "$1" in
+    configure|abort-upgrade|abort-remove|abort-deconfigure)
+        for i in $DIRLIST ; do
+            python$VERSION -O /usr/lib/python$VERSION/compileall.py -q $i
+            python$VERSION /usr/lib/python$VERSION/compileall.py -q $i
+        done
+    ;;
+
+    *)
+        echo "postinst called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+
+
+## Alternatives
+
+if [ "$1" = "configure" ]; then
+    # update-alternatives on things that collide
+    update-alternatives --install /usr/bin/oobrother oobrother /usr/bin/oobrother.python$VERSION 50
+fi
+
+#DEBHELPER#
+
+exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/python2.3-oobrother.prerm	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,23 @@
+#! /bin/sh -e
+#
+# sample prerm script for Debian python packages.
+# Written 1998 by Gregor Hoffleit <flight@debian.org>.
+#
+
+dpkg --listfiles python2.3-oobrother |
+	awk '$0~/\.py$/ {print $0"c\n" $0"o"}' |
+	xargs rm -f >&2
+
+
+## Alternatives
+
+if [ $1 != "upgrade" ]; then
+    # Remove alternatives
+    for i in oobrother ; do
+        update-alternatives --remove $i /usr/bin/$i.python2.3
+    done
+fi
+
+
+#DEBHELPER#
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/rules	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,133 @@
+#!/usr/bin/make -f
+# Sample debian/rules that uses debhelper.
+# GNU copyright 1997 to 1999 by Joey Hess.
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+# This is the debhelper compatability version to use.
+export DH_COMPAT=4
+
+PYVERSIONS=2.3
+
+build: DH_OPTIONS=
+build: build-stamp
+build-stamp: 
+	dh_testdir
+	
+	for v in $(PYVERSIONS) ; do \
+		python$$v setup.py -q build ; \
+	done
+	touch build-stamp
+
+clean: 
+	dh_testdir
+	dh_testroot
+	rm -f build-stamp configure-stamp
+	rm -rf build
+	rm -rf debian/python?.?-tmp*/
+	find . -name "*.pyc" | xargs rm -f
+	rm -f changelog.gz
+	dh_clean
+
+install: DH_OPTIONS=
+install: build
+	dh_testdir
+	dh_testroot
+	dh_clean -k
+	dh_installdirs
+	
+	python2.3 setup.py -q install_lib --no-compile --install-dir=debian/python2.3-oobrother/usr/lib/python2.3/site-packages
+	python2.3 setup.py -q install_headers --install-dir=debian/python2.3-oobrother/usr/include/
+	python2.3 setup.py -q install_scripts --install-dir=debian/python2.3-oobrother/usr/bin/
+	# remove test directory (installed in a separated package)
+	rm -rf debian/python2.3-oobrother/usr/lib/python2.3/site-packages/oobrother/test
+	python setup.py -q install_data --install-dir=debian/oobrother-common/usr/
+            
+	
+	for v in $(PYVERSIONS) ; do \
+		PYTHON=python$$v ; \
+		PYTMP="debian/$$PYTHON-oobrother" ; \
+		i=$$PYTMP/usr/bin/oobrother ; \
+		if head -1 $$i | grep "^#! */usr/bin" | grep "python" >/dev/null ; then \
+			sed "s@^#! */usr/bin/env \+python\$$@#!/usr/bin/$$PYTHON@;s@^#! */usr/bin/python\$$@#!/usr/bin/$$PYTHON@" <$$i >$$i.$$PYTHON; \
+			rm $$i ; \
+		else \
+			mv $$i $$i.$$PYTHON ; \
+		fi ; \
+		chmod a+x $$i.$$PYTHON ; \
+	done
+	
+	
+	
+	# install tests
+	(cd test && find . -type f -not \( -path '*/CVS/*' -or -name '*.pyc' \) -exec install -D --mode=644 {} ../debian/oobrother-test/usr/share/doc/oobrother/test/{} \;)
+
+
+# Build architecture-independent files here.
+binary-indep: DH_OPTIONS=-i
+binary-indep: build install
+	dh_testdir
+	dh_testroot
+	dh_install
+	
+	
+	
+	# install text documentation
+	for v in $(PYVERSIONS) ; do \
+		PACKAGE=python$$v-oobrother ; \
+		cp -r doc/*.txt doc/*.py debian/$$PACKAGE/usr/share/doc/$$PACKAGE/ ; \
+	done
+	# install html documentation
+	for v in $(PYVERSIONS) ; do \
+		PACKAGE=python$$v-oobrother ; \
+		cp -r doc/*.html debian/$$PACKAGE/usr/share/doc/$$PACKAGE/html ; \
+	done
+	gzip -9 -c ChangeLog > changelog.gz
+	dh_installdocs -A README TODO changelog.gz 
+	dh_installchangelogs
+	
+	dh_link
+	dh_compress -X.py -X.ini -X.xml
+	dh_fixperms
+	dh_installdeb
+	dh_gencontrol 
+	dh_md5sums
+	dh_builddeb
+
+# Build architecture-dependent files here.
+binary-arch: DH_OPTIONS=-a
+binary-arch: build install
+	dh_testdir 
+	dh_testroot 
+	dh_install
+	
+	
+	
+	# install text documentation
+	for v in $(PYVERSIONS) ; do \
+		PACKAGE=python$$v-oobrother ; \
+		cp -r doc/*.txt doc/*.py debian/$$PACKAGE/usr/share/doc/$$PACKAGE/ ; \
+	done
+	# install html documentation
+	for v in $(PYVERSIONS) ; do \
+		PACKAGE=python$$v-oobrother ; \
+		cp -r doc/*.html debian/$$PACKAGE/usr/share/doc/$$PACKAGE/html ; \
+	done
+	gzip -9 -c ChangeLog > changelog.gz
+	dh_installdocs -A README TODO changelog.gz 
+	dh_installchangelogs
+	
+	dh_strip
+	dh_link
+	dh_compress -X.py -X.ini -X.xml
+	dh_fixperms
+	dh_installdeb
+	dh_shlibdeps
+	dh_gencontrol
+	dh_md5sums
+	dh_builddeb
+
+binary: binary-indep 
+.PHONY: build clean binary-arch binary-indep binary
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/makefile	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,14 @@
+MKHTML=mkdoc
+MKHTMLOPTS=--doctype book --param toc.section.depth=2  --target html --stylesheet single-file
+SRC=.
+
+TXTFILES:= $(wildcard *.txt)
+TARGET := $(TXTFILES:.txt=.html)
+
+all: ${TARGET}
+
+%.html: %.txt
+	${MKHTML} ${MKHTMLOPTS} $<
+
+clean:
+	rm -f *.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/manifest.fr.txt	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,41 @@
+Quelques petites informations à propos de OOBrother
+---------------------------------------------------
+
+OOBrother est né d'une inspiration subite d'Adrien et moi-même qui
+s'est peu à peu transformé en ce qu'est OOBrother aujourd'hui.
+
+OOBrother est une application générique qui fournit un navigateur de
+fichier ainsi qu'un certain nombre de services (un terminal où
+éxécuter des commandes, un éditeur de texte...). Ensuite, on
+spécialise cette application à l'aide de plugins qui vont s'insérer
+grâce à une interface spécifique. Les services et bibliothèques
+fournies par OOBrother rendent l'écriture de ces plugins très rapide
+(tout en dépendant évidemment de la complexité du plugin désiré).
+
+L'objectif initial étant d'avoir un IDE spécialisé dans le
+développement de Python mais plus particulièrement dans
+l'environnement de Logilab. Nous avons donc commencé par le
+développement de plugin qui vont donner accès aux fonctionnalités de
+devtools (le paquet qui fournit plein d'outils dont personne ne sait se
+servir, voir ne connait l'existence), de pylint et de nombreux outils
+debian. 
+
+Le développement d'un IDE étant quelque chose de très coûteux, on
+voudrait pour l'instant que celui-ci nous rende service non pas pour
+l'écriture de programme python, mais pour tout ce qu'il y a autour :
+
+- la gestion du packaging : fichiers __pkginfo__.py, setup.py,
+  ChangeLog, debian... 
+
+- la publication de releases, source ou debian
+
+
+Dans l'idéal, j'aimerais que le développement de cette application se
+fasse dans un vrai "bazaar" style, et non comme la plupart de nos
+projets internes, développé et maintenu essentiellement par une seule
+personne.. J'encourage donc tout les logilabiens et logilabiennes à
+lire le code de cette application, à l'améliorer, à écrire vos propres
+plugins et à les partager si vous pensez qu'il peuvent être utiles à
+d'autres. Un des gros intérêt de cette application est de toucher à de
+nombreux modules de logilab : gtkmvc, common, pylint, devtools. C'est
+donc une bonne occasion de découvrir tout ça !
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/plugin_tutorial.fr.txt	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,267 @@
+===============================
+Ecrire un plugin pour OOBrother
+===============================
+
+:Author: Sylvain Thénault
+:Organization: Logilab
+:Version: $Revision: 1.6 $
+:Date: $Date: 2004-10-15 10:19:03 $
+
+.. contents::
+
+
+Intro
+-----
+L'objectif de ce document est de suivre pas à pas la création d'un
+plugin pour OOBrother. Nous allons donc suivre la création d'un plugin
+devant intégrer 2 outils debian :
+
+* debc
+* lintian
+
+
+Premier pas
+-----------
+OOBrother fournit une classe abstraite fournissant une implémentation
+par défaut de quelques aspects du plugin. C'est la classe
+AbstractPlugin du module oobrother.plugins. Commençons donc par le
+plus simple module possible: ::
+
+    from oobrother.plugins import AbstractPlugIn
+
+    class DebianPlugin(AbstractPlugIn):
+	"""a plugin for text file, propo"""
+	name = 'debian'
+	mimetypes = ('application/x-debian-package',
+                     'application/x-debian-changes',
+                     'application/x-debian-dsc')
+
+	def __init__(self):
+	    AbstractPlugIn.__init__(self, config_file='debian.ini')
+
+	def get_actions(self, thefile):
+	    """return actions provided by this plugin for the give file"""
+	    return []
+
+
+    def register(registry):
+	"""register plugins from this module to the plugins registry"""
+	registry.register(DebianPlugin())
+
+Quelques points à noter :
+
+* l'attribut *name* est utilisé pour référencer le plugin, on le voit
+  notamment apparaître dans l'arbre de configuration des plugins.
+
+* l'attribut *mimetypes* donne une liste de type MIME que ce plugin
+  sait gérer (c'est à dire qu'il est susceptible de fournir des
+  actions pour les fichiers de ce type). On s'intéresse ici uniquement
+  aux paquets debian (.deb) et aux fichiers .changes et .dsc.
+
+* ici on surcharge le constructeur pour donner un nom de fichier de
+  configuration qui sera utilisé pour charger / sauver la configuration
+  du plugin (mais on reparlera de ça plus tard...)
+
+* la méthode *get_actions* est un des points d'entrée principal du
+  plugin:
+
+  * elle prend un objet oobrother.oobrowser.FileWrapper en argument
+
+  * elle retourne une liste d'actions possibles sur cet objet, qui sera
+    insérée dans le menu déroulant
+
+  * elle est appelée uniquement sur des fichiers dont on supporte le
+    type MIME
+
+  * ici elle retourne une liste vide, donc notre plugin ne va pas être
+    très utile...
+
+* la fonction *register* est nécessaire pour que notre module soit
+  automatiquement chargeable par OOBrother. Elle est appelée avec le
+  registre de l'application en argument afin de fournir au module
+  l'occasion d'enregistrer les plugins qu'il contient. Nous enregistrons
+  donc ici une instance de notre plugin auprès du registre.
+
+Sauvez ça dans un fichier **debian.py** dans le sous-répertoire
+correspondant au package *oobrother.plugins*: ce plugin sera
+automatiquement chargé et enregistré au démarrage l'application à 
+condition d'ajouter "oobrother.plugins.debian" à la liste des plugins à
+charger (Affichage -> Préférences -> Main). Vous pouvez mettre ce
+module n'importe où tant que vous mettez le nom de module qui va bien
+dans vos préférences. 
+
+
+Fournir des actions
+-------------------
+
+Bon ben c'est pas tout ça mais faudrait lui faire faire quelque chose
+à ce truc là. On voudrait :
+
+* pouvoir lancer **debc** sur les fichiers .changes en utilisant la
+  console que nous fournit OOBrother pour afficher le résultat. 
+
+* pouvoir lancer **lintian** sur les fichiers .deb ou .dsc et parser
+  la sortie pour affichier les résultats dans une table configurable.
+
+Commençons donc par fournir l'implémentation de *get_actions* qui va
+bien, avec une implémentation de ces actions utilisant la console pour
+les deux : ::
+
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the give file"""
+        actions = []
+        if thefile.basename.endswith('.changes'):
+            actions.append( ('show content', self.cb_debc) )
+        else:
+            actions.append( ('lintian', self.cb_lintian) )
+        return [('debian', actions)]
+
+    def cb_lintian(self, menuitem, thefile):
+        """execute lintian"""
+        self.execute_in_console('lintian %s' % thefile.abspath)
+        
+    def cb_debc(self, menuitem, thefile):
+        """execute debc"""
+        self.execute_in_console('debc %s' % thefile.abspath)
+
+On voit ici qu'une "action" est un tuple à 2 éléments : le nom de
+l'action suivi soit d'une fonction de rappel, soit d'une liste
+d'action (auquel cas ces actions vont s'insérer dans le sous-menu
+correspondant au nom donné). La fonction de rappel prend 2 arguments :
+le widget correspondant à l'élément de menu sur lequel l'utilisateur a
+cliqué et l'instance de FileWrapper sur lequel éxécuter l'action.
+
+Ici nos fonctions de rappels sont les méthodes cb_lintian et cb_debc
+qui utilisent toutes les deux la console de l'application pour éxécuter
+une commande shell (la méthode *execute_in_console* est un raccourci
+fourni par la classe de base du plugin).
+
+Si vous avez enregistré votre plugin après avoir effectué ces
+modifications et que vous relancez OOBrother, vous devriez voir ces
+actions apparaître et vérifier leur fonctionnement.
+
+
+Affichage de l'analyse fournie par lintian
+------------------------------------------
+Les messages affichés par lintian ont un format simple : ::
+
+  type de message:package:message
+
+On voudrait afficher tout ça dans une fenêtre spéciale contenant une
+liste à 3 colonnes. Très sympatiquement, OOBrother fournit une classe
+qui nous mache le travail, la magique ConfigurableListWindow
+:). Allonzi  pour le code de cette fenêtre : ::
+
+    import commands
+    import gobject
+    from oobrother.uiutils.windows import ConfigurableListWindow
+
+    class LintianReportWindow(ConfigurableListWindow):
+	"""display lintian report in a 3 columns list (configurable)"""
+        name = 'lintian'
+
+	def __init__(self):
+	    ConfigurableListWindow.__init__(self, 'lintian.ini',
+					    (gobject.TYPE_STRING, # type
+					     gobject.TYPE_STRING, # path
+					     gobject.TYPE_STRING, # message
+					     ),
+					    ('Type', 'Package', 'Message'),
+					    )
+	    self.init_columns(self.cfg['columns'], (0, 0, 0))
+
+	def lintian(self, thefile):
+	    """check the given wrapped package using checkpackage in a safe
+	    environment
+	    """
+	    self.set_title('lintian: %s' % thefile.abspath)
+	    self.clear()
+	    self.show()
+	    output = commands.getoutput("lintian %s" % thefile.abspath)
+	    for line in output.splitlines():
+		self.append_line( line.split(':', 2) )
+    
+Il nous suffit donc d'hériter de cette classe, de lui donner en
+argument de son constructeur le nom de fichier où sauver cette
+configuration et enfin une description du store et des titres de
+colonnes utilisés pour configurer le tree view. L'attribut *name* va
+être utilisé pour l'onglet de configuration de cette fenêtre.
+L'appel de la méthode *init_columns* va initialiser la vue en fonction
+des colonnes à afficher (le 2eme paramètre permet de configurer les
+méthodes de tri pour les différentes colonnes).
+
+Le lecteur attentif aura remarqué que je parle de configuration et se
+sera demandé ce que ça vient faire là... Et oui ! C'est la que ça
+devient magique, car vous avez ici une fenêtre dont les colonnes à
+afficher sont configurables sans que vous n'ayez rien d'autre à faire
+que de donner accès à cet objet configurable. En gros donc, de le
+brancher avec notre plugin. Il faut pour cela modifier le callback
+*cb_lintian* pour qu'il utilise notre fenêtre et faire que cette fenêtre
+soit intégrée dans le système de configuration. C'est parti : ::
+
+    def __init__(self):
+        AbstractPlugIn.__init__(self, config_file='debian.ini')
+        self.lintian_window = LintianReportWindow()
+        
+    def cb_lintian(self, menuitem, thefile):
+        """execute lintian"""
+        self.lintian_window.lintian(thefile)
+        
+    def subconfiguration(self):
+        """return subconfiguration objects if any"""
+        return [self.lintian_window]
+
+Et voilà. Pour l'intégration de la configuration, il a suffit de
+retourner la fenêtre dans la méthode *subconfiguration()* de notre
+plugin, qui retourne une liste vide par défaut. Si on relance
+l'application on doit pouvoir maintenant configurer notre fenêtre en
+passant par Préférences -> plugins -> debian -> lintian.
+
+
+Gestion de configuration
+------------------------
+On pourrait s'arrêter là... Mais je voudrais pouvoir dire à lintian
+d'éxécuter toutes ses vérifications ou alors seulement quelques
+unes. Facile : ::
+
+    class LintianReportWindow(ConfigurableListWindow):
+	"""display lintian report in a 3 columns list (configurable)"""
+        name = 'lintian'
+	options = (
+	    ('all-checks', {'type': 'yn', 'default': True}),
+	    ('checks', {'type': 'csv', 'default': ()}),
+	    )
+
+	def lintian(self, thefile):
+	    """check the given wrapped package using checkpackage in a safe
+	    environment
+	    """
+	    cmd = 'lintian'
+	    if not self.cfg['all-checks']:
+		cmd = 'lintian %s' % ','.join(self.cfg['checks'])
+	    self.set_title('lintian: %s' % thefile.abspath)
+	    self.clear()
+	    self.show()
+	    output = commands.getoutput("%s %s" % (cmd, thefile.abspath))
+	    for line in output.splitlines():
+		self.append_line( line.split(':', 2) )
+
+Maintenant je peux configurer via les préférences le comportement de
+lintian (la classe ConfigurableWindow utilise l'attribut *options*
+pour construire sa configuration). Bien entendu la configuration est
+sauvegardée et rechargée automatiquement au besoin.
+
+
+
+Le plugin complet
+-----------------
+Voir le fichier sample_plugin.py_ dans ce répertoire.
+
+
+Aller plus loin
+---------------
+Gestion des répertoires, interaction avec l'éditeur de texte, logger
+des informations...
+
+
+.. _sample_plugin.py: sample_plugin.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/sample_plugin.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,96 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""Basic plugin for some debian files
+"""
+
+__revision__ = '$Id: sample_plugin.py,v 1.3 2004-10-14 16:31:06 arthur Exp $'
+
+import commands
+
+import gobject
+
+from oobrother.plugins import AbstractPlugIn
+from oobrother.uiutils.windows import ConfigurableListWindow
+
+class DebianPlugin(AbstractPlugIn):
+    """a plugin for text file, propo"""
+    name = 'debian'
+    mimetypes = ('application/x-debian-package',
+                 'application/x-debian-changes',
+                 'application/x-debian-dsc')
+    
+    def __init__(self):
+        AbstractPlugIn.__init__(self, config_file='debian.ini')
+        self.lintian_window = LintianReportWindow()
+        
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the give file"""
+        actions = []
+        if thefile.basename.endswith('.changes'):
+            actions.append( ('show content', self.cb_debc) )
+        else:
+            actions.append( ('lintian', self.cb_lintian) )
+        return [('debian', actions)]
+
+    def cb_lintian(self, menuitem, thefile):
+        """execute lintian"""
+        self.lintian_window.lintian(thefile)
+        
+    def cb_debc(self, menuitem, thefile):
+        """execute debc"""
+        self.execute_in_console('debc %s' % thefile.abspath)        
+
+    def subconfiguration(self):
+        """return subconfiguration objects if any"""
+        return [self.lintian_window]
+
+    
+class LintianReportWindow(ConfigurableListWindow):
+    """display lintian report in a 3 columns list (configurable)"""
+    name = 'lintian'
+    options = (
+        ('all-checks', {'type': 'yn', 'default': True}),
+        ('checks', {'type': 'csv', 'default': ()}),
+        )
+    
+    def __init__(self):
+        ConfigurableListWindow.__init__(self, 'lintian.ini',
+                                        (gobject.TYPE_STRING, # type
+                                         gobject.TYPE_STRING, # path
+                                         gobject.TYPE_STRING, # message
+                                          ),
+                                        ('Type', 'Package', 'Message'),
+                                        )
+        self.init_columns(self.cfg['columns'], (0, 0, 0))
+        
+    def lintian(self, thefile):
+        """check the given wrapped package using checkpackage in a safe
+        environment
+        """
+        cmd = 'lintian'
+        if not self.cfg['all-checks']:
+            cmd = 'lintian %s' % ','.join(self.cfg['checks'])
+        self.set_title('lintian: %s' % thefile.abspath)
+        self.clear()
+        self.show()
+        output = commands.getoutput("%s %s" % (cmd, thefile.abspath))
+        for line in output.splitlines():
+            self.append_line( line.split(':', 2) )
+
+
+def register(registry):
+    """register plugins from this module to the plugins registry"""
+    registry.register(DebianPlugin())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/editors.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,45 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""Basic editor adapters
+"""
+
+__revision__ = '$Id: editors.py,v 1.2 2004-10-07 09:19:54 syt Exp $'
+
+import os
+
+from logilab.common.interface import Interface
+
+class IEditor(Interface):
+    def open(self, file, lineno=None):
+        """open the given file in the editor. If a line number is given,
+        go to this line in the opened buffer
+        """
+
+class EmacsClient:
+    __implements__ = IEditor,
+
+    def open(self, filepath, lineno=None):
+        """open the given file using emacs client. If a line number is given,
+        go to this line in the opened buffer
+        """
+        cmd = 'emacsclient --no-wait'
+        if lineno:
+            cmd = '%s +%s' % (cmd, lineno)
+        cmd =  '%s %s' % (cmd, filepath)
+        log(LOG_DEBUG, cmd)
+        os.system(cmd)
+        
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/filelib/__init__.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,269 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""file mangement (including VCS) sub-package
+
+This module also exports VCS-related interfaces
+"""
+
+__revision__ = '$Id: __init__.py,v 1.9 2005-03-10 15:40:11 adim Exp $'
+__metaclass__ = type
+
+import os
+import shutil
+from os.path import join, isdir
+from threading import Lock
+
+import gtk, gobject
+
+from logilab.devtools import vcslib 
+
+from oobrother.uiutils import init_treeview_columns, load_pixbuf
+from oobrother.filelib.filters import FilterDef
+
+pixbuf_folder = load_pixbuf("tree-folder-normal.png", size = 20)
+pixbuf_file = load_pixbuf("tree-file-normal.png")
+
+pixbuf_folder_new = load_pixbuf("tree-folder-new.png", 20)
+pixbuf_folder_changed = load_pixbuf("tree-folder-changed.png", 20)
+pixbuf_file_new = load_pixbuf("tree-file-new.png")
+pixbuf_file_changed = load_pixbuf("tree-file-changed.png")
+pixbuf_unknown = load_pixbuf('unknown.png')
+pixbuf_file_needspatch = load_pixbuf("patch2.png")
+
+TEXT_STYLES = [
+    '<span foreground="#888888">%s</span>', # VCS_IGNORED
+    '<span foreground="#888888">%s</span>', # VCS_NOVERSION
+    '<span foreground="black">%s</span>', # VCS_UPTODATE
+    '<span foreground="#880000" weight="bold">%s</span>', # STATE_MODIFIED
+    '<span foreground="#888888" strikethrough="true">%s</span>', # STATE_MISSING
+    '<span foreground="#008800" weight="bold">%s</span>', # STATE_NEW
+    '<span foreground="#880000" strikethrough="true" weight="bold">%s</span>', # STATE_REMOVED
+    '<span foreground="#ff0000" background="#ffeeee" weight="bold">%s</span>', # STATE_CONFLICT
+    '<span foreground="#ff4444" style="italic" weight="bold">%s</span>', # NEEDS PATCH
+    ]
+
+PIXBUFS = [
+    (pixbuf_file, pixbuf_folder), # IGNORED
+    (pixbuf_file, pixbuf_folder), # NORMAL
+    (pixbuf_file, pixbuf_folder), # UP-TO-DATE
+    (pixbuf_file_changed, pixbuf_folder_changed), # MODIFIED
+    (pixbuf_unknown, pixbuf_unknown), # MISSING
+    (pixbuf_file_new, pixbuf_folder_new), # NEW
+    (pixbuf_file_changed, pixbuf_folder_changed), # REMOVED
+    (pixbuf_file_changed, pixbuf_folder_changed), # CONFLICT
+    (pixbuf_file_needspatch, pixbuf_folder), # NEEDS PATCH   
+    ]
+
+def get_pixbuf(column, cell, model, treeiter):
+    file_node = model.get(treeiter, 0)[0]
+    if file_node.is_directory():
+        cell.set_property('pixbuf', PIXBUFS[file_node.get_status()][1])
+    else:
+        cell.set_property('pixbuf', PIXBUFS[file_node.get_status()][0])
+
+def format_text(column, cell, model, treeiter):
+    file_node = model.get(treeiter, 0)[0]
+    status = file_node.get_status()
+    cell.set_property('markup', TEXT_STYLES[status] % file_node.get_name())
+        
+def vcs_treeview():
+    """build and return a ready-to-use treeview a VC file system view"""
+    treeview = gtk.TreeView()
+    pix_renderer = gtk.CellRendererPixbuf()
+    text_renderer = gtk.CellRendererText()
+    column = gtk.TreeViewColumn(_('Filename'), pix_renderer)
+    column.set_cell_data_func(pix_renderer, get_pixbuf)
+    column.pack_start(text_renderer, True)
+    column.set_cell_data_func(text_renderer, format_text)
+    treeview.append_column(column)
+    column.set_resizable(True)
+    col_names = [_('Status'), _('Revision'), _('Tag')]
+    init_treeview_columns(treeview, ['dummy']*2 + col_names,
+                          displayed=col_names, keep_existing=True)
+    return treeview
+
+def get_fs_treeview(root_dir, ignored_extensions, show_hidden_files):
+    filter_def = FilterDef.from_ignored_extensions(ignored_extensions, show_hidden_files)
+    root_node = vcslib.FileWrapper(root_dir) # FilteredFileWrapper(root_dir) #, filter_def)
+    model = VCSTreeModel(root_node, filter_def)
+    treeview = vcs_treeview()
+    treemodel_filter = model.filter_new()
+    treemodel_filter.set_visible_column(5)
+    # FIXME: Yurk! 
+    root_node.model = model # treemodel_filter
+    treeview.set_model(treemodel_filter)# model)
+    return treeview
+
+def get_gtk_path(node):
+    """returns the path from root until self
+
+    :type node: `logilab.devtools.vcslib.interfaces.INode`
+    :param node: the node for which we need a gtk path
+    """
+    parent = node.get_parent()
+    if parent is None:
+        return (0,)
+    self_index = parent.get_children().index(node)
+    return get_gtk_path(parent) + (self_index,)
+
+def get_child_at_path(node, path):
+    """returns the child accessible from self, using path
+
+    :type node: a oobrother.nodelib.INode compliant object
+    :param node: the node for which we need a gtk path
+
+    :type path: tuple
+    :param path: a tuple of children indexes. For example, (0, 3, 1)
+        means 'The 2nd child (1) of the 4th (3) child of the root node (0)'
+    """
+    for index in path:
+        node = node.get_children()[index]
+    return node
+
+class VCSTreeModel(gtk.GenericTreeModel):
+    """Tree model for the file system view in a gtk tree widget
+    
+      Column 0 : the file wrapper object
+      Column 1 : the file name
+      Column 2 : the file's status as (i18n) string
+      Column 3 : the file's revision
+      Column 4 : the file's tag
+
+    In case of additional columns, the 'on_get_value()' handler will use
+    a method called 'get_%colname%' on the node_object to compute the value
+    """
+    
+    def __init__(self, root_node, filter_def):
+        gtk.GenericTreeModel.__init__(self)
+        self.root_node = root_node
+        self.filter_def = filter_def
+        self._col_types = col_types = []
+        self._col_funcs = col_funcs = []
+        for getter, objtype in (
+            (lambda x: x, gobject.TYPE_PYOBJECT),
+            ('get_name', gobject.TYPE_STRING),
+            (lambda x: _(vcslib.STATUS_LABELS[x.get_status()]), gobject.TYPE_STRING),
+            ('get_revision', gobject.TYPE_STRING),
+            ('get_tag', gobject.TYPE_STRING),
+            # XXX temp
+            (self.filter_def.file_is_displayed, bool),
+            ):
+            col_types.append(objtype)
+            if not callable(getter):
+                getter = lambda x, attr=getter: getattr(x, attr)()
+            col_funcs.append(getter)
+        self.last_diplay_status = {}
+        
+    def __getitem__(self, path):
+        # XXX Quick fix
+        child = get_child_at_path(self.root_node, path[1:])
+        return tuple([getter(child) for getter in self._col_funcs])
+
+    def on_get_flags(self):
+        """return the GtkTreeModelFlags (here 0)"""
+        # return 0 # make row iter persistent to benefit from gtk optimizations ?
+        return 0 #gtk.TREE_MODEL_ITERS_PERSIST
+    
+    def on_get_n_columns(self):
+        """return the number of columns in the model"""
+        return len(self._col_types)
+        
+    def on_get_column_type(self, index):
+        """return the type of a column in the model (here STRING)"""
+        assert index < len(self._col_types), \
+               "%s only has %s columns" % (self.__class__, len(self._col_types))
+        return self._col_types[index]
+    
+    def on_get_path(self, node):
+        """return the tree path"""
+        return get_gtk_path(node)
+    
+    def on_get_iter(self, path):
+        """return the node corresponding to the given path"""
+        # print "ON GET ITER()"
+        # Root node has no sibling => path should be something like
+        # (0, 3, 1, ...)
+        assert path[0] == 0
+        return get_child_at_path(self.root_node, path[1:])
+    
+    def on_get_value(self, node, column):
+        """return the value stored in a particular column for the node"""
+        # print "ON GET VALUE", node, column
+        assert column < len(self._col_types), \
+               "%s only has %s columns" % (self.__class__, len(self._col_types))
+##         # XXX FIXME: we want directory to be printed, so we need to return True
+##         #            but we can probably do that in a cleaner way
+##         if column == 5:
+##             if node.is_directory():
+##                 displayed = True
+##             else:
+##                 displayed = self._col_funcs[column](node)
+##             self.last_diplay_status[id(node)] = displayed
+##             #displayed = self._col_funcs[column](node)
+##             return displayed
+        return self._col_funcs[column](node)
+    
+    def on_iter_next(self, node):
+        """return the next node at this level of the tree"""
+        if not node:
+            return None
+        return node.next_sibling()
+
+    def on_iter_children(self, node):
+        """return the first child of this node"""
+        if node is None or node.is_leaf():
+            return None
+        return node.get_children()[0]
+    
+    def on_iter_has_child(self, node):
+        """return true if this node has children"""
+        return not node.is_leaf()
+
+    def on_iter_n_children(self, node):
+        """return the number of children of this node"""
+        # Special case where iter is None (see pygtk reference)
+        if node is None:
+            return 1
+        return node.get_child_number()
+    
+    def on_iter_nth_child(self, node, n):
+        """return the nth child of this node"""
+        print "ON ITER NTH CHILD", node, n
+        # Special case where node is None (see pygtk reference)
+        if node is None:
+            if n == 0:
+                return self.root_node
+            return None
+        children = node.get_children()
+        if n >= len(children):
+            return None
+        return children[n]
+        
+    def on_iter_parent(self, node):
+        """return the parent of this node"""
+        # print "ON ITER PARENT", node
+        return node.get_parent()
+
+    def update(self):
+        """updates each visible node status (hide/show) according
+        to currrent filter_def
+        """
+        self.root_node.walk(emit_row_changed, self)
+
+def emit_row_changed(node, model):
+    """wraps the TreeModel.row_changed() method"""
+    gtkpath = get_gtk_path(node)
+    model.row_changed(gtkpath, model.get_iter(gtkpath))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/filelib/filters.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,236 @@
+"""this module defines a set of filters used to set the display / hide
+property of nodes (filenames) in the treeview
+"""
+
+__revision__  = '$Id: filters.py,v 1.4 2005-03-10 15:40:11 adim Exp $'
+__metaclass__ = type
+
+import re
+from mimetypes import guess_type
+
+class BaseMatcher:
+    """abstract base class for filters"""
+    def match(self, filenode):
+        return True
+
+class AbstractRegexpMatcher(BaseMatcher):
+    """base class for regexp-based filters"""
+    def __init__(self, pattern, flags = 0):
+        self.pattern = pattern
+        self.sre = re.compile(pattern, flags)
+
+    def from_pattern_list(cls, pattern_list, flags = 0):
+        pattern = '|'.join(['(%s$)' % pattern for pattern in pattern_list])
+        return cls(pattern, flags)
+    from_pattern_list = classmethod(from_pattern_list)
+    
+    def __repr__(self):
+        return '%s(%r)' % (self.__class__.__name__, self.pattern)
+
+class AbspathMatcher(AbstractRegexpMatcher):
+    """checks that filepath (absolute) matches a given regexp"""
+    def match(self, filenode):
+        """returns True if <filenode>'s path matches self.pattern"""
+        if self.sre.search(filenode.abspath) is not None:
+            return True
+        return False
+
+# Should ExtensionMatcher extend AbspathMatcher ?
+class ExtensionMatcher(BaseMatcher):
+    """filter based on filename's extension"""
+    def __init__(self, suffix):
+        self.suffix = suffix
+
+    def match(self, filenode):
+        """returns True if filename endswith self.suffix"""
+        if filenode.basename.endswith(self.suffix):
+            return True
+        return False
+
+    def __repr__(self):
+        return 'ExtensionMatcher(%r)' % self.suffix
+
+class MimeTypeMatcher(BaseMatcher):
+    """filter based on files' mimetypes"""
+    def __init__(self, mimedescr, encoding = -1):
+        self.mimedescr = mimedescr
+        self.encoding = encoding
+
+    def match(self, filenode):
+        """returns True if <filenode>'s mimetype matches self"""
+        mtype, encoding = guess_type(filenode.basename)
+        if mtype != self.mimedescr:
+            return False
+        # checks enconding if required
+        if self.encoding != -1:
+            return self.encoding == encoding
+        return True
+
+# XXX use a function instead ?
+class ReversedMatcher(BaseMatcher):
+    """class used to obtain the opposite behaviour of a given filter"""
+    def __init__(self, base_filter):
+        self.base_filter = base_filter
+
+    def match(self, filenode):
+        """reversed behaviour"""
+        return not self.base_filter.match(filenode)
+
+class ContentMatcher(AbstractRegexpMatcher):
+    """used to filter file according to their content"""
+    def match(self, filenode):
+        """returns True if self.pattern is found in <filenode>'s content"""
+        # XXX only use text files (mimetype ?)
+        fname = filenode.basename
+        if fname.endswith('.txt') or fname.endswith('.py'):
+            fp = file(filenode.abspath)
+            content = fp.read()
+            fp.close()
+            if self.sre.search(content) is not None:
+                return True
+        return False
+
+
+class FilterDef:
+    """A FilterDef is a collection of FileMatcher that define which
+    file should be filtered.
+    (If one of the matchers matches a filenode, then this filenode shouldn't
+    be displayed)
+    """
+    def __init__(self, ignored_ext = None,
+                 fname_restrictions = None,
+                 content_restrictions = None,
+                 show_hidden_files = False):
+        ignored_ext = ignored_ext or []
+        self.ignored_matchers = dict([(ext, ExtensionMatcher(ext)) for ext in ignored_ext])
+        patterns = fname_restrictions or []
+        self.fname_matchers = dict([(pat, AbspathMatcher(pat)) for pat in patterns])
+        patterns = content_restrictions or []
+        self.content_matchers = dict([(pat, ContentMatcher(pat)) for pat in patterns])
+        self.show_hidden_files = show_hidden_files
+
+
+    ## ignore extensions management ##################################
+    def ignored_extensions(self):
+        """returns the list of ignored extensions"""
+        return self.ignored_matchers.keys()
+        
+    def ignore_extension(self, ext):
+        """adds an extension to the ignore list"""
+        self.ignored_matchers[ext] = ExtensionMatcher(ext)
+
+    def unignore_extension(self, ext):
+        """removes an extension from the ignore list"""
+        try:
+            del self.ignored_matchers[ext]
+        except KeyError:
+            print "Warning: wasn't ignoring %s" % ext
+
+    ## filename restriction management ###############################
+    def filename_restrictions(self):
+        """returns the list of filename restrictions"""
+        return self.fname_matchers.keys()
+
+    def add_filename_restriction(self, pattern):
+        """adds a pattern that filenodes will have to match"""
+        self.fname_matchers[pattern] = AbspathMatcher(pattern)
+
+    def remove_filename_restriction(self, pattern):
+        """removes a pattern from the list of patterns to be matched"""
+        try:
+            del self.fname_matchers[pattern]
+        except KeyError:
+            print "Warning: wasn't testing %s" % pattern
+
+    def remove_all_filename_restrictions(self):
+        """removes all filename restrictions"""
+        self.fname_matchers.clear()
+
+    ## content restriction management ################################
+    def content_restrictions(self):
+        """returns the list of content restrictions"""
+        return self.content_matchers.keys()
+
+    def add_content_restriction(self, pattern):
+        """adds a pattern that filenodes' content will have to match"""
+        self.content_matchers[pattern] = ContentMatcher(pattern)
+
+    def remove_content_restriction(self, pattern):
+        """removes a pattern from the list of content restrictions"""
+        try:
+            del self.content_matchers[pattern]
+        except KeyError:
+            print "Warning: wasn't testing %s" % pattern
+
+    def remove_all_content_restrictions(self):
+        """removes all content restrictions"""
+        self.content_matchers.clear()
+
+    ## hidden files management #######################################
+    def get_show_hidden_files(self):
+        return self._show_hidden_files
+
+    def set_show_hidden_files(self, show):
+        self._show_hidden_files = show
+        # When <self.hidden_matcher> matches a filenode, it means
+        # that it should not be displayed
+        if show:
+            # XXX a bit too tricky for what it does
+            # (create a matcher that accepts every 
+            self.hidden_matcher = ReversedMatcher(BaseMatcher())
+        else:
+            self.hidden_matcher = AbspathMatcher('/\.')
+            
+    show_hidden_files = property(get_show_hidden_files, set_show_hidden_files)
+
+    ## displayed / dropped methods ###################################
+    def file_is_displayed(self, filenode):
+        """checks that every matchers match <filenode>.
+        returns True if <filenode> is matched by every matchers
+        (<=> should be displayed)
+        """
+        if self.hidden_matcher.match(filenode):
+            return False
+        for ignored_matcher in self.ignored_matchers.values():
+            if ignored_matcher.match(filenode):
+                return False
+        # XXX dirty trick: the following filters only apply on
+        # files (not directories)
+        if filenode.is_directory():
+            return True
+        if self.fname_matchers:
+            for matcher in self.fname_matchers.itervalues():
+                if matcher.match(filenode):
+                    break
+            else:
+                return False
+        if self.content_matchers:
+            for matcher in self.content_matchers.itervalues():
+                if matcher.match(filenode):
+                    break
+            else:
+                return False
+        return True
+    
+    def file_is_dropped(self, filenode):
+        """returns True if filenode shoud not be displayed"""
+        return not self.file_is_displayed(filenode)
+
+    def from_ignored_extensions(cls, ext_list, show_hidden = False):
+        """constructor based on ignored extensions"""
+        return cls(ignored_ext = ext_list, show_hidden_files = show_hidden)
+    from_ignored_extensions = classmethod(from_ignored_extensions)
+    
+    def from_showed_extensions(cls, ext_list, show_hidden = False):
+        """constructor based on allowed extensions"""
+        ext_list = ['%s$' % ext for ext in ext_list]
+        return cls(fname_restrictions = ext_list, show_hidden_files = show_hidden)
+    from_showed_extensions = classmethod(from_showed_extensions)
+
+
+PY_FILTER = ExtensionMatcher('py')
+TXT_FILTER = ExtensionMatcher('txt')
+RST_FILTER = ExtensionMatcher('rst')
+DEBIAN_FILTER = AbspathMatcher('debian/')
+HIDDEN_FILES_FILTER = AbspathMatcher('/\.')
+HIDE_HIDDEN_FILES = ReversedMatcher(HIDDEN_FILES_FILTER)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/i18n/en.po	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,104 @@
+# French translations for PACKAGE package
+# Traduction anglaise du package PACKAGE.
+# Copyright (C) 2004 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Adrien Di Mascio <Adrien.DiMascio@logilab.fr>, 2004.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2004-10-13 11:48+0200\n"
+"PO-Revision-Date: 2004-10-13 11:48+0200\n"
+"Last-Translator: Adrien Di Mascio <Adrien.DiMascio@logilab.fr>\n"
+"Language-Team: French <traduc@traduc.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ISO-8859-1\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../vcslib/common.py:292
+msgid "Filename"
+msgstr ""
+
+#: ../vcslib/common.py:298
+msgid "Status"
+msgstr ""
+
+#: ../vcslib/common.py:298
+msgid "Revision"
+msgstr ""
+
+#: ../vcslib/common.py:298
+msgid "Tag"
+msgstr ""
+
+#: ../vcslib/common.py:311
+msgid "Add to CVS"
+msgstr ""
+
+#: ../vcslib/common.py:313
+msgid "Commit file"
+msgstr ""
+
+#: ../vcslib/common.py:314
+msgid "Diff with last revision"
+msgstr ""
+
+#: ../vcslib/common.py:316
+msgid "Update"
+msgstr ""
+
+#: oob_glade_strings.c:8
+msgid "OO Browser"
+msgstr ""
+
+#: oob_glade_strings.c:9
+msgid "_Fichier"
+msgstr "_File"
+
+#: oob_glade_strings.c:10
+msgid "reloads the file tree"
+msgstr ""
+
+#: oob_glade_strings.c:11
+msgid "Recharger"
+msgstr "Reload"
+
+#: oob_glade_strings.c:12
+msgid "_Édition"
+msgstr "_Edit"
+
+#: oob_glade_strings.c:13
+msgid "Afficha_ge"
+msgstr "View"
+
+#: oob_glade_strings.c:14
+msgid "_Configuration"
+msgstr ""
+
+#: oob_glade_strings.c:15
+msgid "_Log messages"
+msgstr ""
+
+#: oob_glade_strings.c:16
+msgid "_Terminal"
+msgstr ""
+
+#: oob_glade_strings.c:17
+msgid "_Aide"
+msgstr "_Help"
+
+#: oob_glade_strings.c:18
+msgid "À _propos"
+msgstr "About"
+
+#: oob_glade_strings.c:19
+msgid "Preferences"
+msgstr ""
+
+msgid "restrict filenames"
+msgstr "Filename pattern:"
+
+msgid "restrict content"
+msgstr "File should contain:"
Binary file i18n/en/LC_MESSAGES/oobrother.mo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/i18n/fr.po	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,146 @@
+# French translations for PACKAGE package
+# Traduction anglaise du package PACKAGE.
+# Copyright (C) 2004 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Adrien Di Mascio <Adrien.DiMascio@logilab.fr>, 2004.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2004-10-13 11:48+0200\n"
+"PO-Revision-Date: 2004-10-13 11:48+0200\n"
+"Last-Translator: Adrien Di Mascio <Adrien.DiMascio@logilab.fr>\n"
+"Language-Team: French <traduc@traduc.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ISO-8859-1\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../vcslib/common.py:292
+msgid "Filename"
+msgstr "Nom de fichier"
+
+#: ../vcslib/common.py:298
+msgid "Status"
+msgstr "État"
+
+#: ../vcslib/common.py:298
+msgid "Revision"
+msgstr "Révision"
+
+#: ../vcslib/common.py:298
+msgid "Tag"
+msgstr "Tag"
+
+#: ../vcslib/common.py:311
+msgid "Add to CVS"
+msgstr "Ajouter au CVS"
+
+#: ../vcslib/common.py:313
+msgid "Commit file"
+msgstr "Committer le fichier"
+
+#: ../vcslib/common.py:314
+msgid "Diff with last revision"
+msgstr "Voir les différences avec la version précédente"
+
+#: ../vcslib/common.py:316
+msgid "Update"
+msgstr "Mettre à jour"
+
+#: oob_glade_strings.c:8
+msgid "OO Browser"
+msgstr "OO Browser"
+
+#: oob_glade_strings.c:9
+msgid "_Fichier"
+msgstr "Fichier"
+
+#: oob_glade_strings.c:10
+msgid "reloads the file tree"
+msgstr "Mettre à jour l'arborscence de fichiers"
+
+#: oob_glade_strings.c:11
+msgid "Recharger"
+msgstr "Recharger"
+
+#: oob_glade_strings.c:12
+msgid "_Édition"
+msgstr "Édition"
+
+#: oob_glade_strings.c:13
+msgid "Afficha_ge"
+msgstr "Affichage"
+
+#: oob_glade_strings.c:14
+msgid "_Configuration"
+msgstr "Configuration"
+
+#: oob_glade_strings.c:15
+msgid "_Log messages"
+msgstr "Messages de log"
+
+#: oob_glade_strings.c:16
+msgid "_Terminal"
+msgstr "Terminal"
+
+#: oob_glade_strings.c:17
+msgid "_Aide"
+msgstr "Aide"
+
+#: oob_glade_strings.c:18
+msgid "À _propos"
+msgstr "À propos"
+
+#: oob_glade_strings.c:19
+msgid "Preferences"
+msgstr "Préférences"
+
+msgid "ignored"
+msgstr "ignoré"
+
+msgid "noversion"
+msgstr "pas de version"
+
+msgid "up-to-date"
+msgstr "à jour"
+
+msgid "modified"
+msgstr "Modifié localement"
+
+msgid "missing"
+msgstr "manquant"
+
+msgid "new"
+msgstr "nouveau"
+
+msgid "removed"
+msgstr "retiré"
+
+msgid "conflict"
+msgstr "conflit"
+
+msgid "needs patch"
+msgstr "Besoin de M.A.J"
+
+msgid "clear all messages in the window"
+msgstr "Efface tous les messges dans la fenêtre"
+
+msgid "_Quit"
+msgstr "_Quitter"
+
+msgid "Edit Terminal font"
+msgstr "Changer la police du terminal"
+
+msgid "filenames"
+msgstr "Noms de fichiers"
+
+msgid "content"
+msgstr "Contenu"
+
+msgid "filter"
+msgstr "Filtrer"
+
+msgid "filter box"
+msgstr "Boîte de filtres"
Binary file i18n/fr/LC_MESSAGES/oobrother.mo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/i18n/messages.pot	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,97 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2004-10-13 11:48+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../vcslib/common.py:292
+msgid "Filename"
+msgstr ""
+
+#: ../vcslib/common.py:298
+msgid "Status"
+msgstr ""
+
+#: ../vcslib/common.py:298
+msgid "Revision"
+msgstr ""
+
+#: ../vcslib/common.py:298
+msgid "Tag"
+msgstr ""
+
+#: ../vcslib/common.py:311
+msgid "Add to CVS"
+msgstr ""
+
+#: ../vcslib/common.py:313
+msgid "Commit file"
+msgstr ""
+
+#: ../vcslib/common.py:314
+msgid "Diff with last revision"
+msgstr ""
+
+#: ../vcslib/common.py:316
+msgid "Update"
+msgstr ""
+
+#: oob_glade_strings.c:8
+msgid "OO Browser"
+msgstr ""
+
+#: oob_glade_strings.c:9
+msgid "_Fichier"
+msgstr ""
+
+#: oob_glade_strings.c:10
+msgid "reloads the file tree"
+msgstr ""
+
+#: oob_glade_strings.c:11
+msgid "Recharger"
+msgstr ""
+
+#: oob_glade_strings.c:12
+msgid "_Édition"
+msgstr ""
+
+#: oob_glade_strings.c:13
+msgid "Afficha_ge"
+msgstr ""
+
+#: oob_glade_strings.c:14
+msgid "_Configuration"
+msgstr ""
+
+#: oob_glade_strings.c:15
+msgid "_Log messages"
+msgstr ""
+
+#: oob_glade_strings.c:16
+msgid "_Terminal"
+msgstr ""
+
+#: oob_glade_strings.c:17
+msgid "_Aide"
+msgstr ""
+
+#: oob_glade_strings.c:18
+msgid "À _propos"
+msgstr ""
+
+#: oob_glade_strings.c:19
+msgid "Preferences"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/i18n/oob_glade_strings.c	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,24 @@
+/*
+ * Chaînes de caractères à traduire générées par
+ * Glade. Ajouter ce fichier au fichier POTFILE.in
+ * de votre projet. NE PAS compiler ce fichier
+ * avec le reste de votre application.
+ */
+
+gchar *s = N_("OO Browser");
+gchar *s = N_("_Fichier");
+gchar *s = N_("reloads the file tree");
+gchar *s = N_("Recharger");
+gchar *s = N_("_Quit");
+gchar *s = N_("_Ã\211dition");
+gchar *s = N_("Afficha_ge");
+gchar *s = N_("Edits OoBrother's terminal font");
+gchar *s = N_("Edit Terminal font");
+gchar *s = N_("_Configuration");
+gchar *s = N_("_Log messages");
+gchar *s = N_("_Terminal");
+gchar *s = N_("_Aide");
+gchar *s = N_("Ã\200 _propos");
+gchar *s = N_("Preferences");
+gchar *s = N_("Choisir la police");
+gchar *s = N_("Portez ce vieux whisky au juge blond qui fume. 0123456789. àâçèéêëîïôùûüæÅ\223.");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nodelib.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,5 @@
+"""this modules defines basic node class / operations"""
+
+__revision__ = '$Id: nodelib.py,v 1.3 2004-11-10 16:45:10 syt Exp $'
+
+from logilab.devtools.vcslib.node import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/oobrowser.glade	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,615 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkWindow" id="main_window">
+  <property name="visible">True</property>
+  <property name="title" translatable="yes">OO Browser</property>
+  <property name="type">GTK_WINDOW_TOPLEVEL</property>
+  <property name="window_position">GTK_WIN_POS_NONE</property>
+  <property name="modal">False</property>
+  <property name="default_width">230</property>
+  <property name="default_height">600</property>
+  <property name="resizable">True</property>
+  <property name="destroy_with_parent">False</property>
+  <property name="decorated">True</property>
+  <property name="skip_taskbar_hint">False</property>
+  <property name="skip_pager_hint">False</property>
+  <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
+  <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+
+  <child>
+    <widget class="GtkVBox" id="vbox1">
+      <property name="visible">True</property>
+      <property name="homogeneous">False</property>
+      <property name="spacing">0</property>
+
+      <child>
+	<widget class="GtkMenuBar" id="menubar1">
+	  <property name="visible">True</property>
+
+	  <child>
+	    <widget class="GtkMenuItem" id="menuitem1">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">_Fichier</property>
+	      <property name="use_underline">True</property>
+
+	      <child>
+		<widget class="GtkMenu" id="menuitem1_menu">
+
+		  <child>
+		    <widget class="GtkImageMenuItem" id="imagemenuitem1">
+		      <property name="visible">True</property>
+		      <property name="label">gtk-new</property>
+		      <property name="use_stock">True</property>
+		      <signal name="activate" handler="on_nouveau1_activate" last_modification_time="Wed, 29 Sep 2004 12:47:51 GMT"/>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkImageMenuItem" id="imagemenuitem2">
+		      <property name="visible">True</property>
+		      <property name="label">gtk-open</property>
+		      <property name="use_stock">True</property>
+		      <signal name="activate" handler="on_ouvrir1_activate" last_modification_time="Wed, 29 Sep 2004 12:47:51 GMT"/>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkImageMenuItem" id="refresh">
+		      <property name="visible">True</property>
+		      <property name="tooltip" translatable="yes">reloads the file tree</property>
+		      <property name="label" translatable="yes">Recharger</property>
+		      <property name="use_underline">True</property>
+		      <signal name="activate" handler="on_reload_activate" last_modification_time="Wed, 06 Oct 2004 16:11:53 GMT"/>
+
+		      <child internal-child="image">
+			<widget class="GtkImage" id="image1">
+			  <property name="visible">True</property>
+			  <property name="stock">gtk-refresh</property>
+			  <property name="icon_size">1</property>
+			  <property name="xalign">0.5</property>
+			  <property name="yalign">0.5</property>
+			  <property name="xpad">0</property>
+			  <property name="ypad">0</property>
+			</widget>
+		      </child>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkSeparatorMenuItem" id="separatormenuitem1">
+		      <property name="visible">True</property>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkMenuItem" id="quit">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">_Quit</property>
+		      <property name="use_underline">True</property>
+		      <signal name="activate" handler="on_quit_activate" last_modification_time="Wed, 29 Sep 2004 13:22:43 GMT"/>
+		      <accelerator key="q" modifiers="GDK_CONTROL_MASK" signal="activate"/>
+		    </widget>
+		  </child>
+		</widget>
+	      </child>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkMenuItem" id="menuitem2">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">_Édition</property>
+	      <property name="use_underline">True</property>
+
+	      <child>
+		<widget class="GtkMenu" id="menuitem2_menu">
+
+		  <child>
+		    <widget class="GtkImageMenuItem" id="imagemenuitem6">
+		      <property name="visible">True</property>
+		      <property name="label">gtk-cut</property>
+		      <property name="use_stock">True</property>
+		      <signal name="activate" handler="on_couper1_activate" last_modification_time="Wed, 29 Sep 2004 12:47:51 GMT"/>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkImageMenuItem" id="imagemenuitem7">
+		      <property name="visible">True</property>
+		      <property name="label">gtk-copy</property>
+		      <property name="use_stock">True</property>
+		      <signal name="activate" handler="on_copier1_activate" last_modification_time="Wed, 29 Sep 2004 12:47:51 GMT"/>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkImageMenuItem" id="imagemenuitem8">
+		      <property name="visible">True</property>
+		      <property name="label">gtk-paste</property>
+		      <property name="use_stock">True</property>
+		      <signal name="activate" handler="on_coller1_activate" last_modification_time="Wed, 29 Sep 2004 12:47:51 GMT"/>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkImageMenuItem" id="imagemenuitem9">
+		      <property name="visible">True</property>
+		      <property name="label">gtk-delete</property>
+		      <property name="use_stock">True</property>
+		      <signal name="activate" handler="on_supprimer1_activate" last_modification_time="Wed, 29 Sep 2004 12:47:51 GMT"/>
+		    </widget>
+		  </child>
+		</widget>
+	      </child>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkMenuItem" id="menuitem3">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">Afficha_ge</property>
+	      <property name="use_underline">True</property>
+
+	      <child>
+		<widget class="GtkMenu" id="menuitem3_menu">
+
+		  <child>
+		    <widget class="GtkCheckMenuItem" id="configuration1">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">_Configuration</property>
+		      <property name="use_underline">True</property>
+		      <property name="active">False</property>
+		      <signal name="activate" handler="on_configuration1_activate" last_modification_time="Sun, 03 Oct 2004 21:58:15 GMT"/>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkCheckMenuItem" id="log1">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">_Log messages</property>
+		      <property name="use_underline">True</property>
+		      <property name="active">False</property>
+		      <signal name="activate" handler="on_log1_activate" last_modification_time="Wed, 06 Oct 2004 18:47:36 GMT"/>
+		    </widget>
+		  </child>
+
+		  <child>
+		    <widget class="GtkCheckMenuItem" id="console1">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">_Terminal</property>
+		      <property name="use_underline">True</property>
+		      <property name="active">False</property>
+		      <signal name="activate" handler="on_console1_activate" last_modification_time="Wed, 06 Oct 2004 18:47:36 GMT"/>
+		    </widget>
+		  </child>
+		</widget>
+	      </child>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkMenuItem" id="menuitem4">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">_Aide</property>
+	      <property name="use_underline">True</property>
+
+	      <child>
+		<widget class="GtkMenu" id="menuitem4_menu">
+
+		  <child>
+		    <widget class="GtkMenuItem" id="menuitem5">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">À _propos</property>
+		      <property name="use_underline">True</property>
+		      <signal name="activate" handler="on_À_propos1_activate" last_modification_time="Wed, 29 Sep 2004 12:47:51 GMT"/>
+		    </widget>
+		  </child>
+		</widget>
+	      </child>
+	    </widget>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkFrame" id="frame2">
+	  <property name="visible">True</property>
+	  <property name="label_xalign">0</property>
+	  <property name="label_yalign">0.5</property>
+	  <property name="shadow_type">GTK_SHADOW_ETCHED_IN</property>
+
+	  <child>
+	    <widget class="GtkAlignment" id="alignment1">
+	      <property name="visible">True</property>
+	      <property name="xalign">0.5</property>
+	      <property name="yalign">0.5</property>
+	      <property name="xscale">1</property>
+	      <property name="yscale">1</property>
+	      <property name="top_padding">0</property>
+	      <property name="bottom_padding">0</property>
+	      <property name="left_padding">12</property>
+	      <property name="right_padding">0</property>
+
+	      <child>
+		<widget class="GtkTable" id="table1">
+		  <property name="border_width">10</property>
+		  <property name="visible">True</property>
+		  <property name="n_rows">3</property>
+		  <property name="n_columns">2</property>
+		  <property name="homogeneous">False</property>
+		  <property name="row_spacing">3</property>
+		  <property name="column_spacing">0</property>
+
+		  <child>
+		    <widget class="GtkLabel" id="label3">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">filenames</property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_LEFT</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">0</property>
+		      <property name="ypad">0</property>
+		    </widget>
+		    <packing>
+		      <property name="left_attach">0</property>
+		      <property name="right_attach">1</property>
+		      <property name="top_attach">0</property>
+		      <property name="bottom_attach">1</property>
+		      <property name="x_options">fill</property>
+		      <property name="y_options"></property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkLabel" id="label4">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">content</property>
+		      <property name="use_underline">False</property>
+		      <property name="use_markup">False</property>
+		      <property name="justify">GTK_JUSTIFY_LEFT</property>
+		      <property name="wrap">False</property>
+		      <property name="selectable">False</property>
+		      <property name="xalign">0.5</property>
+		      <property name="yalign">0.5</property>
+		      <property name="xpad">0</property>
+		      <property name="ypad">0</property>
+		    </widget>
+		    <packing>
+		      <property name="left_attach">0</property>
+		      <property name="right_attach">1</property>
+		      <property name="top_attach">1</property>
+		      <property name="bottom_attach">2</property>
+		      <property name="x_options">fill</property>
+		      <property name="y_options"></property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkEntry" id="filename_filter_entry">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="editable">True</property>
+		      <property name="visibility">True</property>
+		      <property name="max_length">0</property>
+		      <property name="text" translatable="yes"></property>
+		      <property name="has_frame">True</property>
+		      <property name="invisible_char">*</property>
+		      <property name="activates_default">False</property>
+		      <signal name="activate" handler="on_filter_entry_activate" last_modification_time="Mon, 13 Dec 2004 15:05:12 GMT"/>
+		    </widget>
+		    <packing>
+		      <property name="left_attach">1</property>
+		      <property name="right_attach">2</property>
+		      <property name="top_attach">0</property>
+		      <property name="bottom_attach">1</property>
+		      <property name="y_options"></property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkEntry" id="content_filter_entry">
+		      <property name="visible">True</property>
+		      <property name="can_focus">True</property>
+		      <property name="editable">True</property>
+		      <property name="visibility">True</property>
+		      <property name="max_length">0</property>
+		      <property name="text" translatable="yes"></property>
+		      <property name="has_frame">True</property>
+		      <property name="invisible_char">*</property>
+		      <property name="activates_default">False</property>
+		      <signal name="activate" handler="on_filter_entry_activate" last_modification_time="Mon, 13 Dec 2004 15:05:12 GMT"/>
+		    </widget>
+		    <packing>
+		      <property name="left_attach">1</property>
+		      <property name="right_attach">2</property>
+		      <property name="top_attach">1</property>
+		      <property name="bottom_attach">2</property>
+		      <property name="y_options"></property>
+		    </packing>
+		  </child>
+
+		  <child>
+		    <widget class="GtkHButtonBox" id="hbuttonbox1">
+		      <property name="visible">True</property>
+		      <property name="layout_style">GTK_BUTTONBOX_SPREAD</property>
+		      <property name="spacing">0</property>
+
+		      <child>
+			<widget class="GtkButton" id="filter_button">
+			  <property name="visible">True</property>
+			  <property name="can_focus">True</property>
+			  <property name="label" translatable="yes">filter</property>
+			  <property name="use_underline">True</property>
+			  <property name="relief">GTK_RELIEF_NORMAL</property>
+			  <property name="focus_on_click">True</property>
+			  <signal name="clicked" handler="on_filter_button_clicked" last_modification_time="Thu, 24 Feb 2005 11:07:09 GMT"/>
+			</widget>
+		      </child>
+		    </widget>
+		    <packing>
+		      <property name="left_attach">0</property>
+		      <property name="right_attach">2</property>
+		      <property name="top_attach">2</property>
+		      <property name="bottom_attach">3</property>
+		      <property name="x_options">fill</property>
+		    </packing>
+		  </child>
+		</widget>
+	      </child>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkLabel" id="label4">
+	      <property name="visible">True</property>
+	      <property name="label" translatable="yes">filter box</property>
+	      <property name="use_underline">False</property>
+	      <property name="use_markup">True</property>
+	      <property name="justify">GTK_JUSTIFY_LEFT</property>
+	      <property name="wrap">False</property>
+	      <property name="selectable">False</property>
+	      <property name="xalign">0.5</property>
+	      <property name="yalign">0.5</property>
+	      <property name="xpad">0</property>
+	      <property name="ypad">0</property>
+	    </widget>
+	    <packing>
+	      <property name="type">label_item</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkScrolledWindow" id="scr_window">
+	  <property name="visible">True</property>
+	  <property name="can_focus">True</property>
+	  <property name="hscrollbar_policy">GTK_POLICY_ALWAYS</property>
+	  <property name="vscrollbar_policy">GTK_POLICY_ALWAYS</property>
+	  <property name="shadow_type">GTK_SHADOW_NONE</property>
+	  <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+	  <child>
+	    <placeholder/>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkStatusbar" id="statusbar">
+	  <property name="visible">True</property>
+	  <property name="has_resize_grip">True</property>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">False</property>
+	</packing>
+      </child>
+    </widget>
+  </child>
+</widget>
+
+<widget class="GtkDialog" id="config_window">
+  <property name="title" translatable="yes">Preferences</property>
+  <property name="type">GTK_WINDOW_TOPLEVEL</property>
+  <property name="window_position">GTK_WIN_POS_CENTER</property>
+  <property name="modal">False</property>
+  <property name="default_width">500</property>
+  <property name="default_height">300</property>
+  <property name="resizable">True</property>
+  <property name="destroy_with_parent">False</property>
+  <property name="decorated">True</property>
+  <property name="skip_taskbar_hint">False</property>
+  <property name="skip_pager_hint">False</property>
+  <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+  <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+  <property name="has_separator">True</property>
+
+  <child internal-child="vbox">
+    <widget class="GtkVBox" id="dialog-vbox1">
+      <property name="visible">True</property>
+      <property name="homogeneous">False</property>
+      <property name="spacing">0</property>
+
+      <child internal-child="action_area">
+	<widget class="GtkHButtonBox" id="dialog-action_area1">
+	  <property name="visible">True</property>
+	  <property name="layout_style">GTK_BUTTONBOX_END</property>
+
+	  <child>
+	    <widget class="GtkButton" id="helpbutton1">
+	      <property name="visible">True</property>
+	      <property name="can_default">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label">gtk-help</property>
+	      <property name="use_stock">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="response_id">-11</property>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkButton" id="cancelbutton1">
+	      <property name="visible">True</property>
+	      <property name="can_default">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label">gtk-cancel</property>
+	      <property name="use_stock">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="response_id">-6</property>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkButton" id="applybutton1">
+	      <property name="visible">True</property>
+	      <property name="can_default">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label">gtk-apply</property>
+	      <property name="use_stock">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="response_id">-10</property>
+	    </widget>
+	  </child>
+
+	  <child>
+	    <widget class="GtkButton" id="okbutton1">
+	      <property name="visible">True</property>
+	      <property name="can_default">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label">gtk-ok</property>
+	      <property name="use_stock">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="response_id">-5</property>
+	    </widget>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">False</property>
+	  <property name="fill">True</property>
+	  <property name="pack_type">GTK_PACK_END</property>
+	</packing>
+      </child>
+
+      <child>
+	<widget class="GtkHPaned" id="hpaned2">
+	  <property name="visible">True</property>
+	  <property name="can_focus">True</property>
+	  <property name="position">200</property>
+
+	  <child>
+	    <widget class="GtkScrolledWindow" id="scrolledwindow2">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+	      <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+	      <property name="shadow_type">GTK_SHADOW_NONE</property>
+	      <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+	      <child>
+		<widget class="GtkTreeView" id="config_treeview">
+		  <property name="visible">True</property>
+		  <property name="can_focus">True</property>
+		  <property name="headers_visible">True</property>
+		  <property name="rules_hint">False</property>
+		  <property name="reorderable">False</property>
+		  <property name="enable_search">True</property>
+		  <signal name="button_press_event" handler="on_config_treeview_button_press_event" last_modification_time="Sat, 02 Oct 2004 22:06:21 GMT"/>
+		</widget>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="shrink">True</property>
+	      <property name="resize">False</property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkFrame" id="frame1">
+	      <property name="visible">True</property>
+	      <property name="label_xalign">0</property>
+	      <property name="label_yalign">0.5</property>
+	      <property name="shadow_type">GTK_SHADOW_NONE</property>
+
+	      <child>
+		<widget class="GtkAlignment" id="config_frame">
+		  <property name="visible">True</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xscale">1</property>
+		  <property name="yscale">1</property>
+		  <property name="top_padding">0</property>
+		  <property name="bottom_padding">0</property>
+		  <property name="left_padding">12</property>
+		  <property name="right_padding">0</property>
+
+		  <child>
+		    <placeholder/>
+		  </child>
+		</widget>
+	      </child>
+
+	      <child>
+		<widget class="GtkLabel" id="label2">
+		  <property name="visible">True</property>
+		  <property name="label" translatable="yes"></property>
+		  <property name="use_underline">False</property>
+		  <property name="use_markup">False</property>
+		  <property name="justify">GTK_JUSTIFY_LEFT</property>
+		  <property name="wrap">False</property>
+		  <property name="selectable">False</property>
+		  <property name="xalign">0.5</property>
+		  <property name="yalign">0.5</property>
+		  <property name="xpad">0</property>
+		  <property name="ypad">0</property>
+		</widget>
+		<packing>
+		  <property name="type">label_item</property>
+		</packing>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="shrink">True</property>
+	      <property name="resize">True</property>
+	    </packing>
+	  </child>
+	</widget>
+	<packing>
+	  <property name="padding">0</property>
+	  <property name="expand">True</property>
+	  <property name="fill">True</property>
+	</packing>
+      </child>
+    </widget>
+  </child>
+</widget>
+
+</glade-interface>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/oobrowser.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,250 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""A file system objects browser. This module contains the main application
+window.
+
+USAGE: oobrother [root directory]
+"""
+
+__revision__ = '$Id: oobrowser.py,v 1.34 2006-03-26 20:20:54 nico Exp $'
+
+import sys
+import os
+from os.path import abspath, basename, isdir, isfile, join
+
+import gtk, gobject, gtk.glade
+
+from pigg.utils import confirm
+
+from oobrother import get_path, localize_app
+from oobrother.uiutils.windows import LogServiceWindow, SystemWindow, \
+     ConfigurationWindow
+from oobrother.uiutils.basemixins import PluggableFrameMixIn, TreeViewMixIn
+from oobrother.uiutils.trees import init_treeview_columns
+from oobrother.uiutils import init_treeview_columns, create_popup_menu, \
+     merge_actions, save_session, restore_session, change_cursor_for_operation
+from oobrother.registry import PlugInsRegistry
+from oobrother.sysutils import OOBROTHER_HOME
+from oobrother.config_tools import PluggableConfig
+from oobrother.editors import EmacsClient
+from oobrother.filelib import get_fs_treeview
+
+__metaclass__ = type
+
+class FSBrowser(PluggableFrameMixIn, TreeViewMixIn):
+    """The FileSystem Browser"""
+    options = (
+        ('plugins', {'type': 'csv',
+                     'default': (#'oobrother.plugins.base',
+                                 'oobrother.plugins.vcs',
+                                 'oobrother.plugins.devtools',
+                                 'oobrother.plugins.python',
+                                 'oobrother.plugins.pychecker',
+                                 'oobrother.plugins.debian')
+                     }),
+        ('ignored-extensions', {'type': 'csv',
+                                'default': ('CVS', 'BAK', 'bak', '~',
+                                            'swp', 'pyc', 'pyo', ',cover')
+                                }),
+        ('show-hidden-files', {'type': 'yn',
+                               'default': False}),
+        )
+
+    name = 'main'
+    def __init__(self, root_dir):
+        # We need to have self.treeview in TreeviewMixIn's init()
+        self.widgets = gtk.glade.XML(get_path('glade'), 'main_window', 'oobrother')
+        super(FSBrowser, self).__init__()
+        self.root_directory = root_dir = abspath(root_dir)
+        self.main_window = self.widgets.get_widget('main_window')
+        # first init the log window to not loose anything
+        self.log_window = LogServiceWindow()
+        # reference main windows / widgets
+        self.console = SystemWindow()
+        self.editor = EmacsClient()
+        self.plugin_frame = self.widgets.get_widget('plugin_frame')
+        self.filename_filter_entry = self.widgets.get_widget('filename_filter_entry')
+        self.content_filter_entry = self.widgets.get_widget('content_filter_entry')
+        self.filename_filter_entry.connect('activate', self.cb_update_fname_filter)
+        self.content_filter_entry.connect('activate', self.cb_update_content_filter)
+        # self.content_filter_entry.connect('activate', self.cb_filter_filetree)
+        # build configuration stuff
+        self.cfg = PluggableConfig(self.name, 'fsbrowser.ini', self.options)
+        self.registry = PlugInsRegistry(self)
+        self.registry.register_all(self.cfg['plugins'])
+        self.cfg_window = ConfigurationWindow( (self, self.registry) )
+        # set tree model
+        scr = self.widgets.get_widget('scr_window')
+        self.treeview = get_fs_treeview(root_dir, self.cfg['ignored-extensions'],
+                                        self.cfg['show-hidden-files'])
+        self.treeview.show()
+        self.activate_default_callbacks()
+        scr.add(self.treeview)
+        # Quit program when main_window gets closed
+        self.main_window.connect('delete-event', self.cb_quit)
+        # Main callbacks
+        handlers = {
+                    'on_reload_activate' : self.cb_reload_filetree,
+                    'on_filter_button_clicked' : self.cb_filter_filetree,
+                    # 'on_refilter_button_clicked' : self.cb_refilter_filetree,
+                    'on_configuration1_activate': self.cfg_window.cb_toggle_visibility,
+                    'on_log1_activate': self.log_window.cb_toggle_visibility,
+                    'on_console1_activate': self.console.cb_toggle_visibility,
+                    'on_quit_activate': self.cb_quit,
+                    }
+        self.widgets.signal_autoconnect(handlers)
+        self._wdefs = {'main': self.main_window,
+                       'log': self.log_window.win,
+                       'console': self.console.win,
+                       'config': self.cfg_window.win,
+                       }
+        self.log_window.set_menu_item(self.widgets.get_widget('log1'))
+        self.console.set_menu_item(self.widgets.get_widget('console1'))
+        self.cfg_window.set_menu_item(self.widgets.get_widget('configuration1'))
+
+        restore_session(self._wdefs, join(OOBROTHER_HOME, 'session.pickle'))
+        
+    def subconfiguration(self):
+        """return a list of configuration nodes for subconfiguration
+        (i.e. only the log window at this day...)
+        """
+        return [self.log_window]
+
+    def cb_reload_filetree(self, menuitem):
+        """force file browser to reload"""
+        # FIXME : find a way to restore the original unfolded aspect
+        model = self.treeview.get_model() 
+        self.treeview.collapse_all()
+        model.root_node.force_update()
+
+    def cb_filter_filetree(self, source):
+        """set a filter on the file browser"""
+        filter_model = self.treeview.get_model()
+        basemodel = filter_model.get_model()
+        pattern = self.filename_filter_entry.get_text().strip()
+        basemodel.filter_def.remove_all_filename_restrictions()
+        if pattern:
+            basemodel.filter_def.add_filename_restriction(pattern)
+        pattern = self.content_filter_entry.get_text().strip()
+        basemodel.filter_def.remove_all_content_restrictions()
+        if pattern:
+            basemodel.filter_def.add_content_restriction(pattern)
+        change_cursor_for_operation(basemodel.update, self.treeview)
+            
+    def cb_update_fname_filter(self, fname_entry):
+        pattern = fname_entry.get_text().strip()
+        basemodel = self.treeview.get_model().get_model()
+        basemodel.filter_def.remove_all_filename_restrictions()
+        basemodel.filter_def.add_filename_restriction(pattern)
+        change_cursor_for_operation(basemodel.update, self.treeview)
+
+    def cb_update_content_filter(self, content_entry):
+        pattern = content_entry.get_text().strip()
+        basemodel = self.treeview.get_model().get_model()
+        basemodel.filter_def.remove_all_content_restrictions()
+        basemodel.filter_def.add_content_restriction(pattern)
+        change_cursor_for_operation(basemodel.update, self.treeview)
+    
+    def cb_quit(self, *args):
+        """exit the application after having saved the current session"""
+        save_session(self._wdefs, join(OOBROTHER_HOME, 'session.pickle'))
+        gtk.main_quit()
+        
+    def on_double_click(self, row_model, event):
+        """called on double-click in the tree view:
+        
+        open the selected file in the editor
+        """
+        thefile = row_model[0]
+        self.editor.open(thefile.abspath)
+        return True
+
+    def on_right_click(self, row_model, event):
+        """called on right button click in the tree view:
+        
+        display available plugin actions for the selected file in a popup menu
+        """
+        thefile = row_model[0]
+        if thefile is not None:
+            plugins = self.registry.get_plugins(thefile)
+            self._popup_action_menu(event, plugins, thefile)
+        else:
+            return True
+
+    def on_delete_pressed(self, row_model, event):
+        """called when the Delete key is pressed in the tree view:
+        
+        remove the selected file or directory
+        """
+        thefile = row_model[0]
+        # FIXME: ask confirmation ?
+        if thefile is not None and confirm(_('are you sure you want to delete this item ?')):
+            thefile.get_parent().remove(thefile.get_name())
+        else:
+            return True
+        
+    def _popup_action_menu(self, event, plugins, thefile):
+        """Popups the actions contextual menu
+        """
+        actions = []
+        for plugin in plugins:
+            actions += plugin.get_actions(thefile)
+        actions = merge_actions(actions)
+        popup_menu = create_popup_menu(actions, thefile)
+        popup_menu.show_all()
+        popup_menu.popup(None, None, None, event.button, event.time)
+        return True
+
+    def show(self):
+        """Shows the main window"""
+        self.main_window.show()
+
+import hotshot
+import atexit
+def run_profiled(args=None):
+    import hotshot as profile
+    prof = profile.Profile("oobrother.prof")
+    atexit.register(prof.close)
+    try:
+        prof.runctx('run(args)', globals(), locals())
+    except SystemExit:
+        pass
+    
+    
+def run(args=None):
+    """main()"""
+
+    if args is None:
+        args = sys.argv[1:]
+    if len(args) > 1:
+        print "Bad usage"
+        print __doc__
+        sys.exit(1)
+    elif args:
+        root_dir = args[0]
+    else:
+        root_dir = os.getcwd()
+    localize_app()
+    assert isdir(root_dir)
+    spb = FSBrowser(root_dir)
+    spb.show()
+    gtk.main()
+
+    
+if __name__ == '__main__':
+    run(sys.argv[1:])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pixmaps/button_apply0.xpm	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,18 @@
+/* XPM */
+static char * button_apply_xpm[] = {
+"16 12 3 1",
+" 	c None",
+".	c #000000",
+"+	c #FFFFFF",
+"                ",
+"        ...     ",
+"         ...    ",
+"          ...   ",
+"          ....  ",
+" .............. ",
+" ...............",
+" .............. ",
+"          ....  ",
+"          ...   ",
+"         ...    ",
+"        ...     "};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pixmaps/button_apply1.xpm	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,20 @@
+/* XPM */
+static char *button_apply1[] = {
+/* columns rows colors chars-per-pixel */
+"16 12 2 1",
+"  c black",
+". c None",
+/* pixels */
+"................",
+".....   ........",
+"....   .........",
+"...   ..........",
+"..    ..........",
+".              .",
+"               .",
+".              .",
+"..    ..........",
+"...   ..........",
+"....   .........",
+".....   ........"
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pixmaps/button_copy0.xpm	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,32 @@
+/* XPM */
+static char *button_copy1[] = {
+/* columns rows colors chars-per-pixel */
+"16 24 2 1",
+"  c black",
+". c None",
+/* pixels */
+"........        ",
+"........        ",
+"............    ",
+"...........     ",
+".....   ..      ",
+".....   .    .  ",
+".....   .   ..  ",
+".....   .  ...  ",
+".....   . ....  ",
+".....   ......  ",
+".....   ........",
+"             ...",
+"             ...",
+"             ...",
+".....   ........",
+".....   . ....  ",
+".....   .  ...  ",
+".....   .   ..  ",
+".....   .    .  ",
+".....   ..      ",
+"...........     ",
+"............    ",
+"........        ",
+"........        "
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pixmaps/button_copy1.xpm	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,32 @@
+/* XPM */
+static char *button_copy0[] = {
+/* columns rows colors chars-per-pixel */
+"16 24 2 1",
+"  c black",
+". c None",
+/* pixels */
+"        ........",
+"        ........",
+"    ............",
+"     ...........",
+"      ..   .....",
+"  .    .   .....",
+"  ..   .   .....",
+"  ...  .   .....",
+"  .... .   .....",
+"  ......   .....",
+"........   .....",
+"...             ",
+"...             ",
+"...             ",
+"........   .....",
+"  .... .   .....",
+"  ...  .   .....",
+"  ..   .   .....",
+"  .    .   .....",
+"      ..   .....",
+"     ...........",
+"    ............",
+"        ........",
+"        ........"
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pixmaps/button_delete.xpm	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,18 @@
+/* XPM */
+static char * button_delete_xpm[] = {
+"16 12 2 1",
+".	c black",
+" 	c None",
+"                ",
+" ....      .... ",
+"  ....    ....  ",
+"   ....  ....   ",
+"    ........    ",
+"     ......     ",
+"      ....      ",
+"     ......     ",
+"    ........    ",
+"   ....  ....   ",
+"  ....    ....  ",
+" ....      .... "
+};
Binary file pixmaps/class.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pixmaps/close.xpm	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,13 @@
+/* XPM */
+static char * close_xpm[] = {
+"8 8 2 1",
+" 	c None",
+".	c #000000",
+"..   ...",
+"... ....",
+" .......",
+"  ..... ",
+" .....  ",
+"....... ",
+".... ...",
+"...   .."};
Binary file pixmaps/cvs-add-16.png has changed
Binary file pixmaps/cvs-commit-16.png has changed
Binary file pixmaps/cvs-icon-small.png has changed
Binary file pixmaps/cvs-icon.png has changed
Binary file pixmaps/cvs-remove-16.png has changed
Binary file pixmaps/cvs-update-16.png has changed
Binary file pixmaps/icon.png has changed
Binary file pixmaps/missing.jpeg has changed
Binary file pixmaps/missing.png has changed
Binary file pixmaps/oobrother_background.jpg has changed
Binary file pixmaps/oobrother_background2.jpg has changed
Binary file pixmaps/oobrother_background3.jpg has changed
Binary file pixmaps/patch.png has changed
Binary file pixmaps/patch2.png has changed
Binary file pixmaps/svn-icon-small.png has changed
Binary file pixmaps/svn-icon.png has changed
Binary file pixmaps/tree-file-changed.png has changed
Binary file pixmaps/tree-file-needspatch.png has changed
Binary file pixmaps/tree-file-new.png has changed
Binary file pixmaps/tree-file-newer.png has changed
Binary file pixmaps/tree-file-normal.png has changed
Binary file pixmaps/tree-folder-changed.png has changed
Binary file pixmaps/tree-folder-new.png has changed
Binary file pixmaps/tree-folder-normal.png has changed
Binary file pixmaps/unknown.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/__init__.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,63 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""
+oobrother plugins
+"""
+
+__revision__ = '$Id: __init__.py,v 1.7 2006-04-23 13:53:52 nico Exp $'
+
+from oobrother.config_tools import PluggableConfig
+
+class AbstractPlugIn:
+    """the docstring of a plugin is used to explain it in the gui"""
+
+    # the short name of the plugin
+    name = ''
+    # mime types handled by this module. See the Registry.register
+    # documentation
+    mimetypes = ()
+    # list of options. See logilab.common.configuration.
+    options = ()
+    
+    def __init__(self, config_file=None):
+        self.appl = None # set on registration
+        self.cfg = None
+        if self.options:
+            self.cfg = PluggableConfig(self.name, config_file, self.options)
+
+    def set_application(self, appl):
+        """set the application (on registration)"""
+        assert self.appl is None
+        self.appl = appl
+        
+    def support_directory(self, thedir):
+        """return true if this plugin may have some actions for the
+        given directory
+        """
+        return False
+    
+    def get_actions(self, thefile):
+        """return a list of 2-uple (action label, callback method)"""
+        raise NotImplementedError()
+
+    def subconfiguration(self):
+        """return subconfiguration objects if any"""
+        return []
+
+    def execute_in_console(self, cmd):
+        """run the command in the shared console"""
+        self.appl.console.execute(cmd)
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/base.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,53 @@
+"""Basic plugin for python modules
+"""
+
+__revision__ = '$Id: base.py,v 1.5 2006-03-26 20:19:59 nico Exp $'
+
+import os
+import mailcap
+CAPS = mailcap.getcaps()
+
+
+from logilab.astng.builder import ASTNGBuilder
+
+from oobrother.plugins import AbstractPlugIn
+
+DEFAULT_EDITOR = os.environ.get('EDITOR', 'emacsclient --no-wait')
+
+class TextPlugin(AbstractPlugIn):
+    """a plugin for text file, propo"""
+    name = 'text'
+    mimetypes = 'text/*',
+
+    options = (
+        ('editor_command', {'type': 'string',
+                            'default': DEFAULT_EDITOR }),
+        )
+    
+    def __init__(self):
+        AbstractPlugIn.__init__(self, 'base_text.ini')
+        self.text_editor = os.environ.get('EDITOR', 'emacsclient --no-wait') 
+        
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the give file"""
+        return [('view', self.cb_view_file),
+                ('edit', self.cb_edit_file),
+                ]
+
+    def cb_view_file(self, menuitem, thefile):
+        """call back for the 'view file' action : display the file
+        according to the mailcap configuration
+        """
+        commandline = mailcap.findmatch(CAPS, thefile.mimetype, thefile.abspath)[0]
+        os.system(commandline)
+        
+    def cb_edit_file(self, menuitem, thefile):
+        """call back for the 'view file' action : display the file
+        according to the mailcap configuration
+        """
+        os.system('%s %s' % (self.text_editor, thefile.abspath))
+        
+
+def register(registry):
+    """register plugins from this module to the plugins registry"""
+    registry.register(TextPlugin())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/debian.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,114 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""Basic plugin for some debian files, using standard debian command line tools
+
+FIXME: add dput support
+"""
+
+__revision__ = '$Id: debian.py,v 1.7 2004-10-13 09:37:28 adim Exp $'
+
+import os
+import commands
+
+import gobject
+
+from oobrother.plugins import AbstractPlugIn
+from oobrother.uiutils.windows import ConfigurableListWindow
+from oobrother.config_tools import PluggableConfig, ConfigurableNode
+debsign_options = (
+    ('gpg-key-id', {'type': 'string',
+                    'default':os.environ.get('GPGKEYID', '')}),
+    )
+
+class DebianPlugin(AbstractPlugIn):
+    """a plugin for text file, propo"""
+    name = 'debian'
+    mimetypes = ('application/x-debian-package',
+                 'application/x-debian-changes',
+                 'application/x-debian-dsc')
+    
+    def __init__(self):
+        AbstractPlugIn.__init__(self, config_file='debian.ini')
+        self.lintian_window = LintianReportWindow()
+        plug = PluggableConfig('options', 'debsign.ini', debsign_options)
+        self.debsign_conf = ConfigurableNode('debsign', plug)
+        
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the give file"""
+        actions = []
+        if thefile.basename.endswith('.changes'):
+            actions.append( ('show content', self.cb_debc) )
+            actions.append( ('sign', self.cb_debsign) )
+        else:
+            actions.append( ('lintian', self.cb_lintian) )
+        return actions
+
+    def cb_lintian(self, menuitem, thefile):
+        """execute lintian"""
+        self.lintian_window.lintian(thefile)
+        
+    def cb_debc(self, menuitem, thefile):
+        """execute debc"""
+        self.execute_in_console('debc %s' % thefile.abspath)        
+        
+    def cb_debsign(self, menuitem, thefile):
+        """execute debsign"""
+        keyid = self.debsign_conf.cfg['gpg-key-id']
+        self.execute_in_console('debsign -k %s %s' % (keyid, thefile.abspath))
+
+    def subconfiguration(self):
+        """return subconfiguration objects if any"""
+        return [self.lintian_window, self.debsign_conf]
+
+    
+class LintianReportWindow(ConfigurableListWindow):
+    """display lintian report in a 3 columns list (configurable)"""
+    name = 'lintian'
+    options = (
+        ('all-checks', {'type': 'yn', 'default': True}),
+        # FIXME: use a multiple choice using lintian existing checks (anyway
+        # to get them dynamically ?)
+        ('checks', {'type': 'csv', 'default': ()}),
+        )
+    
+    def __init__(self):
+        ConfigurableListWindow.__init__(self, 'lintian.ini',
+                                        (gobject.TYPE_STRING, # type
+                                         gobject.TYPE_STRING, # path
+                                         gobject.TYPE_STRING, # message
+                                         ),
+                                        ('Type', 'Package', 'Message'),
+                                        )
+        self.init_columns(self.cfg['columns'], (0, 0, 0))
+        
+    def lintian(self, thefile):
+        """check the given wrapped package using checkpackage in a safe
+        environment
+        """
+        cmd = 'lintian'
+        if not self.cfg['all-checks']:
+            cmd = 'lintian %s' % ','.join(self.cfg['checks'])
+        self.set_title('lintian: %s' % thefile.abspath)
+        self.clear()
+        self.show()
+        output = commands.getoutput("%s %s" % (cmd, thefile.abspath))
+        for line in output.splitlines():
+            self.append_line( line.split(':', 2) )
+
+
+def register(registry):
+    """register plugins from this module to the plugins registry"""
+    registry.register(DebianPlugin())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/devtools.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,929 @@
+# pylint: disable-msg=C0301
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""Basic plugin providing a gui for the logilab.devtools package
+"""
+
+__revision__ = '$Id: devtools.py,v 1.35 2005-02-25 17:25:12 adim Exp $'
+
+import sys
+import os
+import glob
+import time
+import shutil
+import tempfile
+from os.path import exists, join, split, basename, dirname
+
+import gtk, gobject
+from pigg import form, wgenerator, utils
+
+from stat import S_IWRITE
+from logilab.common.fileutils import ensure_fs_mode
+from logilab.common.modutils import load_module_from_name
+from logilab.common.textutils import normalize_text
+
+from logilab.devtools import TEMPLATE_DIR, lib, check_package, buildeb, \
+     debianize
+from logilab.devtools.lib import coverage, changelog, pkginfo
+try:
+    from logilab.devtools.lib import pkghandlers
+except:
+    from logilab.devtools import debhdlrs as pkghandlers
+    
+from oobrother.plugins import AbstractPlugIn
+from oobrother.sysutils import PathNormalizerMixIn, call_in_directory, \
+     call_and_restore
+from oobrother.uiutils import WindowMixIn, ask_for_message
+from oobrother.uiutils.windows import ConfigurableListWindow, \
+     SystemCommandWindow
+from oobrother.config_tools import PluggableConfig
+
+__metaclass__ = type
+
+# FIXME : ChangeLogModel was using devtools.lib.changelog.TODAY wich doesn't
+#         exist. TODAY should go elsewhere (logilab.common.utils.today() ?)
+TODAY = time.strftime("%Y/%m/%d", time.localtime(time.time()))
+
+# FIXME: rewrite this function to use the schema in the pkginfo module
+# and move it there
+
+def pkginfo_create(schema, pkginfo_file, data):
+    """create a __pkginfo__file"""
+    stream = open(pkginfo_file, 'w')
+    # insert license
+    license = data['license']
+    license_text = normalize_text(pkginfo.get_license_text(license), indent='# ')
+    stream.write(license_text + '\n')
+    # insert docstring
+    stream.write('"""package information for %s"""\n\n' % data['modname'])
+    # insert revision string
+    stream.write("__revision__ = '$I"+"d:$'\n\n")
+    # insert configuration
+    for key, wtype, wlabel, wargs, required, default in schema:
+        value = data.get(key)
+        if value is None:
+            #if required:
+            #    log(LOG_ERR, 'missing required value for %r' % key)
+            #else:
+            log(LOG_INFO, 'skipping None value for %r', key)
+            continue
+        stream.write('%s = %r\n' % (key, value))
+    stream.write("numversion = [int(v) for v in version.split('.')]\n")
+    stream.close()
+
+DEB_PKG_HANDLERS = pkghandlers.PKG_HANDLERS.keys()
+DEB_PKG_HANDLERS.sort()
+    
+
+README_TEMPLATE = """%s
+%s
+
+
+What's this ?
+-------------
+%s
+
+
+Installation
+------------
+
+Extract the tarball, jump into the created directory and run ::
+
+	python setup.py install
+
+For installation options, see ::
+
+	python setup.py install --help
+
+License
+-------
+%s
+"""
+
+
+def is_root_package(path):
+    """return true if path is not part of an upper package"""
+    return not exists(join(split(path)[0], '__init__.py'))
+
+
+COLORS = {
+    lib.INFO : 'black',
+    lib.WARNING : 'blue',
+    lib.ERROR : 'red',
+    lib.FATAL : 'red'
+    }
+
+class DevtoolsLogWindow(PathNormalizerMixIn, ConfigurableListWindow):
+        
+    def __init__(self, config_file, name=None):
+        self.name = name or self.name
+        ConfigurableListWindow.__init__(self, config_file,
+                                        (gobject.TYPE_STRING, # severity
+                                         gobject.TYPE_STRING, # file path
+                                         gobject.TYPE_INT, # line no
+                                         gobject.TYPE_STRING, # message
+                                         gobject.TYPE_STRING, # color
+                                         ),
+                                        ('Severity', 'Path', 'Line', 'Message'),
+                                        )
+        self.win.set_default_size(500, 200)
+        self.editor = None # will be set later, I promise
+        self.init_columns(self.cfg['columns'], (0, 0, 0, 0), foreground=4)
+        self.treeview.connect('button-press-event', self.cb_button_pressed)
+        
+    def log(self, severity, path, line, msg):
+        """log a message of a given severity
+        
+        line may be None if unknown
+        """
+        path = self.normalize_path(path)
+        self.append_line( (lib.REVERSE_SEVERITIES[severity], path, line or 0,
+                           msg, COLORS[severity]) )
+
+    def cb_button_pressed(self, treeview, event):
+        """open file on the corresponding line on double click"""
+        # left click
+        if event.button == 1:
+            # double_click
+            if event.type == gtk.gdk._2BUTTON_PRESS:
+                _, file_path, lineno, _, _ = self.get_selected()
+                self.editor.open(self.absolute_path(file_path), lineno)
+
+    
+# pkg info handling ###########################################################
+
+class PkgInfoModel(form.PyFormModel):
+    """a pkginfo specific model"""
+    
+    def __init__(self, pkginfo_file):
+        schema = (
+            ('modname', 'string', 'name', (), True, self.default_modname), 
+            ('version', 'string', 'version', (), True, '0.1'),
+            ('copyright', 'text', 'copyright', (), True,
+'''Copyright (c) %s LOGILAB S.A. (Paris, FRANCE).
+http://www.logilab.fr/ -- mailto:contact@logilab.fr''' % time.localtime()[0]),
+            ('license', 'choice', 'license', (pkginfo.get_known_licenses(),),
+             False, 'GPL'),
+            ('subpackage_of', 'string', 'sub-package of', (), False, None),
+            ('pyversions', 'multiple_choice', 'supported python versions',
+             (('2.1', '2.2', '2.3'),), False, None),
+
+            ('short_desc', 'string', 'short description', (), True, ''),
+            ('long_desc', 'text', 'long description', (), True, ''),
+
+            ('author', 'string', 'author name', (), True, 'Logilab'),
+            ('author_email', 'string', 'author email', (), True, 'devel@logilab.fr'),
+            ('web', 'string', 'home page', (), False, self.default_homepage),
+            ('ftp', 'string', 'ftp download directory', (), False, self.default_ftp),
+            ('mailinglist', 'string', 'mailing list', (), False, None),
+            ('debian_maintainer', 'string', 'debian maintainer name', (), False, None),
+            ('debian_maintainer_email', 'string', 'debian maintainer email', (), False, None),
+            ('debian_handler', 'choice', 'debian handler', (DEB_PKG_HANDLERS,), False, None),
+            )
+        self.file = pkginfo_file
+        form.PyFormModel.__init__(self, schema, {})
+        base = split(pkginfo_file)[0]
+        # initialize model with data from the __pkginfo__ module if it exists
+        if pkginfo_file is not None and exists(pkginfo_file):
+            # FIXME: ugly hack to avoid importing the oobrother's __pkginfo__ that
+            # is registered in sys.modules
+            sys.path.insert(0, base)
+            module = call_in_directory(base, load_module_from_name, '__pkginfo__')
+            sys.path.pop(0)
+            values = {}
+            for attr in self.schema:
+                attr = attr[0]
+                try:
+                    values[attr] = getattr(module, attr)
+                except AttributeError:
+                    log(LOG_INFO, '__pkginfo__ has no attribute %s', attr)
+                    continue
+            self.set_values(values, True)
+
+    def default_modname(self):
+        """get the default value of the 'modname' field"""
+        return basename(dirname(self.file))
+    
+    def default_homepage(self):
+        """get the default value of the 'homepage' field"""
+        return 'http://www.logilab.org/projects/%s' % (self._data.get('modname') or
+                                                       self.default_modname())
+    def default_ftp(self):
+        """get the default value of the 'ftp' field"""
+        return 'ftp://ftp.logilab.org/pub/%s' % (self._data.get('modname') or
+                                                 self.default_modname())
+    
+    def _commit(self):
+        """write back the model's content (the form is completed at this point)
+        """
+        pkfinfo = self.file
+        if not exists(pkfinfo):
+            # FIXME: notify FS model
+            pkginfo_create(self.schema, pkfinfo, self.as_dict())
+            log(LOG_INFO, '__pkginfo__.py created')
+        else:
+            modifs = self.get_modifications()
+            if modifs:
+                pkginfo.pkginfo_save(pkfinfo, modifs)
+                log(LOG_INFO, '__pkginfo__.py saved')
+            else:
+                log(LOG_INFO, 'no modification to save to __pkginfo__.py')
+
+
+
+class ModalFormWindow(WindowMixIn):
+    def __init__(self, title, model):
+        super(ModalFormWindow, self).__init__()
+        self.model = model
+        self.ctrl = form.PyFormController(model)
+        self.win = gtk.Dialog(title, None, 0,
+                         (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
+                          gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
+        self.win.set_default_size(400, 500)
+        
+    def run(self):
+        """open a window to edit __pkginfo__ fields"""
+        result = self.win.run()
+        if result == gtk.RESPONSE_ACCEPT:
+            try:
+                self.model.commit()
+            except form.IncompleteForm:
+                utils.show_msg('The form is not complete. Bold fields are required.',
+                               gtk.MESSAGE_ERROR)
+                return self.run()
+        elif self.model.get_modifications():
+            if utils.confirm('There are some unsaved modifications. Close anyway ?'):
+                # FIXME: restore original values ?
+                self.model.rollback()
+            else:
+                return self.run()                
+        self.hide()
+        return result
+
+
+class PkgInfoWindow(ModalFormWindow):
+    """a window to edit __pkginfo__.py file"""
+    
+    def __init__(self, pkginfo_file):
+        super(PkgInfoWindow, self).__init__(pkginfo_file,
+                                            PkgInfoModel(pkginfo_file))
+        schema = self.model.schema
+        nb_desc = (
+            ('base', schema[0:6]),
+            ('description', schema[6:8]),
+            ('miscellaneous', schema[8:13]),
+            ('debian', schema[13:]),
+            )
+        wdg = wgenerator.notebook_generate(nb_desc, self.model, self.ctrl)
+        self.win.vbox.pack_end(wdg.wdg)
+        self.win.show_all()
+        # FIXME: why do I have to call notify to set form values according to the model ??
+        self.model.notify_all()
+
+
+# ChangeLog handling ##########################################################
+
+class ChangeLogModel(form.PyFormModel):
+    """a changelog specific model"""
+    cl_class = changelog.ChangeLog
+    
+    def __init__(self, thedir, close=False, schema=None, filename='ChangeLog'):
+        schema = schema or (
+            ('version', 'string', 'version', (), False, self.default_version), 
+            ('date', 'string', 'date', (), False, self.default_date), 
+            ('entries', 'text', 'entries', (), True, self.default_entries), 
+            )
+        self.file = join(thedir.abspath, filename)
+        self.thedir = thedir
+        form.PyFormModel.__init__(self, schema, {})
+        if close:
+            self.set_values({'version': changelog.get_pkg_version(self.thedir.abspath),
+                             'date': TODAY})
+            
+    def default_version(self):
+        """get the current change log version"""        
+        return self._get_entry().version
+
+    def default_date(self):
+        """get the current change log version"""
+        return self._get_entry().date
+
+    def default_entries(self):
+        """get the default change log entries"""        
+        stream = []
+        for msg in self._get_entry().messages:
+            stream.append('%s %s' % (changelog.BULLET, msg[0]))
+            for more in msg[1:]:
+                stream.append('  %s' % (more))
+            stream.append('')
+        return '\n'.join(stream)
+    
+    def update_entry(self):
+        """update the associated changelog's entry according to current
+        model values
+        """
+        entry = self._entry
+        entry.version = self.get_value('version')
+        entry.date = self.get_value('date')
+        entry.messages = []
+        data = self.get_value('entries')
+        try:
+            for line in data.splitlines():
+                line = line.strip()
+                if not line :
+                    continue
+                if line[0] == changelog.BULLET:
+                    entry.add_message(line[1:].lstrip())
+                else:
+                    entry.complete_latest_message(line)
+        except IndexError, err:
+            err_msg = "An error occured (%s). Please, make sure each entry " \
+                      "is correctly formatted (each entry should begin with " \
+                      "'%s'" % (err, changelog.BULLET)
+            utils.show_msg(err_msg, gtk.MESSAGE_ERROR)
+            # FIXME need a better error management
+            raise
+        return entry
+    
+    def _commit(self):
+        """write back the model's content (the form is completed at this point)
+        """
+        self.update_entry()
+        self._chlg.save()
+        log(LOG_INFO, 'ChangeLog saved')
+
+    def _get_entry(self):
+        """return the changelog's entry associated with the model"""
+        try:
+            entry = self._entry
+        except AttributeError:
+            version = changelog.get_pkg_version(self.thedir.abspath)
+            chlg = self._get_changelog()
+            try:
+                entry = chlg.get_entry(version, create=True)
+            except (changelog.EntryNotFound, changelog.NoEntry):
+                entry = chlg.get_entry(create=True)
+            self._entry = entry
+        return entry
+    
+    def _get_changelog(self):
+        """return the changelog object associated with the model"""
+        try:
+            chlg = self._chlg
+        except AttributeError:
+            chlg = self._chlg = self.cl_class(self.file)
+        return chlg
+
+class DebianChangeLogModel(ChangeLogModel):
+    """a changelog specific model"""
+    cl_class = changelog.DebianChangeLog
+    
+    def __init__(self, thedir, close=False, filename='debian/changelog'):
+        schema = (
+            ('package', 'string', 'package', (), False, self.default_package), 
+            ('version', 'string', 'version', (), False, self.default_version), 
+            ('distrib', 'string', 'distrib', (), False, 'unstable'), 
+            ('urgent', 'string', 'urgent', (), False, 'low'), 
+            ('author', 'string', 'author', (), False, self.default_author), 
+            ('email', 'string', 'author email', (), False, self.default_email),
+            ('date', 'string', 'date', (), False, self.default_date), 
+            ('entries', 'text', 'entries', (), True, self.default_entries), 
+            )
+        ChangeLogModel.__init__(self, thedir, close, schema, filename)
+
+    def default_package(self):
+        return 'FIXME'
+    
+    def default_author(self):
+        return 'Logilab'
+
+    def default_email(self):
+        return 'devel@logilab.fr'
+    
+    def default_date(self):
+        """get the current change log version"""
+        entry = self._get_entry()
+        if not entry.date:
+            entry.date = self._get_changelog().formatted_date()
+        self._entry = entry
+        return entry.date
+
+    def update_entry(self):
+        entry = ChangeLogModel.update_entry(self)
+        entry.author = '%s <%s>' % (self.get_value('author'),
+                                    self.get_value('email'))
+        entry.package = self.get_value('package')
+        entry.distrib = self.get_value('distrib')
+        entry.urgency = self.get_value('urgent')
+        
+class ChangeLogWindow(ModalFormWindow):
+    """a window to edit ChangeLog latest entries"""
+    
+    def __init__(self, thedir, close=False,
+                 filename='ChangeLog', model=ChangeLogModel):
+        super(ChangeLogWindow, self).__init__(filename, model(thedir, close, filename=filename))
+        wdg = wgenerator.generate(self.model.schema, self.model, self.ctrl)
+        self.win.vbox.pack_end(wdg)
+        self.win.show_all()
+        # FIXME: why do I have to call notify to set form values according
+        #        to the model ??
+        self.model.notify_all()
+
+
+# Check package, test coverage windows ########################################
+
+class CheckPackageWindow(DevtoolsLogWindow):
+    name = 'checkpackage'
+        
+    def __init__(self):
+        DevtoolsLogWindow.__init__(self, 'check_package.ini')
+        
+    def check(self, thefile):
+        """check the given wrapped package using checkpackage in a safe
+        environment
+        """
+        self.clear()
+        self.show()
+        self.set_base(thefile)
+        self.set_title('Check package: %s' % thefile.abspath)
+        call_and_restore(self._check, thefile)
+        
+    def _check(self, thefile):
+        """check the given wrapped package using checkpackage"""
+        package_dir = thefile.abspath
+        for func_name in check_package.__all__:
+            check_func = getattr(check_package, func_name)
+            try:
+                check_func(self, package_dir, '__pkginfo__')
+            except TypeError:
+                check_func(self, package_dir)
+
+
+class TestCoverageWindow(ConfigurableListWindow):
+    name = 'test-coverage'
+        
+    def __init__(self):
+        ConfigurableListWindow.__init__(self, 'check_package.ini',
+                                        (gobject.TYPE_STRING, # path
+                                         gobject.TYPE_INT, # statements num
+                                         gobject.TYPE_INT, # executed statements
+                                         gobject.TYPE_INT, # % executed statements
+                                         gobject.TYPE_STRING, # % not executed
+                                         ),
+                                        ('Path', 'Statements', 'Executed', 'Covered', 'Missing'),
+                                        )
+        self.win.set_default_size(500, 400)
+        self.init_columns(self.cfg['columns'], (0, 0, 0, 0, 0))
+        self._cover_tool = coverage.Coverage()
+        
+    def extract_coverage(self, package_dir, coverage_file):
+        """check the given wrapped package using checkpackage in a safe
+        environment
+        """
+        self.clear()
+        self.set_title('Test coverage: %s' % package_dir)
+        self.show()
+        self._cover_tool.restore(coverage_file)
+        stats = self._cover_tool.report_stat(package_dir, ignore_errors=0)
+        for name in stats.keys():
+            if name == coverage.TOTAL_ENTRY:
+                continue
+            self.append_line( (name,) + stats[name] )
+
+
+# debianize callbacks #########################################################
+
+def debianize_replace(tmp_file, dest_file):
+    """interactivly replace the old file with the new file according to
+    user decision
+    """
+    if exists(dest_file):
+        p_output = os.popen('diff -u %s %s' % (dest_file, tmp_file), 'r')
+        diffs = p_output.read()
+        if diffs:
+            if utils.confirm('Differences:\n\n%s\n\nApply update ?' % diffs):
+##                try:
+                ensure_fs_mode(dest_file, S_IWRITE)
+                shutil.copyfile(tmp_file, dest_file)
+##                except IOError:
+##                    os.system('cvs edit %s'%dest_file)
+##                    shutil.copyfile(tmp_file, dest_file)
+                log(LOG_INFO, '%s replaced', dest_file)
+            else:
+                log(LOG_INFO, 'keep current version of %s', dest_file)
+        else:
+            log(LOG_INFO, '%s created', dest_file)
+    else:
+        shutil.copyfile(tmp_file, dest_file)
+        log(LOG_INFO, '%s created', dest_file)
+    os.remove(tmp_file)
+
+
+def debianize_empty(dest_file):
+    """helper function for files to remove
+    """
+    if exists(dest_file):
+        if utils.confirm('updated %s is empty. Remove it ?' % dest_file):
+            log(LOG_INFO, '%s removed', dest_file)
+            os.remove(dest_file)
+        else:
+            log(LOG_INFO, 'keep current version of %s', dest_file)
+
+
+# The release manager #########################################################
+
+class ReleaseManager:
+    """The release manager is responsible to do release related commands.
+    As those commands are interdependant, each action is build from a list
+    of basic actions which should be executed, and different commands may
+    share the same actions.
+    
+    FIXME missing actions:
+    
+    # read/write debian changelog
+    
+    # test package
+    #   dput
+    #   ssh / chroot
+    #   apt-get
+    #   runtest
+    
+    # validate package
+    #   commit
+    #   tag package
+    #   debsign
+    #   dput
+    """
+    name = 'release manager'
+    options = (
+        ('debian-build-command',
+         {'type': 'string',
+          'default': buildeb.BUILD_COMMAND}),
+        ('setup-template',
+         {'type': 'string',
+          'default': join(TEMPLATE_DIR, 'setup.py')}),
+        ('local-repository',
+         {'type' : 'string',
+          'default' : join(os.environ.get('HOME', ''), 'public_html', 'debian',
+                           'mini-dinstall', 'incoming'),}),
+        ('chroot-dir',
+         {'type' : "string",
+          'default': join('/sandbox', 'sarge-%s' % os.getlogin())},),
+        )
+    def __init__(self):
+        self._commands = {
+            'commit': ( self.action_check_uptodate, ),
+            'setup': ( self.action_setup, ),
+            'pkginfo': ( self.action_pkginfo, ),
+            'debianize': ( self.action_debianize, ),
+            'changelog': ( self.action_changelog, ),
+            'source': ( self.action_source_distrib, ),
+            
+            'build': ( self.action_check_uptodate,
+                       self.action_check_pkginfo, self.action_setup,
+                       self.action_check_debian, self.action_build_distrib ),
+            
+            'upstream': ( self.action_check_uptodate,
+                          self.action_pkginfo, self.action_setup,
+                          self.action_readme, self.action_changelog,
+                          self.action_make_doc,
+                          self.action_debianize, self.action_debian_changelog,
+                          self.action_build_distrib, self.action_tag_package),
+            
+            'debian': ( self.action_check_uptodate,
+                        self.action_check_pkginfo, self.action_setup,
+                        self.action_check_debian, self.action_debian_changelog,
+                        self.action_build_distrib ),
+            
+            'validate': ( self.action_check_uptodate,
+                          self.action_check_pkginfo, self.action_setup,
+                          self.action_validate_distrib, self.action_tag_package),
+            }
+        self.cfg = PluggableConfig(self.name, 'release-manager.ini', self.options)
+        self._shell = None
+
+    def _get_shell(self):
+        """returns a shell to execute command when needed"""
+        if self._shell is None:
+            self._shell = SystemCommandWindow()
+        return self._shell
+        # return SystemCommandWindow()
+    shell = property(_get_shell, doc = "The release manager's shell")
+
+    def subconfiguration(self):
+        return []
+    
+    def do(self, thedir, command):
+        """execute a command (the command determines the actions chain"""
+        self._current_cmd = command
+        actions_chain = self._commands[command]
+        for action in actions_chain:
+            log(LOG_DEBUG, 'going to action %s', action.__name__)
+            try:
+                if not action(thedir):
+                    log(LOG_DEBUG, 'abort actions chain')
+                    break
+            except Exception, ex:
+                import traceback
+                traceback.print_exc()
+                utils.show_msg('Error: %s' % ex, dlg_type=gtk.MESSAGE_ERROR)
+                break
+        else:
+            if len(actions_chain) > 1:
+                utils.show_msg('%s completed' % command)
+        self._current_cmd = None
+        
+    def action_check_uptodate(self, thedir):
+        """check for not up-to-date files"""
+        not_up_to_date = thedir.not_up_to_date()
+        if not_up_to_date:
+            if utils.confirm('Package is not up to date: \n%s\n Do you want to '
+                             'commit these files ?' % '\n'.join(["%s: %s"%r for r in not_up_to_date])):
+                cmd = thedir.commit(ask_for_message('Enter your commit message'))
+                if self.shell.execute(cmd) != 0:
+                    self.shell.hide()
+                    return False 
+            elif utils.confirm('Abort release ?'):
+                self.shell.hide()
+                return False
+        return True
+
+    def action_check_pkginfo(self, thedir):
+        """return true if the package has all necessary files to create/build
+        a distribution
+        """
+        pkginfo_file = join(thedir.abspath, '__pkginfo__.py')
+        if not exists(pkginfo_file):
+            return self.action_pkginfo(thedir)
+        return True
+    
+    def action_check_debian(self, thedir):
+        """return true if the package has all necessary files to create/build
+        a debian distribution
+        """
+        if not exists(join(thedir.abspath, 'debian')):
+            return self.action_debianize(thedir)
+        return True
+    
+    def action_pkginfo(self, thedir):
+        """open a window to edit __pkginfo__ fields"""
+        window = PkgInfoWindow(join(thedir.abspath, '__pkginfo__.py'))
+        result = window.run()
+        window.win.destroy()
+        return result == gtk.RESPONSE_ACCEPT
+    
+    def action_setup(self, thedir):
+        """create a default setup.py"""
+        setup_file = join(thedir.abspath, 'setup.py')
+        if not exists(setup_file):
+            shutil.copy(self.cfg['setup-template'], setup_file)
+            thedir.force_update()
+            if utils.confirm('setup.py file has been created. Do you want to edit it ?'):
+                # FIXME: wait and return True
+                self.appl.editor.open(setup_file)
+                return False
+        # FIXME: check for differences and propose to update
+        return True
+
+    def action_readme(self, thedir):
+        """create a default README"""
+        readme_file = join(thedir.abspath, 'README')
+        if not exists(readme_file):
+            pi = call_and_restore(pkginfo.PackageInfo, directory=thedir.abspath)
+            stream = open(readme_file, 'w')
+            stream.write(README_TEMPLATE % (pi.name, "="*len(pi.name),
+                                            pi.long_desc, pi.license_text))
+            stream.close()
+            thedir.force_update()
+            if not utils.confirm('default README file has been created.\n'
+                                 'Do you want to commit it ?\n'
+                                 '(You can edit it manually, and then click on OK\n'):
+                readme = thedir.create_child('README')
+                add_cmd = readme.add()
+                commit_cmd = thedir.commit('Added REAME')
+                self.shell.show()
+                if self.shell.execute(add_cmd) != 0:
+                    self.shell.hide()
+                    return False
+                if self.shell.execute(commit_cmd) != 0:
+                    self.shell.hide()
+                    return False
+                # FIXME: wait and return True
+                # self.appl.editor.open(readme_file)
+                self.shell.hide()
+                return False
+        return True
+    
+    def action_make_doc(self, thedir):
+        """create a source distribution using a setup.py file"""
+        if exists(join(thedir.abspath, 'doc', 'makefile')):
+            self.execute_in_console('cd %s/doc' % thedir.abspath)
+            self.execute_in_console('make')
+            thedir.force_update()
+        return True
+
+    def action_changelog(self, thedir):
+        """edit an upstream ChangeLog"""
+        window = ChangeLogWindow(thedir, self._current_cmd == 'upstream')
+        result = window.run()
+        window.win.destroy()
+        del window
+        return result == gtk.RESPONSE_ACCEPT
+    
+    def action_debian_changelog(self, thedir):
+        """edit a debian changelog"""
+        window = ChangeLogWindow(thedir, self._current_cmd == 'upstream',
+                                 filename='debian/changelog',
+                                 model=DebianChangeLogModel)
+        result = window.run()
+        window.win.destroy()
+        del window
+        return result == gtk.RESPONSE_ACCEPT
+    
+    def action_debianize(self, thedir):
+        """debianize a package"""
+        reporter = DevtoolsLogWindow('debianize_display.ini','debianize log')
+        reporter.set_title('Debianize : %s' % thedir.abspath)
+        reporter.editor = self.editor
+        reporter.set_base(thedir)
+        reporter.show()
+        debianize.debianize(reporter=reporter, directory=thedir.abspath,
+                            replace_func=debianize_replace,
+                            empty_func=debianize_empty)
+        thedir.force_update()
+        if self._current_cmd != 'debianize' \
+               and utils.confirm('debianize succeed, continue ?'):
+            reporter.win.destroy()
+            return True
+        return False
+        
+    
+    def action_source_distrib(self, thedir):
+        """create a source distribution using a setup.py file"""
+        self.execute_in_console('cd %s' % thedir.abspath)
+        self.execute_in_console('python setup.py sdist')
+        thedir.force_update()
+        return True
+    
+    def action_build_distrib(self, thedir):
+        """create a debian distribution using build_debian"""
+        self.shell.show()
+        sys.stdout = self.shell
+        sys.stderr = self.shell
+        try:
+            buildeb.build_debian(dest_dir=join(thedir.abspath, 'dist'),
+                                 pkg_dir=thedir.abspath,
+                                 command=self.cfg['debian-build-command'],
+                                 system=self.shell.execute)
+        finally:
+            sys.stdout = sys.__stdout__
+            sys.stderr = sys.__stderr__
+        return True
+    
+    def action_validate_distrib(self, thedir):
+        return True
+
+    def action_tag_package(self, thedir):
+        """adds release tags on package"""
+        pi = call_and_restore(pkginfo.PackageInfo, directory=thedir.abspath)
+        release_tag = pi.release_tag()
+        cmds = []
+        if utils.confirm('Do you want to tag package with %r' % release_tag):
+            cmds.append(thedir.tag(release_tag))
+        package_dir = join(thedir.abspath, 'debian')
+        if exists(package_dir):
+            debian_tag = pi.debian_release_tag()
+            if utils.confirm('Do you want to tag package with %r' % debian_tag):
+                cmds.append(thedir.tag(debian_tag))
+        if cmds:
+            for cmd in cmds:
+                if self.shell.execute(cmd) != 0:
+                    return False 
+        return True
+
+    def action_debsign(self, thedir):
+        """signs the debian packages"""
+        return True
+    
+
+# the plugin itself ###########################################################
+
+class DevtoolsDirectoryPlugin(AbstractPlugIn):
+    """Plugin for Logilab's developpement tools support"""
+    
+    name = 'devtools'
+    mimetypes = ()
+
+    def __init__(self):
+        AbstractPlugIn.__init__(self)
+        self.check_pkg_window = CheckPackageWindow()
+        self.test_cover_window = TestCoverageWindow()
+        self._rm = ReleaseManager()
+        
+    def set_application(self, appl):
+        """set the application (on registration)"""
+        AbstractPlugIn.set_application(self, appl)
+        # FIXME: monkey patch
+        self.check_pkg_window.editor = self.appl.editor
+        self._rm.editor = self.appl.editor
+        self._rm.execute_in_console = self.execute_in_console
+        
+    def subconfiguration(self):
+        return [self._rm, self.check_pkg_window, self.test_cover_window]
+    
+    def support_directory(self, thedir):
+        """return true if the directory contains at least one python module
+        """
+        return bool(glob.glob(join(thedir.abspath, '*.py')))
+    
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the given directory
+        """
+        pkg_actions, dist_actions = [], []
+        actions = [('packaging', pkg_actions), ('distribution', dist_actions)]
+        filepath = thefile.abspath
+        if exists(join(filepath, '__pkginfo__.py')) or is_root_package(filepath):
+            actions.insert(0, ('check', self.cb_check_package) )
+            pkg_actions.append( ('package information',
+                                 self.cb_do_release_action, 'pkginfo') )
+            if not exists(join(filepath, 'setup.py')):
+                pkg_actions.append( ('create default setup.py',
+                                     self.cb_do_release_action, 'setup') )
+            pkg_actions.append( ('debianize',
+                                 self.cb_do_release_action, 'debianize') )
+            dist_actions.append( ('new upstream distribution',
+                                  self.cb_do_release_action, 'upstream') )
+            dist_actions.append( ('new debian distribution',
+                                  self.cb_do_release_action, 'debian') )
+            dist_actions.append( ('build source distribution',
+                                  self.cb_do_release_action, 'source') )
+            dist_actions.append( ('build distribution',
+                                  self.cb_do_release_action, 'build') )
+            dist_actions.append( ('validate distribution',
+                                  self.cb_do_release_action, 'validate') )
+            thefile.root_package = True
+        else:
+            thefile.root_package = False
+        if exists(join(filepath, 'ChangeLog')):
+            actions.append( ('add change log entry',
+                             self.cb_do_release_action, 'changelog') )
+        if exists(join(filepath, 'runtests.py')):
+            actions.append( ('run', self.cb_run_tests) )
+            actions.append( ('covered run', self.cb_covered_tests) )
+        return actions
+
+    def cb_run_tests(self, menuitem, thedir):
+        self.execute_in_console('cd %s' % thedir.abspath)
+        self.execute_in_console('python runtests.py')
+        
+    def cb_covered_tests(self, menuitem, thedir):
+        # FIXME: currently execute_in_console is returning without waiting the
+        # end of the command
+        self.execute_in_console('cd %s' % thedir.abspath)
+        self.execute_in_console('pycoverage -xe runtests.py')
+        self.test_cover_window.extract_coverage(split(thedir.abspath)[0],
+                                                join(thedir.abspath, '.coverage'))
+        
+    def cb_check_package(self, menuitem, thedir):
+        """run checkpackage on a python package"""
+        self.check_pkg_window.check(thedir)
+    
+    def cb_add_change_log_entry(self, menuitem, thedir):
+        """add an entry to the ChangeLog file"""
+        raise NotImplementedError()
+    
+    def cb_do_release_action(self, menuitem, thedir, action):
+        """create a debian distribution using build_debian"""
+        self._rm.do(thedir, action)
+
+
+class DevtoolsFilePlugin(AbstractPlugIn):
+    """Plugin for Logilab's developpement tools file support"""
+    
+    name = 'devtools-file'
+    mimetypes = 'text/x-python',
+
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the given directory
+        """
+        return []
+
+    
+def register(registry):
+    """register plugins from this module to the plugins registry"""
+    registry.register(DevtoolsDirectoryPlugin())
+    #registry.register(DevtoolsFilePlugin())
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/pychecker.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,102 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""Basic plugin for pychecker
+"""
+
+__revision__ = '$Id: pychecker.py,v 1.3 2004-12-22 11:00:18 arthur Exp $'
+
+
+import commands
+import gobject
+import gtk
+from oobrother.uiutils.windows import ConfigurableListWindow
+from oobrother.plugins import AbstractPlugIn
+from oobrother.sysutils import PathNormalizerMixIn
+
+class PycheckerPlugin(AbstractPlugIn):
+    """a plugin for text file, propo"""
+    name = 'pychecker'
+    mimetypes = 'text/x-python',
+
+    def __init__(self):
+        AbstractPlugIn.__init__(self, config_file='pychecker.ini')
+        self.pychecker_window = PycheckerReportWindow()
+
+    def set_application(self, appl):
+        """set the application (on registration)"""
+        AbstractPlugIn.set_application(self, appl)
+        self.pychecker_window.editor = self.appl.editor
+
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the give file"""
+        return [('pychecker', self.pycheck),]
+
+    def pycheck(self, menuitem, thefile):
+        """ execute pychecker """ 
+        self.pychecker_window.pycheck(thefile)
+
+    def subconfiguration(self):
+        """return subconfiguration objects if any"""
+        return [self.pychecker_window]
+
+
+class PycheckerReportWindow(PathNormalizerMixIn, ConfigurableListWindow):
+    """display pychecker report in a 3 columns list (configurable)"""
+    name = 'pychecker'
+
+    def __init__(self):
+        ConfigurableListWindow.__init__(self, 'pychecker.ini',
+                                        (gobject.TYPE_STRING, # type
+                                         gobject.TYPE_INT, # path
+                                         gobject.TYPE_STRING, # message
+                                         ),
+                                        ('File', 'Line', 'Message'),
+                                        )
+        self.init_columns(self.cfg['columns'], (0, 0, 0))
+        self.editor = None # will be set later, I promise
+        self.treeview.connect('button-press-event', self.cb_button_pressed)
+
+    def pycheck(self, thefile):
+        """check the given wrapped package using checkpackage in a safe
+        environment
+        """
+        self.set_title('pychecker: %s' % thefile.abspath)
+        self.clear()
+        self.show()
+        output = commands.getoutput("pychecker %s" % thefile.abspath)
+        self.set_base(thefile)
+        for line in output.splitlines():
+            file =  ''
+            line_no = 0
+            try:
+                file, line_no, message = line.split(':', 2)
+            except ValueError:
+                message = line
+            self.append_line((file, int(line_no), message))
+
+    def cb_button_pressed(self, treeview, event):
+        """open file on the corresponding line on double click
+        """
+        # left click
+        if event.button == 1:
+            # double_click
+            if event.type == gtk.gdk._2BUTTON_PRESS:
+                file_path, lineno, _ = self.get_selected()
+                self.editor.open(self.absolute_path(file_path), lineno)
+
+def register(registry):
+    """register plugins from this module to the plugins registry"""
+    registry.register(PycheckerPlugin())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/python.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,393 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""Basic plugin for python modules
+"""
+
+__revision__ = '$Id: python.py,v 1.25 2006-03-26 20:19:31 nico Exp $'
+
+import os
+from os.path import exists, join
+
+import gtk, gobject
+
+from pigg.form import PyFormController
+from pigg.mvc import VSensitivity
+
+from logilab import astng
+from logilab.common.configuration import OptionsManager2ConfigurationAdapter
+
+from pylint.lint import PyLinter
+from pylint.reporters import BaseReporter
+from pylint import checkers    
+    
+from oobrother.plugins import AbstractPlugIn
+from oobrother.sysutils import OOBROTHER_HOME, PathNormalizerMixIn, call_and_restore
+from oobrother.uiutils import init_treeview_columns, DelegatedWindowMixIn, \
+    TreeViewMixIn, load_pixbuf
+from oobrother.uiutils.windows import ConfigurableListWindow
+from oobrother.config_tools import ConfigurationModel, notebook_from_config, \
+     format_config_entry, PluggableConfig, ConfigurableNode
+
+__metaclass__ = type
+    
+
+PYLINT_MESSAGE_LEVEL = {
+    'E' : 1,
+    'W' : 2,
+    'R' : 3,
+    'C' : 4,
+    'I' : 5,
+    'F' : 6,
+    }
+
+def compare_message(sigle1, sigle2):
+    """compares severity of two pylint messages"""
+    sigle1_level = PYLINT_MESSAGE_LEVEL.get(sigle1, -1)
+    sigle2_level = PYLINT_MESSAGE_LEVEL.get(sigle2, -1)
+    if sigle1_level == -1:
+        return -1
+    if sigle2_level == -1:
+        return 1
+    return cmp(sigle1_level, sigle2_level)
+
+def row_sort(model, iter1, iter2, data):
+    """Sort method for pylint report table"""
+    sigle1 = model.get_value(iter1, 3)
+    sigle2 = model.get_value(iter2, 3)
+    return compare_message(sigle1, sigle2)    
+    
+def display_name(node):
+    """return a display string for an astng node (may be a package,
+    module, class or function)
+    """
+    if isinstance(node, astng.Class):
+        base = node.basenames and '(%s)' % ', '.join(node.basenames) or ''
+        return node.name + base
+    if isinstance(node, astng.Function):
+        return '%s(%s)' % (node.name, node.format_args())
+    #if isinstance(node, astng.Module) or isinstance(node, astng.Package):
+    return node.name
+
+def schema_from_options_manager(opt_manager):
+    """return a pigg.mvc compatible schema created from options registered in
+    the given logilab.common.configuration.OptionsManager instance
+    """
+    schema = []
+    for provider in opt_manager.options_providers:
+        default_options = []
+        sections = {}
+        for opt_name, opt_dict in provider.options:
+            if opt_dict.get('type') is None:
+                continue                
+            attr_def = format_config_entry(opt_name, opt_dict)
+            schema.append(attr_def)
+    return schema
+
+        
+class PylintPluggableConfig(OptionsManager2ConfigurationAdapter):
+    """pylint specific pluggable config"""
+    
+    def __init__(self, provider, model, ctrl):
+        OptionsManager2ConfigurationAdapter.__init__(self, provider)
+        self.model, self.ctrl = model, ctrl
+        form = notebook_from_config(provider, model, ctrl)
+        self.wdg = form.wdg
+        self.wdg.show_all()
+        # FIXME: should probably be in another place
+        if provider.name != 'master':
+            key = 'enable-%s' % provider.name
+            for opt_name, opt_dict in provider.options:
+                if opt_dict.get('type') is None:
+                    continue
+                if opt_name == key:
+                    continue
+                try:
+                    VSensitivity(form[opt_name], model, ctrl, key)
+                except KeyError:
+                    continue
+
+
+# plugin's windows ############################################################
+
+class PylintReport(PathNormalizerMixIn, BaseReporter, ConfigurableListWindow):
+    """A simple table for pylint reports"""
+    name = 'pylint'
+    
+    def __init__(self):
+        BaseReporter.__init__(self)
+        ConfigurableListWindow.__init__(self, 'pylint_display.ini',
+                                        (gobject.TYPE_STRING, # msg_type
+                                         gobject.TYPE_STRING, # file
+                                         gobject.TYPE_INT, # line
+                                         gobject.TYPE_STRING, # message
+                                         gobject.TYPE_STRING, # obj
+                                         gobject.TYPE_STRING, # color
+                                         ),
+                                        ('Type', 'File', 'Line',
+                                         'Object', 'Message'),
+                                        )
+        self.editor = None # will be set later, I promise
+        self.init_columns(self.cfg['columns'],
+                          (0, 0, 0, 0, 4), foreground=5)
+        store = self.treeview.get_model()
+        store.set_sort_func(4, row_sort)
+        self.win.set_default_size(600, 400)
+        # pylint initialization
+        self.pylint = pylint = PyLinter()
+        checkers.initialize(pylint)
+        pylint.config_file = join(OOBROTHER_HOME, 'pylint.ini')
+        pylint.load_file_configuration()
+        pylint.set_reporter(self)
+        pylint.set_option('reports', False)
+        # remove "report" options from options definition
+        options = [(o_name, o_dict) for o_name, o_dict in pylint.options
+                   if o_dict.get('group') != 'Reports']
+        pylint.options = options
+        # pylint's configuration objects
+        model = ConfigurationModel(OptionsManager2ConfigurationAdapter(pylint),
+                                   schema_from_options_manager(pylint))
+        model.update()
+        ctrl = PyFormController(model)
+        self._subcfgs = [ConfigurableNode(prov.name,
+                                          PylintPluggableConfig(prov, model, ctrl))
+                         for prov in pylint.options_providers]
+        self.treeview.connect('button-press-event', self.cb_button_pressed)
+        
+    def check(self, thefile):
+        """check the given wrapped file using pylint"""
+        # FIXME: pylint should be able to use an existent astng implementation
+        #astng = self._get_astng(thefile)
+        self.clear()
+        self.set_title('PyLint: (%s)' % thefile.abspath)
+        self.show()
+        self.set_base(thefile)
+        #call_and_restore(self.pylint.check, thefile.abspath)
+        self.pylint.check(thefile.abspath)
+
+    # IConfigurable
+    
+    def subconfiguration(self):
+        """return a list of configuration nodes for subconfiguration
+        (i.e. pylint's checkers)        
+        """
+        return self._subcfgs
+
+    # gui callbacks
+
+    def cb_button_pressed(self, treeview, event):
+        """open file on the corresponding line on double click
+        """
+        # left click
+        if event.button == 1:
+            # double_click
+            if event.type == gtk.gdk._2BUTTON_PRESS:
+                _, file_path, lineno, _, _, _ = self.get_selected()
+                self.editor.open(self.absolute_path(file_path), lineno)
+        
+    # pylint reporter interface
+    
+    def add_message(self, msg_id, location, msg):
+        """get pylint messages"""
+        # self.win.freeze_notify()
+        path, module, obj, line = location
+        if msg_id[0] == 'W':
+            color = 'blue'
+        elif msg_id[0] == 'E':
+            color = 'red'
+        else:
+            color = 'black'
+        self.append_line((msg_id, self.normalize_path(path), line, str(obj),
+                          msg, color))
+                
+    def display_results(self, layout):
+        """asked to display the pylint reports"""
+        print 'DISPLAYING PYLINT REPORTS'
+
+
+PYOBJECTS_PIXBUFS = {
+    'Class'    : load_pixbuf('class.png', 20),
+    'Module'   : load_pixbuf('class.png', 20),
+    'Method'   : load_pixbuf('class.png', 20),
+    'Function' : load_pixbuf('class.png', 20),
+    'Global'   : load_pixbuf('class.png', 20),
+    }
+
+def pyobject_pixbuf(column, cell, model, iter):
+    pyobj = model.get_value(iter, 2)
+    pix = PYOBJECTS_PIXBUFS.get(pyobj.__class__.__name__, None)
+    cell.set_property('pixbuf', pix)
+
+def pyobject_name(column, cell, model, iter):
+    pyobj_name = model.get_value(iter, 0)
+    cell.set_property('text', pyobj_name)
+
+class ModulesBrowser(DelegatedWindowMixIn, TreeViewMixIn):
+    """Python Objects Browser, using the gtk tree view"""
+    name = 'objects browser'
+    options = (('hide-private', {'type' : 'yn', 'default': True}),
+               )
+    
+    def __init__(self):
+        # We need to have self.treeview in TreeviewMixIn's init()
+        super(ModulesBrowser, self).__init__()
+        self.win.set_default_size(200, 300)
+        self.set_title('Python objects browser')
+        self.cfg = PluggableConfig(self.name, 'pyobrowser.ini', self.options)
+        self.editor = None # will be set later, I promise
+        scr = gtk.ScrolledWindow()
+        scr.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        self.win.add(scr)
+        self._store = gtk.TreeStore(gobject.TYPE_STRING, # obj name
+                                    gobject.TYPE_INT,    # line number
+                                    gobject.TYPE_PYOBJECT, # the astng node
+                                    )
+        self.treeview = gtk.TreeView(self._store)
+        self.activate_mouse_callbacks()
+        # init columns / set model
+        # init_treeview_columns(self.treeview, ['Module objects'])
+        pixbuf_renderer = gtk.CellRendererPixbuf()
+        col = gtk.TreeViewColumn('Module objects', pixbuf_renderer)
+        col.set_cell_data_func(pixbuf_renderer, pyobject_pixbuf)
+        text_renderer = gtk.CellRendererText()
+        col.pack_start(text_renderer, False)
+        col.set_cell_data_func(text_renderer, pyobject_name)
+        self.treeview.append_column(col)
+        scr.add(self.treeview)
+        self.win.connect('delete-event', self.hide)
+
+    def subconfiguration(self):
+        return []
+    
+    def add_object(self, thefile):
+        """add a python file to the module browser"""
+        store = self._store
+        node = thefile.astng
+        mod_iter = store.append(None, (display_name(node), 0, node))
+        self.add_subobjects(mod_iter, node)
+
+    def add_subobjects(self, store_iter, node):
+        """fill the treemodel with package/module/class/method/function
+        informations
+        """
+        #print 'add subobjects of', node.__class__.__name__, node.name
+        hide_priv = self.cfg['hide-private']
+        for name in node:
+            if hide_priv and name.startswith('_'):
+                continue
+            cnode = node[name]
+            new_iter = self._store.append(store_iter, (display_name(cnode), cnode.lineno or 0, cnode))
+            self.add_subobjects(new_iter, cnode)
+
+
+    def on_double_click(self, row_model, event):
+        """called on double-click"""
+        obj_name, lineno, astng = row_model
+        if lineno != -1:
+            filepath = astng.root().file
+            self.editor.open(filepath, lineno)
+
+        return gtk.TRUE
+    
+##     def oob_button_pressed(self, treeview, event):
+##         """Callback called when the user clicks on the FS treeview"""
+##         # left click
+##         if event.button == 1:
+##             # double_click
+##             if event.type == gtk.gdk._2BUTTON_PRESS:
+##                 obj_name, lineno, astng = self.get_selected()
+##                 if lineno != -1:
+##                     filepath = astng.root().file
+##                     self.editor.open(filepath, lineno)
+
+
+# the plugin itself ###########################################################
+
+class PythonPlugin(AbstractPlugIn):
+    """Plugin for python files"""
+    name = 'python'
+    mimetypes = 'text/x-python',
+
+    def __init__(self):
+        AbstractPlugIn.__init__(self)
+        self.pylint_window = PylintReport()
+        try:
+            self.browser_window = ModulesBrowser()
+        except:
+            import traceback
+            traceback.print_exc()
+        self.astng_manager = astng.ASTNGManager()
+        
+    def set_application(self, appl):
+        """set the application (on registration)"""
+        AbstractPlugIn.set_application(self, appl)
+        self.pylint_window.editor = self.appl.editor
+        self.browser_window.editor = self.appl.editor
+        
+    def support_directory(self, thedir):
+        """return true if the directory is a python package"""
+        if exists(join(thedir.abspath, '__init__.py')):
+            return True
+        return False
+    
+    def get_actions(self, thefile):
+        """return actions provided by this plugin for the given file (python
+        module) or directory (python package)
+        """
+        if thefile.is_directory():
+            # python package
+            actions = [
+                ('lint', self.cb_lint),
+                ('add to objects browser', self.cb_add_to_browser),
+                ]
+            return actions
+        # python module
+        return [
+            ('execute', self.cb_execute),
+            ('lint', self.cb_lint),
+            ('add to objects browser', self.cb_add_to_browser),
+            ]
+
+    def cb_execute(self, menuitem, thefile):
+        """execute a python file, output to the shared console"""
+        self.execute_in_console('python %s' % thefile.abspath)        
+
+    def cb_lint(self, menuitem, thefile):
+        """lint a python module or package"""
+        self.pylint_window.check(thefile)
+    
+    def cb_add_to_browser(self, menuitem, thefile):
+        """a a python module or package to the python objects browser"""
+        self.browser_window.show()
+        self._get_astng(thefile)
+        self.browser_window.add_object(thefile)
+        
+    def _get_astng(self, thefile):
+        """return the astng representation for this object (cached or new build)
+        """
+        if not hasattr(thefile, 'astng'):
+            if thefile.is_directory():
+                thefile.astng = self.astng_manager.from_directory(thefile.abspath)
+            else:
+                thefile.astng = self.astng_manager.from_file(thefile.abspath)
+        return thefile.astng
+    
+    def subconfiguration(self):
+        return [self.pylint_window, self.browser_window]
+
+        
+def register(registry):
+    """register plugins from this module to the plugins registry"""
+    registry.register(PythonPlugin())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/vcs.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,126 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""Basic plugin for VCS-related actions
+This plugin is based on the oobrother's filelib subpackage, and use its
+builtin support of Version Control System
+"""
+
+__revision__ = '$Id: vcs.py,v 1.6 2004-12-16 09:36:26 syt Exp $'
+__metaclass__ = type
+
+import os.path as osp
+
+from logilab.common.interface import implements
+from logilab.devtools.vcslib import VCS_UPTODATE, VCS_MODIFIED, \
+     VCS_MISSING, VCS_NEW, VCS_CONFLICT, VCS_NOVERSION, VCS_IGNORED, \
+     VCS_REMOVED, VCS_NEEDSPATCH, IVCSFile
+
+from oobrother.plugins import AbstractPlugIn
+from oobrother.uiutils import ask_for_message
+
+
+class VCSPlugin(AbstractPlugIn):
+    """plugin for vcs-related actions (update, commit, tag, etc.)"""
+    name = 'vcs'
+    mimetypes = '*',
+
+    vcs_actions = ('update', 'commit', 'tag', 'add', 'remove')
+    
+    def __init__(self):
+        AbstractPlugIn.__init__(self)
+
+    def _execute_vcs_command(self, cmd):
+        """executes 'cmd' in 'thefile's parent directory"""
+        self.execute_in_console(cmd)
+        thefile.update_parent()        
+        
+    def get_actions(self, thefile):
+        """returns consistent list of actions depending of thefile's status"""
+        actions = []
+        for action_label in VCSPlugin.vcs_actions:
+            actions_method = getattr(self, '_actions_for_%s' % action_label,
+                                     lambda f: [])
+            actions += actions_method(thefile)
+        return [('version control', actions)]
+
+    def support_directory(self, thedir):
+        """VCSPlugin supports directory"""
+        return True
+    
+    def is_vc_dir(self, thefile):
+        """return true if thefile is a directory under version control """
+        return thefile.is_directory() and thefile.status != VCS_NOVERSION
+    
+    def _actions_for_add(self, thefile):
+        """tests if thefile supports 'add' and returns related actions"""
+        parent = thefile.get_parent()
+        if thefile.status == VCS_NOVERSION and \
+               parent and parent.status != VCS_NOVERSION:
+            return [(_('add'), self.cb_add)]
+        return []
+    
+    def _actions_for_remove(self, thefile):
+        """tests if thefile supports 'add' and returns related actions"""
+        if thefile.status != VCS_NOVERSION:
+            return [(_('remove'), self.cb_remove)]
+        return []
+    
+    def _actions_for_update(self, thefile):
+        """tests if thefile supports 'update' and returns related actions"""
+        if self.is_vc_dir(thefile) or thefile.status in (VCS_MODIFIED, VCS_MISSING, VCS_NEEDSPATCH):
+            return [(_('update'), self.cb_update)]
+        return []
+    
+    def _actions_for_commit(self, thefile):
+        """tests if thefile supports 'commit' and returns related actions"""
+        if self.is_vc_dir(thefile) or thefile.status in (VCS_MODIFIED, VCS_REMOVED, VCS_NEW):
+            return [(_('commit'), self.cb_commit)]
+        return []
+    
+    def _actions_for_tag(self, thefile):
+        """tests if thefile supports 'tag' and returns related actions"""
+        if thefile.status not in (VCS_NOVERSION, VCS_IGNORED, VCS_REMOVED):
+            return [(_('tag'), self.cb_tag)]
+        return []
+
+    def cb_update(self, menuitem, thefile):
+        """callback for 'update' item'"""
+        self._execute_vcs_command(thefile.update())
+
+    def cb_add(self, menuitem, thefile):
+        """callback for 'add to vcs' item'"""
+        self._execute_vcs_command(thefile.add())
+
+    def cb_remove(self, menuitem, thefile):
+        """callback for 'remove from vcs' item'"""
+        self._execute_vcs_command(thefile.remove())
+
+    def cb_commit(self, menuitem, thefile):
+        """callback for 'commit' item'"""
+        commit_msg = ask_for_message('Commit message for %s' % thefile)
+        if commit_msg is not None:
+            self._execute_vcs_command(thefile.commit(commit_msg))
+            
+    def cb_tag(self, menuitem, thefile):
+        """callback for 'tag' item'"""
+        tagname = ask_for_message('Tag name' % thefile)
+        if tagname is not None:
+            self._execute_vcs_command(thefile.tag(tagname))
+        
+    
+def register(registry):
+    """register plugins from this module to the plugins registry"""
+    registry.register(VCSPlugin())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/registry.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,124 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""plugins registry
+
+a plugin is associated to a mime type
+"""
+
+__revision__ = '$Id: registry.py,v 1.8 2004-12-16 09:43:58 syt Exp $'
+
+import sys
+
+from logilab.common.modutils import load_module_from_name
+
+__metaclass__ = type
+
+
+class PlugInsRegistry:
+    """the registry is responsible to handle loaded file/directory
+    plugins
+    """
+    
+    name = 'plugins'
+    
+    def __init__(self, appl):
+        self.appl = appl
+        # the registry has no options by itself
+        self.cfg = None
+        # loaded plugins
+        self.plugins = []
+        # plugins indexes
+        self._plugins_idx = {}
+        self._default_plugins = []
+
+    def register_all(self, modnames):
+        """load each python module in the modnames list and register it
+        as a plugin.
+
+        A Plugin module must have a "register" function taking this
+        registry as only argument. The function should call the
+        "register" method of this object to register handlers for some
+        mime types.
+        """
+        for modname in modnames:
+            try:
+                module = load_module_from_name(modname)
+            except:
+                if __debug__:
+                    import traceback
+                    traceback.print_exc()
+                log_traceback(LOG_ERR, sys.exc_info())
+                continue
+            try:
+                register = module.register
+            except AttributeError:
+                log(LOG_ERR, 'plugin module %s has not the required function\
+ "register(registry)"', modname)
+                continue
+            try:
+                register(self)
+            except:
+                log_traceback(LOG_ERR, sys.exc_info())
+            
+    def register(self, plugin):
+        """register a new plugin
+        the plugin should have a mimetypes attribute indicating the
+        mime types it is able to handle.
+
+        wildcard are allowed in a mime type:
+          - '*' will register plugin for any file
+          - 'text/*' will register plugin for any text file
+          - 'text/html' will register plugin for any html text file
+          - '*/whatever' is not supported and won't work as espected
+        """
+        plugin.set_application(self.appl)
+        self.plugins.append(plugin)
+        for mimetype in plugin.mimetypes:
+            mimetype = mimetype.strip()
+            try:
+                major, minor = mimetype.split('/')
+            except:
+                if mimetype == '*':
+                    self._default_plugins.append(plugin)
+                    continue
+                raise
+            idx = self._plugins_idx.setdefault(major, {})
+            idx.setdefault(minor, []).append(plugin)
+
+    def get_plugins(self, fileobj):
+        """return a list of plugins able to handle the mime type of the
+        given file
+        """
+        if fileobj.mimetype is None:
+            if fileobj.is_directory():
+                return [plug for plug in self.plugins
+                        if plug.support_directory(fileobj)]
+            return self._default_plugins
+        major, minor = fileobj.mimetype.split('/')
+        major_plugins = self._plugins_idx.get(major, {})
+        return (self._default_plugins
+                + major_plugins.get('*', [])
+                + major_plugins.get(minor, []))
+
+    def subconfiguration(self):
+        """return a list of configuration nodes for subconfiguration
+        (i.e. plugins for the registry)
+        """
+        return self.plugins
+    
+    
+#REGISTRY = PlugInsRegistry()
+#del PlugInsRegistry
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,182 @@
+#!/usr/bin/env python
+# pylint: disable-msg=W0404,W0622,W0704,W0613,W0403
+# Copyright (c) 2003 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+""" Generic Setup script, takes package info from __pkginfo__.py file """
+
+__revision__ = '$Id: setup.py,v 1.2 2004-11-10 09:29:13 adim Exp $'
+
+from __future__ import nested_scopes
+import os
+import sys
+import shutil
+from distutils.core import setup
+from distutils import command
+from distutils.command import install_lib
+from os.path import isdir, exists, join, walk
+
+# import required features
+from __pkginfo__ import modname, version, license, short_desc, long_desc, \
+     web, author, author_email
+# import optional features
+try:
+    from __pkginfo__ import distname
+except ImportError:
+    distname = modname
+try:
+    from __pkginfo__ import scripts
+except ImportError:
+    scripts = []
+try:
+    from __pkginfo__ import data_files
+except ImportError:
+    data_files = None
+try:
+    from __pkginfo__ import subpackage_of
+except ImportError:
+    subpackage_of = None
+try:
+    from __pkginfo__ import include_dirs
+except ImportError:
+    include_dirs = []
+try:
+    from __pkginfo__ import ext_modules
+except ImportError:
+    ext_modules = None
+
+BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
+IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
+    
+
+def ensure_scripts(linux_scripts):
+    """
+    Creates the proper script names required for each platform
+    (taken from 4Suite)
+    """
+    from distutils import util
+    if util.get_platform()[:3] == 'win':
+        scripts_ = [script + '.bat' for script in linux_scripts]
+    else:
+        scripts_ = linux_scripts
+    return scripts_
+
+
+def get_packages(directory, prefix):
+    """return a list of subpackages for the given directory
+    """
+    result = []
+    for package in os.listdir(directory):
+        absfile = join(directory, package)
+        if isdir(absfile):
+            if exists(join(absfile, '__init__.py')) or \
+                   package in ('test', 'tests'):
+                if prefix:
+                    result.append('%s.%s' % (prefix, package))
+                else:
+                    result.append(package)
+                result += get_packages(absfile, result[-1])
+    return result
+
+def export(from_dir, to_dir,
+           blacklist=BASE_BLACKLIST,
+           ignore_ext=IGNORED_EXTENSIONS):
+    """make a mirror of from_dir in to_dir, omitting directories and files
+    listed in the black list
+    """
+    def make_mirror(arg, directory, fnames):
+        """walk handler"""
+        for norecurs in blacklist:
+            try:
+                fnames.remove(norecurs)
+            except ValueError:
+                pass
+        for filename in fnames:
+            # don't include binary files
+            if filename[-4:] in ignore_ext:
+                continue
+            if filename[-1] == '~':
+                continue
+            src = '%s/%s' % (directory, filename)
+            dest = to_dir + src[len(from_dir):]
+            print >> sys.stderr, src, '->', dest
+            if os.path.isdir(src):
+                if not exists(dest):
+                    os.mkdir(dest)
+            else:
+                if exists(dest):
+                    os.remove(dest)
+                shutil.copy2(src, dest)
+    try:
+        os.mkdir(to_dir)
+    except OSError, ex:
+        # file exists ?
+        import errno
+        if ex.errno != errno.EEXIST:
+            raise
+    walk(from_dir, make_mirror, None)
+
+
+EMPTY_FILE = '"""generated file, don\'t modify or your data will be lost"""\n'
+
+class BuildScripts(command.install_lib.install_lib):
+
+    def run(self):
+        command.install_lib.install_lib.run(self)
+        # create Products.__init__.py if needed
+        if subpackage_of:
+            product_init = join(self.install_dir, subpackage_of, '__init__.py')
+            if not exists(product_init):
+                self.announce('creating %s' % product_init)
+                stream = open(product_init, 'w')
+                stream.write(EMPTY_FILE)
+                stream.close()
+        # manually install included directories if any
+        if include_dirs:
+            if subpackage_of:
+                base = join(subpackage_of, modname)
+            else:
+                base = modname
+            for directory in include_dirs:
+                dest = join(self.install_dir, base, directory)
+                export(directory, dest)
+        
+def install(**kwargs):
+    """setup entry point"""
+    if subpackage_of:
+        package = subpackage_of + '.' + modname
+        kwargs['package_dir'] = {package : '.'}
+        packages = [package] + get_packages(os.getcwd(), package)
+    else:
+        kwargs['package_dir'] = {modname : '.'}
+        packages = [modname] + get_packages(os.getcwd(), modname)
+    kwargs['packages'] = packages
+    return setup(name = distname,
+                 version = version,
+                 license =license,
+                 description = short_desc,
+                 long_description = long_desc,
+                 author = author,
+                 author_email = author_email,
+                 url = web,
+                 scripts = ensure_scripts(scripts),
+                 data_files=data_files,
+                 ext_modules=ext_modules,
+                 cmdclass={'install_lib': BuildScripts},
+                 **kwargs
+                 )
+            
+if __name__ == '__main__' :
+    install()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sysutils.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,85 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""some miscellaneous 'system' utilities
+"""
+   
+__revision__ = '$Id: sysutils.py,v 1.7 2004-12-16 09:43:58 syt Exp $'
+
+import sys
+import os
+from os.path import expanduser, exists, join, dirname
+
+# locate oobrother's home directory
+if os.environ.has_key('OOBROTHERHOME'):
+    OOBROTHER_HOME = os.environ['OOBROTHERHOME']
+else:
+    USER_HOME = expanduser('~')
+    if USER_HOME == '~' or USER_HOME == '/root':
+        OOBROTHER_HOME = ".oobrother"
+    else:
+        OOBROTHER_HOME = join(USER_HOME, '.oobrother')
+        
+if not exists(OOBROTHER_HOME):
+    try:
+        os.mkdir(OOBROTHER_HOME)
+    except OSError:
+        print >> sys.stderr, 'Unable to create directory %s' % OOBROTHER_HOME
+
+
+def call_in_directory(directory, func, *args, **kwargs):
+    """call the function after a move in the directory, then go back to
+    the original location
+
+    no side effect on sys.path and sys.modules
+    """
+    path = os.getcwd()
+    try:
+        os.chdir(directory)
+        return call_and_restore(func, *args, **kwargs)
+    finally:
+        os.chdir(path)
+    
+def call_and_restore(func, *args, **kwargs):
+    """call the function, avoiding side effect on sys.path and sys.modules
+    """
+    path = sys.path[:]
+    modules = dict(sys.modules) # .copy()
+    try:
+        return func(*args, **kwargs)
+    finally:
+        sys.path = path
+        for modname in sys.modules.keys():
+            if not modname in modules:
+                del sys.modules[modname]
+
+
+class PathNormalizerMixIn:
+    
+    def set_base(self, thefile):
+        """set the current base according to the given wrapped file"""
+        if thefile.is_directory():
+            self.__base = thefile.abspath
+        else:
+            self.__base = dirname(thefile.abspath)
+
+    def normalize_path(self, filepath):
+        """return a normalized version of the given file path"""
+        return filepath[len(self.__base)+1:] or filepath
+    
+    def absolute_path(self, filepath):
+        """return a normalized version of the given file path"""
+        return join(self.__base, filepath)
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/bar.txt	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,1 @@
+this doesn't contain digits !!
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/foo.txt	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,1 @@
+this file contains some digits (1234) !!
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/runtests.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,5 @@
+from logilab.common.testlib import main
+
+if __name__ == '__main__':
+    import sys, os
+    main(os.path.dirname(sys.argv[0]) or '.')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_devtools.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,99 @@
+import unittest
+import tempfile
+import os
+from os.path import join
+from logilab.common import testlib
+from logilab.devtools.lib.pkginfo import pkginfo_save
+from oobrother.plugins.devtools import pkginfo_create
+
+__revision__ = '$Id: unittest_devtools.py,v 1.7 2006-03-26 17:58:48 nico Exp $'
+
+PKGINFO_SCHEMA  = (
+    ('modname', 'string', 'name', (), True, 'duh'), 
+    ('version', 'string', 'version', (), True, '0.1'),
+    ('copyright', 'text', 'copyright', (), True,
+'Copyright (c) %s LOGILAB S.A. (Paris, FRANCE).'),
+    ('license', 'choice', 'license', ('GPL', 'unk'),
+    False, 'GPL'),
+)
+
+class PkgInfoEditFuncTC(testlib.TestCase):
+    pkginfo = join(tempfile.gettempdir(), '__pkginfo__.py')
+
+    def tearDown(self):
+        try:
+            os.remove(self.pkginfo)
+        except:
+            pass
+        
+    def test_create(self):
+        pkginfo_create(PKGINFO_SCHEMA, self.pkginfo,
+                       {'modname': 'TestDevtools',
+                        'license': 'GPL',
+                        'copyright': 'copyright Logilab 2003'})
+        LICENSE = ('''
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
+# St, Fifth Floor, Boston, MA 02110-1301, USA.
+
+# On Debian systems, the complete text of the GNU General Public License may be
+# found in '/usr/share/common-licenses/GPL'.
+"""package information for TestDevtools"""
+
+__revision__ = '$I'''+'''d:$'
+
+modname = 'TestDevtools'
+copyright = 'copyright Logilab 2003'
+license = 'GPL'
+numversion = [int(v) for v in version.split('.')]
+''').strip()
+        self.assertTextEquals(LICENSE, open(self.pkginfo).read().strip())
+        
+                       
+    def test_save(self):
+        pkginfo_create(PKGINFO_SCHEMA, self.pkginfo,
+                       {'modname': 'TestDevtools',
+                        'license': 'GPL',
+                        'copyright': 'copyright Logilab 2003'})
+        pkginfo_save(self.pkginfo, {'modname': 'TestDevtoolsYO',
+                                    'license': 'LCL'})
+        LICENSE = ('''
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
+# St, Fifth Floor, Boston, MA 02110-1301, USA.
+
+# On Debian systems, the complete text of the GNU General Public License may be
+# found in '/usr/share/common-licenses/GPL'.
+"""package information for TestDevtools"""
+
+__revision__ = '$I'''+'''d:$'
+
+modname = 'TestDevtoolsYO'
+copyright = 'copyright Logilab 2003'
+license = 'LCL'
+numversion = [int(v) for v in version.split('.')]
+''').strip()
+        self.assertTextEquals(LICENSE, open(self.pkginfo).read().strip())
+
+
+
+if __name__ == '__main__':
+    unittest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_filters.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,274 @@
+"""unit tests for filters"""
+
+__revision__ = '$Id: unittest_filters.py,v 1.4 2006-03-26 19:52:22 nico Exp $'
+
+from logilab.common import testlib
+from oobrother.filelib import filters
+from os.path import join, dirname, abspath
+import re
+from sets import Set
+import sys
+from cStringIO import StringIO
+
+class FileNode:
+    """mock class, only defines what we really need for filters"""
+    def __init__(self, filename = 'foo.txt'):
+        self.basename = filename
+        self.abspath = join(abspath(dirname(__file__)), 'data', filename)
+
+    def is_directory(self):
+        return False
+    
+class MatchersTC(testlib.TestCase):
+    """test suite for existing filters"""
+
+    def setUp(self):
+        self.pyfile = FileNode('fo.py')
+        self.txtfile = FileNode('foo.txt')
+        self.binfile = FileNode('foo.bin')
+
+    def test_abstract_filter(self):
+        """checks default behaviour of a filter (<=> returning True)"""
+        flt = filters.BaseMatcher()
+        self.assertEquals(flt.match(self.pyfile), True)
+        self.assertEquals(flt.match(self.txtfile), True)
+        self.assertEquals(flt.match(self.binfile), True)
+        self.assertEquals(flt.match(None), True)
+        
+    def test_extension_filter(self):
+        """tests extensino filter"""
+        flt = filters.ExtensionMatcher('py')
+        self.assertEquals(flt.match(self.pyfile), True)
+        self.assertEquals(flt.match(self.txtfile), False)
+        self.assertEquals(flt.match(self.binfile), False)
+
+    def test_hide_filter(self):
+        """tests hide filter"""
+        flt = filters.ReversedMatcher(filters.ExtensionMatcher('py'))
+        self.assertEquals(flt.match(self.pyfile), False)
+        self.assertEquals(flt.match(self.txtfile), True)
+        self.assertEquals(flt.match(self.binfile), True)
+
+    def test_abspath_filter(self):
+        """tests filters.AbspathMatcher"""
+        flt = filters.AbspathMatcher('fo{2}')
+        self.assertEquals(flt.match(self.pyfile), False)
+        self.assertEquals(flt.match(self.txtfile), True)
+        self.assertEquals(flt.match(self.binfile), True)
+        
+    def test_content_filter(self):
+        """tests content filter"""
+        flt = filters.ContentMatcher('\d') # file must have digits !
+        self.assertRaises(IOError, flt.match, self.pyfile)
+        self.assertEquals(flt.match(self.txtfile), True)
+        self.assertEquals(flt.match(FileNode('bar.txt')), False)
+
+    def test_content_filter_with_flags(self):
+        """tests content filter with flags"""
+        flt = filters.ContentMatcher('Contains')
+        self.assertEquals(flt.match(self.txtfile), False)
+        flt = filters.ContentMatcher('Contains', re.I)
+        self.assertEquals(flt.match(self.txtfile), True)
+
+    def test_mimetype_filter(self):
+        """tests MimeTypeMatcher's behaviour"""
+        flt = filters.MimeTypeMatcher('text/plain')
+        self.assertEquals(flt.match(self.pyfile), False)
+        self.assertEquals(flt.match(self.txtfile), True)
+        flt = filters.MimeTypeMatcher('text/plain', 'gzip')
+        self.assertEquals(flt.match(self.pyfile), False)
+        self.assertEquals(flt.match(self.txtfile), False)
+        self.assertEquals(flt.match(FileNode('toto.txt.gz')), True)
+
+    def test_hidden_files_filter(self):
+        """tests HIDDEN_FILE_FILTER"""
+        flt = filters.HIDDEN_FILES_FILTER
+        self.assertEquals(flt.match(FileNode('.foo')), True)
+        self.assertEquals(flt.match(FileNode('bar/.foo')), True)
+        self.assertEquals(flt.match(FileNode('foo')), False)
+        flt = filters.HIDE_HIDDEN_FILES
+        self.assertEquals(flt.match(FileNode('.foo')), False)
+        self.assertEquals(flt.match(FileNode('bar/.foo')), False)
+        self.assertEquals(flt.match(FileNode('foo')), True)
+        
+
+class FilterChainTC(testlib.TestCase):
+    """test suite for filter chains"""
+    def setUp(self):
+        self.pyfile = FileNode('foo.py')
+        self.txtfile = FileNode('foo.txt')
+        self.txtfile2 = FileNode('bar.txt')
+        self.pycfile = FileNode('foo.pyc')
+        self.binfile = FileNode('foo.bin')
+        self.hiddenfile = FileNode('.foo')
+        # file must have digits !
+        self.fchain = filters.FilterDef(fname_restrictions = ['txt$'],
+                                        content_restrictions = ['\d'])
+        self.stdout_backup = sys.stdout
+        sys.stdout = StringIO()
+
+    def tearDown(self):
+        sys.stdout = self.stdout_backup
+        
+    def test_basic_chain(self):
+        """tests basic properties of the chain"""
+        self.assertEquals(self.fchain.file_is_dropped(self.txtfile), False)
+        self.assertEquals(self.fchain.file_is_dropped(self.txtfile2), True)
+        self.assertEquals(self.fchain.file_is_dropped(self.pyfile), True)
+
+    def test_filter_from_ignored_extensions(self):
+        """tests the 'from_ignored_extensions' constructor"""
+        flt = filters.FilterDef.from_ignored_extensions(['bak', '~', 'pyc'])
+        # self.assertEquals(len(flt), 4)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.bak')), True)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), True)
+        self.assertEquals(flt.file_is_dropped(self.pycfile), True)
+        self.assertEquals(flt.file_is_dropped(self.txtfile), False)
+        # hidden files are ignored
+        self.assertEquals(flt.file_is_dropped(self.hiddenfile), True)
+
+    def test_filter_from_ignored_extensions_hidden(self):
+        """tests 'from_ingored_extensions', but still showing hidden files"""
+        flt = filters.FilterDef.from_ignored_extensions(['bak', '~', 'pyc'], True)
+        # self.assertEquals(len(flt), 3)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.bak')), True)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), True)
+        self.assertEquals(flt.file_is_dropped(self.pycfile), True)
+        self.assertEquals(flt.file_is_dropped(self.txtfile), False)
+        # hidden files are not ignored
+        self.assertEquals(flt.file_is_dropped(self.hiddenfile), False)
+
+    def test_filter_from_showed_extensions(self):
+        """tests 'from_showed_extensions'"""
+        flt = filters.FilterDef.from_showed_extensions(['txt', 'py'])
+        # self.assertEquals(len(flt), 3)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.bak')), True)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), True)
+        self.assertEquals(flt.file_is_dropped(self.pycfile), True)
+        self.assertEquals(flt.file_is_dropped(self.pyfile), False)
+        self.assertEquals(flt.file_is_dropped(self.txtfile), False)
+        # hidden files are filtered
+        self.assertEquals(flt.file_is_dropped(self.hiddenfile), True)
+        
+    def test_filter_from_showed_extensions_hidden(self):
+        """tests 'from_showed_extensions' but still showing hidden files"""
+        flt = filters.FilterDef.from_showed_extensions(['txt', 'py'], True)
+        # self.assertEquals(len(flt), 2)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.bak')), True)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), True)
+        self.assertEquals(flt.file_is_dropped(self.pycfile), True)
+        self.assertEquals(flt.file_is_dropped(self.pyfile), False)
+        self.assertEquals(flt.file_is_dropped(self.txtfile), False)
+        # hidden files are not filtered
+        self.assertEquals(flt.file_is_dropped(FileNode('.foo.py')), False)
+        self.assertEquals(flt.file_is_dropped(FileNode('.foo.txt')), False)
+        self.assertEquals(flt.file_is_dropped(self.hiddenfile), True)
+        
+    def test_set_show_hidden_files(self):
+        """tests that we can change the 'show_hidden_file' option"""
+        pyfile = FileNode('.foo.py')
+        flt = filters.FilterDef.from_showed_extensions(['py'])
+        # hidden files are filtered
+        self.assertEquals(flt.file_is_dropped(pyfile), True)
+        flt.show_hidden_files = True
+        # hidden files are not filtered
+        self.assertEquals(flt.file_is_dropped(pyfile), False)
+
+    def test_ignore_extension(self):
+        """tests that we can add an extension to ignore after creation"""
+        flt = filters.FilterDef.from_ignored_extensions(['bak'])
+        self.assertEquals(Set(flt.ignored_extensions()), Set(['bak']))
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.bak')), True)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), False)
+        flt.ignore_extension('~')
+        self.assertEquals(Set(flt.ignored_extensions()), Set(['bak', '~']))
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), True)
+
+    def test_unignore_extension(self):
+        """tests that we can remove an extension to ignore after creation"""
+        flt = filters.FilterDef.from_ignored_extensions(['bak', '~'])
+        self.assertEquals(Set(flt.ignored_extensions()), Set(['bak', '~']))
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.bak')), True)
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), True)
+        flt.unignore_extension('~')
+        self.assertEquals(Set(flt.ignored_extensions()), Set(['bak']))
+        self.assertEquals(flt.file_is_dropped(FileNode('foo.txt~')), False)
+        # test that we can remove already unignored extension
+        flt.unignore_extension('~')
+        self.assertEquals(Set(flt.ignored_extensions()), Set(['bak']))
+
+    def test_add_filename_restrictions(self):
+        """tests that we can add filename restrictions after creation"""
+        fchain = filters.FilterDef(fname_restrictions = ['txt$'])
+        self.assertEquals(Set(fchain.filename_restrictions()), Set(['txt$']))
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pyfile), True)
+        fchain.add_filename_restriction('.py$')
+        self.assertEquals(Set(fchain.filename_restrictions()), Set(['txt$', '.py$']))
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pyfile), False)
+        
+    def test_remove_filename_restrictions(self):
+        """tests that we can remove filename restrictions after creation"""
+        fchain = filters.FilterDef(fname_restrictions = ['txt$', 'py$'])
+        self.assertEquals(Set(fchain.filename_restrictions()), Set(['txt$', 'py$']))
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pyfile), False)
+        fchain.remove_filename_restriction('py$')
+        self.assertEquals(Set(fchain.filename_restrictions()), Set(['txt$']))
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pyfile), True)
+        # test that we can re-remove a restriction
+        fchain.remove_filename_restriction('py$')
+        self.assertEquals(Set(fchain.filename_restrictions()), Set(['txt$']))
+
+    def test_remove_all_filename_restrictions(self):
+        """tests that we can remove all filename restrictions"""
+        fchain = filters.FilterDef(fname_restrictions = ['txt$', 'py$'])
+        self.assertEquals(Set(fchain.filename_restrictions()), Set(['txt$', 'py$']))
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pyfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pycfile), True)
+        self.assertEquals(fchain.file_is_dropped(self.binfile), True)
+        fchain.remove_all_filename_restrictions()
+        self.assertEquals(fchain.filename_restrictions(), [])
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pyfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.pycfile), False)
+        self.assertEquals(fchain.file_is_dropped(self.binfile), False)
+        
+    def test_add_content_restriction(self):
+        """tests that we can add content restrictions after creation"""
+        fchain = filters.FilterDef()
+        self.assertEquals(fchain.content_restrictions(), [])
+        self.assertEquals(fchain.file_is_dropped(self.txtfile2), False)
+        fchain.add_content_restriction('\d')
+        self.assertEquals(fchain.content_restrictions(), ['\d'])
+        self.assertEquals(fchain.file_is_dropped(self.txtfile2), True)
+
+    def test_remove_content_restriction(self):
+        """tests that we can remove content restrictions after creation"""
+        fchain = filters.FilterDef(content_restrictions = ['\d'])
+        self.assertEquals(fchain.content_restrictions(), ['\d'])
+        self.assertEquals(fchain.file_is_dropped(self.txtfile2), True)
+        fchain.remove_content_restriction('\d')
+        self.assertEquals(fchain.content_restrictions(), [])
+        self.assertEquals(fchain.file_is_dropped(self.txtfile2), False)
+        # test that we can re-remove a restriction
+        fchain.remove_content_restriction('\d')
+        self.assertEquals(fchain.content_restrictions(), [])
+        
+    def test_remove_all_content_restrictions(self):
+        """tests that we can remove all content restrictions"""
+        fchain = filters.FilterDef(content_restrictions = ['\d'])
+        self.assertEquals(fchain.file_is_dropped(self.txtfile2), True)
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        fchain.remove_all_content_restrictions()
+        self.assertEquals(fchain.content_restrictions(), [])
+        self.assertEquals(fchain.file_is_dropped(self.txtfile2), False)
+        self.assertEquals(fchain.file_is_dropped(self.txtfile), False)
+        
+
+
+if __name__ == '__main__':
+    testlib.unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_pyplugin.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,23 @@
+# -*- coding: ISO-8859-1 -*-
+"""Unittests for python plugin
+"""
+
+__revision__ = '$Id: unittest_pyplugin.py,v 1.1 2004-09-30 08:32:22 adim Exp $'
+
+import unittest
+from oobrother.plugins.python import compare_message
+
+class PypluginTC(unittest.TestCase):
+    """Test case for python plugin"""
+
+    def test_pylint_sigle_sort(self):
+        """Tests the pylint table sort method"""
+        test_list = ['E', 'W', 'C', 'E', 'I', 'F', 'W', 'R', 'E']
+        expected = ['E', 'E', 'E', 'W', 'W', 'R', 'C', 'I', 'F']
+        test_list.sort(compare_message)
+        self.assertEquals(test_list, expected)
+
+
+if __name__ == '__main__': 
+    unittest.main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_registry.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,62 @@
+"""unit tests for the plugin registry"""
+
+__revision__ = '$Id: unittest_registry.py,v 1.2 2004-11-10 09:54:45 adim Exp $'
+
+import unittest
+
+from oobrother.registry import PlugInsRegistry
+from oobrother.filelib.basefile import FileWrapper
+from oobrother.plugins import AbstractPlugIn
+
+class DummyPlugin(AbstractPlugIn):
+    def __init__(self, mt, name):
+        AbstractPlugIn.__init__(self)
+        self.name = name
+        self.mimetypes = (mt,)
+
+    def __cmp__(self, other):
+        name = getattr(other, 'name', other)
+        return cmp(self.name, name)
+                   
+class PlugInsRegistryTC(unittest.TestCase):
+
+    def setUp(self):
+        # hack: the PlugInsRegistry class is deleted to enforce singleton
+        self.reg = PlugInsRegistry(appl = None)
+
+    def test_wildcard_1(self):
+        self.reg.register(DummyPlugin('text/*', 2))
+        self.reg.register(DummyPlugin('text/plain', 3))
+        self.reg.register(DummyPlugin('text/html', 4))
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.txt')), [2, 3])
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.html')), [2, 4])
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.py')), [2])
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.pdf')), [])
+
+    def test_wildcard_2(self):
+        self.reg.register(DummyPlugin('*', 1))
+        self.reg.register(DummyPlugin('text/*', 2))
+        self.reg.register(DummyPlugin('text/plain', 3))
+        self.reg.register(DummyPlugin('text/html', 4))
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.txt')), [1, 2, 3])
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.html')), [1, 2, 4])
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.py')), [1, 2])
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto.pdf')), [1])
+        
+    def test_wildcard_3(self):
+        self.reg.register(DummyPlugin('*', 1))
+        self.reg.register(DummyPlugin('text/*', 2))
+        self.reg.register(DummyPlugin('text/plain', 3))
+        self.reg.register(DummyPlugin('text/html', 4))
+        self.assertEquals(self.reg.get_plugins(FileWrapper('toto')), [1])
+        
+    def test_plugins(self):
+        self.reg.register(DummyPlugin('*', 1))
+        self.reg.register(DummyPlugin('text/*', 2))
+        self.reg.register(DummyPlugin('text/plain', 3))
+        self.reg.register(DummyPlugin('text/html', 4))
+        plugins = [p.name for p in self.reg.plugins]
+        self.assertEquals(plugins, [1, 2, 3, 4])
+
+if __name__ == '__main__': 
+    unittest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uiutils/__init__.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,164 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# Copyright (C) 2002-2003 Stephen Kennedy <stevek@gnome.org>
+#   The load_pixbuf() function was taken from meld's source code
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+# pylint: disable-msg=W0402
+
+"""uiutils subpackage defines some misc useful functions and classes for
+building windows in OoBrother
+
+__init__.py exports misc useful functions, annd also init_treeview_columns,
+and all mixins for convenience
+"""
+
+__revision__ = '$Id: __init__.py,v 1.6 2005-03-10 15:38:49 adim Exp $'
+__metaclass__ = type
+
+import os.path as osp
+import pickle
+import gtk
+
+from oobrother import get_path
+# Export these class directly in __init__.py for convenience
+from oobrother.uiutils.trees import init_treeview_columns
+from oobrother.uiutils.basemixins import *
+
+def ask_for_message(message):
+    """popup a window for the user to type a message"""
+    dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, 
+                            gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, 
+                            message)
+    scr = gtk.ScrolledWindow()
+    scr.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+    text_view = gtk.TextView()
+    scr.add(text_view)
+    scr.show_all()
+    dlg.vbox.pack_start(scr)
+    try:
+        if dlg.run() == gtk.RESPONSE_OK:
+            buf = text_view.get_buffer()
+            msg = buf.get_text(*buf.get_bounds())
+            return msg.strip()
+        return None
+    finally:
+        dlg.destroy()
+
+
+def save_session(wdefs, filepath):
+    """save session information (window visibility/size/position)
+
+    filepath is the pickle file where session data will be written
+    wdefs is a dictionary mapping window identifier to gtk windows
+    """
+    data = {}
+    for wid, window in wdefs.items():
+        visible = window.get_property('visible')
+        position = window.get_position()
+        size = window.get_size()
+        data[wid] = (visible, position, size)
+    pickle.dump(data, open(filepath, 'w'))
+
+
+def restore_session(wdefs, filepath):
+    """restore session information (window visibility/size/position)
+
+    filepath is the pickle file where session information can be found
+    wdefs is a dictionary mapping window identifier to gtk windows
+    """
+    try:
+        data = pickle.load(open(filepath))
+    except IOError:
+        return
+    for wid, window in wdefs.items():
+        visible, position, size = data[wid]
+        if visible:
+            window.show_all()
+            window.window.move_resize(*(position+size))
+        else:
+            window.hide()
+            #window.window.move_resize(*(position+size))
+            window.resize(*size)
+
+
+def merge_actions(actions, _path='', _groups=None):
+    """merge a list of actions definitions"""
+    if _groups is None:
+        _groups = {}
+    result = []
+    for action in actions:
+        label, callback = action[0], action[1]
+        if not callable(callback):
+            assert isinstance(callback, list) or isinstance(callback, tuple)
+            if not callback:
+                continue
+            group_path = '%s/%s' % (_path, label)
+            try:
+                group_menu = _groups[group_path]
+            except KeyError:
+                group_menu = []
+                result.append( (label, group_menu) )
+            group_menu += merge_actions(callback, group_path, _groups)
+        else:
+            result.append( action )
+    return result
+
+
+def create_popup_menu(actions, thefile):
+    """creates a popup menu widget from a list of actions"""
+    menu = gtk.Menu()
+    for action in actions:
+        label, callback = action[0], action[1]
+        item = gtk.MenuItem(label)
+        if callable(callback):
+            item.connect('activate', callback, thefile, *action[2:])
+        elif callback:
+            # must be a list or a tuple with a sub-menu definition
+            group_menu = create_popup_menu(callback, thefile)
+            item.set_submenu(group_menu)
+        menu.append(item)
+    return menu
+
+
+def load_pixbuf(pix_name, size = 14):
+    """Load an image from a file as a pixbuf, with optional resizing
+
+    this code has been adapted from meld/gnomeglade.py
+    """
+    fname = osp.join(get_path('pixmaps'), pix_name)
+    image = gtk.Image()
+    image.set_from_file(fname)
+    image = image.get_pixbuf()
+    if size:
+        aspect = float(image.get_height()) / image.get_width()
+        image = image.scale_simple(size, int(aspect*size), 2)
+    return image
+
+def change_cursor_for_operation(operation, wdg, cursor = gtk.gdk.WATCH):
+    """Changes the cursor of the widget during a given operation
+    :type operation: callable
+    :param operation: the operation to execute
+
+    :type wdg: gtk.Widget
+    :param wdg: the widget for which the cursor is changed
+
+    :type cursor: int
+    :param cursor: cursor's constant value
+    """
+    gdk_cursor = gtk.gdk.Cursor(cursor)
+    wdg.window.set_cursor(gdk_cursor)
+    operation()
+    wdg.window.set_cursor(None)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uiutils/basemixins.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,239 @@
+# -*- coding: ISO-8859-1 -*-
+# pylint: disable-msg=W0142,W0232,R0903,W0613
+# (disable some pylint message caused by use of mixin classes)
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+
+"""this module exports all base mixins used to build text_windows,
+treeviews, etc.
+"""
+
+__revision__ = '$Id: basemixins.py,v 1.3 2004-12-13 17:22:12 syt Exp $'
+__metaclass__ = type
+
+__all__ = [ 'WindowMixIn', 'MenuControlledWindowMixIn', 'TreeViewMixIn',
+            'PluggableFrameMixIn', 'DelegatedWindowMixIn', 'TextWindowMixIn',
+            ]
+import gtk
+
+class WindowMixIn:
+    """a mixin for implementing some method like show/hide whose call
+    will be delegated to the mixed instance "win" attribute
+    """
+    def set_title(self, title):
+        """sets the window title"""
+        self.win.set_title(title)
+
+    def show(self, *args):
+        """show the window"""
+        self.win.show_all()
+
+    def hide(self, *args):
+        """hides the window"""
+        self.win.hide()
+        return gtk.TRUE
+
+class MenuControlledWindowMixIn:
+    """a mix in class for windows that can be shown/hidden using a stateful
+    button of the main window
+    """
+
+    def __init__(self, **kwargs):
+        super(MenuControlledWindowMixIn, self).__init__(**kwargs)
+        self.__mitem = None
+        
+    def set_menu_item(self, menuitem):
+        """set the menu item used to show/hide the window"""
+        assert self.__mitem is None
+        self.win.connect('show', self.cb_visibility_changed)
+        self.win.connect('hide', self.cb_visibility_changed)
+        self.__mitem = menuitem
+        
+    def cb_visibility_changed(self, *args):
+        """set the menu item used to show/hide the window"""
+        self.__mitem.set_active(self.win.get_property('visible'))
+
+    def cb_toggle_visibility(self, item, *args):
+        """sync the window visibility according to the item activity"""
+        if item.get_active():
+            self.show()
+        else:
+            self.hide()
+
+
+class TreeViewMixIn:
+    """a mixin providing some convenience function for windows with a TreeView
+    instance stored as the "treeview" attribute
+    """
+    def activate_keyboard_callbacks(self, kpress_data):
+        """activates keyboard callbacks management.
+
+        Once this method called, any keyboard's key pressed will be connected
+        to a callback called 'on_%KEYNAME%_pressed' where KEYNAME is the
+        key's name (on_a_pressed will be called after 'a' is pressed)
+
+        :type kpress_data: tuple or None
+        :param kpress_data: if specified, kpress_data will be passed as
+                            additional arguments to the callback
+        """
+        if kpress_data:
+            self.treeview.connect('key-press-event', self.on_key_pressed,
+                                  *kpress_data)
+        else:
+            self.treeview.connect('key-press-event', self.on_key_pressed)
+
+    
+    def activate_mouse_callbacks(self, bpress_data = None):
+        """activates mouse callbacks management.
+
+        Once this method called, the methods :
+          - on_right_click()
+          - on_left_click()
+          - on_double_click()
+        will be considered as callbacks to call when the appropriate event
+        is emiited
+        
+        :type bpress_data: tuple or None
+        :param bpress_data: if specified, bpress_data will be passed as
+                            additional arguments to the callback
+        """
+        if bpress_data:
+            self.treeview.connect('button-press-event', self.on_button_pressed,
+                                  *bpress_data)
+        else:
+            self.treeview.connect('button-press-event', self.on_button_pressed)
+
+
+    def activate_default_callbacks(self, kpress_data = None, bpress_data = None):
+        """activates default mouse and keyboard callbacks
+
+        :type kpress_data: tuple or None
+        :param kpress_data: the same as for activate_keyboard_callbacks
+
+        :type bpress_data: tuple or None
+        :param bpress_data: the same as for activate_mouse_callbacks
+        """
+        self.activate_keyboard_callbacks(kpress_data)
+        self.activate_mouse_callbacks(bpress_data)
+
+          
+    def get_selected(self):
+        """returns the object corresponding to the selected line and in columns
+        store_col_index of the store
+        
+        :returns: the selected value or None if no selection
+        """
+        selection = self.treeview.get_selection()
+        store, tree_iter = selection.get_selected()
+        if tree_iter is None:
+            return None
+        treepath = store.get_path(tree_iter)
+        return store[treepath]
+    
+    def clear(self, *args):
+        """resets the treeview's content"""
+        self.treeview.get_model().clear()        
+
+    def on_button_pressed(self, treeview, event, *args):
+        """called when the user clicks on the FS treeview"""
+        click_cb = None
+        # left click
+        if event.button == 1:
+            # double_click
+            if event.type == gtk.gdk._2BUTTON_PRESS:
+                try:
+                    click_cb = getattr(self, 'on_double_click')
+                except AttributeError:
+                    pass
+            else:
+                try:
+                    click_cb = getattr(self, 'on_left_click')
+                except AttributeError:
+                    pass
+        elif event.button == 3:
+            try:
+                click_cb = getattr(self, 'on_right_click')
+            except AttributeError:
+                pass
+        if click_cb:
+            click_cb(self.get_selected(), event, *args)
+
+    def on_key_pressed(self, treeview, event, *args):
+        """called when a keyboard key is pressed"""
+        key = event.keyval
+        #if key < 256:
+        keyname = gtk.gdk.keyval_name(key).lower()
+        try:
+            key_pressed_cb = getattr(self, 'on_%s_pressed' % keyname)
+        except AttributeError:
+            pass
+        else:
+            key_pressed_cb(self.get_selected(), event, *args)
+           
+
+class PluggableFrameMixIn:
+    """a mixin implementing some usefulle methods for window made of a tree
+    and a frame where plugins can set their own widget
+    """
+
+    def set_plugin_widget(self, plugin_widget):
+        """resets the plugin widget (removes the current one if necessary)"""
+        frame_children = self.plugin_frame.get_children()
+        if frame_children:
+            # should maybe save existing widget
+            existing_widget = frame_children[0]
+            self.plugin_frame.remove(existing_widget)
+        if plugin_widget:
+            self.plugin_frame.add(plugin_widget)
+
+    
+class DelegatedWindowMixIn(WindowMixIn):
+    """a mixin implementing some method like show/hide whose call will
+    be delegated to the mixed instance "win" attribute
+    """
+
+    def __init__(self, window_type=gtk.WINDOW_TOPLEVEL, **kwargs):
+        super(DelegatedWindowMixIn, self).__init__(**kwargs)
+        self.win = gtk.Window(window_type)
+        self.win.connect('delete-event', self.hide)
+
+
+class TextWindowMixIn(DelegatedWindowMixIn):
+    """a mixin for a window containing a text view, with some method
+    providing direct access to the text view (`write`, `reset`)
+    """
+
+    def __init__(self, **kwargs):
+        super(TextWindowMixIn, self).__init__(**kwargs)
+        scr = gtk.ScrolledWindow()
+        scr.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        self.win.add(scr)
+        self.text_view = gtk.TextView()
+        scr.add(self.text_view)
+
+    def write(self, msg):
+        """appends a message to the text buffer"""
+        text_buffer = self.text_view.get_buffer()
+        enditer = text_buffer.get_end_iter()
+        text_buffer.insert(enditer, msg)
+        self.text_view.scroll_mark_onscreen(text_buffer.get_insert())
+
+    def reset(self):
+        """resets the text buffer"""
+        text_buffer = self.text_view.get_buffer()
+        text_buffer.set_text('')
+        
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uiutils/trees.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,51 @@
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+"""this module defines some treeview / treemodels utilities"""
+
+__revision__ = '$Id: trees.py,v 1.6 2004-12-16 09:44:52 syt Exp $'
+__metaclass__ = type
+
+import gtk
+        
+
+def init_treeview_columns(treeview, titles,  displayed=None, sort_info=None,
+                          keep_existing=False, **kwargs):
+    """initalizes all the treeviews columns
+
+    keep_exsiting must be set to True if existing columns must not be removed
+    sort_info is an optional list where the element with index <n> is the
+    column_id used to sort column <n>.
+    If column_id equals 0, gtk will use its default (trivial) sort function.
+    """
+    if not keep_existing:
+        for column in treeview.get_columns():
+            treeview.remove_column(column)
+    for col_index, col_name in enumerate(titles):
+        if displayed is not None and col_name not in displayed:
+            continue
+        column = gtk.TreeViewColumn(col_name, gtk.CellRendererText(),
+                                    text=col_index, **kwargs)
+        column.set_resizable(True)
+        if sort_info is not None:
+            sort_id = sort_info[col_index]
+            if sort_id == 0:
+                column.set_sort_column_id(col_index)
+            else:
+                column.set_sort_column_id(sort_id)
+            column.set_sort_indicator(True)
+        treeview.append_column(column)
+        
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uiutils/windows.py	Wed Apr 26 10:48:09 2006 +0000
@@ -0,0 +1,311 @@
+# -*- coding: ISO-8859-1 -*-
+# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
+# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This program 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; either version 2 of the License, or (at your option) any later
+# version.
+#
+# This program 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
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+
+"""some user interface utilities"""
+
+__revision__ = '$Id: windows.py,v 1.10 2004-12-13 17:22:12 syt Exp $'
+
+import commands
+import os.path as osp
+
+import gtk, gobject
+import gtk.glade
+
+from pigg.utils import update_gui, confirm
+from logilab.common import logservice
+
+from oobrother import get_path, OOBLogger
+from oobrother.uiutils.basemixins import DelegatedWindowMixIn, TreeViewMixIn, \
+     TextWindowMixIn, MenuControlledWindowMixIn, PluggableFrameMixIn, \
+     WindowMixIn
+from oobrother.uiutils.trees import init_treeview_columns
+from oobrother.config_tools import PluggableConfig, build_config_model, \
+     configuration_models, GlobalConfigurationModel
+
+
+__metaclass__ = type
+
+
+class ConfigurableListWindow(DelegatedWindowMixIn, TreeViewMixIn):
+    """a user configurable list window
+
+    Handling of a max items number is triggered by the presence of a
+    "default_max_items" attribute, giving a positive int a default
+    maximum number of items in the list.
+    
+    FIXME:
+    * afficher le nombre de lignes dans une barre d'etat
+    """
+    options = ()
+    
+    def __init__(self, config_file, store_desc, column_titles, **kwargs):
+        self.options += (
+            ('columns', {'type': 'multiple_choice', 'group': 'display',
+                         'choices': column_titles, 'default': column_titles}),
+            )
+        if hasattr(self, 'default_max_items'):
+            self.options += (
+                ('max-items', {'type': 'int', 'group': 'display',
+                               'default': self.default_max_items}),
+                )
+        self.cfg = PluggableConfig(self.name, config_file, self.options)
+        self.cfg.model.register_commit_notification(self.cb_config_changed)
+        self.treeview = gtk.TreeView()
+        super(ConfigurableListWindow, self).__init__(**kwargs)
+        self.column_titles = column_titles
+        self.__sort_info = None
+        self.__kwargs = None
+        treestore = gtk.ListStore(*store_desc)
+        scr = gtk.ScrolledWindow()
+        scr.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        self.__vbox = vbox = gtk.VBox(False)
+        vbox.pack_end(scr)
+        self.win.add(vbox)
+        self.treeview.set_model(treestore)
+        scr.add(self.treeview)
+        self.win.connect('delete-event', self.hide)
+
+    def subconfiguration(self):
+        """return a list of configuration nodes for subconfiguration
+        (i.e. nothing here)
+        """
+        return []
+
+    def init_columns(self, displayed, sort_info=None, **kwargs):
+        """init the displayed tree view column"""
+        if self.__kwargs is None:
+            # remember first call arguments
+            self.__sort_info = sort_info
+            self.__kwargs = kwargs
+        sort_info = sort_info or self.__sort_info
+        init_treeview_columns(self.treeview, self.column_titles,
+                              displayed, self.__sort_info, **self.__kwargs)
+
+    # FIXME: should maybe go in DelegatedWindowMixIn
+    def create_toolbar(self):
+        """insert and return a default toolbar with a Clear button"""
+        toolbar = gtk.Toolbar()
+        toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL)
+        toolbar.set_style(gtk.TOOLBAR_BOTH)
+        clear_button = gtk.ToolButton(gtk.STOCK_CLEAR)
+        clear_button.connect('clicked', self.clear)
+        tip =  gtk.Tooltips()
+        tip.set_tip(clear_button, _("clear all messages in the window"), 'Private')
+        clear_button.set_tooltip(tip)
+        toolbar.insert(clear_button, 0)
+##         toolbar.append_widget(clear_button, "clear all messages in the window",
+##                               'Private')
+        self.__vbox.pack_start(toolbar, False, False)
+        return toolbar
+        
+    def append_line(self, line, scroll_to_line=False, update=True):
+        """append a line to the store"""
+        # be sure not to exceed the message queue size
+        store = self.treeview.get_model()
+        max_item = self.cfg.get('max-items')
+        if max_item and len(store) == max_item:
+            first_line_iter = store.get_iter((0,))
+            store.remove(first_line_iter)
+        treeiter = store.append(line)
+        if scroll_to_line:
+            path = store.get_path(treeiter)
+            self.treeview.scroll_to_cell(path)
+        if update:
+            update_gui()
+
+    def cb_config_changed(self):
+        """the configuration has changed, the list columns may have changed"""
+        self.init_columns(self.cfg['columns'])
+
+
+class ConfigurationWindow(WindowMixIn, MenuControlledWindowMixIn, PluggableFrameMixIn):
+    """the configuration window is used to edit the application and
+    plugins configurations
+    """
+    
+    def __init__(self, configurables):
+        super(ConfigurationWindow, self).__init__()
+        self.widgets = gtk.glade.XML(get_path('glade'), 'config_window', 'oobrother')
+        self.win = self.widgets.get_widget('config_window')
+        self.win.connect('response', self.cb_response)
+        self.win.set_default_size(600, 500)
+        self.set_title('OOBrother - Configuration')
+        self.treeview = self.widgets.get_widget('config_treeview')
+        treeselection = self.treeview.get_selection()
+        treeselection.set_select_function(self.selection_changed)
+        self.plugin_frame = self.widgets.get_widget('config_frame')
+        # init columns / set model
+        init_treeview_columns(self.treeview, ['Section'])
+        treestore = build_config_model(configurables)
+        self.treeview.set_model(treestore)
+        # main callbacks
+        # handlers = {'on_config_treeview_button_press_event' :
+        #             self.treeview_button_pressed,
+        #             #'response', self.ctrl.cb_response,
+        #             }
+        # self.widgets.signal_autoconnect(handlers)
+        self._model = GlobalConfigurationModel(
+            configuration_models(configurables))
+        
+    def selection_changed(self, path):#selection, model, path, is_selected):
+        """selection-changed callback, sets the plugin widget"""
+        model = self.treeview.get_model()
+        configurable = model[path][1]
+        if configurable.cfg and configurable.cfg.wdg:
+            # FIXME
+            configurable.cfg.model.notify_all()
+        self.set_plugin_widget(configurable.cfg and configurable.cfg.wdg)
+        return gtk.TRUE
+
+    def cb_response(self, widget, response):
+        """handle 'response' events"""
+        if response ==  gtk.RESPONSE_OK:
+            self._model.commit()
+            widget.hide()
+        elif response ==  gtk.RESPONSE_APPLY:
+            self._model.commit()
+        elif response ==  gtk.RESPONSE_HELP:
+            pass
+        elif response ==  gtk.RESPONSE_CANCEL:
+            return self.close_window(widget)
+        elif response ==  gtk.RESPONSE_DELETE_EVENT:
+            return self.close_window(widget)
+        else:
+            assert False, 'UNKOWN RESPONSE %s' % response
+
+    def close_window(self, widget):
+        """close the window if there is no pending modifications, else
+        ask for confirmation
+        """
+        if self._model.get_modifications():
+            if confirm('There are some unsaved modifications. Close anyway ?'):
+                # FIXME: restore original values ?
+                self._model.rollback()
+                widget.hide()
+        else:
+            widget.hide()
+        return gtk.TRUE
+
+
+class LogServiceWindow(MenuControlledWindowMixIn, ConfigurableListWindow):
+    """a window to display logged messages"""
+    name = 'messages'
+    options = (('log-threshold', {'type': 'int', 'default': LOG_DEBUG}),
+               )
+    default_max_items = 100
+    
+    def __init__(self):
+        super(LogServiceWindow, self).__init__(
+            config_file='messages_display.ini',
+            store_desc=(gobject.TYPE_STRING, # time
+                        gobject.TYPE_STRING, # message type
+                        gobject.TYPE_STRING, # message
+                        gobject.TYPE_STRING, # color
+                        ),
+            column_titles=('Time', 'Type', 'Message'))
+        self.set_title('OOBrother - Log messages')
+        self.win.set_default_size(600, 200)
+        self.create_toolbar()
+        self.init_columns(self.cfg['columns'], foreground=3)
+        logservice.init_log(self.cfg['log-threshold'], logger=OOBLogger(self))
+
+
+
+class SystemCommandWindow(TextWindowMixIn, MenuControlledWindowMixIn):
+    """a mixin extending the TextWindowMixIn to add a `execute` method
+    that may by use to execute an arbitrary command and get its output
+    in the text view.
+    """
+
+    def __init_(self, **kwargs):
+        super(SystemWindow, self).__init__(**kwargs)
+        self.set_title('OOBrother - Simple Terminal')
+        self.win.set_size_request(600, 400)
+
+    def execute(self, command):
+        """execute a python file"""
+        self.write('>>> %s\n' % command)
+        status, output = commands.getstatusoutput(command)
+        for line in output.splitlines():
+            self.write(line + '\n')
+        return status
+    
+    def flush(self):
+        """from the file interface"""
+        update_gui()
+
+try:
+    import vte
+
+    class SystemWindow(DelegatedWindowMixIn, MenuControlledWindowMixIn): # ConfigurableListWindow, MenuControlledWindowMixIn):
+        """a terminal emulator"""
+        name = 'ooterm'
+        options = (('font', {'type' : 'font', 'default' : "fixed 10"}),
+                   ('background', {'type' : 'file', 'default' : None}),
+                   ('foreground', {'type' : 'color', 'default': '#000000'}),
+                   )
+        def __init__(self, **kwargs):
+            super(SystemWindow, self).__init__(**kwargs)
+            self.cfg = PluggableConfig(self.name, 'ooterm_display.ini',
+                                       self.options)
+            self.cfg.model.register_commit_notification(self.cb_config_changed)
+            self.set_title('OOBrother - Terminal')
+            self._term = vte.Terminal()
+            self._term.set_emulation('xterm')
+            self._term.fork_command()
+            self._term.set_size_request(200, 200)
+            self.win.set_default_size(600, 400)
+            self.win.add(self._term)
+            self.win.connect('delete-event', self.hide)
+            # Update term prefs with initial config
+            self.cb_config_changed()
+
+        def subconfiguration(self):
+            """return a list of configuration nodes for subconfiguration
+            (i.e. nothing here)
+            """
+            return []
+
+        def execute(self, command):
+            """execute command"""
+            self.show()
+            self.win.grab_focus()
+            self._term.feed_child(command + '\n')
+
+        def _set_font(self, font_name):
+            """uses <font_name> to set terminal's font"""
+            import pango
+            font_desc = pango.FontDescription(font_name)
+            # FIXME: stretch is not taken in account !
+            font_desc.set_stretch(pango.STRETCH_ULTRA_EXPANDED)
+            self._term.set_font_full(font_desc, True)
+
+        def cb_config_changed(self):
+            """config has changed, update terminal preferences"""
+            pix_file = self.cfg['background']
+            if pix_file is not None:
+                self._term.set_background_image_file(pix_file)
+                self._term.set_background_saturation(0.2)
+            font_name = self.cfg['font']
+            self._set_font(font_name)
+            gdk_color = gtk.gdk.color_parse(self.cfg['foreground'])
+            self._term.set_color_foreground(gdk_color)
+            
+except ImportError:
+    log(LOG_WARN, 'python-vte not found, full shell won\'t be available')
+    SystemWindow = SystemCommandWindow
+