added initial fast model implementation
svn: r5872
This commit is contained in:
		| @@ -1,3 +1,9 @@ | ||||
| 2006-02-03 Richard Taylor <rjt-gramps@thegrindstone.me.uk> | ||||
|         * 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  <don@gramps-project.org> | ||||
| 	* src/PersonView.py: history | ||||
| 	* src/EditPerson.py: start the save routine. | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -48,6 +48,7 @@ from _GrampsDbFactories import \ | ||||
|  | ||||
| from _ReadGedcom import GedcomParser | ||||
| from _WriteGedcom import GedcomWriter | ||||
|  | ||||
| from _WriteXML import XmlWriter | ||||
|  | ||||
| from _GrampsDbExceptions import GrampsDbException | ||||
|   | ||||
							
								
								
									
										153
									
								
								src/Models/_FastFilterModel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/Models/_FastFilterModel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										195
									
								
								src/Models/_FastModel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								src/Models/_FastModel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										176
									
								
								src/Models/_ListCursor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/Models/_ListCursor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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<http://pybsddb.sourceforge.net/bsddb3.html>}. | ||||
|         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]) | ||||
|  | ||||
|  | ||||
							
								
								
									
										324
									
								
								src/Models/_PathCursor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								src/Models/_PathCursor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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<http://pybsddb.sourceforge.net/bsddb3.html>}. | ||||
|         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 | ||||
|  | ||||
							
								
								
									
										29
									
								
								src/Models/_PersonFilterModel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/Models/_PersonFilterModel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										36
									
								
								src/Models/_PersonListModel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/Models/_PersonListModel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										33
									
								
								src/Models/_PersonTreeModel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/Models/_PersonTreeModel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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']  | ||||
							
								
								
									
										4
									
								
								src/Models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/Models/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
|  | ||||
| from _PersonListModel import PersonListModel | ||||
| from _PersonTreeModel import PersonTreeModel | ||||
| from _PersonFilterModel import PersonFilterModel | ||||
							
								
								
									
										82
									
								
								src/TreeViews/_PersonTreeView.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/TreeViews/_PersonTreeView.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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', "") | ||||
|  | ||||
|          | ||||
							
								
								
									
										1
									
								
								src/TreeViews/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/TreeViews/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| from _PersonTreeView import PersonTreeView | ||||
							
								
								
									
										140
									
								
								test/try_tree_model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								test/try_tree_model.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
		Reference in New Issue
	
	Block a user