diff --git a/ChangeLog b/ChangeLog index 564016f82..5db0f14e4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,9 @@ 2007-04-01 Don Allingham + * src/GrampsDb/_GrampsDbBase.py: handle close/delete of active database + * src/DbManager.py: clean up + * src/DbState.py: issue database-changed signal on db close + * src/GrampsDbUtils/_GedcomParse.py: fix adding of notes + * src/DbLoader.py: don't give undo warning if importing into empty db * src/DataViews/_PedigreeView.py: display matches in statusbar * src/DataViews/_PersonView.py: display matches in statusbar * src/DataViews/_RelationView.py: display matches in statusbar diff --git a/src/DbLoader.py b/src/DbLoader.py index b767c741d..88f17a78e 100644 --- a/src/DbLoader.py +++ b/src/DbLoader.py @@ -260,17 +260,19 @@ class DbLoader: def import_file(self): # First thing first: import is a batch transaction # so we will lose the undo history. Warn the user. - warn_dialog = QuestionDialog.QuestionDialog2( - _('Undo history warning'), - _('Proceeding with import will erase the undo history ' - 'for this session. In particular, you will not be able ' - 'to revert the import or any changes made prior to it.\n\n' - 'If you think you may want to revert the import, ' - 'please stop here and backup your database.'), - _('_Proceed with import'), _('_Stop'), - self.uistate.window) - if not warn_dialog.run(): - return False + + if self.dbstate.db.get_number_of_people() > 0: + warn_dialog = QuestionDialog.QuestionDialog2( + _('Undo history warning'), + _('Proceeding with import will erase the undo history ' + 'for this session. In particular, you will not be able ' + 'to revert the import or any changes made prior to it.\n\n' + 'If you think you may want to revert the import, ' + 'please stop here and backup your database.'), + _('_Proceed with import'), _('_Stop'), + self.uistate.window) + if not warn_dialog.run(): + return False choose = gtk.FileChooserDialog( _('GRAMPS: Import database'), @@ -336,7 +338,7 @@ class DbLoader: const.app_gramps_xml, const.app_gedcom): importer = GrampsDbUtils.gramps_db_reader_factory(filetype) - self.do_import(choose,importer,filename) + self.do_import(choose, importer, filename) return True # Then we try all the known plugins @@ -345,7 +347,7 @@ class DbLoader: for (importData,mime_filter,mime_type,native_format,format_name) \ in import_list: if filetype == mime_type or the_file == mime_type: - self.do_import(choose,importData,filename) + self.do_import(choose, importData, filename) return True # Finally, we give up and declare this an unknown format diff --git a/src/DbManager.py b/src/DbManager.py index 9ae553b42..4fc5d17a9 100644 --- a/src/DbManager.py +++ b/src/DbManager.py @@ -18,12 +18,13 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -# $Id: Bookmarks.py 8197 2007-02-20 20:56:48Z hippy $ +""" +Provides the management of databases. This includes opening, renaming, +creating, and deleting of databases. +""" -"Handle bookmarks for the gramps interface" - -__author__ = "Donald N. Allingham" -__version__ = "$Revision: 8197 $" +__author__ = "Donald N. Allingham" +__revision__ = "$Revision: 8197 $" #------------------------------------------------------------------------- # @@ -41,7 +42,7 @@ from gettext import gettext as _ # #------------------------------------------------------------------------- import logging -log = logging.getLogger(".DbManager") +LOG = logging.getLogger(".DbManager") #------------------------------------------------------------------------- # @@ -58,7 +59,6 @@ import gtk.glade #------------------------------------------------------------------------- import QuestionDialog - #------------------------------------------------------------------------- # # constants @@ -74,6 +74,7 @@ PATH_COL = 1 FILE_COL = 2 DATE_COL = 3 OPEN_COL = 5 +STOCK_COL = 6 class DbManager: """ @@ -98,6 +99,10 @@ class DbManager: self.dblist = self.glade.get_widget('dblist') self.rename = self.glade.get_widget('rename') self.model = None + self.dbstate = dbstate + self.column = None + self.data_to_delete = None + if dbstate: self.active = dbstate.db.get_save_path() else: @@ -122,47 +127,93 @@ class DbManager: self.dblist.connect('button-press-event', self.button_press) def button_press(self, obj, event): + """ + Checks for a double click event. In the tree view, we want to + treat a double click as if it was OK button press. However, we have + to make sure that an item was selected first. + """ if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1: - self.top.response(gtk.RESPONSE_OK) + data = self.selection.get_selected() + if data[1]: + self.top.response(gtk.RESPONSE_OK) return True return False def selection_changed(self, selection): - store, iter = selection.get_selected() - if not iter or store.get_value(iter, OPEN_COL): - self.remove.set_sensitive(False) + """ + Called with the selection is changed in the TreeView. What we + are trying to detect is the selection or unselection of a row. + When a row is unselected, the Open, Rename, and Remove buttons + are set insensitive. If a row is selected, the rename and remove + buttons are disabled, and the Open button is disabled if the + row represents a open database. + """ + + # Get the current selection + store, node = selection.get_selected() + + if not node: self.connect.set_sensitive(False) self.rename.set_sensitive(False) + self.remove.set_sensitive(False) else: - self.remove.set_sensitive(True) - self.connect.set_sensitive(True) + if store.get_value(node, OPEN_COL): + self.connect.set_sensitive(False) + else: + self.connect.set_sensitive(True) self.rename.set_sensitive(True) + self.remove.set_sensitive(True) def build_interface(self): - render = gtk.CellRendererPixbuf() - column = gtk.TreeViewColumn('', render, stock_id=6) - self.dblist.append_column(column) + """ + Builds the columns for the TreeView. The columns are: + Icon, Database Name, Last Modified + + The Icon column gets its data from column 6 of the database model. + It is expecting either None, or a GTK stock icon name + + The Database Name column is an editable column. We connect to the + 'edited' signal, so that we can change the name when the user changes + the column. + + The last modified column simply displays the last modification time. + """ + + # build the icon column + render = gtk.CellRendererPixbuf() + icon_column = gtk.TreeViewColumn('', render, stock_id=STOCK_COL) + self.dblist.append_column(icon_column) + + # build the database name column render = gtk.CellRendererText() - render.set_property('editable',True) + render.set_property('editable', True) render.connect('edited', self.change_name) self.column = gtk.TreeViewColumn(_('Family tree name'), render, text=NAME_COL) self.dblist.append_column(self.column) + # build the last modified cocolumn render = gtk.CellRendererText() column = gtk.TreeViewColumn(_('Last modified'), render, text=DATE_COL) self.dblist.append_column(column) + + # set the rules hit self.dblist.set_rules_hint(True) def populate(self): + """ + Builds the display model. + """ + self.model = gtk.ListStore(str, str, str, str, int, bool, str) - try: - if not os.path.isdir(DEFAULT_DIR): - os.mkdir(DEFAULT_DIR) - except: - print "did not make default dir" + # make the default directory if it does not exist + try: + if not os.path.isdir(DEFAULT_DIR): + os.mkdir(DEFAULT_DIR) + except (IOError, OSError), msg: + LOG.error(_("Could not make database directory: ") + str(msg)) self.current_names = [] for dpath in os.listdir(DEFAULT_DIR): @@ -171,115 +222,190 @@ class DbManager: if os.path.isfile(path_name): name = file(path_name).readline().strip() - meta = os.path.join(dirpath, META_NAME) - if os.path.isfile(meta): - tval = os.stat(meta)[9] - last = time.asctime(time.localtime(tval)) - else: - tval = 0 - last = _("Never") + (tval, last) = time_val(dirpath) + (enable, stock_id) = icon_values(dirpath, self.active) - if dirpath == self.active: - enable = True - stock_id = gtk.STOCK_OPEN - else: - enable = False - stock_id = "" - - self.current_names.append((name, - os.path.join(DEFAULT_DIR, dpath), - path_name, - last, - tval, - enable, - stock_id)) + self.current_names.append( + (name, os.path.join(DEFAULT_DIR, dpath), path_name, + last, tval, enable, stock_id)) self.current_names.sort() for items in self.current_names: - data = [items[0], items[1], items[2], items[3], items[4], items[5], items[6]] + data = [items[0], items[1], items[2], items[3], + items[4], items[5], items[6]] self.model.append(data) self.dblist.set_model(self.model) def run(self): + """ + Runs the dialog, returning None if nothing has been chosen, + or the path and name if something has been selected + """ value = self.top.run() if value == gtk.RESPONSE_OK: (model, node) = self.selection.get_selected() if node: self.top.destroy() - return (self.model.get_value(node, PATH_COL), - self.model.get_value(node, NAME_COL)) - else: - self.top.destroy() - return None - else: - self.top.destroy() - return None + return (model.get_value(node, PATH_COL), + model.get_value(node, NAME_COL)) + self.top.destroy() + return None def change_name(self, text, path, new_text): + """ + Changes the name of the database. This is a callback from the + column, which has been marked as editable. + + If the new string is empty, do nothing. Otherwise, renaming the + database is simply changing the contents of the name file. + """ if len(new_text) > 0: - iter = self.model.get_iter(path) - filename = self.model.get_value(iter, FILE_COL) + node = self.model.get_iter(path) + filename = self.model.get_value(node, FILE_COL) try: - f = open(filename, "w") - f.write(new_text) - f.close() - self.model.set_value(iter, NAME_COL, new_text) - except: - pass + name_file = open(filename, "w") + name_file.write(new_text) + name_file.close() + self.model.set_value(node, NAME_COL, new_text) + except (OSError, IOError), msg: + QuestionDialog.ErrorDialog( + _("Could not rename family tree"), + str(msg)) def remove_db(self, obj): - store, iter = self.selection.get_selected() - path = store.get_path(iter) - row = store[path] - if row[OPEN_COL]: - return - self.data_to_delete = (row[0], row[1], row[2]) + """ + Callback associated with the Remove button. Get the selected + row and data, then call the verification dialog. + """ + store, node = self.selection.get_selected() + self.data_to_delete = store[store.get_path(node)] QuestionDialog.QuestionDialog( _("Remove the '%s' database?") % self.data_to_delete[0], - _("Removing this database will permanently destroy " - "the data."), + _("Removing this database will permanently destroy the data."), _("Remove database"), self.really_delete_db) + + # rebuild the display self.populate() def really_delete_db(self): - for (top, dirs, files) in os.walk(self.data_to_delete[1]): - for f in files: - os.unlink(os.path.join(top,f)) - os.rmdir(top) + """ + Delete the selected database. If the databse is open, close it first. + Then scan the database directory, deleting the files, and finally + removing the directory. + """ + # close the database if the user has requested to delete the + # active database + if self.data_to_delete[OPEN_COL]: + self.dbstate.no_database() + + try: + for (top, dirs, files) in os.walk(self.data_to_delete[1]): + for filename in files: + os.unlink(os.path.join(top, filename)) + os.rmdir(self.data_to_delete[1]) + except (IOError, OSError), msg: + QuestionDialog.ErrorDialog(_("Could not delete family tree"), + str(msg)) + def rename_db(self, obj): + """ + Start the rename process by calling the start_editing option on + the line with the cursor. + """ store, node = self.selection.get_selected() path = self.model.get_path(node) - self.dblist.set_cursor(path, focus_column=self.column, start_editing=True) + self.dblist.set_cursor(path, focus_column=self.column, + start_editing=True) def new_db(self, obj): - while True: - base = "%x" % int(time.time()) - new_path = os.path.join(DEFAULT_DIR, base) - if not os.path.isdir(new_path): - break + """ + Callback wrapper around the actual routine that creates the + new database. Catch OSError and IOError and display a warning + message. + """ + try: + self.mk_db() + except (OSError, IOError), msg: + QuestionDialog.ErrorDialog(_("Could not create family tree"), + str(msg)) + + def mk_db(self): + """ + Create a new database. + """ + + new_path = find_next_db_dir() os.mkdir(new_path) path_name = os.path.join(new_path, NAME_FILE) name_list = [ name[0] for name in self.current_names ] - i = 1 - while True: - title = "%s %d" % (DEFAULT_TITLE, i) - if title not in name_list: - break - i += 1 + title = find_next_db_name(name_list) - f = open(path_name, "w") - f.write(title) - f.close() + name_file = open(path_name, "w") + name_file.write(title) + name_file.close() self.current_names.append(title) - node = self.model.append([title, new_path, path_name, _("Never"), 0, False, '']) + node = self.model.append([title, new_path, path_name, + _("Never"), 0, False, '']) self.selection.select_iter(node) path = self.model.get_path(node) - self.dblist.set_cursor(path, focus_column=self.column, start_editing=True) + self.dblist.set_cursor(path, focus_column=self.column, + start_editing=True) + +def find_next_db_name(name_list): + """ + Scan the name list, looking for names that do not yet exist. + Use the DEFAULT_TITLE as the basis for the database name. + """ + i = 1 + while True: + title = "%s %d" % (DEFAULT_TITLE, i) + if title not in name_list: + return title + i += 1 + +def find_next_db_dir(): + """ + Searches the default directory for the first available default + database name. Base the name off the current time. In all actuality, + the first should be valid. + """ + while True: + base = "%x" % int(time.time()) + new_path = os.path.join(DEFAULT_DIR, base) + if not os.path.isdir(new_path): + break + return new_path + +def time_val(dirpath): + """ + Return the last modified time of the database. We do this by looking + at the modification time of the meta db file. If this file does not + exist, we indicate that database as never modified. + """ + meta = os.path.join(dirpath, META_NAME) + if os.path.isfile(meta): + tval = os.stat(meta)[9] + last = time.asctime(time.localtime(tval)) + else: + tval = 0 + last = _("Never") + return (tval, last) + +def icon_values(dirpath, active): + """ + If the directory path is the active path, then return values + that indicate to use the icon, and which icon to use. + """ + if dirpath == active: + return (True, gtk.STOCK_OPEN) + else: + return (False, "") + diff --git a/src/DbState.py b/src/DbState.py index 211a80b48..724450ac4 100644 --- a/src/DbState.py +++ b/src/DbState.py @@ -86,4 +86,4 @@ class DbState(GrampsDBCallback): self.db = GrampsDbBase() self.active = None self.open = False - self.emit('no-database') + self.emit('database-changed', (self.db, )) diff --git a/src/GrampsDb/_GrampsDbBase.py b/src/GrampsDb/_GrampsDbBase.py index b6f230606..e38c943a5 100644 --- a/src/GrampsDb/_GrampsDbBase.py +++ b/src/GrampsDb/_GrampsDbBase.py @@ -691,6 +691,9 @@ class GrampsDbBase(GrampsDBCallback): Commits the specified Note to the database, storing the changes as part of the transaction. """ + if not note.gramps_id: + import traceback + traceback.print_stack() self._commit_base(note, self.note_map, NOTE_KEY, transaction.note_update, @@ -1487,7 +1490,7 @@ class GrampsDbBase(GrampsDBCallback): """ self.nprefix = self._validated_id_prefix(val, "N") - def transaction_begin(self, msg="",batch=False,no_magic=False): + def transaction_begin(self, msg="", batch=False, no_magic=False): """ Creates a new Transaction tied to the current UNDO database. The transaction has no effect until it is committed using the diff --git a/src/GrampsDbUtils/_GedcomParse.py b/src/GrampsDbUtils/_GedcomParse.py index 99b67c7d8..ae5148fff 100644 --- a/src/GrampsDbUtils/_GedcomParse.py +++ b/src/GrampsDbUtils/_GedcomParse.py @@ -1072,7 +1072,7 @@ class GedcomParser(UpdateCallback): note.set_handle(intid) note.set_gramps_id(gramps_id) if need_commit: - self.dbase.commit_note(note, self.trans) + self.dbase.add_note(note, self.trans) return note def __find_or_create_place(self, title): @@ -4258,7 +4258,7 @@ class GedcomParser(UpdateCallback): else: new_note = RelLib.Note(line.data) new_note.set_handle(Utils.create_id()) - self.dbase.commit_note(new_note, self.trans) + self.dbase.add_note(new_note, self.trans) self.__skip_subordinate_levels(level+1) def __parse_inline_note(self, line, level): @@ -4267,7 +4267,7 @@ class GedcomParser(UpdateCallback): handle = self.nid2id.get(gid) new_note.set_handle(handle) new_note.set_gramps_id(gid) - self.dbase.commit_note(new_note,self.trans) + self.dbase.add_note(new_note,self.trans) self.nid2id[new_note.gramps_id] = new_note.handle self.__skip_subordinate_levels(level+1) diff --git a/src/GrampsDbUtils/gedcomimport.glade b/src/GrampsDbUtils/gedcomimport.glade index cd8f26223..ad051cd5c 100644 --- a/src/GrampsDbUtils/gedcomimport.glade +++ b/src/GrampsDbUtils/gedcomimport.glade @@ -560,7 +560,7 @@ True GRAMPS - GEDCOM Encoding GTK_WINDOW_TOPLEVEL - GTK_WIN_POS_NONE + GTK_WIN_POS_CENTER_ON_PARENT False True False diff --git a/src/PageView.py b/src/PageView.py index ba3a21d32..f9ab31db7 100644 --- a/src/PageView.py +++ b/src/PageView.py @@ -735,11 +735,11 @@ class ListView(BookMarkView): self.tooltips = TreeTips.TreeTips( self.list, self.model.tooltip_column, True) self.dirty = False + self.uistate.show_filter_results(self.dbstate, + self.model.displayed, + self.model.total) else: self.dirty = True - self.uistate.show_filter_results(self.dbstate, - self.model.displayed, - self.model.total) def filter_toggle_action(self,obj): if obj.get_active(): @@ -853,6 +853,10 @@ class ListView(BookMarkView): return False def change_page(self): + if self.model: + self.uistate.show_filter_results(self.dbstate, + self.model.displayed, + self.model.total) self.edit_action.set_sensitive(not self.dbstate.db.readonly) def key_delete(self): diff --git a/src/gramps_main.py b/src/gramps_main.py index 734b35bd3..67ac11ae5 100644 --- a/src/gramps_main.py +++ b/src/gramps_main.py @@ -244,9 +244,6 @@ class Gramps: ah.handle_args() self.vm.post_init_interface() -# state.db.request_rebuild() -# state.change_active_person(state.db.get_default_person()) - if Config.get(Config.USE_TIPS): TipOfDay.TipOfDay(self.vm.uistate)