From ee69317b6243d532a336544560945c43b251aec5 Mon Sep 17 00:00:00 2001 From: Benny Malengier Date: Wed, 5 Aug 2009 10:32:05 +0000 Subject: [PATCH] Fix for: 1277: database corroption on delete outside of DisplayTabs while tab open Introduces the concept of callman.py as one single way to follow handles an interface is interested in. dbguielement.py contains a small base class using that, usable for all windows/ guielements that need to track database changes to handles svn: r12881 --- po/POTFILES.in | 6 +- src/DisplayTabs/_EmbeddedList.py | 3 +- src/DisplayTabs/_EventEmbedList.py | 60 +++- src/DisplayTabs/_GalleryTab.py | 24 +- src/DisplayTabs/_GrampsTab.py | 23 -- src/DisplayTabs/_NoteTab.py | 21 +- src/DisplayTabs/_RepoEmbedList.py | 54 +++- src/DisplayTabs/_SourceEmbedList.py | 68 ++++- src/Editors/_EditChildRef.py | 17 ++ src/Editors/_EditEvent.py | 8 + src/Editors/_EditEventRef.py | 8 + src/Editors/_EditFamily.py | 119 +++++--- src/Editors/_EditMedia.py | 8 + src/Editors/_EditMediaRef.py | 8 + src/Editors/_EditNote.py | 10 +- src/Editors/_EditPerson.py | 10 +- src/Editors/_EditPersonRef.py | 17 ++ src/Editors/_EditPlace.py | 8 + src/Editors/_EditPrimary.py | 60 +++- src/Editors/_EditReference.py | 35 ++- src/Editors/_EditRepoRef.py | 10 +- src/Editors/_EditRepository.py | 8 + src/Editors/_EditSecondary.py | 26 +- src/Editors/_EditSource.py | 8 + src/Editors/_EditSourceRef.py | 10 + src/gen/db/base.py | 4 + src/gen/utils/Makefile.am | 1 + src/gen/utils/callman.py | 431 ++++++++++++++++++++++++++++ src/gui/Makefile.am | 1 + src/gui/dbguielement.py | 89 ++++++ 30 files changed, 1048 insertions(+), 107 deletions(-) create mode 100644 src/gen/utils/callman.py create mode 100644 src/gui/dbguielement.py diff --git a/po/POTFILES.in b/po/POTFILES.in index adbe43340..cb728e2b8 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -56,11 +56,10 @@ src/cli/grampscli.py src/gen/__init__.py # gen utils API -src/gen/utils/dbutils.py -src/gen/utils/progressmon.py src/gen/utils/__init__.py -src/gen/utils/dbutils.py src/gen/utils/callback.py +src/gen/utils/callman.py +src/gen/utils/dbutils.py src/gen/utils/longop.py src/gen/utils/progressmon.py @@ -182,6 +181,7 @@ src/gen/plug/docbackend/docbackend.py # gui - GUI code src/gui/__init__.py +src/gui/dbguielement.py src/gui/dbloader.py src/gui/dbman.py src/gui/grampsgui.py diff --git a/src/DisplayTabs/_EmbeddedList.py b/src/DisplayTabs/_EmbeddedList.py index 4c30ceec5..ee12041f4 100644 --- a/src/DisplayTabs/_EmbeddedList.py +++ b/src/DisplayTabs/_EmbeddedList.py @@ -495,7 +495,8 @@ class EmbeddedList(ButtonTab): """ The view must be remade when data changes outside this tab. Use this method to connect to after a db change. It makes sure the - data is obtained again from db and the view rebuild + data is obtained again from the present object and the db what is not + present in the obj, and the view rebuild """ self.changed = True self.rebuild() diff --git a/src/DisplayTabs/_EventEmbedList.py b/src/DisplayTabs/_EventEmbedList.py index 5023ac9ee..3c7e0e04c 100644 --- a/src/DisplayTabs/_EventEmbedList.py +++ b/src/DisplayTabs/_EventEmbedList.py @@ -39,13 +39,14 @@ import Errors from DdTargets import DdTargets from _GroupEmbeddedList import GroupEmbeddedList from _EventRefModel import EventRefModel +from gui.dbguielement import DbGUIElement #------------------------------------------------------------------------- # # EventEmbedList # #------------------------------------------------------------------------- -class EventEmbedList(GroupEmbeddedList): +class EventEmbedList(DbGUIElement, GroupEmbeddedList): _HANDLE_COL = 7 _DND_TYPE = DdTargets.EVENTREF @@ -86,9 +87,55 @@ class EventEmbedList(GroupEmbeddedList): self.obj = obj self._groups = [] self._data = [] + DbGUIElement.__init__(self, dbstate.db) GroupEmbeddedList.__init__(self, dbstate, uistate, track, _('_Events'), build_model, share_button=True, move_buttons=True) + + def _connect_db_signals(self): + """ + called on init of DbGUIElement, connect to db as required. + """ + #note: event-rebuild closes the editors, so no need to connect to it + self.callman.register_callbacks( + {'event-update': self.event_change, #change to an event we track + 'event-delete': self.event_delete, #delete of event we track + }) + self.callman.connect_all(keys=['event']) + + def event_change(self, *obj): + """ + Callback method called when a tracked event changes (description + changes, source added, ...) + Note that adding an event + """ + self.rebuild_callback() + + def event_delete(self, obj): + """ + Callback method called when a tracked event is deleted. + There are two possibilities: + * a tracked non-workgroup event is deleted, just rebuilding the view + will correct this. + * a workgroup event is deleted. The event must be removed from the obj + so that no inconsistent data is shown. + """ + for handle in obj: + refs = self.get_data()[self._WORKGROUP] + ref_list = [eref.ref for eref in refs] + indexlist = [] + last = 0 + while True: + try: + last = ref_list.index(handle) + indexlist.append(last) + except ValueError: + break + #remove the deleted workgroup events from the object + for index in indexlist.reverse(): + del refs[index] + #now rebuild the display tab + self.rebuild_callback() def get_ref_editor(self): from Editors import EditFamilyEventRef @@ -118,6 +165,10 @@ class EventEmbedList(GroupEmbeddedList): if mdata: self._groups.append((mhandle, self._MOTHNAME)) self._data.append(mdata) + #we register all events that need to be tracked + for group in self._data: + self.callman.register_handles( + {'event': [eref.ref for eref in group]}) self.changed = False return self._data @@ -195,10 +246,17 @@ class EventEmbedList(GroupEmbeddedList): def object_added(self, reference, primary): reference.ref = primary.handle self.get_data()[self._WORKGROUP].append(reference) + self.callman.register_handles({'event': [primary.handle]}) self.changed = True self.rebuild() def object_edited(self, ref, event): + """ + Called as callback after eventref has been edited. + Note that if the event changes too (so not only the ref data), then + an event-update signal from the database will also be raised, and the + rebuild done here will not be needed. There is no way to avoid this ... + """ self.changed = True self.rebuild() diff --git a/src/DisplayTabs/_GalleryTab.py b/src/DisplayTabs/_GalleryTab.py index eca213d99..975cb9807 100644 --- a/src/DisplayTabs/_GalleryTab.py +++ b/src/DisplayTabs/_GalleryTab.py @@ -46,6 +46,7 @@ import gobject # #------------------------------------------------------------------------- from gui.utils import open_file_with_default_application +from gui.dbguielement import DbGUIElement import gen.lib import Utils import ThumbNails @@ -67,7 +68,7 @@ def make_launcher(path): # GalleryTab # #------------------------------------------------------------------------- -class GalleryTab(ButtonTab): +class GalleryTab(ButtonTab, DbGUIElement): _DND_TYPE = DdTargets.MEDIAREF _DND_EXTRA = DdTargets.URI_LIST @@ -75,8 +76,11 @@ class GalleryTab(ButtonTab): def __init__(self, dbstate, uistate, track, media_list, update=None): self.iconlist = gtk.IconView() ButtonTab.__init__(self, dbstate, uistate, track, _('_Gallery'), True) + DbGUIElement.__init__(self, dbstate.db) self.track_ref_for_deletion("iconlist") self.media_list = media_list + self.callman.register_handles({'media': [mref.ref for mref + in self.media_list]}) self.update = update self._set_dnd() @@ -84,11 +88,16 @@ class GalleryTab(ButtonTab): self.rebuild() self.show_all() - def connect_db_signals(self): - #connect external remove/change of object to rebuild of grampstab - self._add_db_signal('media-delete', self.media_delete) - self._add_db_signal('media-rebuild', self.rebuild) - self._add_db_signal('media-update', self.media_update) + def _connect_db_signals(self): + """ + Implement base class DbGUIElement method + """ + #note: media-rebuild closes the editors, so no need to connect to it + self.callman.register_callbacks( + {'media-delete': self.media_delete, # delete a mediaobj we track + 'media-update': self.media_update, # change a mediaobj we track + }) + self.callman.connect_all(keys=['media']) def double_click(self, obj, event): """ @@ -259,12 +268,13 @@ class GalleryTab(ButtonTab): def add_callback(self, media_ref, media): media_ref.ref = media.handle self.get_data().append(media_ref) + self.callman.register_handles({'media': [media.handle]}) self.changed = True self.rebuild() def share_button_clicked(self, obj): """ - Function called when the Add button is clicked. + Function called when the Share button is clicked. This function should be overridden by the derived class. diff --git a/src/DisplayTabs/_GrampsTab.py b/src/DisplayTabs/_GrampsTab.py index bd8a156a6..b1b5752d0 100644 --- a/src/DisplayTabs/_GrampsTab.py +++ b/src/DisplayTabs/_GrampsTab.py @@ -73,8 +73,6 @@ class GrampsTab(gtk.VBox): self.changed = False self.__refs_for_deletion = [] - self._add_db_signal = None - # save name used for notebook label, and build the widget used # for the label @@ -168,19 +166,6 @@ class GrampsTab(gtk.VBox): return return True - def add_db_signal_callback(self, add_db_signal): - """ - The grampstab must be able to react to database signals, however - on destroy of the editor to which the tab is attached, these signals - must be disconnected. - This method sets the method with which to add database signals on tabs, - typically EditPrimary and EditSecondary add tabs, and have methods to - connect signals and register them so they are correctly disconnected - on close - """ - self._add_db_signal = add_db_signal - self.connect_db_signals() - def _set_label(self, show_image=True): """ Updates the label based of if the tab contains information. Tabs @@ -208,14 +193,6 @@ class GrampsTab(gtk.VBox): can be used to add widgets to the interface. """ pass - - def connect_db_signals(self): - """ - Function to connect db signals to GrampsTab methods. This function - should be overridden in the derived class. - It is called after the interface is build. - """ - pass def set_parent_notebook(self, book): self.parent_notebook = book diff --git a/src/DisplayTabs/_NoteTab.py b/src/DisplayTabs/_NoteTab.py index fef18dd1a..2a7af9fbf 100644 --- a/src/DisplayTabs/_NoteTab.py +++ b/src/DisplayTabs/_NoteTab.py @@ -40,6 +40,7 @@ from gettext import gettext as _ #------------------------------------------------------------------------- import Errors import gen.lib +from gui.dbguielement import DbGUIElement from _NoteModel import NoteModel from _EmbeddedList import EmbeddedList from DdTargets import DdTargets @@ -49,7 +50,7 @@ from DdTargets import DdTargets # NoteTab # #------------------------------------------------------------------------- -class NoteTab(EmbeddedList): +class NoteTab(EmbeddedList, DbGUIElement): """ Note List display tab for edit dialogs. @@ -83,12 +84,19 @@ class NoteTab(EmbeddedList): EmbeddedList.__init__(self, dbstate, uistate, track, _("_Notes"), NoteModel, share_button=True, move_buttons=True) + DbGUIElement.__init__(self, dbstate.db) + self.callman.register_handles({'note': self.data}) - def connect_db_signals(self): - #connect external remove/change of object to rebuild of grampstab - self._add_db_signal('note-delete', self.note_delete) - self._add_db_signal('note-rebuild', self.rebuild) - self._add_db_signal('note-update',self.note_update) + def _connect_db_signals(self): + """ + Implement base class DbGUIElement method + """ + #note: note-rebuild closes the editors, so no need to connect to it + self.callman.register_callbacks( + {'note-delete': self.note_delete, # delete a note we track + 'note-update': self.note_update, # change a note we track + }) + self.callman.connect_all(keys=['note']) def get_editor(self): pass @@ -133,6 +141,7 @@ class NoteTab(EmbeddedList): Called to update the screen when a new note is added """ self.get_data().append(name) + self.callman.register_handles({'note': [name]}) self.changed = True self.rebuild() diff --git a/src/DisplayTabs/_RepoEmbedList.py b/src/DisplayTabs/_RepoEmbedList.py index 6b67631f0..fc6220aaa 100644 --- a/src/DisplayTabs/_RepoEmbedList.py +++ b/src/DisplayTabs/_RepoEmbedList.py @@ -33,6 +33,7 @@ from gettext import gettext as _ # #------------------------------------------------------------------------- import gen.lib +from gui.dbguielement import DbGUIElement import Errors from DdTargets import DdTargets from _RepoRefModel import RepoRefModel @@ -43,7 +44,7 @@ from _EmbeddedList import EmbeddedList # RepoEmbedList # #------------------------------------------------------------------------- -class RepoEmbedList(EmbeddedList): +class RepoEmbedList(EmbeddedList, DbGUIElement): _HANDLE_COL = 4 _DND_TYPE = DdTargets.REPOREF @@ -72,6 +73,20 @@ class RepoEmbedList(EmbeddedList): EmbeddedList.__init__(self, dbstate, uistate, track, _('_Repositories'), RepoRefModel, share_button=True, move_buttons=True) + DbGUIElement.__init__(self, dbstate.db) + self.callman.register_handles({'repository': [rref.ref for rref + in self.obj]}) + + def _connect_db_signals(self): + """ + Implement base class DbGUIElement method + """ + #note: repository-rebuild closes the editors, so no need to connect + self.callman.register_callbacks( + {'repository-delete': self.repo_delete, # delete a repo we track + 'repository-update': self.repo_update, # change a repo we track + }) + self.callman.connect_all(keys=['repository']) def get_icon_name(self): return 'gramps-repository' @@ -135,6 +150,7 @@ class RepoEmbedList(EmbeddedList): def add_callback(self, value): value[0].ref = value[1].handle self.get_data().append(value[0]) + self.callman.register_handles({'repository': [value[1].handle]}) self.changed = True self.rebuild() @@ -164,3 +180,39 @@ class RepoEmbedList(EmbeddedList): def edit_callback(self, name): self.changed = True self.rebuild() + + def repo_delete(self, del_repo_handle_list): + """ + Outside of this tab repo objects have been deleted. Check if tab + and object must be changed. + Note: delete of object will cause reference on database to be removed, + so this method need not do this + """ + rebuild = False + ref_handles = [rref.ref for rref in self.obj] + for handle in del_repo_handle_list : + while 1: + pos = None + try : + pos = ref_handles.index(handle) + except ValueError : + break + + if pos is not None: + #oeps, we need to remove this reference, and rebuild tab + del self.obj[pos] + del ref_handles[pos] + rebuild = True + if rebuild: + self.rebuild() + + def repo_update(self, upd_repo_handle_list): + """ + Outside of this tab repo objects have been changed. Check if tab + and object must be changed. + """ + ref_handles = [rref.ref for rref in self.obj] + for handle in upd_repo_handle_list : + if handle in ref_handles: + self.rebuild() + break diff --git a/src/DisplayTabs/_SourceEmbedList.py b/src/DisplayTabs/_SourceEmbedList.py index 41352cd97..2dd1e5d21 100644 --- a/src/DisplayTabs/_SourceEmbedList.py +++ b/src/DisplayTabs/_SourceEmbedList.py @@ -33,6 +33,7 @@ from gettext import gettext as _ # #------------------------------------------------------------------------- import gen.lib +from gui.dbguielement import DbGUIElement import Errors from DdTargets import DdTargets from _SourceRefModel import SourceRefModel @@ -43,7 +44,7 @@ from _EmbeddedList import EmbeddedList # SourceEmbedList # #------------------------------------------------------------------------- -class SourceEmbedList(EmbeddedList): +class SourceEmbedList(EmbeddedList, DbGUIElement): _HANDLE_COL = 4 _DND_TYPE = DdTargets.SOURCEREF @@ -72,6 +73,20 @@ class SourceEmbedList(EmbeddedList): EmbeddedList.__init__(self, dbstate, uistate, track, _('_Sources'), SourceRefModel, share_button=True, move_buttons=True) + DbGUIElement.__init__(self, dbstate.db) + self.callman.register_handles({'source': [sref.ref for sref + in self.obj.get_source_references()]}) + + def _connect_db_signals(self): + """ + Implement base class DbGUIElement method + """ + #note: source-rebuild closes the editors, so no need to connect to it + self.callman.register_callbacks( + {'source-delete': self.source_delete, # delete a source we track + 'source-update': self.source_update, # change a source we track + }) + self.callman.connect_all(keys=['source']) def get_icon_name(self): return 'gramps-source' @@ -141,12 +156,26 @@ class SourceEmbedList(EmbeddedList): ) def object_added(self, reference, primary): + """ + Callback from sourceref editor after adding a new reference (to a new + or an existing source). + Note that if it was to an existing source already present in the + sourcelist, then the source-update signal will also cause a rebuild + at that time. + """ reference.ref = primary.handle self.get_data().append(reference) + self.callman.register_handles({'source': [primary.handle]}) self.changed = True self.rebuild() def object_edited(self, refererence, primary): + """ + Callback from sourceref editor. If the source changes itself, also + the source-change signal will cause a rebuild. + This could be solved in the source editor if it only calls this + method in the case the sourceref part only changes. + """ self.changed = True self.rebuild() @@ -160,3 +189,40 @@ class SourceEmbedList(EmbeddedList): src, sref, self.object_added) except Errors.WindowActiveError: pass + + def source_delete(self, del_src_handle_list): + """ + Outside of this tab source objects have been deleted. Check if tab + and object must be changed. + Note: delete of object will cause reference on database to be removed, + so this method need not do this + """ + rebuild = False + sourceref_list = self.get_data() + ref_handles = [sref.ref for sref in sourceref_list] + for handle in del_src_handle_list : + while 1: + pos = None + try : + pos = ref_handles.index(handle) + except ValueError : + break + + if pos is not None: + #oeps, we need to remove this reference, and rebuild tab + del sourceref_list[pos] + del ref_handles[pos] + rebuild = True + if rebuild: + self.rebuild() + + def source_update(self, upd_src_handle_list): + """ + Outside of this tab media objects have been changed. Check if tab + and object must be changed. + """ + ref_handles = [sref.ref for sref in self.get_data()] + for handle in upd_src_handle_list : + if handle in ref_handles: + self.rebuild() + break diff --git a/src/Editors/_EditChildRef.py b/src/Editors/_EditChildRef.py index 06e7f9978..90d1a7f44 100644 --- a/src/Editors/_EditChildRef.py +++ b/src/Editors/_EditChildRef.py @@ -129,7 +129,15 @@ class EditChildRef(EditSecondary): self.define_ok_button(self.ok_button, self.save) self.edit_button.connect('button-press-event', self.edit_child) self.edit_button.connect('key-press-event', self.edit_child) + + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ self._add_db_signal('person-update', self.person_change) + self._add_db_signal('person-rebuild', self.close) + self._add_db_signal('person-delete', self.check_for_close) def _create_tabbed_pages(self): """ @@ -185,6 +193,15 @@ class EditChildRef(EditSecondary): self.callback(self.obj) self.close() + def check_for_close(self, handles): + """ + Callback method for delete signals. + If there is a delete signal of the primary object we are editing, the + editor (and all child windows spawned) should be closed + """ + if self.obj.ref in handles: + self.close() + def button_activated(event, mouse_button): if (event.type == gtk.gdk.BUTTON_PRESS and \ event.button == mouse_button) or \ diff --git a/src/Editors/_EditEvent.py b/src/Editors/_EditEvent.py index efd52cd70..f73511bf0 100644 --- a/src/Editors/_EditEvent.py +++ b/src/Editors/_EditEvent.py @@ -116,6 +116,14 @@ class EditEvent(EditPrimary): self.ok_button.set_sensitive(not self.db.readonly) self.ok_button.connect('clicked', self.save) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('event-rebuild', self._do_close) + self._add_db_signal('event-delete', self.check_for_close) + def _setup_fields(self): # place, select_place, add_del_place diff --git a/src/Editors/_EditEventRef.py b/src/Editors/_EditEventRef.py index cee91611d..81707893b 100644 --- a/src/Editors/_EditEventRef.py +++ b/src/Editors/_EditEventRef.py @@ -96,6 +96,14 @@ class EditEventRef(EditReference): # FIXME: activate when help page is available #self.define_help_button(self.top.get_object('help')) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('event-rebuild', self.close) + self._add_db_signal('event-delete', self.check_for_close) + def _setup_fields(self): self.ref_privacy = PrivacyButton( diff --git a/src/Editors/_EditFamily.py b/src/Editors/_EditFamily.py index b09d9ce37..6bf2fe9e4 100644 --- a/src/Editors/_EditFamily.py +++ b/src/Editors/_EditFamily.py @@ -445,47 +445,92 @@ class EditFamily(EditPrimary): def _local_init(self): self.build_interface() - - self._add_db_signal('family-update', self.check_for_family_change) - self._add_db_signal('family-delete', self.check_for_close) - - # Add a signal pick up changes to events, bug #1329 - self._add_db_signal('event-update', self.event_updated) self.added = self.obj.handle is None if self.added: self.obj.handle = Utils.create_id() self.load_data() - - def check_for_close(self, handles): - if self.obj.get_handle() in handles: - self._do_close() + + def _connect_db_signals(self): + """ + implement from base class DbGUIElement + Register the callbacks we need. + Note: + * we do not connect to person-delete, as a delete of a person in + the family outside of this editor will cause a family-update + signal of this family + """ + self.callman.register_handles({'family': [self.obj.get_handle()]}) + self.callman.register_callbacks( + {'family-update': self.check_for_family_change, + 'family-delete': self.check_for_close, + 'family-rebuild': self._do_close, + 'event-update': self.topdata_updated, # change eg birth event fath + 'event-rebuild': self.topdata_updated, + 'event-delete': self.topdata_updated, # delete eg birth event fath + 'person-update': self.topdata_updated, # change eg name of father + 'person-rebuild': self._do_close, + }) + self.callman.connect_all(keys=['family', 'event', 'person']) def check_for_family_change(self, handles): - - # check to see if the handle matches the current object + """ + Callback for family-update signal + 1. This method checks to see if the family shown has been changed. This + is possible eg in the relationship view. If the family was changed, + the view is refreshed and a warning dialog shown to indicate all + changes have been lost. + If a source/note/event is deleted, this method is called too. This + is unfortunate as the displaytabs can track themself a delete and + correct the view for this. Therefore, these tabs are not rebuild. + Conclusion: this method updates so that remove/change of parent or + remove/change of children in relationship view reloads the family + from db. + 2. Changes in other families are of no consequence to the family shown + """ if self.obj.get_handle() in handles: + #rebuild data + ## Todo: Gallery and note tab are not rebuild ?? + objreal = self.dbstate.db.get_family_from_handle( + self.obj.get_handle()) + #update selection of data that we obtain from database change: + maindatachanged = (self.obj.gramps_id != objreal.gramps_id or + self.obj.father_handle != objreal.father_handle or + self.obj.mother_handle != objreal.mother_handle or + self.obj.private != objreal.private or + self.obj.type != objreal.type or + self.obj.marker != objreal.marker or + self.obj.child_ref_list != objreal.child_ref_list) + if maindatachanged: + self.obj.gramps_id = objreal.gramps_id + self.obj.father_handle = objreal.father_handle + self.obj.mother_handle = objreal.mother_handle + self.obj.private = objreal.private + self.obj.type = objreal.type + self.obj.marker = objreal.marker + self.obj.child_ref_list = objreal.child_ref_list + self.reload_people() - self.obj = self.dbstate.db.get_family_from_handle(self.obj.get_handle()) - self.reload_people() - self.event_list.rebuild() - self.source_list.rebuild() - self.attr_list.data = self.obj.get_attribute_list() - self.attr_list.rebuild() - self.lds_embed.data = self.obj.get_lds_ord_list() - self.lds_embed.rebuild() - + # No matter why the family changed (eg delete of a source), we notify + # the user WarningDialog( - _("Family has changed"), - _("The family you are editing has changed. To make sure that the " - "database is not corrupted, GRAMPS has updated the family to " - "reflect these changes. Any edits you have made may have been lost.")) + _("Family has changed"), + _("The %(object)s you are editing has changed outside this editor." + " This can be due to a change in one of the main views, for " + "example a source used here is deleted in the source view.\n" + "To make sure the information shown is still correct, the " + "data shown has been updated. Some edits you have made may have" + " been lost.") % {'object': _('family')}, parent=self.window) - def event_updated(self, obj): + def topdata_updated(self, *obj): + """ + Callback method called if data shown in top part of family editor + (a parent, birth/death event of parent) changes + Note: person events shown in the event list are not tracked, the + tabpage itself tracks it + """ self.load_data() - #place in event might have changed, or person event shown in the list - self.event_list.rebuild_callback() def show_buttons(self): """ @@ -617,6 +662,10 @@ class EditFamily(EditPrimary): ) def load_data(self): + """ + Show top data of family editor: father and mother info + and set self.phandles with all person handles in the family + """ fhandle = self.obj.get_father_handle() self.update_father(fhandle) @@ -839,7 +888,8 @@ class EditFamily(EditPrimary): 'in the database. If you save, you will create ' 'a duplicate family. It is recommended that ' 'you cancel the editing of this window, and ' - 'select the existing family')) + 'select the existing family'), + parent=self.window) def edit_father(self, obj, event): handle = self.obj.get_father_handle() @@ -871,10 +921,17 @@ class EditFamily(EditPrimary): name = "%s [%s]" % (name_displayer.display(person), person.gramps_id) birth = ReportUtils.get_birth_or_fallback(db, person) + self.callman.register_handles({'person': [handle]}) + if birth: + #if event changes it view needs to update + self.callman.register_handles({'event': [birth.get_handle()]}) if birth and birth.get_type() == gen.lib.EventType.BAPTISM: birth_label.set_label(_("Baptism:")) death = ReportUtils.get_death_or_fallback(db, person) + if death: + #if event changes it view needs to update + self.callman.register_handles({'event': [death.get_handle()]}) if death and death.get_type() == gen.lib.EventType.BURIAL: death_label.set_label(_("Burial:")) @@ -985,9 +1042,7 @@ class EditFamily(EditPrimary): # We disconnect the callbacks to all signals we connected earlier. # This prevents the signals originating in any of the following # commits from being caught by us again. - for key in self.signal_keys: - self.db.disconnect(key) - self.signal_keys = [] + self._cleanup_callbacks() if not original and not self.object_is_empty(): trans = self.db.transaction_begin() diff --git a/src/Editors/_EditMedia.py b/src/Editors/_EditMedia.py index 69a9c1f53..1d4e95463 100644 --- a/src/Editors/_EditMedia.py +++ b/src/Editors/_EditMedia.py @@ -103,6 +103,14 @@ class EditMedia(EditPrimary): self.define_ok_button(self.glade.get_object('ok'), self.save) self.define_help_button(self.glade.get_object('button102')) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('media-rebuild', self._do_close) + self._add_db_signal('media-delete', self.check_for_close) + def _setup_fields(self): self.date_field = MonitoredDate(self.glade.get_object("date_entry"), self.glade.get_object("date_edit"), diff --git a/src/Editors/_EditMediaRef.py b/src/Editors/_EditMediaRef.py index 3643109aa..804e14611 100644 --- a/src/Editors/_EditMediaRef.py +++ b/src/Editors/_EditMediaRef.py @@ -475,6 +475,14 @@ class EditMediaRef(EditReference): self.define_cancel_button(self.top.get_object('button84')) self.define_ok_button(self.top.get_object('button82'),self.save) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('media-rebuild', self.close) + self._add_db_signal('media-delete', self.check_for_close) + def _create_tabbed_pages(self): """ Create the notebook tabs and inserts them into the main diff --git a/src/Editors/_EditNote.py b/src/Editors/_EditNote.py index 361a00e42..45582a3bd 100644 --- a/src/Editors/_EditNote.py +++ b/src/Editors/_EditNote.py @@ -226,7 +226,15 @@ class EditNote(EditPrimary): self.define_ok_button(self.top.get_object('ok'), self.save) self.define_cancel_button(self.top.get_object('cancel')) self.define_help_button(self.top.get_object('help')) - + + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('note-rebuild', self._do_close) + self._add_db_signal('note-delete', self.check_for_close) + def _create_tabbed_pages(self): """Create the notebook tabs and inserts them into the main window.""" notebook = self.top.get_object("note_notebook") diff --git a/src/Editors/_EditPerson.py b/src/Editors/_EditPerson.py index 4322687bb..eebbbd410 100644 --- a/src/Editors/_EditPerson.py +++ b/src/Editors/_EditPerson.py @@ -167,9 +167,7 @@ class EditPerson(EditPrimary): def _connect_signals(self): """ Connect any signals that need to be connected. - Called by the init routine of the base class (_EditPrimary). - """ self.define_cancel_button(self.top.get_object("button15")) self.define_ok_button(self.top.get_object("ok"), self.save) @@ -182,6 +180,13 @@ class EditPerson(EditPrimary): self.eventbox.connect('button-press-event', self._image_button_press) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('person-rebuild', self._do_close) + self._add_db_signal('person-delete', self.check_for_close) self._add_db_signal('family-rebuild', self.family_change) self._add_db_signal('family-delete', self.family_change) self._add_db_signal('family-update', self.family_change) @@ -409,7 +414,6 @@ class EditPerson(EditPrimary): notebook.show_all() self.top.get_object('vbox').pack_start(notebook, True) - def _changed_name(self, obj): """ callback to changes typed by user to the person name. diff --git a/src/Editors/_EditPersonRef.py b/src/Editors/_EditPersonRef.py index ffc5aa8e6..42fe66db5 100644 --- a/src/Editors/_EditPersonRef.py +++ b/src/Editors/_EditPersonRef.py @@ -107,6 +107,23 @@ class EditPersonRef(EditSecondary): self.define_ok_button(self.top.get_object('ok'),self.save) self.top.get_object('select').connect('clicked',self._select_person) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('person-rebuild', self.close) + self._add_db_signal('person-delete', self.check_for_close) + + def check_for_close(self, handles): + """ + Callback method for delete signals. + If there is a delete signal of the primary object we are editing, the + editor (and all child windows spawned) should be closed + """ + if self.obj.ref in handles: + self.close() + def _select_person(self, obj): from Selectors import selector_factory SelectPerson = selector_factory('Person') diff --git a/src/Editors/_EditPlace.py b/src/Editors/_EditPlace.py index 34f919094..caf34f23f 100644 --- a/src/Editors/_EditPlace.py +++ b/src/Editors/_EditPlace.py @@ -142,6 +142,14 @@ class EditPlace(EditPrimary): self.define_cancel_button(self.top.get_object('cancel')) self.define_help_button(self.top.get_object('help')) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('place-rebuild', self._do_close) + self._add_db_signal('place-delete', self.check_for_close) + def _setup_fields(self): mloc = self.obj.get_main_location() diff --git a/src/Editors/_EditPrimary.py b/src/Editors/_EditPrimary.py index c20b0fc7e..374e49f29 100644 --- a/src/Editors/_EditPrimary.py +++ b/src/Editors/_EditPrimary.py @@ -21,9 +21,25 @@ # $Id$ +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- from gettext import gettext as _ + +#------------------------------------------------------------------------- +# +# GTK modules +# +#------------------------------------------------------------------------- import gtk +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- import ManagedWindow import DateHandler from BasicUtils import name_displayer @@ -31,8 +47,9 @@ import Config import GrampsDisplay from QuestionDialog import SaveDialog import gen.lib +from gui.dbguielement import DbGUIElement -class EditPrimary(ManagedWindow.ManagedWindow): +class EditPrimary(ManagedWindow.ManagedWindow, DbGUIElement): QR_CATEGORY = -1 @@ -52,19 +69,23 @@ class EditPrimary(ManagedWindow.ManagedWindow): self.uistate = uistate self.db = state.db self.callback = callback - self.signal_keys = [] self.ok_button = None self.get_from_handle = get_from_handle self.get_from_gramps_id = get_from_gramps_id self.contexteventbox = None + self.__tabs = [] ManagedWindow.ManagedWindow.__init__(self, uistate, track, obj) + DbGUIElement.__init__(self, self.db) self._local_init() self._set_size() self._create_tabbed_pages() self._setup_fields() self._connect_signals() + #if the database is changed, all info shown is invalid and the window + # should close + self.dbstate.connect('database-changed', self._do_close) self.show() self._post_init() @@ -80,18 +101,15 @@ class EditPrimary(ManagedWindow.ManagedWindow): """ pass - def _add_db_signal(self, name, callback): - self.signal_keys.append(self.db.connect(name, callback)) - - def _connect_signals(self): - pass - def _setup_fields(self): pass def _create_tabbed_pages(self): pass + def _connect_signals(self): + pass + def build_window_key(self, obj): if obj and obj.get_handle(): return obj.get_handle() @@ -126,8 +144,8 @@ class EditPrimary(ManagedWindow.ManagedWindow): notebook.set_current_page(page_no) def _add_tab(self, notebook, page): + self.__tabs.append(page) notebook.insert_page(page, page.get_tab_widget()) - page.add_db_signal_callback(self._add_db_signal) page.label.set_use_underline(True) return page @@ -151,11 +169,31 @@ class EditPrimary(ManagedWindow.ManagedWindow): section)) def _do_close(self, *obj): - for key in self.signal_keys: - self.db.disconnect(key) + self._cleanup_db_connects() self._cleanup_on_exit() ManagedWindow.ManagedWindow.close(self) + def _cleanup_db_connects(self): + """ + All connects that happened to signals of the db must be removed on + closed. This implies two things: + 1. The connects on the main view must be disconnected + 2. Connects done in subelements must be disconnected + """ + #cleanup callbackmanager of this editor + self._cleanup_callbacks() + for tab in [tab for tab in self.__tabs if hasattr(tab, 'callman')]: + tab._cleanup_callbacks() + + def check_for_close(self, handles): + """ + Callback method for delete signals. + If there is a delete signal of the primary object we are editing, the + editor (and all child windows spawned) should be closed + """ + if self.obj.get_handle() in handles: + self._do_close() + def close(self, *obj): """If the data has changed, give the user a chance to cancel the close window""" diff --git a/src/Editors/_EditReference.py b/src/Editors/_EditReference.py index 5eca8c4fe..98a392210 100644 --- a/src/Editors/_EditReference.py +++ b/src/Editors/_EditReference.py @@ -36,6 +36,7 @@ import gtk import ManagedWindow from DisplayTabs import GrampsTab import Config +from gui.dbguielement import DbGUIElement #------------------------------------------------------------------------- # @@ -85,7 +86,7 @@ class RefTab(GrampsTab): # EditReference class # #------------------------------------------------------------------------- -class EditReference(ManagedWindow.ManagedWindow): +class EditReference(ManagedWindow.ManagedWindow, DbGUIElement): def __init__(self, state, uistate, track, source, source_ref, update): self.db = state.db @@ -95,10 +96,11 @@ class EditReference(ManagedWindow.ManagedWindow): self.source = source self.source_added = False self.update = update - self.signal_keys = [] self.warn_box = None + self.__tabs = [] ManagedWindow.ManagedWindow.__init__(self, uistate, track, source_ref) + DbGUIElement.__init__(self, self.db) self._local_init() self._set_size() @@ -155,14 +157,11 @@ class EditReference(ManagedWindow.ManagedWindow): notebook.set_current_page(page_no) def _add_tab(self, notebook,page): + self.__tabs.append(page) notebook.insert_page(page, page.get_tab_widget()) - page.add_db_signal_callback(self._add_db_signal) page.label.set_use_underline(True) return page - def _add_db_signal(self, name, callback): - self.signal_keys.append(self.db.connect(name,callback)) - def _connect_signals(self): pass @@ -190,6 +189,15 @@ class EditReference(ManagedWindow.ManagedWindow): self._cleanup_on_exit() self.close(obj) + def check_for_close(self, handles): + """ + Callback method for delete signals. + If there is a delete signal of the primary object we are editing, the + editor (and all child windows spawned) should be closed + """ + if self.source.get_handle() in handles: + self.close() + def define_help_button(self, button, webpage='', section=''): import GrampsDisplay button.connect('clicked', lambda x: GrampsDisplay.help(webpage, @@ -200,6 +208,17 @@ class EditReference(ManagedWindow.ManagedWindow): pass def close(self,*obj): - for key in self.signal_keys: - self.db.disconnect(key) + self._cleanup_db_connects() ManagedWindow.ManagedWindow.close(self) + + def _cleanup_db_connects(self): + """ + All connects that happened to signals of the db must be removed on + closed. This implies two things: + 1. The connects on the main view must be disconnected + 2. Connects done in subelements must be disconnected + """ + #cleanup callbackmanager of this editor + self._cleanup_callbacks() + for tab in [tab for tab in self.__tabs if hasattr(tab, 'callman')]: + tab._cleanup_callbacks() diff --git a/src/Editors/_EditRepoRef.py b/src/Editors/_EditRepoRef.py index fc35b6562..c03fc48f0 100644 --- a/src/Editors/_EditRepoRef.py +++ b/src/Editors/_EditRepoRef.py @@ -83,7 +83,15 @@ class EditRepoRef(EditReference): def _connect_signals(self): self.define_ok_button(self.top.get_object('ok'),self.ok_clicked) self.define_cancel_button(self.top.get_object('cancel')) - + + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('repository-rebuild', self.close) + self._add_db_signal('repository-delete', self.check_for_close) + def _setup_fields(self): self.callno = MonitoredEntry( self.top.get_object("call_number"), diff --git a/src/Editors/_EditRepository.py b/src/Editors/_EditRepository.py index f4ceb869c..278b54845 100644 --- a/src/Editors/_EditRepository.py +++ b/src/Editors/_EditRepository.py @@ -148,6 +148,14 @@ class EditRepository(EditPrimary): self.define_cancel_button(self.glade.get_object('cancel')) self.define_ok_button(self.glade.get_object('ok'), self.save) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('repository-rebuild', self._do_close) + self._add_db_signal('repository-delete', self.check_for_close) + def save(self, *obj): self.ok_button.set_sensitive(False) if self.object_is_empty(): diff --git a/src/Editors/_EditSecondary.py b/src/Editors/_EditSecondary.py index 273e47f54..ccb649847 100644 --- a/src/Editors/_EditSecondary.py +++ b/src/Editors/_EditSecondary.py @@ -24,8 +24,9 @@ import ManagedWindow import GrampsDisplay import Config +from gui.dbguielement import DbGUIElement -class EditSecondary(ManagedWindow.ManagedWindow): +class EditSecondary(ManagedWindow.ManagedWindow, DbGUIElement): def __init__(self, state, uistate, track, obj, callback=None): """Create an edit window. Associates a person with the window.""" @@ -35,9 +36,10 @@ class EditSecondary(ManagedWindow.ManagedWindow): self.uistate = uistate self.db = state.db self.callback = callback - self.signal_keys = [] + self.__tabs = [] ManagedWindow.ManagedWindow.__init__(self, uistate, track, obj) + DbGUIElement.__init__(self, self.db) self._local_init() self._set_size() @@ -60,9 +62,6 @@ class EditSecondary(ManagedWindow.ManagedWindow): """ pass - def _add_db_signal(self, name, callback): - self.signal_keys.append(self.db.connect(name,callback)) - def _connect_signals(self): pass @@ -101,8 +100,8 @@ class EditSecondary(ManagedWindow.ManagedWindow): notebook.set_current_page(page_no) def _add_tab(self, notebook,page): + self.__tabs.append(page) notebook.insert_page(page, page.get_tab_widget()) - page.add_db_signal_callback(self._add_db_signal) page.label.set_use_underline(True) return page @@ -121,7 +120,18 @@ class EditSecondary(ManagedWindow.ManagedWindow): section)) def close(self,*obj): - for key in self.signal_keys: - self.db.disconnect(key) + self._cleanup_db_connects() self._cleanup_on_exit() ManagedWindow.ManagedWindow.close(self) + + def _cleanup_db_connects(self): + """ + All connects that happened to signals of the db must be removed on + closed. This implies two things: + 1. The connects on the main view must be disconnected + 2. Connects done in subelements must be disconnected + """ + #cleanup callbackmanager of this editor + self._cleanup_callbacks() + for tab in [tab for tab in self.__tabs if hasattr(tab, 'callman')]: + tab._cleanup_callbacks() diff --git a/src/Editors/_EditSource.py b/src/Editors/_EditSource.py index f763992f9..ad6ffc1dd 100644 --- a/src/Editors/_EditSource.py +++ b/src/Editors/_EditSource.py @@ -92,6 +92,14 @@ class EditSource(EditPrimary): self.define_cancel_button(self.glade.get_object('cancel')) self.define_help_button(self.glade.get_object('help')) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('source-rebuild', self._do_close) + self._add_db_signal('source-delete', self.check_for_close) + def _setup_fields(self): self.author = MonitoredEntry(self.glade.get_object("author"), self.obj.set_author, self.obj.get_author, diff --git a/src/Editors/_EditSourceRef.py b/src/Editors/_EditSourceRef.py index 469c5a90e..782603789 100644 --- a/src/Editors/_EditSourceRef.py +++ b/src/Editors/_EditSourceRef.py @@ -86,6 +86,16 @@ class EditSourceRef(EditReference): self.define_cancel_button(self.top.get_object('cancel')) self.define_help_button(self.top.get_object("help")) + def _connect_db_signals(self): + """ + Connect any signals that need to be connected. + Called by the init routine of the base class (_EditPrimary). + """ + self._add_db_signal('source-rebuild', self.close) + self._add_db_signal('source-delete', self.check_for_close) + #note: at the moment, a source cannot be updated while an editor with + # that source shown is open. So no need to connect to source-update + def _setup_fields(self): self.ref_privacy = PrivacyButton( self.top.get_object('privacy'), self.source_ref, self.db.readonly) diff --git a/src/gen/db/base.py b/src/gen/db/base.py index c597e974c..83464e093 100644 --- a/src/gen/db/base.py +++ b/src/gen/db/base.py @@ -417,6 +417,10 @@ class GrampsDbBase(Callback): """ Notify clients that the data has changed significantly, and that all internal data dependent on the database should be rebuilt. + Note that all rebuild signals on all objects are emitted at the same + time. It is correct to assume that this is always the case. + TODO: it might be better to replace these rebuild signals by one single + database-rebuild signal. """ self.emit('person-rebuild') self.emit('family-rebuild') diff --git a/src/gen/utils/Makefile.am b/src/gen/utils/Makefile.am index 6540c6021..c769bb71a 100644 --- a/src/gen/utils/Makefile.am +++ b/src/gen/utils/Makefile.am @@ -9,6 +9,7 @@ pkgdata_PYTHON = \ __init__.py \ dbutils.py \ callback.py \ + callman.py \ progressmon.py \ longop.py diff --git a/src/gen/utils/callman.py b/src/gen/utils/callman.py new file mode 100644 index 000000000..745c1c825 --- /dev/null +++ b/src/gen/utils/callman.py @@ -0,0 +1,431 @@ +# +# 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.GrampsDbBase` 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: + signal = key + method + self.__do_unconnect(signal) + self.__callbacks[signal][1] = self.database.connect( + signal, + self.__callbackcreator(key, signal)) + + 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 GrampsDbBase 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, key, signal): + """ + 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. + """ + if key == PERSONKEY: + if signal == 'person-update': + return lambda arg: self.__do_callback('person-update', *(arg,)) + elif signal == 'person-add': + return lambda arg: self.__do_callback('person-add', *(arg,)) + elif signal == 'person-delete': + return lambda arg: self.__do_callback('person-delete', *(arg,)) + elif signal == 'person-rebuild': + return lambda *arg: self.__do_callback('person-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + elif key == FAMILYKEY: + if signal == 'family-update': + return lambda arg: self.__do_callback('family-update', *(arg,)) + elif signal == 'family-add': + return lambda arg: self.__do_callback('family-add', *(arg,)) + elif signal == 'family-delete': + return lambda arg: self.__do_callback('family-delete', *(arg,)) + elif signal == 'family-rebuild': + return lambda *arg: self.__do_callback('family-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + elif key == EVENTKEY: + if signal == 'event-update': + return lambda arg: self.__do_callback('event-update', *(arg,)) + elif signal == 'event-add': + return lambda arg: self.__do_callback('event-add', *(arg,)) + elif signal == 'event-delete': + return lambda arg: self.__do_callback('event-delete', *(arg,)) + elif signal == 'event-rebuild': + return lambda *arg: self.__do_callback('event-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + elif key == PLACEKEY: + if signal == 'place-update': + return lambda arg: self.__do_callback('place-update', *(arg,)) + elif signal == 'place-add': + return lambda arg: self.__do_callback('place-add', *(arg,)) + elif signal == 'place-delete': + return lambda arg: self.__do_callback('place-delete', *(arg,)) + elif signal == 'place-rebuild': + return lambda *arg: self.__do_callback('place-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + elif key == SOURCEKEY: + if signal == 'source-update': + return lambda arg: self.__do_callback('source-update', *(arg,)) + elif signal == 'source-add': + return lambda arg: self.__do_callback('source-add', *(arg,)) + elif signal == 'source-delete': + return lambda arg: self.__do_callback('source-delete', *(arg,)) + elif signal == 'source-rebuild': + return lambda *arg: self.__do_callback('source-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + elif key == REPOKEY: + if signal == 'repository-update': + return lambda arg: self.__do_callback('repository-update', + *(arg,)) + elif signal == 'repository-add': + return lambda arg: self.__do_callback('repository-add', + *(arg,)) + elif signal == 'repository-delete': + return lambda arg: self.__do_callback('repository-delete', + *(arg,)) + elif signal == 'repository-rebuild': + return lambda *arg: self.__do_callback('repository-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + elif key == MEDIAKEY: + if signal == 'media-update': + return lambda arg: self.__do_callback('media-update', *(arg,)) + elif signal == 'media-add': + return lambda arg: self.__do_callback('media-add', *(arg,)) + elif signal == 'media-delete': + return lambda arg: self.__do_callback('media-delete', *(arg,)) + elif signal == 'media-rebuild': + return lambda *arg: self.__do_callback('media-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + elif key == NOTEKEY: + if signal == 'note-update': + return lambda arg: self.__do_callback('note-update', *(arg,)) + elif signal == 'note-add': + return lambda arg: self.__do_callback('note-add', *(arg,)) + elif signal == 'note-delete': + return lambda arg: self.__do_callback('note-delete', *(arg,)) + elif signal == 'note-rebuild': + return lambda *arg: self.__do_callback('note-rebuild') + else: + raise AttributeError, 'Signal ' + signal + 'not supported.' + 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 + diff --git a/src/gui/Makefile.am b/src/gui/Makefile.am index a30e35abe..76adbdbdc 100644 --- a/src/gui/Makefile.am +++ b/src/gui/Makefile.am @@ -10,6 +10,7 @@ pkgdatadir = $(datadir)/@PACKAGE@/gui pkgdata_PYTHON = \ __init__.py \ + dbguielement.py \ dbloader.py \ dbman.py \ grampsgui.py \ diff --git a/src/gui/dbguielement.py b/src/gui/dbguielement.py new file mode 100644 index 000000000..fd10f5027 --- /dev/null +++ b/src/gui/dbguielement.py @@ -0,0 +1,89 @@ +# +# 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$ + +""" +Group common stuff GRAMPS GUI elements must be able to do when tracking a DB: + * connect to db signals + * listen to db changes to update themself on relevant changes + * determine if the GUI has become out of sync with the db +""" + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gen.utils.callman import CallbackManager + +#------------------------------------------------------------------------- +# +# GUIElement class +# +#------------------------------------------------------------------------- +class DbGUIElement(object): + """ + Group common stuff GRAMPS GUI elements must be able to do when tracking + a DB: + * connect to db signals + * listen to db changes to update themself on relevant changes + * determine if the GUI has become out of sync with the db + Most interaction with the DB should be done via the callman attribute. + On initialization, the method _connect_db_signals is called. Inheriting + objects are advised to group the setup of the callman attribute here. + + .. attribute callman : a `~gen.utils.callman.CallbackManager` object, to + be used to track specific changes in the db and set up callbacks + """ + def __init__(self, database): + self.callman = CallbackManager(database) + self._connect_db_signals() + + def _add_db_signal(self, name, callback): + """ + Convenience function to add a custom db signal. The attributes are just + passed to the callman object. + For primary objects, use the register method of the callman attribute. + + :param name: name of the signal to connect to + :type name: string + :param callback: function to call when signal is emitted + :type callback: a funtion or method with the correct signature for the + signal + """ + self.callman.add_db_signal(name, callback) + + def _connect_db_signals(self): + """ + Convenience method that is called on initialization of DbGUIElement. + Use this to group setup of the callman attribute + """ + pass + + def _cleanup_callbacks(self): + """ + Remove all db callbacks. + This is done automatically on destruction of the object, but is + normally needed earlier, calling this method does so. + """ + database = self.callman.database + self.callman.disconnect_all() + self.callman = CallbackManager(database)