5adc2102a2
svn: r21727
378 lines
13 KiB
Python
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
|
|
|