# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2009 Benny Malengier # # 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 # # $Id$ """ Module providing support for callback handling in the GUI * track object handles * register new handles * manage callback functions """ #------------------------------------------------------------------------- # # Python modules # #------------------------------------------------------------------------- #------------------------------------------------------------------------- # # Constants # #------------------------------------------------------------------------- PERSONKEY = 'person' FAMILYKEY = 'family' EVENTKEY = 'event' PLACEKEY = 'place' MEDIAKEY = 'media' SOURCEKEY = 'source' REPOKEY = 'repository' NOTEKEY = 'note' ADD = '-add' UPDATE = '-update' DELETE = '-delete' REBUILD = '-rebuild' KEYS = [PERSONKEY, FAMILYKEY, EVENTKEY, PLACEKEY, MEDIAKEY, SOURCEKEY, REPOKEY, NOTEKEY] METHODS = [ADD, UPDATE, DELETE, REBUILD] METHODS_LIST = [ADD, UPDATE, DELETE] METHODS_NONE = [REBUILD] PERSONCLASS = 'Person' FAMILYCLASS = 'Family' EVENTCLASS = 'Event' PLACECLASS = 'Place' MEDIACLASS = 'MediaObject' SOURCECLASS = 'Source' REPOCLASS = 'Repository' NOTECLASS = 'Note' CLASS2KEY = { PERSONCLASS: PERSONKEY, FAMILYCLASS: FAMILYKEY, EVENTCLASS: EVENTKEY, PLACECLASS: PLACEKEY, MEDIACLASS: MEDIAKEY, SOURCECLASS: SOURCEKEY, REPOCLASS: REPOKEY, NOTECLASS: NOTEKEY } def _return(*args): """ Function that does nothing with the arguments """ return True #------------------------------------------------------------------------- # # CallbackManager class # #------------------------------------------------------------------------- class CallbackManager(object): """ Manage callback handling from GUI to the db. It is unique to a db and some GUI element. When a db is changed, one should destroy the CallbackManager and set up a new one (or delete the GUI element as it shows info from a previous db). Track changes to your relevant objects, calling callback functions as needed. """ def __init__(self, database): """ :param database: database to which to connect the callbacks of this CallbackManager object :type database: a class:`~gen.db.base.DbBase` object """ #no handles to track self.database = database self.__handles = { PERSONKEY: [], FAMILYKEY: [], EVENTKEY: [], PLACEKEY: [], MEDIAKEY: [], SOURCEKEY: [], REPOKEY: [], NOTEKEY: [], } #no custom callbacks to do self.custom_signal_keys = [] #set up callbacks to do nothing self.__callbacks = {} self.__init_callbacks() def __init_callbacks(self): """ set up callbacks to do nothing """ self.__callbacks = {} for key in KEYS: for method in METHODS: self.__callbacks[key+method] = [_return, None] def disconnect_all(self): """ Disconnect from all signals from the database This method should always be called before a the callback methods become invalid. """ for key in self.custom_signal_keys: self.database.disconnect(key) self.custom_signal_keys = [] for key, value in self.__callbacks.iteritems(): if not value[1] is None: self.database.disconnect(value[1]) self.__init_callbacks() def register_obj(self, baseobj, directonly=False): """ Convenience method, will register all directly and not directly referenced prim objects connected to baseobj with the CallbackManager If directonly is True, only directly registered objects will be registered. Note that baseobj is not registered itself as it can be a sec obj. """ if directonly: self.register_handles(directhandledict(baseobj)) else: self.register_handles(handledict(baseobj)) def register_handles(self, ahandledict): """ Register handles that need to be tracked by the manager. This function can be called several times, adding to existing registered handles. :param ahandledict: a dictionary with key one of the KEYS, and value a list of handles to track """ for key in KEYS: handles = ahandledict.get(key) if handles: self.__handles[key] = list( set(self.__handles[key]).union(handles)) def unregister_handles(self, ahandledict): """ All handles in handledict are no longer tracked :param handledict: a dictionary with key one of the KEYS, and value a list of handles to track """ for key in KEYS: handles = ahandledict.get(key) if handles: for handle in handles: self.__handles[key].remove(handle) def unregister_all(self): """ Unregister all handles that are registered """ self.__handles = { PERSONKEY: [], FAMILYKEY: [], EVENTKEY: [], PLACEKEY: [], MEDIAKEY: [], SOURCEKEY: [], REPOKEY: [], NOTEKEY: [], } def register_callbacks(self, callbackdict): """ register callback functions that need to be called for a specific db action. This function can be called several times, adding to and if needed overwriting, existing callbacks. No db connects are done. If a signal already is connected to the db, it is removed from the connect list of the db. :param callbackdict: a dictionary with key one of KEYS+METHODS, or one of KEYS, and value a function to be called when signal is raised. """ for key in KEYS: function = callbackdict.get(key) if function: for method in METHODS: self.__add_callback(key+method, function) for method in METHODS: function = callbackdict.get(key+method) if function: self.__add_callback(key+method, function) def connect_all(self, keys=None): """ Convenience function, connects all database signals related to the primary objects given in keys to the callbacks attached to self. Note that only those callbacks registered with register_callbacks will effectively result in an action, so one can connect to all keys even if not all keys have a registered callback. :param keys: list of keys of primary objects for which to connect the signals, default is no connects being done. One can enable signal activity to needed objects by passing a list, eg keys=[callman.SOURCEKEY, callman.PLACEKEY], or to all with keys=callman.KEYS """ if keys is None: return for key in keys: for method in METHODS_LIST: signal = key + method self.__do_unconnect(signal) self.__callbacks[signal][1] = self.database.connect( signal, self.__callbackcreator(signal)) for method in METHODS_NONE: signal = key + method self.__do_unconnect(signal) self.__callbacks[signal][1] = self.database.connect( signal, self.__callbackcreator(signal, noarg=True)) def __do_callback(self, signal, *arg): """ Execute a specific callback. This is only actually done if one of the registered handles is involved. Arg must conform to the requirements of the signal emitter. For a DbBase that is that arg must be not given (rebuild methods), or arg[0] must be the list of handles affected. """ key = signal.split('-')[0] if arg: handles = arg[0] affected = list(set(self.__handles[key]).intersection(handles)) if affected: self.__callbacks[signal][0](affected) else: affected = self.__handles[key] if affected: self.__callbacks[signal][0]() def __add_callback(self, signal, callback): """ Add a callback to a signal. There can be only one callback per signal that is managed, so if there is a previous one, it is removed """ self.__do_unconnect(signal) self.__callbacks[signal] = [callback, None] def __do_unconnect(self, signal): """ unconnect a signal from the database if it is already connected """ oldcall, oldconnectkey = self.__callbacks[signal] if not oldconnectkey is None: self.database.disconnect(oldconnectkey) def add_db_signal(self, name, callback): """ Do a custom db connect signal outside of the primary object ones managed automatically. """ self.custom_signal_keys.append(self.database.connect(name, callback)) def __callbackcreator(self, signal, noarg=False): """ helper function, a lambda function needs a string to be defined explicitly. This function creates the correct lambda function to use as callback based on the key/signal one needs to connect to. AttributeError is raised for unknown key or signal. """ def gen(self, signal): """ Generate lambda function that does call with an argument """ return lambda arg: self.__do_callback(signal, *(arg,)) def gen_noarg(self, signal): """ Generate lambda function that does call without argument """ return lambda *arg: self.__do_callback(signal) if signal in self.__callbacks: if noarg: return gen_noarg(self, signal) else: return gen(self, signal) else: raise AttributeError, 'Signal ' + signal + 'not supported.' def directhandledict(baseobj): """ Build a handledict from baseobj with all directly referenced objects """ handles = { PERSONKEY: [], FAMILYKEY: [], EVENTKEY: [], PLACEKEY: [], MEDIAKEY: [], SOURCEKEY: [], REPOKEY: [], NOTEKEY: [], } for classn, handle in baseobj.get_referenced_handles(): handles[CLASS2KEY[classn]].append(handle) return handles def handledict(baseobj): """ Build a handledict from baseobj with all directly and not directly referenced base obj that are present """ handles = { PERSONKEY: [], FAMILYKEY: [], EVENTKEY: [], PLACEKEY: [], MEDIAKEY: [], SOURCEKEY: [], REPOKEY: [], NOTEKEY: [], } for classn, handle in baseobj.get_referenced_handles_recursively(): handles[CLASS2KEY[classn]].append(handle) return handles