diff --git a/ChangeLog b/ChangeLog index a5256b5a3..0b220c19d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2006-02-03 Richard Taylor + * src/GrampsDb/_GrampsBSDDB.py: fixed typo + * src/Models: added initial fast model implementation + * src/TreeViews: added treeview that uses new Models + * src/try_tree_model.py: harness for running treeview + 2006-02-02 Don Allingham * src/PersonView.py: history * src/EditPerson.py: start the save routine. diff --git a/src/GrampsDb/_GrampsBSDDB.py b/src/GrampsDb/_GrampsBSDDB.py index e017ecced..6114f7bb9 100644 --- a/src/GrampsDb/_GrampsBSDDB.py +++ b/src/GrampsDb/_GrampsBSDDB.py @@ -440,7 +440,7 @@ class GrampsBSDDB(GrampsDbBase): def rebuild_secondary(self,callback=None): - if self.read_only: + if self.readonly: return table_flags = db.DB_CREATE|db.DB_AUTO_COMMIT diff --git a/src/GrampsDb/__init__.py b/src/GrampsDb/__init__.py index 9be06825c..a0c614a25 100644 --- a/src/GrampsDb/__init__.py +++ b/src/GrampsDb/__init__.py @@ -48,6 +48,7 @@ from _GrampsDbFactories import \ from _ReadGedcom import GedcomParser from _WriteGedcom import GedcomWriter + from _WriteXML import XmlWriter from _GrampsDbExceptions import GrampsDbException diff --git a/src/Models/_FastFilterModel.py b/src/Models/_FastFilterModel.py new file mode 100644 index 000000000..bdc4feafe --- /dev/null +++ b/src/Models/_FastFilterModel.py @@ -0,0 +1,153 @@ + +import gtk +import time +import logging +log = logging.getLogger(".") + + + +class FastFilterModel(gtk.GenericTreeModel): + """A I{gtk.GenericTreeModel} that links to a BSDB cursor to provide + fast access to large tables. This is a pure virtual class, it must be + subclassed and the subclass must implement L{_get_table}, L{_get_cursor} and + L{_get_object_class}. + + The primary trick is to use the path specification as the tree iter. + This means that when the TreeView asks for the iter for path=[1,2] + we just echo it straight back. The onlt hard part is making sure that + on_iter_next can do something sensible. It needs to know how many + non duplicate records are in the table and then it can just accept the + iter from the TreeView and increment it until it reaches the total + length. + + The record itself is only fetched when its value is requested from + on_get_value() and when the number of childen need to calculated. The + cursor looks after the number of children calculation but it does require + walking the list of duplicate keys, usually this is quite short. + + @ivar _db: handle of the Gramps DB + @ivar _table: main table to be displayed + @ivar _cursor: cursor for accessing the table. + @ivar _obj_class: the class of the object that is being pulled from + the database. This should probably be one of the primary RelLib + classes. + @ivar _num_children_cache: dictionary to hold the number of + children for each primary record so that we don't have to look + it up every time. + @ivar _length: the number of primary (non duplicate) records. + """ + + column_types = (object,) + + def __init__(self,db,data_filter): + gtk.GenericTreeModel.__init__(self) + + self._db = db + self._data_filter = data_filter + self._fetch_func = self._get_fetch_func(db) + + self._build_data() + + def _build_data(self): + if not self._data_filter.is_empty(): + self._keys = self._data_filter.apply(self._db) + else: + return + + self._length = len(self._keys) + + + # Methods that must be implemented by subclasses. + def _get_fetch_func(self,db): + raise NotImplementedError("subclass of FastModel must implement _get_fetch_func") + + + # GenericTreeModel methods + + def on_get_flags(self): + return gtk.TREE_MODEL_LIST_ONLY|gtk.TREE_MODEL_ITERS_PERSIST + + def on_get_n_columns(self): + return len(self.__class__.column_types) + + def on_get_column_type(self, index): + return self.column_types[index] + + def on_get_iter(self, path): + return list(path) + + def on_get_path(self, rowref): + return list(rowref) + + def on_get_value(self, rowref, column): + """ + Fetch the real object from the database. + """ + + # We only have one column + if column is 0: + record = self._fetch_func(self._keys[rowref[0]]) + + # This should never return none, but there is a subtle bug + # somewhere that I can't find and sometimes it does. + if record is None: + log.warn("Failed to fetch a record from the cursor rowref = %s" % (str(rowref))) + + return (record,rowref) + + def on_iter_next(self, rowref): + """ + Calculate the next iter at the same level in the tree. + """ + + # The length of the rowref (i.e. the number of elements in the path) + # tells us the level in the tree. + if len(rowref) == 1: + + # If we are at the top of the tree we just increment the + # first element in the iter until we reach the total length. + if rowref[0]+1 >= self._length: + ret = None + else: + ret = [rowref[0]+1,] + + else: + # We only support one level. + ret = None + + return ret + + + def on_iter_children(self, rowref): + """ + Return the first child of the given rowref. + """ + if rowref: + # If the rowref is not none then we must be + # asking for the second level so the first + # child is always 0. + ret = [rowref[0],0] + else: + # If rowref is None the we are asking for the + # top level and that is always [0] + ret = [0,] + + return ret + + def on_iter_has_child(self, rowref): + return False + + def on_iter_n_children(self, rowref): + return self._length + + def on_iter_nth_child(self, parent, n): + if parent: + ret = [parent[0],n] + else: + ret = [n,] + return ret + + def on_iter_parent(self, child): + if len(child) > 1: + return [child[0]] + return None diff --git a/src/Models/_FastModel.py b/src/Models/_FastModel.py new file mode 100644 index 000000000..b303a182b --- /dev/null +++ b/src/Models/_FastModel.py @@ -0,0 +1,195 @@ + +import gtk +import time +import logging +log = logging.getLogger(".") + + + +class FastModel(gtk.GenericTreeModel): + """A I{gtk.GenericTreeModel} that links to a BSDB cursor to provide + fast access to large tables. This is a pure virtual class, it must be + subclassed and the subclass must implement L{_get_table}, L{_get_cursor} and + L{_get_object_class}. + + The primary trick is to use the path specification as the tree iter. + This means that when the TreeView asks for the iter for path=[1,2] + we just echo it straight back. The onlt hard part is making sure that + on_iter_next can do something sensible. It needs to know how many + non duplicate records are in the table and then it can just accept the + iter from the TreeView and increment it until it reaches the total + length. + + The record itself is only fetched when its value is requested from + on_get_value() and when the number of childen need to calculated. The + cursor looks after the number of children calculation but it does require + walking the list of duplicate keys, usually this is quite short. + + @ivar _db: handle of the Gramps DB + @ivar _table: main table to be displayed + @ivar _cursor: cursor for accessing the table. + @ivar _obj_class: the class of the object that is being pulled from + the database. This should probably be one of the primary RelLib + classes. + @ivar _num_children_cache: dictionary to hold the number of + children for each primary record so that we don't have to look + it up every time. + @ivar _length: the number of primary (non duplicate) records. + """ + + column_types = (object,) + + def __init__(self,db): + gtk.GenericTreeModel.__init__(self) + + self._db = db + self._table = self._get_table(db) + self._cursor = self._get_cursor(db) + self._object_class = self._get_object_class(db) + self._length = self._get_length(db) + + self._num_children_cache = {} + + + # Methods that must be implemented by subclasses. + + def _get_table(self,db): + raise NotImplementedError("subclass of FastModel must implement _get_table") + + def _get_cursor(self,db): + raise NotImplementedError("subclass of FastModel must implement _get_cursor") + + def _get_object_class(self,db): + raise NotImplementedError("subclass of FastModel must implement _get_cursor") + + def _get_length(self,db): + raise NotImplementedError("subclass of FastModel must implement _get_length") + + # GenericTreeModel methods + + def on_get_flags(self): + return gtk.TREE_MODEL_ITERS_PERSIST + + def on_get_n_columns(self): + return len(self.__class__.column_types) + + def on_get_column_type(self, index): + return self.column_types[index] + + def on_get_iter(self, path): + return list(path) + + def on_get_path(self, rowref): + return list(rowref) + + def on_get_value(self, rowref, column): + """ + Fetch the real object from the database. + """ + + # We only have one column + if column is 0: + obj = self._object_class() + + # Use the rowref as the path, because the iter methods + # simple return the path as the iter it is safe to use + # it here. + record = self._cursor.lookup_path(rowref) + + # This should never return none, but there is a subtle bug + # somewhere that I can't find and sometimes it does. + if record is not None: + obj.unserialize(record[1]) + else: + log.warn("Failed to fetch a record from the cursor rowref = %s" % (str(rowref))) + + return (obj,rowref) + + def on_iter_next(self, rowref): + """ + Calculate the next iter at the same level in the tree. + """ + + # The length of the rowref (i.e. the number of elements in the path) + # tells us the level in the tree. + if len(rowref) == 1: + + # If we are at the top of the tree we just increment the + # first element in the iter until we reach the total length. + if rowref[0]+1 >= self._length: + ret = None + else: + ret = [rowref[0]+1,] + + elif len(rowref) == 2: + + # If we are at the second level we first check to see if we + # have the number of children of this row already in the cache + if not self._num_children_cache.has_key(rowref[0]): + + # If not calculate the number of children and put it in the cache. + self._num_children_cache[rowref[0]] = self._cursor.get_n_children([rowref[0],]) + + num_children = self._num_children_cache[rowref[0]] + + # Now increment the second element of the iter path until we + # reach the number of children. + if rowref[1]+1 < num_children: + ret = [rowref[0],rowref[1]+1] + else: + ret = None + else: + # We only support two levels. + ret = None + + return ret + + + def on_iter_children(self, rowref): + """ + Return the first child of the given rowref. + """ + if rowref: + # If the rowref is not none then we must be + # asking for the second level so the first + # child is always 0. + ret = [rowref[0],0] + else: + # If rowref is None the we are asking for the + # top level and that is always [0] + ret = [0,] + + return ret + + def on_iter_has_child(self, rowref): + if rowref: + ret = self._cursor.has_children(rowref) + else: + ret = range(0,self._length) + return ret + + def on_iter_n_children(self, rowref): + if rowref: + # If we are at the second level we first check to see if we + # have the number of children of this row already in the cache + if not self._num_children_cache.has_key(rowref[0]): + + # If not calculate the number of children and put it in the cache. + self._num_children_cache[rowref[0]] = self._cursor.get_n_children([rowref[0],]) + + ret = self._num_children_cache[rowref[0]] + else: + ret = self._length + return ret + + def on_iter_nth_child(self, parent, n): + if parent: + ret = [parent[0],n] + else: + ret = [n,] + return ret + + def on_iter_parent(self, child): + if len(child) > 1: + return [child[0]] + return None diff --git a/src/Models/_ListCursor.py b/src/Models/_ListCursor.py new file mode 100644 index 000000000..4e63a90d0 --- /dev/null +++ b/src/Models/_ListCursor.py @@ -0,0 +1,176 @@ + +import cPickle +import logging +log = logging.getLogger(".") + +class ListCursor(object): + """ + Provides a wrapper around the cursor class that provides fast + traversal using treeview paths for models that are LISTONLY, i.e. + they have no tree structure. + + It keeps track of the current index that the cursor is pointing + at. + + @ivar _index: The current index pointed to by the cursor. + + To speed up lookups the cursor is kept as close as possible to the + likely next lookup and is moved using next()/prev() were ever + possible. + + @ivar _object_cache: A cache of previously fetched records. These are + indexed by the values of the L{_index}. + """ + + def __init__(self,cursor): + """ + @param cursor: The cursor used to fetch the records. + @type cursor: An object supporting the cursor methods of a U{BSDB + cursor}. + It must have a BTREE index type and DB_DUP to support duplicate + records. It should probably also have DB_DUPSORT if you want to + have sorted records. + """ + self._cursor = cursor + self._object_cache = {} + + self.top() + + + def top(self): + self._cursor.first() + self._index = 0 + + def next(self): + """ + Move to the next record. + """ + data = self._cursor.next() + + # If there was a next record that data will + # not be None + if data is not None: + # Up date the index pointers so that + # they point to the current record. + self._index+= 1 + + return data + + def prev(self): + """ + Move to the previous record. + """ + data = self._cursor.prev() + + # If there was a next record that data will + # not be None + if data is not None: + # Up date the index pointers so that + # they point to the current record. + self._index -= 1 + + return data + + + def has_children(self,path): + """ + This cursor is only for simple lists so no records have + children. + + @param path: The path spec to check. + @type path: A TreeView path. + """ + + return False + + def get_n_children(self,path): + """ + Return the number of children that the record at I{path} has. + + @param path: The path spec to check. + @type path: A TreeView path. + """ + + return 0 + + def lookup(self,index,use_cache=True): + """ + Lookup a primary record. + + @param index: The index of the primary record. This is its + possition in the sequence of non_duplicate keys. + @type index: int + @para use_case: If B{True} the record will be looked up in the + object cache and will be returned from there. This will not + update the possition of the cursor. If B{False} the record will + fetched from the cursor even if it is in the object cache and + cursor will be left possitioned on the record. + """ + + # See if the record is in the cache. + if self._object_cache.has_key(index) and use_cache is True: + ret = self._object_cache[index] + + # If the record is not in the cache or we are ignoring the + # cache. + else: + + # If the cursor points to the record we want + # the first index will be equal to the + # index required + if index == self._index: + ret = self._cursor.current() + + # If the current cursor is behind the + # requested index move it forward. + elif index < self._index: + while index < self._index: + ret = self.prev() + if ret is None: + log.warn("Failed to move back to index = %s" % (str(index))) + break + + ret = self._cursor.current() + + # If the current cursor is in front of + # requested index move it backward. + else: + while index > self._index: + ret = self.next() + if ret is None: + log.warn("Failed to move forward to index = %s" % (str(index))) + break + + ret = self._cursor.current() + + # when we have got the record save it in + # the cache + if ret is not None: + ret = self._unpickle(ret) + self._object_cache[index] = ret + + return ret + + def _unpickle(self,rec): + """ + It appears that reading an object from a cursor does not + automatically unpickle it. So this method provides + a convenient way to unpickle the object. + """ + if rec and type(rec[1]) == type(""): + tmp = [rec[0],None] + tmp[1] = cPickle.loads(rec[1]) + rec = tmp + return rec + + def lookup_path(self,path): + """ + Lookup a record from a patch specification. + + @param path: The path spec to check. + @type path: A TreeView path. + + """ + return self.lookup(path[0]) + + diff --git a/src/Models/_PathCursor.py b/src/Models/_PathCursor.py new file mode 100644 index 000000000..69656cc9a --- /dev/null +++ b/src/Models/_PathCursor.py @@ -0,0 +1,324 @@ + +import cPickle +import logging +log = logging.getLogger(".") + +class PathCursor(object): + """ + Provides a wrapper around the cursor class that provides fast + traversal using treeview paths. + + It keeps track of the current index that the cursor is pointing + at by using a two stage index. The first element of the index is + the sequence number of the record in the list of non_duplicate + keys and the second element is the sequence number of the record + within the duplicate keys to which it is a member. + + For example, with the following table indexed on Surname:: + + Record Value Index + ============ ===== + + Blogs, Jo [0,0] + Blogs, Jane [0,1] + Smith, Wilman [1,0] + Smith, John [1,1] + + @ivar _index: The current index pointed to by the cursor. + + To speed up lookups the cursor is kept as close as possible to the + likely next lookup and is moved using next_dup()/prev_dup() were ever + possible. + + @ivar _object_cache: A cache of previously fetched records. These are + indexed by the values of the L{_index}. + """ + + def __init__(self,cursor): + """ + @param cursor: The cursor used to fetch the records. + @type cursor: An object supporting the cursor methods of a U{BSDB + cursor}. + It must have a BTREE index type and DB_DUP to support duplicate + records. It should probably also have DB_DUPSORT if you want to + have sorted records. + """ + self._cursor = cursor + self._object_cache = {} + + self.top() + + + def top(self): + self._cursor.first() + self._index = [0,0] + + def next_nodup(self): + """ + Move to the next non-duplcate record. + """ + data = self._cursor.next_nodup() + + # If there was a next record that data will + # not be None + if data is not None: + # Up date the index pointers so that + # they point to the current record. + self._index[0] += 1 + self._index[1] = 0 + + return data + + def prev_nodup(self): + """ + Move to the previous non-duplicate record. + """ + data = self._cursor.prev_nodup() + + # If there was a next record that data will + # not be None + if data is not None: + # Up date the index pointers so that + # they point to the current record. + self._index[0] -= 1 + self._index[1] = 0 + + return data + + def next_dup(self): + """ + Move to the next record with a duplicate key to the current record. + """ + data = self._cursor.next_dup() + + # If there was a next record that data will + # not be None + if data is not None: + # Update the secondary index. + self._index[1] += 1 + + return data + + def has_children(self,path): + """ + Check is the I{path} has any children. + + At the moment this method lies. There is no fast way to check + if a given key has any duplicates and the TreeView insists on + checking for every row. So this methods returns True if the + path is 1 element long and False if it is more. This works + for us because we show the first record in a set of duplicates + as the first child. So all top level rows have at least one child. + + @param path: The path spec to check. + @type path: A TreeView path. + """ + + if len(path) == 1: + return True + else: + return False + + def get_n_children(self,path): + """ + Return the number of children that the record at I{path} has. + + @param path: The path spec to check. + @type path: A TreeView path. + """ + + # Only top level records can have children. + if len(path) > 1: + return 0 + + # Fetch the primary record + ret = self.lookup(path[0],use_cache=False) + + if ret is not None: + # Now count the duplicates. We start at 1 because + # we want to include the primary in the duplicates. + count = 1 + ret = self.next_dup() + while ret: + ret = self.next_dup() + count += 1 + self._index[1] += 1 + + ret = count + else: + # If we failed to find the primary something is + # wrong. + ret = 0 + + return ret + + def lookup(self,index,use_cache=True): + """ + Lookup a primary record. + + @param index: The index of the primary record. This is its + possition in the sequence of non_duplicate keys. + @type index: int + @para use_case: If B{True} the record will be looked up in the + object cache and will be returned from there. This will not + update the possition of the cursor. If B{False} the record will + fetched from the cursor even if it is in the object cache and + cursor will be left possitioned on the record. + """ + + # See if the record is in the cache. + if self._object_cache.has_key(index) and use_cache is True: + ret = self._object_cache[index]['primary'] + + # If the record is not in the cache or we are ignoring the + # cache. + else: + + # If the cursor points to a duplicate record + # it will have a second index value of 0. + if self._index[1] != 0: + # We need to move the cursor to the + # first of a set of duplicates so that + # we can then shift it to the required + # index. + self.next_nodup() + + + # If the cursor points to the record we want + # the first index will be equal to the + # index required + if index == self._index[0]: + ret = self._cursor.current() + + # If the current cursor is behind the + # requested index move it forward. + elif index < self._index[0]: + while index < self._index[0]: + ret = self.prev_nodup() + if ret is None: + log.warn("Failed to move back to index = %s" % (str(index))) + break + + # Because prev_nodup() leaves the cursor on + # the last of a set of duplicates we need + # to go up one further and then back down + # again. + ret = self.prev_nodup() + if ret is None: + # We are at the start + self.top() + ret = self._cursor.current() + else: + ret = self.next_nodup() + + # If the current cursor is in front of + # requested index move it backward. + else: + while index > self._index[0]: + ret = self.next_nodup() + if ret is None: + log.warn("Failed to move forward to index = %s" % (str(index))) + break + + ret = self._cursor.current() + + # when we have got the record save it in + # the cache + if ret is not None: + ret = self._unpickle(ret) + self._object_cache[index] = {'primary':ret} + + return ret + + def _unpickle(self,rec): + """ + It appears that reading an object from a cursor does not + automatically unpickle it. So this method provides + a convenient way to unpickle the object. + """ + if rec and type(rec[1]) == type(""): + tmp = [rec[0],None] + tmp[1] = cPickle.loads(rec[1]) + rec = tmp + return rec + + def lookup_path(self,path): + """ + Lookup a record from a patch specification. + + @param path: The path spec to check. + @type path: A TreeView path. + + """ + + # If the path is for a primary record it will only + # be 1 element long. + if len(path) == 1: + ret = self.lookup(path[0]) + + # If it is for a secondary object we need to + # traverse the duplicates. + else: + + # First check to see if the record has already + # been fetched. + if self._object_cache.has_key(path[0]) and \ + self._object_cache[path[0]].has_key(path[1]): + + # return record from cache. + ret = self._object_cache[path[0]][path[1]] + + else: + # If we already in the duplicates for this + # primary index then the first index will + # be the same as the first element of the + # path. + if self._index[0] == path[0]: + # If the second elements match we are + # already looking at the correct record. + if self._index[1] == path[1]: + ret = self._cursor.current() + + # If the cursor is in front we can + # move it back. Unfortunately there is no + # prev_dup() method so we have to + # reposition of the cursor at the start + # of the duplicates and step forward + else: + self.prev_nodup() + self.next_nodup() + ret = self.lookup(path[0],use_cache=False) + + # If the request if not for the first duplicate + # we step forward the number of duplicates + # that have been requested. + count = 0 + while count < path[1]: + ret = self.next_dup() + count += 1 + + # If the primary elements do not match we + # must move the cursor to the start of the + # duplicates that are requested. + else: + self.prev_nodup() + self.next_nodup() + + ret = self.lookup(path[0],use_cache=False) + + # If the request if not for the first duplicate + # we step forward the number of duplicates + # that have been requested. + count = 0 + while count < path[1]: + ret = self.next_dup() + count += 1 + + + # Put the fetched record in the cache + if ret is not None: + ret = self._unpickle(ret) + self._object_cache[path[0]][path[1]] = ret + + return ret + diff --git a/src/Models/_PersonFilterModel.py b/src/Models/_PersonFilterModel.py new file mode 100644 index 000000000..25692cc77 --- /dev/null +++ b/src/Models/_PersonFilterModel.py @@ -0,0 +1,29 @@ + +import gtk +import time +import bsddb +import cPickle +import logging +log = logging.getLogger(".") + +from _PathCursor import PathCursor +from _ListCursor import ListCursor + +from _FastFilterModel import FastFilterModel +import RelLib + + +class PersonFilterModel(FastFilterModel): + """Provides a fast model interface to the Person table. + """ + + def __init__(self,db,apply_filter): + FastFilterModel.__init__(self,db,apply_filter) + + + def _get_object_class(self,db): + return RelLib.Person + + + def _get_fetch_func(self,db): + return db.get_person_from_handle diff --git a/src/Models/_PersonListModel.py b/src/Models/_PersonListModel.py new file mode 100644 index 000000000..dd5fe1f02 --- /dev/null +++ b/src/Models/_PersonListModel.py @@ -0,0 +1,36 @@ + +import gtk +import time +import bsddb +import cPickle +import logging +log = logging.getLogger(".") + +from _PathCursor import PathCursor +from _ListCursor import ListCursor + +from _FastModel import FastModel +import RelLib + + +class PersonListModel(FastModel): + """Provides a fast model interface to the Person table. + """ + + def __init__(self,db): + FastModel.__init__(self,db) + + def _get_table(self,db): + return db.surnames + + def _get_cursor(self,db): + return ListCursor(db.surnames.cursor()) + + def _get_object_class(self,db): + return RelLib.Person + + def _get_length(self,db): + return self._table.stat()['ndata'] + + def on_get_flags(self): + return gtk.TREE_MODEL_LIST_ONLY|gtk.TREE_MODEL_ITERS_PERSIST diff --git a/src/Models/_PersonTreeModel.py b/src/Models/_PersonTreeModel.py new file mode 100644 index 000000000..18c494bc1 --- /dev/null +++ b/src/Models/_PersonTreeModel.py @@ -0,0 +1,33 @@ + +import gtk +import time +import bsddb +import cPickle +import logging +log = logging.getLogger(".") + +from _PathCursor import PathCursor +from _ListCursor import ListCursor + +from _FastModel import FastModel +import RelLib + + +class PersonTreeModel(FastModel): + """Provides a fast model interface to the Person table. + """ + + def __init__(self,db): + FastModel.__init__(self,db) + + def _get_table(self,db): + return db.surnames + + def _get_cursor(self,db): + return PathCursor(db.surnames.cursor()) + + def _get_object_class(self,db): + return RelLib.Person + + def _get_length(self,db): + return self._table.stat()['nkeys'] diff --git a/src/Models/__init__.py b/src/Models/__init__.py new file mode 100644 index 000000000..31dce9552 --- /dev/null +++ b/src/Models/__init__.py @@ -0,0 +1,4 @@ + +from _PersonListModel import PersonListModel +from _PersonTreeModel import PersonTreeModel +from _PersonFilterModel import PersonFilterModel diff --git a/src/TreeViews/_PersonTreeView.py b/src/TreeViews/_PersonTreeView.py new file mode 100644 index 000000000..b9fc73714 --- /dev/null +++ b/src/TreeViews/_PersonTreeView.py @@ -0,0 +1,82 @@ + +from gettext import gettext as _ + +import gtk + +from Models import \ + PersonTreeModel, PersonListModel, PersonFilterModel + +from NameDisplay import displayer +display_given = displayer.display_given + +class PersonTreeView(gtk.TreeView): + + def __init__(self,db,apply_filter=None): + gtk.TreeView.__init__(self) + + self._db = db + + # Add the Name column + self._name_col = gtk.TreeViewColumn(_("Family Name")) + self._name_col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self._name_col.set_fixed_width(300) + self._id_cell1 = gtk.CellRendererText() + self._name_col.pack_start(self._id_cell1,True) + self._name_col.set_cell_data_func(self._id_cell1,self._family_name) + + # Add the Name column + self._given_col = gtk.TreeViewColumn(_("Given Name")) + self._given_col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self._given_col.set_fixed_width(300) + self._id_cell1 = gtk.CellRendererText() + self._given_col.pack_start(self._id_cell1,True) + self._given_col.set_cell_data_func(self._id_cell1,self._given_name) + + # Add the ID column + self._id_col = gtk.TreeViewColumn(_("ID")) + self._id_col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self._id_col.set_fixed_width(100) + self._id_cell = gtk.CellRendererText() + self._id_col.pack_start(self._id_cell,True) + self._id_col.set_cell_data_func(self._id_cell,self._object_id) + + self.append_column(self._name_col) + self.append_column(self._given_col) + self.append_column(self._id_col) + + self.set_enable_search(False) + self.set_fixed_height_mode(True) + + if apply_filter is not None: + self.set_filter(apply_filter) + else: + self.clear_filter() + + def set_filter(self,apply_filter=None): + self.set_model(PersonFilterModel(self._db,apply_filter)) + + def clear_filter(self): + self.set_model(PersonModel(self._db)) + + + # Accessor methods for the columns + + def _object_id(self, column, cell, model, iter, user_data=None): + (o,rowref) = model.get_value(iter, 0) + if len(rowref) > 1: + cell.set_property('text', o.get_gramps_id()) + else: + cell.set_property('text', "") + + def _family_name(self, column, cell, model, iter, user_data=None): + (o,rowref) = model.get_value(iter, 0) + cell.set_property('text', str(rowref) + " " + o.get_primary_name().get_surname()) + + def _given_name(self, column, cell, model, iter, user_data=None): + (o,rowref) = model.get_value(iter, 0) + if len(rowref) > 1: + cell.set_property('text', display_given(o)) + else: + cell.set_property('text', "") + + diff --git a/src/TreeViews/__init__.py b/src/TreeViews/__init__.py new file mode 100644 index 000000000..9b7ca3375 --- /dev/null +++ b/src/TreeViews/__init__.py @@ -0,0 +1 @@ +from _PersonTreeView import PersonTreeView diff --git a/test/try_tree_model.py b/test/try_tree_model.py new file mode 100644 index 000000000..e7126042c --- /dev/null +++ b/test/try_tree_model.py @@ -0,0 +1,140 @@ +import time +import gtk +import gobject + +import sys, os +sys.path.append("..") + +from Models import PersonModel,PersonFilterModel +from TreeViews import PersonTreeView +import GenericFilter + +## class ProxyPerson(object): +## """ +## This class provides a wrapper around the real object that +## is stored in the model. +## """ + +## def __init__(self,id,db): +## self._id = id +## self._db = db +## self._obj = None + +## def row_ref(self): +## """This should return the value that is used +## as the row reference in the model.""" +## return self._id + +## def __getattr__(self, name): +## """ +## Delegate to the real object. +## """ + +## # Fetch the object from the database if we +## # don't already have it +## if self._obj is None: +## self._obj = self._get_object() + +## # Call the method that we were asked +## # for on the real object. +## return getattr(self._obj, name) + +## def _get_object(self): +## """ +## Fetch the real object from the database. +## """ +## print "getting object = ", self._id +## return self._db.get_person_from_handle(self._id) + + +class PersonWindow(gtk.Window): + + def __init__(self,db): + gtk.Window.__init__(self) + + self.set_default_size(700,300) + + self._db = db + + fil = GenericFilter.GenericFilter() + fil.add_rule(GenericFilter.SearchName(["Taylor"])) + + person_tree = PersonTreeView(db,fil) + person_tree.show() + + scrollwindow = gtk.ScrolledWindow() + scrollwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrollwindow.set_shadow_type(gtk.SHADOW_ETCHED_IN) + scrollwindow.show() + scrollwindow.add(person_tree) + self.add(scrollwindow) + + person_tree.clear_filter() + + #person_tree.set_filter(fil) + #person_model = PersonFilterModel(db,fil) + #person_tree.set_model(person_model) + + self._person_tree = person_tree + #self._person_model = person_model + + #gobject.idle_add(self.load_tree().next) + + self._expose_count = 0 + +## def load_tree(self): +## self._person_tree.freeze_child_notify() + +## for i in self._db.get_person_handles(): +## self._person_model.add(ProxyPerson(i,self._db)) +## yield True + +## self._person_tree.thaw_child_notify() + +## self._person_tree.set_model(self._person_model) + +## yield False + +if __name__ == "__main__": + import sys, os + sys.path.append("..") + + import GrampsDb + import const + import logging + + form = logging.Formatter(fmt="%(relativeCreated)d: %(levelname)s: %(filename)s: line %(lineno)d: %(message)s") + stderrh = logging.StreamHandler(sys.stderr) + stderrh.setFormatter(form) + stderrh.setLevel(logging.DEBUG) + + # everything. + l = logging.getLogger() + l.setLevel(logging.DEBUG) + l.addHandler(stderrh) + + + def cb(d): + pass + + def main(): + print "start", sys.argv[1] + + db = GrampsDb.gramps_db_factory(const.app_gramps)() + db.load(os.path.realpath(sys.argv[1]), + cb, # callback + "w") + + print "window" + w = PersonWindow(db) + w.show() + + w.connect("destroy", gtk.main_quit) + + print "main" + gtk.main() + + #import profile + #profile.run('main()','profile.out') + + main()