class FlatBaseModel(GObject.GObject, Gtk.TreeModel, BaseModel): """ The base class for all flat treeview models. It keeps a FlatNodeMap, and obtains data from database as needed ..Note: glocale.sort_key is applied to the underlying sort key, so as to have localized sort """ def __init__(self, db, uistate, scol=0, order=Gtk.SortType.ASCENDING, search=None, skip=set(), sort_map=None): cput = time.clock() GObject.GObject.__init__(self) BaseModel.__init__(self) self.uistate = uistate self.user = User(parent=uistate.window, uistate=uistate) #inheriting classes must set self.map to obtain the data self.prev_handle = None self.prev_data = None #GTK3 We leak ref, yes?? #self.set_property("leak_references", False) self.db = db #normally sort on first column, so scol=0 if sort_map: #sort_map is the stored order of the columns and if they are #enabled or not. We need to store on scol of that map self.sort_map = [f for f in sort_map if f[0]] #we need the model col, that corresponds with scol col = self.sort_map[scol][1] else: col = scol # get the function that maps data to sort_keys self.sort_func = lambda x: glocale.sort_key(self.smap[col](x)) self.sort_col = scol self.skip = skip self._in_build = False self.node_map = FlatNodeMap() self.set_search(search) self._reverse = (order == Gtk.SortType.DESCENDING) self.rebuild_data() _LOG.debug(self.__class__.__name__ + ' __init__ ' + str(time.clock() - cput) + ' sec') def destroy(self): """ Unset all elements that prevent garbage collection """ BaseModel.destroy(self) self.db = None self.sort_func = None if self.node_map: self.node_map.destroy() self.node_map = None self.rebuild_data = None self.search = None def set_search(self, search): """ Change the search function that filters the data in the model. When this method is called, make sure: # you call self.rebuild_data() to recalculate what should be seen in the model # you reattach the model to the treeview so that the treeview updates with the new entries """ if search: if search[0]: #following is None if no data given in filter sidebar self.search = search[1] self.rebuild_data = self._rebuild_filter else: if search[1]: # Search from topbar in columns # we have search[1] = (index, text_unicode, inversion) col = search[1][0] text = search[1][1] inv = search[1][2] func = lambda x: self._get_value(x, col) or UEMPTY if search[2]: self.search = ExactSearchFilter(func, text, inv) else: self.search = SearchFilter(func, text, inv) else: self.search = None self.rebuild_data = self._rebuild_search else: self.search = None self.rebuild_data = self._rebuild_search def total(self): """ Total number of items that maximally can be shown """ return self.node_map.max_rows() def displayed(self): """ Number of items that are currently displayed """ return len(self.node_map) def reverse_order(self): """ reverse the sort order of the sort column """ self._reverse = not self._reverse self.node_map.reverse_order() def color_column(self): """ Return the color column. """ return None def sort_keys(self): """ Return the (sort_key, handle) list of all data that can maximally be shown. This list is sorted ascending, via localized string sort. """ # use cursor as a context manager with self.gen_cursor() as cursor: #loop over database and store the sort field, and the handle srt_keys = [(self.sort_func(data), key) for key, data in cursor] srt_keys.sort() return srt_keys def _rebuild_search(self, ignore=None): """ function called when view must be build, given a search text in the top search bar """ self.clear_cache() self._in_build = True if (self.db is not None) and self.db.is_open(): allkeys = self.node_map.full_srtkey_hndl_map() if not allkeys: allkeys = self.sort_keys() if self.search and self.search.text: dlist = [ h for h in allkeys if self.search.match(h[1], self.db) and h[1] not in self.skip and h[1] != ignore ] ident = False elif ignore is None and not self.skip: #nothing to remove from the keys present ident = True dlist = allkeys else: ident = False dlist = [ h for h in allkeys if h[1] not in self.skip and h[1] != ignore ] self.node_map.set_path_map(dlist, allkeys, identical=ident, reverse=self._reverse) else: self.node_map.clear_map() self._in_build = False def _rebuild_filter(self, ignore=None): """ function called when view must be build, given filter options in the filter sidebar """ self.clear_cache() self._in_build = True if (self.db is not None) and self.db.is_open(): cdb = CacheProxyDb(self.db) allkeys = self.node_map.full_srtkey_hndl_map() if not allkeys: allkeys = self.sort_keys() if self.search: ident = False if ignore is None: dlist = self.search.apply(cdb, allkeys, tupleind=1, user=self.user) else: dlist = self.search.apply( cdb, [k for k in allkeys if k[1] != ignore], tupleind=1) elif ignore is None: ident = True dlist = allkeys else: ident = False dlist = [k for k in allkeys if k[1] != ignore] self.node_map.set_path_map(dlist, allkeys, identical=ident, reverse=self._reverse) else: self.node_map.clear_map() self._in_build = False def add_row_by_handle(self, handle): """ Add a row. This is called after object with handle is created. Row is only added if search/filter data is such that it must be shown """ assert isinstance(handle, str) if self.node_map.get_path_from_handle(handle) is not None: return # row is already displayed data = self.map(handle) insert_val = (self.sort_func(data), handle) if not self.search or \ (self.search and self.search.match(handle, self.db)): #row needs to be added to the model insert_path = self.node_map.insert(insert_val) if insert_path is not None: node = self.do_get_iter(insert_path)[1] self.row_inserted(insert_path, node) else: self.node_map.insert(insert_val, allkeyonly=True) def delete_row_by_handle(self, handle): """ Delete a row, called after the object with handle is deleted """ assert isinstance(handle, str) if self.node_map.get_path_from_handle(handle) is None: return # row is not currently displayed self.clear_cache(handle) delete_val = (self.node_map.get_sortkey(handle), handle) delete_path = self.node_map.delete(delete_val) #delete_path is an integer from 0 to n-1 if delete_path is not None: self.row_deleted(delete_path) def update_row_by_handle(self, handle): """ Update a row, called after the object with handle is changed """ if self.node_map.get_path_from_handle(handle) is None: return # row is not currently displayed self.clear_cache(handle) oldsortkey = self.node_map.get_sortkey(handle) newsortkey = self.sort_func(self.map(handle)) if oldsortkey is None or oldsortkey != newsortkey: #or the changed object is not present in the view due to filtering #or the order of the object must change. self.delete_row_by_handle(handle) self.add_row_by_handle(handle) else: #the row is visible in the view, is changed, but the order is fixed path = self.node_map.get_path_from_handle(handle) node = self.do_get_iter(path)[1] self.row_changed(path, node) def get_iter_from_handle(self, handle): """ Get the iter for a gramps handle. """ if self.node_map.get_path_from_handle(handle) is None: return None return self.node_map.new_iter(handle) def get_handle_from_iter(self, iter): """ Get the gramps handle for an iter. """ index = iter.user_data if index is None: ##GTK3: user data may only be an integer, we store the index ##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct ## when using user_data for that! ##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366 index = 0 path = self.node_map.real_path(index) return self.node_map.get_handle(path) # The following implement the public interface of Gtk.TreeModel def do_get_flags(self): """ Returns the GtkTreeModelFlags for this particular type of model See Gtk.TreeModel """ #print 'do_get_flags' return Gtk.TreeModelFlags.LIST_ONLY #| Gtk.TreeModelFlags.ITERS_PERSIST def do_get_n_columns(self): """Internal method. Don't inherit""" return self.on_get_n_columns() def on_get_n_columns(self): """ Return the number of columns. Must be implemented in the child objects See Gtk.TreeModel. Inherit as needed """ #print 'do_get_n_col' raise NotImplementedError def do_get_path(self, iter): """ Return the tree path (a tuple of indices at the various levels) for a particular iter. We use handles for unique key iters See Gtk.TreeModel """ #print 'do_get_path', iter return self.node_map.get_path(iter) def do_get_column_type(self, index): """ See Gtk.TreeModel """ #print 'do_get_col_type' return str def do_get_iter_first(self): #print 'get iter first' raise NotImplementedError def do_get_iter(self, path): """ See Gtk.TreeModel """ #print 'do_get_iter', path for p in path: break try: return True, self.node_map.get_iter(p) except IndexError: return False, Gtk.TreeIter() def _get_value(self, handle, col): """ Given handle and column, return unicode value in the column We need this to search in the column in the GUI """ if handle != self.prev_handle: cached, data = self.get_cached_value(handle, col) if not cached: data = self.map(handle) self.set_cached_value(handle, col, data) if data is None: #object is no longer present return '' self.prev_data = data self.prev_handle = handle return self.fmap[col](self.prev_data) def do_get_value(self, iter, col): """ See Gtk.TreeModel. col is the model column that is needed, not the visible column! """ #print ('do_get_val', iter, iter.user_data, col) index = iter.user_data if index is None: ##GTK3: user data may only be an integer, we store the index ##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct ## when using user_data for that! ##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366 index = 0 handle = self.node_map._index2hndl[index][1] val = self._get_value(handle, col) #print 'val is', val, type(val) return val def do_iter_previous(self, iter): #print 'do_iter_previous' raise NotImplementedError def do_iter_next(self, iter): """ Sets iter to the next node at this level of the tree See Gtk.TreeModel """ return self.node_map.iter_next(iter) def do_iter_children(self, iterparent): """ Return the first child of the node See Gtk.TreeModel """ #print 'do_iter_children' print('ERROR: iter children, should not be called in flat base!!') raise NotImplementedError if handle is None and len(self.node_map): return self.node_map.get_first_handle() return None def do_iter_has_child(self, iter): """ Returns true if this node has children See Gtk.TreeModel """ #print 'do_iter_has_child' print('ERROR: iter has_child', iter, 'should not be called in flat base') return False if handle is None: return len(self.node_map) > 0 return False def do_iter_n_children(self, iter): """ See Gtk.TreeModel """ #print 'do_iter_n_children' print('ERROR: iter_n_children', iter, 'should not be called in flat base') return 0 if handle is None: return len(self.node_map) return 0 def do_iter_nth_child(self, iter, nth): """ See Gtk.TreeModel """ #print 'do_iter_nth_child', iter, nth if iter is None: return True, self.node_map.get_iter(nth) return False, None def do_iter_parent(self, iter): """ Returns the parent of this node See Gtk.TreeModel """ #print 'do_iter_parent' return False, None
class FlatBaseModel(GObject.GObject, Gtk.TreeModel): """ The base class for all flat treeview models. It keeps a FlatNodeMap, and obtains data from database as needed ..Note: glocale.sort_key is applied to the underlying sort key, so as to have localized sort """ def __init__(self, db, scol=0, order=Gtk.SortType.ASCENDING, search=None, skip=set(), sort_map=None): cput = time.clock() super(FlatBaseModel, self).__init__() #inheriting classes must set self.map to obtain the data self.prev_handle = None self.prev_data = None #GTK3 We leak ref, yes?? #self.set_property("leak_references", False) self.db = db #normally sort on first column, so scol=0 if sort_map: #sort_map is the stored order of the columns and if they are #enabled or not. We need to store on scol of that map self.sort_map = [ f for f in sort_map if f[0]] #we need the model col, that corresponds with scol col = self.sort_map[scol][1] else: col = scol # get the function that maps data to sort_keys self.sort_func = lambda x: glocale.sort_key(self.smap[col](x)) self.sort_col = scol self.skip = skip self._in_build = False self.node_map = FlatNodeMap() self.set_search(search) self._reverse = (order == Gtk.SortType.DESCENDING) self.rebuild_data() _LOG.debug(self.__class__.__name__ + ' __init__ ' + str(time.clock() - cput) + ' sec') def destroy(self): """ Unset all elements that prevent garbage collection """ self.db = None self.sort_func = None if self.node_map: self.node_map.destroy() self.node_map = None self.rebuild_data = None self.search = None def set_search(self, search): """ Change the search function that filters the data in the model. When this method is called, make sure: # you call self.rebuild_data() to recalculate what should be seen in the model # you reattach the model to the treeview so that the treeview updates with the new entries """ if search: if search[0]: #following is None if no data given in filter sidebar self.search = search[1] self.rebuild_data = self._rebuild_filter else: if search[1]: # Search from topbar in columns # we have search[1] = (index, text_unicode, inversion) col = search[1][0] text = search[1][1] inv = search[1][2] func = lambda x: self._get_value(x, col) or UEMPTY if search[2]: self.search = ExactSearchFilter(func, text, inv) else: self.search = SearchFilter(func, text, inv) else: self.search = None self.rebuild_data = self._rebuild_search else: self.search = None self.rebuild_data = self._rebuild_search def total(self): """ Total number of items that maximally can be shown """ return self.node_map.max_rows() def displayed(self): """ Number of items that are currently displayed """ return len(self.node_map) def reverse_order(self): """ reverse the sort order of the sort column """ self._reverse = not self._reverse self.node_map.reverse_order() def color_column(self): """ Return the color column. """ return None def clear_cache(self, handle=None): """ If you use a cache, overwrite here so it is cleared when this method is called (on rebuild) :param handle: if None, clear entire cache, otherwise clear the handle entry if present """ pass def sort_keys(self): """ Return the (sort_key, handle) list of all data that can maximally be shown. This list is sorted ascending, via localized string sort. """ # use cursor as a context manager with self.gen_cursor() as cursor: #loop over database and store the sort field, and the handle srt_keys=[(self.sort_func(data), key.decode('utf8')) for key, data in cursor] srt_keys.sort() return srt_keys def _rebuild_search(self, ignore=None): """ function called when view must be build, given a search text in the top search bar """ self.clear_cache() self._in_build = True if self.db.is_open(): allkeys = self.node_map.full_srtkey_hndl_map() if not allkeys: allkeys = self.sort_keys() if self.search and self.search.text: dlist = [h for h in allkeys if self.search.match(h[1], self.db) and h[1] not in self.skip and h[1] != ignore] ident = False elif ignore is None and not self.skip: #nothing to remove from the keys present ident = True dlist = allkeys else: ident = False dlist = [h for h in allkeys if h[1] not in self.skip and h[1] != ignore] self.node_map.set_path_map(dlist, allkeys, identical=ident, reverse=self._reverse) else: self.node_map.clear_map() self._in_build = False def _rebuild_filter(self, ignore=None): """ function called when view must be build, given filter options in the filter sidebar """ self.clear_cache() self._in_build = True if self.db.is_open(): allkeys = self.node_map.full_srtkey_hndl_map() if not allkeys: allkeys = self.sort_keys() if self.search: ident = False if ignore is None: dlist = self.search.apply(self.db, allkeys, tupleind=1) else: dlist = self.search.apply(self.db, [ k for k in allkeys if k[1] != ignore], tupleind=1) elif ignore is None : ident = True dlist = allkeys else: ident = False dlist = [ k for k in allkeys if k[1] != ignore ] self.node_map.set_path_map(dlist, allkeys, identical=ident, reverse=self._reverse) else: self.node_map.clear_map() self._in_build = False def add_row_by_handle(self, handle): """ Add a row. This is called after object with handle is created. Row is only added if search/filter data is such that it must be shown """ assert isinstance(handle, str) if self.node_map.get_path_from_handle(handle) is not None: return # row is already displayed data = self.map(handle) insert_val = (self.sort_func(data), handle) if not self.search or \ (self.search and self.search.match(handle, self.db)): #row needs to be added to the model insert_path = self.node_map.insert(insert_val) if insert_path is not None: node = self.do_get_iter(insert_path)[1] self.row_inserted(insert_path, node) else: self.node_map.insert(insert_val, allkeyonly=True) def delete_row_by_handle(self, handle): """ Delete a row, called after the object with handle is deleted """ assert isinstance(handle, str) if self.node_map.get_path_from_handle(handle) is None: return # row is not currently displayed self.clear_cache(handle) delete_val = (self.node_map.get_sortkey(handle), handle) delete_path = self.node_map.delete(delete_val) #delete_path is an integer from 0 to n-1 if delete_path is not None: self.row_deleted(delete_path) def update_row_by_handle(self, handle): """ Update a row, called after the object with handle is changed """ if self.node_map.get_path_from_handle(handle) is None: return # row is not currently displayed self.clear_cache(handle) oldsortkey = self.node_map.get_sortkey(handle) newsortkey = self.sort_func(self.map(handle)) if oldsortkey is None or oldsortkey != newsortkey: #or the changed object is not present in the view due to filtering #or the order of the object must change. self.delete_row_by_handle(handle) self.add_row_by_handle(handle) else: #the row is visible in the view, is changed, but the order is fixed path = self.node_map.get_path_from_handle(handle) node = self.do_get_iter(path)[1] self.row_changed(path, node) def get_iter_from_handle(self, handle): """ Get the iter for a gramps handle. """ if self.node_map.get_path_from_handle(handle) is None: return None return self.node_map.new_iter(handle) def get_handle_from_iter(self, iter): """ Get the gramps handle for an iter. """ index = iter.user_data if index is None: ##GTK3: user data may only be an integer, we store the index ##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct ## when using user_data for that! ##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366 index = 0 path = self.node_map.real_path(index) return self.node_map.get_handle(path) # The following implement the public interface of Gtk.TreeModel def do_get_flags(self): """ Returns the GtkTreeModelFlags for this particular type of model See Gtk.TreeModel """ #print 'do_get_flags' return Gtk.TreeModelFlags.LIST_ONLY #| Gtk.TreeModelFlags.ITERS_PERSIST def do_get_n_columns(self): """Internal method. Don't inherit""" return self.on_get_n_columns() def on_get_n_columns(self): """ Return the number of columns. Must be implemented in the child objects See Gtk.TreeModel. Inherit as needed """ #print 'do_get_n_col' raise NotImplementedError def do_get_path(self, iter): """ Return the tree path (a tuple of indices at the various levels) for a particular iter. We use handles for unique key iters See Gtk.TreeModel """ #print 'do_get_path', iter return self.node_map.get_path(iter) def do_get_column_type(self, index): """ See Gtk.TreeModel """ #print 'do_get_col_type' return str def do_get_iter_first(self): #print 'get iter first' raise NotImplementedError def do_get_iter(self, path): """ See Gtk.TreeModel """ #print 'do_get_iter', path for p in path: break try: return True, self.node_map.get_iter(p) except IndexError: return False, Gtk.TreeIter() def _get_value(self, handle, col): """ Given handle and column, return unicode value in the column We need this to search in the column in the GUI """ if handle != self.prev_handle: data = self.map(handle) if data is None: #object is no longer present return '' self.prev_data = data self.prev_handle = handle return self.fmap[col](self.prev_data) def do_get_value(self, iter, col): """ See Gtk.TreeModel. col is the model column that is needed, not the visible column! """ #print ('do_get_val', iter, iter.user_data, col) index = iter.user_data if index is None: ##GTK3: user data may only be an integer, we store the index ##PROBLEM: pygobject 3.8 stores 0 as None, we need to correct ## when using user_data for that! ##upstream bug: https://bugzilla.gnome.org/show_bug.cgi?id=698366 index = 0 handle = self.node_map._index2hndl[index][1] val = self._get_value(handle, col) #print 'val is', val, type(val) #GTK 3 should convert unicode objects automatically, but this # gives wrong column values, so we convert for python 2.7 if not isinstance(val, str): return val.encode('utf-8') else: return val def do_iter_previous(self, iter): #print 'do_iter_previous' raise NotImplementedError def do_iter_next(self, iter): """ Sets iter to the next node at this level of the tree See Gtk.TreeModel """ return self.node_map.iter_next(iter) def do_iter_children(self, iterparent): """ Return the first child of the node See Gtk.TreeModel """ #print 'do_iter_children' print('ERROR: iter children, should not be called in flat base!!') raise NotImplementedError if handle is None and len(self.node_map): return self.node_map.get_first_handle() return None def do_iter_has_child(self, iter): """ Returns true if this node has children See Gtk.TreeModel """ #print 'do_iter_has_child' print('ERROR: iter has_child', iter, 'should not be called in flat base') return False if handle is None: return len(self.node_map) > 0 return False def do_iter_n_children(self, iter): """ See Gtk.TreeModel """ #print 'do_iter_n_children' print('ERROR: iter_n_children', iter, 'should not be called in flat base') return 0 if handle is None: return len(self.node_map) return 0 def do_iter_nth_child(self, iter, nth): """ See Gtk.TreeModel """ #print 'do_iter_nth_child', iter, nth if iter == None: return True, self.node_map.get_iter(nth) return False, None def do_iter_parent(self, iter): """ Returns the parent of this node See Gtk.TreeModel """ #print 'do_iter_parent' return False, None