gramps/gramps/gen/utils/callman.py
2013-03-23 22:26:35 +00:00

378 lines
13 KiB
Python

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2009 Benny Malengier
# Copyright (C) 2011 Tim G L Lyons
#
# 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'
CITATIONKEY = 'citation'
REPOKEY = 'repository'
NOTEKEY = 'note'
TAGKEY = 'tag'
ADD = '-add'
UPDATE = '-update'
DELETE = '-delete'
REBUILD = '-rebuild'
KEYS = [PERSONKEY, FAMILYKEY, EVENTKEY, PLACEKEY, MEDIAKEY, SOURCEKEY,
CITATIONKEY, REPOKEY, NOTEKEY, TAGKEY]
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'
CITATIONCLASS = 'Citation'
REPOCLASS = 'Repository'
NOTECLASS = 'Note'
TAGCLASS = 'Tag'
CLASS2KEY = {
PERSONCLASS: PERSONKEY,
FAMILYCLASS: FAMILYKEY,
EVENTCLASS: EVENTKEY,
PLACECLASS: PLACEKEY,
MEDIACLASS: MEDIAKEY,
SOURCECLASS: SOURCEKEY,
CITATIONCLASS: CITATIONKEY,
REPOCLASS: REPOKEY,
NOTECLASS: NOTEKEY,
TAGCLASS: TAGKEY
}
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: [],
CITATIONKEY: [],
REPOKEY: [],
NOTEKEY: [],
TAGKEY: [],
}
#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.
"""
list(map(self.database.disconnect, self.custom_signal_keys))
self.custom_signal_keys = []
for key, value in self.__callbacks.items():
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:
list(map(self.__handles[key].remove, handles))
def unregister_all(self):
"""
Unregister all handles that are registered
"""
self.__handles = {
PERSONKEY: [],
FAMILYKEY: [],
EVENTKEY: [],
PLACEKEY: [],
MEDIAKEY: [],
SOURCEKEY: [],
CITATIONKEY: [],
REPOKEY: [],
NOTEKEY: [],
TAGKEY: [],
}
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: [],
CITATIONKEY: [],
REPOKEY: [],
NOTEKEY: [],
TAGKEY: [],
}
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: [],
CITATIONKEY: [],
REPOKEY: [],
NOTEKEY: [],
TAGKEY: [],
}
for classn, handle in baseobj.get_referenced_handles_recursively():
handles[CLASS2KEY[classn]].append(handle)
return handles