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 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] == 1: # Filter #following is None if no data given in filter sidebar self.search = search[1] if self.has_secondary: self.search2 = search[1] _LOG.debug("search2 filter %s %s" % (search[0], search[1])) self._build_data = self._rebuild_filter elif search[0] == 0: # Search if search[1]: # we have search[1] = (index, text_unicode, inversion) col, text, inv = search[1] func = lambda x: self._get_value(x, col, secondary=False) or "" if self.has_secondary: func2 = lambda x: self._get_value(x, col, secondary=True) or "" if search[2]: self.search = ExactSearchFilter(func, text, inv) if self.has_secondary: self.search2 = ExactSearchFilter(func2, text, inv) else: self.search = SearchFilter(func, text, inv) if self.has_secondary: self.search2 = SearchFilter(func2, text, inv) else: self.search = None if self.has_secondary: self.search2 = None _LOG.debug("search2 search with no data") self._build_data = self._rebuild_search else: # Fast filter self.search = search[1] if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 fast filter") self._build_data = self._rebuild_search else: self.search = None if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 no search parameter") self._build_data = self._rebuild_search self.current_filter = self.search if self.has_secondary: self.current_filter2 = self.search2
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] == 1: # Filter #following is None if no data given in filter sidebar self.search = search[1] if self.has_secondary: self.search2 = search[1] _LOG.debug("search2 filter %s %s" % (search[0], search[1])) self._build_data = self._rebuild_filter elif search[0] == 0: # Search if search[1]: # we have search[1] = (index, text_unicode, inversion) col, text, inv = search[1] func = lambda x: self._get_value(x, col, secondary=False ) or "" if self.has_secondary: func2 = lambda x: self._get_value( x, col, secondary=True) or "" if search[2]: self.search = ExactSearchFilter(func, text, inv) if self.has_secondary: self.search2 = ExactSearchFilter(func2, text, inv) else: self.search = SearchFilter(func, text, inv) if self.has_secondary: self.search2 = SearchFilter(func2, text, inv) else: self.search = None if self.has_secondary: self.search2 = None _LOG.debug("search2 search with no data") self._build_data = self._rebuild_search else: # Fast filter self.search = search[1] if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 fast filter") self._build_data = self._rebuild_search else: self.search = None if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 no search parameter") self._build_data = self._rebuild_search self.current_filter = self.search if self.has_secondary: self.current_filter2 = self.search2
class TreeBaseModel(GObject.GObject, Gtk.TreeModel, BaseModel): """ The base class for all hierarchical treeview models. The model defines the mapping between a unique node and a path. Paths are defined by a tuple. The first element is an integer specifying the position in the top level of the hierarchy. The next element relates to the next level in the hierarchy. The number of elements depends on the depth of the node within the hierarchy. The following data is stored: tree A dictionary of unique identifiers which correspond to nodes in the hierarchy. Each entry is a node object. handle2node A dictionary of gramps handles. Each entry is a node object. nodemap A NodeMap, mapping id's of the nodes to the node objects. Node refer to other nodes via id's in a linked list form. The model obtains data from database as needed and holds a cache of most recently used data. As iter for generictreemodel, node is used. This will be the handle for database objects. Creation: db : the database search : the search that must be shown skip : values not to show scol : column on which to sort order : order of the sort sort_map : mapping from columns seen on the GUI and the columns as defined here nrgroups : maximum number of grouping level, 0 = no group, 1= one group, .... Some optimizations can be for only one group. nrgroups=0 should never be used, as then a flatbasemodel should be used group_can_have_handle : can groups have a handle. If False, this means groups are only used to group subnodes, not for holding data and showing subnodes has_secondary : If True, the model contains two Gramps object types. The suffix '2' is appended to variables relating to the secondary object type. """ def __init__(self, db, uistate, search=None, skip=set(), scol=0, order=Gtk.SortType.ASCENDING, sort_map=None, nrgroups=1, group_can_have_handle=False, has_secondary=False): cput = time.clock() GObject.GObject.__init__(self) BaseModel.__init__(self) #We create a stamp to recognize invalid iterators. From the docs: #Set the stamp to be equal to your model's stamp, to mark the #iterator as valid. When your model's structure changes, you should #increment your model's stamp to mark all older iterators as invalid. #They will be recognised as invalid because they will then have an #incorrect stamp. self.stamp = 0 #two unused attributes pesent to correspond to flatbasemodel self.prev_handle = None self.prev_data = None self.uistate = uistate self.__reverse = (order == Gtk.SortType.DESCENDING) self.scol = scol self.nrgroups = nrgroups self.group_can_have_handle = group_can_have_handle self.has_secondary = has_secondary self.db = db self.dont_change_active = False self._set_base_data() # Initialise data structures self.tree = {} self.nodemap = NodeMap() self.handle2node = {} #GTK3 We leak ref, yes?? #self.set_property("leak_references", False) #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] self.sort_func = self.smap[col] if self.has_secondary: self.sort_func2 = self.smap2[col] self.sort_col = col else: self.sort_func = self.smap[scol] if self.has_secondary: self.sort_func2 = self.smap2[scol] self.sort_col = scol self._in_build = False self.__total = 0 self.__displayed = 0 self.set_search(search) if self.has_secondary: self.rebuild_data(self.current_filter, self.current_filter2, skip) else: self.rebuild_data(self.current_filter, skip=skip) _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.has_secondary: self.sort_func2 = None if self.nodemap: self.nodemap.destroy() self.nodemap = None self.rebuild_data = None self._build_data = None self.search = None self.search2 = None self.current_filter = None self.current_filter2 = None def _set_base_data(self): """ This method must be overwritten in the inheriting class, setting all needed information gen_cursor : func to create cursor to loop over objects in model number_items : func to obtain number of items that are shown if all shown map : function to obtain the raw bsddb object datamap smap : the map with functions to obtain sort value based on sort col fmap : the map with functions to obtain value of a row with handle """ self.gen_cursor = None self.number_items = None # function self.map = None self.smap = None self.fmap = None if self.has_secondary: self.gen_cursor2 = None self.number_items2 = None # function self.map2 = None self.smap2 = None self.fmap2 = None def displayed(self): """ Return the number of rows displayed. """ return self.__displayed def total(self): """ Return the total number of rows without a filter or search condition. """ return self.__total def color_column(self): """ Return the color column. """ return None def clear(self): """ Clear the data map. """ self.clear_cache() self.tree.clear() self.handle2node.clear() self.stamp += 1 self.nodemap.clear() #start with creating the new iters topnode = Node(None, None, None, None, False) self.nodemap.add_node(topnode) self.tree[None] = topnode 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] == 1: # Filter #following is None if no data given in filter sidebar self.search = search[1] if self.has_secondary: self.search2 = search[1] _LOG.debug("search2 filter %s %s" % (search[0], search[1])) self._build_data = self._rebuild_filter elif search[0] == 0: # Search if search[1]: # we have search[1] = (index, text_unicode, inversion) col, text, inv = search[1] func = lambda x: self._get_value(x, col, secondary=False ) or "" if self.has_secondary: func2 = lambda x: self._get_value( x, col, secondary=True) or "" if search[2]: self.search = ExactSearchFilter(func, text, inv) if self.has_secondary: self.search2 = ExactSearchFilter(func2, text, inv) else: self.search = SearchFilter(func, text, inv) if self.has_secondary: self.search2 = SearchFilter(func2, text, inv) else: self.search = None if self.has_secondary: self.search2 = None _LOG.debug("search2 search with no data") self._build_data = self._rebuild_search else: # Fast filter self.search = search[1] if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 fast filter") self._build_data = self._rebuild_search else: self.search = None if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 no search parameter") self._build_data = self._rebuild_search self.current_filter = self.search if self.has_secondary: self.current_filter2 = self.search2 def rebuild_data(self, data_filter=None, data_filter2=None, skip=[]): """ Rebuild the data map. When called externally (from listview), data_filter and data_filter2 should be None; set_search will already have been called to establish the filter functions. When called internally (from __init__) both data_filter and data_filter2 will have been set from set_search """ cput = time.clock() self.clear_cache() self._in_build = True if not ((self.db is not None) and self.db.is_open()): return self.clear() if self.has_secondary: self._build_data(self.current_filter, self.current_filter2, skip) else: self._build_data(self.current_filter, None, skip) self._in_build = False self.current_filter = data_filter if self.has_secondary: self.current_filter2 = data_filter2 _LOG.debug(self.__class__.__name__ + ' rebuild_data ' + str(time.clock() - cput) + ' sec') def _rebuild_search(self, dfilter, dfilter2, skip): """ Rebuild the data map where a search condition is applied. """ self.__total = 0 self.__displayed = 0 items = self.number_items() _LOG.debug("rebuild search primary") self.__rebuild_search(dfilter, skip, items, self.gen_cursor, self.add_row) if self.has_secondary: _LOG.debug("rebuild search secondary") items = self.number_items2() self.__rebuild_search(dfilter2, skip, items, self.gen_cursor2, self.add_row2) def __rebuild_search(self, dfilter, skip, items, gen_cursor, add_func): """ Rebuild the data map for a single Gramps object type, where a search condition is applied. """ pmon = progressdlg.ProgressMonitor(progressdlg.StatusProgress, (self.uistate, ), popup_time=2, title=_("Loading items...")) status = progressdlg.LongOpStatus(total_steps=items, interval=items // 20) pmon.add_op(status) with gen_cursor() as cursor: for handle, data in cursor: status.heartbeat() self.__total += 1 if not (handle in skip or (dfilter and not dfilter.match(handle, self.db))): _LOG.debug(" add %s %s" % (handle, data)) self.__displayed += 1 add_func(handle, data) status.end() def _rebuild_filter(self, dfilter, dfilter2, skip): """ Rebuild the data map where a filter is applied. """ self.__total = 0 self.__displayed = 0 if not self.has_secondary: # The tree only has primary data items = self.number_items() _LOG.debug("rebuild filter primary") self.__rebuild_filter(dfilter, skip, items, self.gen_cursor, self.map, self.add_row) else: # The tree has both primary and secondary data. The navigation type # (navtype) which governs the filters that are offered, is for the # secondary data. items = self.number_items2() _LOG.debug("rebuild filter secondary") self.__rebuild_filter(dfilter2, skip, items, self.gen_cursor2, self.map2, self.add_row2) def __rebuild_filter(self, dfilter, skip, items, gen_cursor, data_map, add_func): """ Rebuild the data map for a single Gramps object type, where a filter is applied. """ pmon = progressdlg.ProgressMonitor(progressdlg.StatusProgress, (self.uistate, ), popup_time=2, title=_("Loading items...")) status_ppl = progressdlg.LongOpStatus(total_steps=items, interval=items // 20) pmon.add_op(status_ppl) self.__total += items assert not skip if dfilter: for handle in dfilter.apply(self.db, user=User(parent=self.uistate.window)): status_ppl.heartbeat() data = data_map(handle) add_func(handle, data) self.__displayed += 1 else: with gen_cursor() as cursor: for handle, data in cursor: status_ppl.heartbeat() add_func(handle, data) self.__displayed += 1 status_ppl.end() def add_node(self, parent, child, sortkey, handle, add_parent=True, secondary=False): """ Add a node to the map. parent The parent node for the child. None for top level. If this node does not exist, it will be added under the top level if add_parent=True. For performance, if you have added the parent, passing add_parent=False, will skip adding missing parent child A unique ID for the node. sortkey A key by which to sort child nodes of each parent. handle The gramps handle of the object corresponding to the node. None if the node does not have a handle. add_parent Bool, if True, check if parent is present, if not add the parent as a top group with no handle """ self.clear_path_cache() if add_parent and not (parent in self.tree): #add parent to self.tree as a node with no handle, as the first #group level self.add_node(None, parent, parent, None, add_parent=False) if child in self.tree: #a node is added that is already present, child_node = self.tree[child] self._add_dup_node(child_node, parent, child, sortkey, handle, secondary) else: parent_node = self.tree[parent] child_node = Node(child, id(parent_node), sortkey, handle, secondary) parent_node.add_child(child_node, self.nodemap) self.tree[child] = child_node self.nodemap.add_node(child_node) if not self._in_build: # emit row_inserted signal iternode = self._get_iter(child_node) path = self.do_get_path(iternode) self.row_inserted(path, iternode) if handle: self.__total += 1 self.__displayed += 1 if handle: self.handle2node[handle] = child_node def _add_dup_node(self, node, parent, child, sortkey, handle, secondary): """ How to handle adding a node a second time Default: if group nodes can have handles, it is allowed to add it again, and this time setting the handle Otherwise, a node should never be added twice! """ if not self.group_can_have_handle: print('WARNING: Attempt to add node twice to the model (%s: %s)' % (str(parent), str(child))) return if handle: node.set_handle(handle, secondary) if not self._in_build: self.__total += 1 self.__displayed += 1 def remove_node(self, node): """ Remove a node from the map. """ self.clear_path_cache() if node.children: del self.handle2node[node.handle] node.set_handle(None) self.__displayed -= 1 self.__total -= 1 elif node.parent: # don't remove the hidden root node iternode = self._get_iter(node) path = self.do_get_path(iternode) self.nodemap.node(node.parent).remove_child(node, self.nodemap) del self.tree[node.ref] if node.handle is not None: del self.handle2node[node.handle] self.__displayed -= 1 self.__total -= 1 self.nodemap.del_node(node) del node # emit row_deleted signal self.row_deleted(path) def reverse_order(self): """ Reverse the order of the map. Only for Gtk 3.9+ does this signal rows_reordered, so to propagate the change to the view, you need to reattach the model to the view. """ self.clear_path_cache() self.__reverse = not self.__reverse top_node = self.tree[None] self._reverse_level(top_node) def _reverse_level(self, node): """ Reverse the order of a single level in the map and signal rows_reordered so the view is updated. If many changes are done, it is better to detach the model, do the changes to reverse the level, and reattach the model, so the view does not update for every change signal. """ if node.children: rows = list(range(len(node.children) - 1, -1, -1)) if node.parent is None: path = iter = None else: iternode = self._get_iter(node) path = self.do_get_path(iternode) # activate when https://bugzilla.gnome.org/show_bug.cgi?id=684558 # is resolved if False: self.rows_reordered(path, iter, rows) if self.nrgroups > 1: for child in node.children: self._reverse_level(self.nodemap.node(child[1])) def get_tree_levels(self): """ Return the headings of the levels in the hierarchy. """ raise NotImplementedError def add_row(self, handle, data): """ Add a row to the model. In general this will add more than one node by using the add_node method. """ self.clear_path_cache() def add_row_by_handle(self, handle): """ Add a row to the model. """ assert isinstance(handle, str) self.clear_path_cache() if self._get_node(handle) is not None: return # row already exists cput = time.clock() data = self.map(handle) if data: if not self.search or \ (self.search and self.search.match(handle, self.db)): self.add_row(handle, data) else: if not self.search2 or \ (self.search2 and self.search2.match(handle, self.db)): self.add_row2(handle, self.map2(handle)) _LOG.debug(self.__class__.__name__ + ' add_row_by_handle ' + str(time.clock() - cput) + ' sec') _LOG.debug("displayed %d / total: %d" % (self.__displayed, self.__total)) def delete_row_by_handle(self, handle): """ Delete a row from the model. """ assert isinstance(handle, str) cput = time.clock() self.clear_cache(handle) node = self._get_node(handle) if node is None: return # row not currently displayed parent = self.nodemap.node(node.parent) self.remove_node(node) while parent is not None: next_parent = parent.parent and self.nodemap.node(parent.parent) if not parent.children: if parent.handle: # emit row_has_child_toggled signal iternode = self._get_iter(parent) path = self.do_get_path(iternode) self.row_has_child_toggled(path, iternode) else: self.remove_node(parent) parent = next_parent _LOG.debug(self.__class__.__name__ + ' delete_row_by_handle ' + str(time.clock() - cput) + ' sec') _LOG.debug("displayed %d / total: %d" % (self.__displayed, self.__total)) def update_row_by_handle(self, handle): """ Update a row in the model. We have to do delete/add because sometimes row position changes when object name changes. A delete action causes the listview module to set a prior row to active. In some cases (merge) the prior row may have been already removed from the db. To avoid invalid handle exceptions in gramplets at the change active, we tell listview not to change active. The add_row below changes to current active again so we end up in right place. """ assert isinstance(handle, str) self.clear_cache(handle) if self._get_node(handle) is None: return # row not currently displayed self.dont_change_active = True self.delete_row_by_handle(handle) self.dont_change_active = False self.add_row_by_handle(handle) def _new_iter(self, nodeid): """ Return a new iter containing the nodeid in the nodemap """ iter = Gtk.TreeIter() iter.stamp = self.stamp #user data should be an object, so we store the long as str iter.user_data = nodeid return iter def _get_iter(self, node): """ Return an iter from the node. iters are always created afresh Will raise IndexError if the maps are not filled yet, or if it is empty. Caller should take care of this if it allows calling with invalid path :param path: node as it appears in the treeview :type path: Node """ if node is None: raise Exception('Not allowed to add None as node') iter = self._new_iter(id(node)) return iter def _get_node(self, handle): """ Get the node for a handle. """ return self.handle2node.get(handle) def get_iter_from_handle(self, handle): """ Get the iter for a gramps handle. Should return None if iter not visible """ node = self._get_node(handle) if node is None: return None return self._get_iter(node) def get_handle_from_iter(self, iter): """ Get the gramps handle for an iter. Return None if the iter does not correspond to a gramps object. """ node = self.get_node_from_iter(iter) return node.handle # The following implement the public interface of Gtk.TreeModel def do_get_flags(self): """ See Gtk.TreeModel """ return 0 #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 """ raise NotImplementedError def do_get_column_type(self, index): """ See Gtk.TreeModel """ return str def do_get_value(self, iter, col): """ See Gtk.TreeModel """ nodeid = iter.user_data node = self.nodemap.node(nodeid) if node.handle is None: # Header rows dont get the foreground color set if col == self.color_column(): #color must not be utf-8 return "" # Return the node name for the first column if col == 0: val = self.column_header(node) else: #no value to show in other header column val = '' else: # return values for 'data' row, calling a function # according to column_defs table val = self._get_value(node.handle, col, node.secondary) if val is None: return '' return val def _get_value(self, handle, col, secondary=False, store_cache=True): """ Returns the contents of a given column of a gramps object """ if secondary is None: raise NotImplementedError cached, data = self.get_cached_value(handle, col) if not cached: if not secondary: data = self.map(handle) else: data = self.map2(handle) if store_cache: self.set_cached_value(handle, col, data) if data is None: return '' if not secondary: # None is used to indicate this column has no data if self.fmap[col] is None: return '' value = self.fmap[col](data) else: if self.fmap2[col] is None: return '' value = self.fmap2[col](data) return value def do_get_iter(self, path): """ Returns a node from a given path. """ if not self.tree or not self.tree[None].children: return False, Gtk.TreeIter() node = self.tree[None] if isinstance(path, tuple): pathlist = path else: pathlist = path.get_indices() for index in pathlist: _index = (-index - 1) if self.__reverse else index try: if len(node.children[_index]) > 0: node = self.nodemap.node(node.children[_index][1]) else: return False, Gtk.TreeIter() except IndexError: return False, Gtk.TreeIter() return True, self._get_iter(node) def get_node_from_iter(self, iter): if iter and iter.user_data: return self.nodemap.node(iter.user_data) else: print('Problem', iter, iter.user_data) return None def do_get_path(self, iter): """ Returns a path from a given node. """ cached, path = self.get_cached_path(iter.user_data) if cached: (treepath, pathtup) = path return treepath node = self.get_node_from_iter(iter) pathlist = [] while node.parent is not None: parent = self.nodemap.node(node.parent) index = -1 while node is not None: # Step backwards nodeid = node.next if self.__reverse else node.prev # Let's see if sibling is cached: cached, sib_path = self.get_cached_path(nodeid) if cached: (sib_treepath, sib_pathtup) = sib_path # Does it have an actual path? if sib_pathtup: # Compute path to here from sibling: # parent_path + sib_path + offset newtup = (sib_pathtup[:-1] + (sib_pathtup[-1] + index + 2, ) + tuple(reversed(pathlist))) #print("computed path:", iter.user_data, newtup) retval = Gtk.TreePath(newtup) self.set_cached_path(iter.user_data, (retval, newtup)) return retval node = nodeid and self.nodemap.node(nodeid) index += 1 pathlist.append(index) node = parent if pathlist: pathlist.reverse() #print("actual path :", iter.user_data, tuple(pathlist)) retval = Gtk.TreePath(tuple(pathlist)) else: retval = None self.set_cached_path(iter.user_data, (retval, tuple(pathlist) if pathlist else None)) return retval def do_iter_next(self, iter): """ Sets iter to the next node at this level of the tree See Gtk.TreeModel Get the next node with the same parent as the given node. """ node = self.get_node_from_iter(iter) val = node.prev if self.__reverse else node.next if val: #user_data contains the nodeid iter.user_data = val return True else: return False def do_iter_children(self, iterparent): """ Get the first child of the given node. """ if iterparent is None: nodeid = id(self.tree[None]) else: nodeparent = self.get_node_from_iter(iterparent) if nodeparent.children: nodeid = nodeparent.children[-1 if self.__reverse else 0][1] else: return False, None return True, self._new_iter(nodeid) def do_iter_has_child(self, iter): """ Find if the given node has any children. """ node = self.get_node_from_iter(iter) return True if node.children else False def do_iter_n_children(self, iter): """ Get the number of children of the given node. """ if iter is None: node = self.tree[None] else: node = self.get_node_from_iter(iter) return len(node.children) def do_iter_nth_child(self, iterparent, index): """ Get the nth child of the given node. """ if iterparent is None: node = self.tree[None] else: node = self.get_node_from_iter(iterparent) if node.children: if len(node.children) > index: _index = (-index - 1) if self.__reverse else index return True, self._new_iter(node.children[_index][1]) else: return False, None else: return False, None def do_iter_parent(self, iterchild): """ Get the parent of the given node. """ node = self.get_node_from_iter(iterchild) if node.parent: return True, self._new_iter(node.parent) else: return False, None
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
class TreeBaseModel(GObject.GObject, Gtk.TreeModel, BaseModel): """ The base class for all hierarchical treeview models. The model defines the mapping between a unique node and a path. Paths are defined by a tuple. The first element is an integer specifying the position in the top level of the hierarchy. The next element relates to the next level in the hierarchy. The number of elements depends on the depth of the node within the hierarchy. The following data is stored: tree A dictionary of unique identifiers which correspond to nodes in the hierarchy. Each entry is a node object. handle2node A dictionary of gramps handles. Each entry is a node object. nodemap A NodeMap, mapping id's of the nodes to the node objects. Node refer to other nodes via id's in a linked list form. The model obtains data from database as needed and holds a cache of most recently used data. As iter for generictreemodel, node is used. This will be the handle for database objects. Creation: db : the database search : the search that must be shown skip : values not to show scol : column on which to sort order : order of the sort sort_map : mapping from columns seen on the GUI and the columns as defined here nrgroups : maximum number of grouping level, 0 = no group, 1= one group, .... Some optimizations can be for only one group. nrgroups=0 should never be used, as then a flatbasemodel should be used group_can_have_handle : can groups have a handle. If False, this means groups are only used to group subnodes, not for holding data and showing subnodes has_secondary : If True, the model contains two Gramps object types. The suffix '2' is appended to variables relating to the secondary object type. """ def __init__(self, db, search=None, skip=set(), scol=0, order=Gtk.SortType.ASCENDING, sort_map=None, nrgroups = 1, group_can_have_handle = False, has_secondary=False): cput = time.clock() GObject.GObject.__init__(self) BaseModel.__init__(self) #We create a stamp to recognize invalid iterators. From the docs: #Set the stamp to be equal to your model's stamp, to mark the #iterator as valid. When your model's structure changes, you should #increment your model's stamp to mark all older iterators as invalid. #They will be recognised as invalid because they will then have an #incorrect stamp. self.stamp = 0 #two unused attributes pesent to correspond to flatbasemodel self.prev_handle = None self.prev_data = None self.__reverse = (order == Gtk.SortType.DESCENDING) self.scol = scol self.nrgroups = nrgroups self.group_can_have_handle = group_can_have_handle self.has_secondary = has_secondary self.db = db self._set_base_data() # Initialise data structures self.tree = {} self.nodemap = NodeMap() self.handle2node = {} #GTK3 We leak ref, yes?? #self.set_property("leak_references", False) #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] self.sort_func = self.smap[col] if self.has_secondary: self.sort_func2 = self.smap2[col] self.sort_col = col else: self.sort_func = self.smap[scol] if self.has_secondary: self.sort_func2 = self.smap2[scol] self.sort_col = scol self._in_build = False self.__total = 0 self.__displayed = 0 self.set_search(search) if self.has_secondary: self.rebuild_data(self.current_filter, self.current_filter2, skip) else: self.rebuild_data(self.current_filter, skip=skip) _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.has_secondary: self.sort_func2 = None if self.nodemap: self.nodemap.destroy() self.nodemap = None self.rebuild_data = None self._build_data = None self.search = None self.search2 = None self.current_filter = None self.current_filter2 = None def _set_base_data(self): """ This method must be overwritten in the inheriting class, setting all needed information gen_cursor : func to create cursor to loop over objects in model number_items : func to obtain number of items that are shown if all shown map : function to obtain the raw bsddb object datamap smap : the map with functions to obtain sort value based on sort col fmap : the map with functions to obtain value of a row with handle """ self.gen_cursor = None self.number_items = None # function self.map = None self.smap = None self.fmap = None if self.has_secondary: self.gen_cursor2 = None self.number_items2 = None # function self.map2 = None self.smap2 = None self.fmap2 = None def displayed(self): """ Return the number of rows displayed. """ return self.__displayed def total(self): """ Return the total number of rows without a filter or search condition. """ return self.__total def color_column(self): """ Return the color column. """ return None def clear(self): """ Clear the data map. """ self.clear_cache() self.tree.clear() self.handle2node.clear() self.stamp += 1 self.nodemap.clear() #start with creating the new iters topnode = Node(None, None, None, None, False) self.nodemap.add_node(topnode) self.tree[None] = topnode 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] == 1: # Filter #following is None if no data given in filter sidebar self.search = search[1] if self.has_secondary: self.search2 = search[1] _LOG.debug("search2 filter %s %s" % (search[0], search[1])) self._build_data = self._rebuild_filter elif search[0] == 0: # Search if search[1]: # we have search[1] = (index, text_unicode, inversion) col, text, inv = search[1] func = lambda x: self._get_value(x, col, secondary=False) or "" if self.has_secondary: func2 = lambda x: self._get_value(x, col, secondary=True) or "" if search[2]: self.search = ExactSearchFilter(func, text, inv) if self.has_secondary: self.search2 = ExactSearchFilter(func2, text, inv) else: self.search = SearchFilter(func, text, inv) if self.has_secondary: self.search2 = SearchFilter(func2, text, inv) else: self.search = None if self.has_secondary: self.search2 = None _LOG.debug("search2 search with no data") self._build_data = self._rebuild_search else: # Fast filter self.search = search[1] if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 fast filter") self._build_data = self._rebuild_search else: self.search = None if self.has_secondary: self.search2 = search[2] _LOG.debug("search2 no search parameter") self._build_data = self._rebuild_search self.current_filter = self.search if self.has_secondary: self.current_filter2 = self.search2 def rebuild_data(self, data_filter=None, data_filter2=None, skip=[]): """ Rebuild the data map. When called externally (from listview), data_filter and data_filter2 should be None; set_search will already have been called to establish the filter functions. When called internally (from __init__) both data_filter and data_filter2 will have been set from set_search """ cput = time.clock() self.clear_cache() self._in_build = True if not self.db.is_open(): return self.clear() if self.has_secondary: self._build_data(self.current_filter, self.current_filter2, skip) else: self._build_data(self.current_filter, None, skip) self._in_build = False self.current_filter = data_filter if self.has_secondary: self.current_filter2 = data_filter2 _LOG.debug(self.__class__.__name__ + ' rebuild_data ' + str(time.clock() - cput) + ' sec') def _rebuild_search(self, dfilter, dfilter2, skip): """ Rebuild the data map where a search condition is applied. """ self.__total = 0 self.__displayed = 0 items = self.number_items() _LOG.debug("rebuild search primary") self.__rebuild_search(dfilter, skip, items, self.gen_cursor, self.add_row) if self.has_secondary: _LOG.debug("rebuild search secondary") items = self.number_items2() self.__rebuild_search(dfilter2, skip, items, self.gen_cursor2, self.add_row2) def __rebuild_search(self, dfilter, skip, items, gen_cursor, add_func): """ Rebuild the data map for a single Gramps object type, where a search condition is applied. """ pmon = progressdlg.ProgressMonitor(progressdlg.GtkProgressDialog, popup_time=2) status = progressdlg.LongOpStatus(msg=_("Building View"), total_steps=items, interval=items//20, can_cancel=True) pmon.add_op(status) with gen_cursor() as cursor: for handle, data in cursor: # for python3 this returns a byte object, so conversion needed if not isinstance(handle, str): handle = handle.decode('utf-8') status.heartbeat() if status.should_cancel(): break self.__total += 1 if not (handle in skip or (dfilter and not dfilter.match(handle, self.db))): _LOG.debug(" add %s %s" % (handle, data)) self.__displayed += 1 add_func(handle, data) if not status.was_cancelled(): status.end() def _rebuild_filter(self, dfilter, dfilter2, skip): """ Rebuild the data map where a filter is applied. """ self.__total = 0 self.__displayed = 0 if not self.has_secondary: # The tree only has primary data items = self.number_items() _LOG.debug("rebuild filter primary") self.__rebuild_filter(dfilter, skip, items, self.gen_cursor, self.map, self.add_row) else: # The tree has both primary and secondary data. The navigation type # (navtype) which governs the filters that are offered, is for the # secondary data. items = self.number_items2() _LOG.debug("rebuild filter secondary") self.__rebuild_filter(dfilter2, skip, items, self.gen_cursor2, self.map2, self.add_row2) def __rebuild_filter(self, dfilter, skip, items, gen_cursor, data_map, add_func): """ Rebuild the data map for a single Gramps object type, where a filter is applied. """ pmon = progressdlg.ProgressMonitor(progressdlg.GtkProgressDialog, popup_time=2) status = progressdlg.LongOpStatus(msg=_("Building View"), total_steps=3, interval=1) pmon.add_op(status) status_ppl = progressdlg.LongOpStatus(msg=_("Loading items..."), total_steps=items, interval=items//10) pmon.add_op(status_ppl) self.__total += items with gen_cursor() as cursor: for handle, data in cursor: if not isinstance(handle, str): handle = handle.decode('utf-8') status_ppl.heartbeat() if not handle in skip: if not dfilter or dfilter.match(handle, self.db): add_func(handle, data) self.__displayed += 1 status_ppl.end() status.end() def add_node(self, parent, child, sortkey, handle, add_parent=True, secondary=False): """ Add a node to the map. parent The parent node for the child. None for top level. If this node does not exist, it will be added under the top level if add_parent=True. For performance, if you have added the parent, passing add_parent=False, will skip adding missing parent child A unique ID for the node. sortkey A key by which to sort child nodes of each parent. handle The gramps handle of the object corresponding to the node. None if the node does not have a handle. add_parent Bool, if True, check if parent is present, if not add the parent as a top group with no handle """ self.clear_path_cache() if add_parent and not (parent in self.tree): #add parent to self.tree as a node with no handle, as the first #group level self.add_node(None, parent, parent, None, add_parent=False) if child in self.tree: #a node is added that is already present, child_node = self.tree[child] self._add_dup_node(child_node, parent, child, sortkey, handle, secondary) else: parent_node = self.tree[parent] child_node = Node(child, id(parent_node), sortkey, handle, secondary) parent_node.add_child(child_node, self.nodemap) self.tree[child] = child_node self.nodemap.add_node(child_node) if not self._in_build: # emit row_inserted signal iternode = self._get_iter(child_node) path = self.do_get_path(iternode) self.row_inserted(path, iternode) if handle: self.__total += 1 self.__displayed += 1 if handle: self.handle2node[handle] = child_node def _add_dup_node(self, node, parent, child, sortkey, handle, secondary): """ How to handle adding a node a second time Default: if group nodes can have handles, it is allowed to add it again, and this time setting the handle Otherwise, a node should never be added twice! """ if not self.group_can_have_handle: print ('WARNING: Attempt to add node twice to the model (%s: %s)' % (str(parent), str(child))) return if handle: node.set_handle(handle, secondary) if not self._in_build: self.__total += 1 self.__displayed += 1 def remove_node(self, node): """ Remove a node from the map. """ self.clear_path_cache() if node.children: del self.handle2node[node.handle] node.set_handle(None) self.__displayed -= 1 self.__total -= 1 elif node.parent: # don't remove the hidden root node iternode = self._get_iter(node) path = self.do_get_path(iternode) self.nodemap.node(node.parent).remove_child(node, self.nodemap) del self.tree[node.ref] if node.handle is not None: del self.handle2node[node.handle] self.__displayed -= 1 self.__total -= 1 self.nodemap.del_node(node) del node # emit row_deleted signal self.row_deleted(path) def reverse_order(self): """ Reverse the order of the map. Only for Gtk 3.9+ does this signal rows_reordered, so to propagate the change to the view, you need to reattach the model to the view. """ self.clear_path_cache() self.__reverse = not self.__reverse top_node = self.tree[None] self._reverse_level(top_node) def _reverse_level(self, node): """ Reverse the order of a single level in the map and signal rows_reordered so the view is updated. If many changes are done, it is better to detach the model, do the changes to reverse the level, and reattach the model, so the view does not update for every change signal. """ if node.children: rows = list(range(len(node.children)-1,-1,-1)) if node.parent is None: path = iter = None else: iternode = self._get_iter(node) path = self.do_get_path(iternode) # activate when https://bugzilla.gnome.org/show_bug.cgi?id=684558 # is resolved if False: self.rows_reordered(path, iter, rows) if self.nrgroups > 1: for child in node.children: self._reverse_level(self.nodemap.node(child[1])) def get_tree_levels(self): """ Return the headings of the levels in the hierarchy. """ raise NotImplementedError def add_row(self, handle, data): """ Add a row to the model. In general this will add more than one node by using the add_node method. """ self.clear_path_cache() def add_row_by_handle(self, handle): """ Add a row to the model. """ assert isinstance(handle, str) self.clear_path_cache() if self._get_node(handle) is not None: return # row already exists cput = time.clock() data = self.map(handle) if data: if not self.search or \ (self.search and self.search.match(handle, self.db)): self.add_row(handle, data) else: if not self.search2 or \ (self.search2 and self.search2.match(handle, self.db)): self.add_row2(handle, self.map2(handle)) _LOG.debug(self.__class__.__name__ + ' add_row_by_handle ' + str(time.clock() - cput) + ' sec') _LOG.debug("displayed %d / total: %d" % (self.__displayed, self.__total)) def delete_row_by_handle(self, handle): """ Delete a row from the model. """ assert isinstance(handle, str) cput = time.clock() self.clear_cache(handle) node = self._get_node(handle) if node is None: return # row not currently displayed parent = self.nodemap.node(node.parent) self.remove_node(node) while parent is not None: next_parent = parent.parent and self.nodemap.node(parent.parent) if not parent.children: if parent.handle: # emit row_has_child_toggled signal iternode = self._get_iter(parent) path = self.do_get_path(iternode) self.row_has_child_toggled(path, iternode) else: self.remove_node(parent) parent = next_parent _LOG.debug(self.__class__.__name__ + ' delete_row_by_handle ' + str(time.clock() - cput) + ' sec') _LOG.debug("displayed %d / total: %d" % (self.__displayed, self.__total)) def update_row_by_handle(self, handle): """ Update a row in the model. """ assert isinstance(handle, str) self.clear_cache(handle) if self._get_node(handle) is None: return # row not currently displayed self.delete_row_by_handle(handle) self.add_row_by_handle(handle) # If the node hasn't moved, all we need is to call row_changed. #self.row_changed(path, node) def _new_iter(self, nodeid): """ Return a new iter containing the nodeid in the nodemap """ iter = Gtk.TreeIter() iter.stamp = self.stamp #user data should be an object, so we store the long as str iter.user_data = nodeid return iter def _get_iter(self, node): """ Return an iter from the node. iters are always created afresh Will raise IndexError if the maps are not filled yet, or if it is empty. Caller should take care of this if it allows calling with invalid path :param path: node as it appears in the treeview :type path: Node """ if node is None: raise Exception('Not allowed to add None as node') iter = self._new_iter(id(node)) return iter def _get_node(self, handle): """ Get the node for a handle. """ return self.handle2node.get(handle) def get_iter_from_handle(self, handle): """ Get the iter for a gramps handle. Should return None if iter not visible """ node = self._get_node(handle) if node is None: return None return self._get_iter(node) def get_handle_from_iter(self, iter): """ Get the gramps handle for an iter. Return None if the iter does not correspond to a gramps object. """ node = self.get_node_from_iter(iter) handle = node.handle if handle and not isinstance(handle, str): handle = handle.decode('utf-8') return handle # The following implement the public interface of Gtk.TreeModel def do_get_flags(self): """ See Gtk.TreeModel """ return 0 #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 """ raise NotImplementedError def do_get_column_type(self, index): """ See Gtk.TreeModel """ return str def do_get_value(self, iter, col): """ See Gtk.TreeModel """ nodeid = iter.user_data node = self.nodemap.node(nodeid) if node.handle is None: # Header rows dont get the foreground color set if col == self.color_column(): #color must not be utf-8 return "#000000000000" # Return the node name for the first column if col == 0: val = self.column_header(node) else: #no value to show in other header column val = '' else: # return values for 'data' row, calling a function # according to column_defs table val = self._get_value(node.handle, col, node.secondary) #GTK 3 should convert unicode objects automatically, but this # gives wrong column values, so convert for python 2.7 if val is None: return '' elif not isinstance(val, str): return val.decode('utf-8') else: return val def _get_value(self, handle, col, secondary=False, store_cache=True): """ Returns the contents of a given column of a gramps object """ if secondary is None: raise NotImplementedError cached, data = self.get_cached_value(handle, col) if not cached: if not secondary: data = self.map(handle) else: data = self.map2(handle) if store_cache: self.set_cached_value(handle, col, data) if data is None: return '' if not secondary: # None is used to indicate this column has no data if self.fmap[col] is None: return '' value = self.fmap[col](data) else: if self.fmap2[col] is None: return '' value = self.fmap2[col](data) return value def do_get_iter(self, path): """ Returns a node from a given path. """ if not self.tree or not self.tree[None].children: return False, Gtk.TreeIter() node = self.tree[None] if isinstance(path, tuple): pathlist = path else: pathlist = path.get_indices() for index in pathlist: _index = (-index - 1) if self.__reverse else index try: if len(node.children[_index]) > 0: node = self.nodemap.node(node.children[_index][1]) else: return False, Gtk.TreeIter() except IndexError: return False, Gtk.TreeIter() return True, self._get_iter(node) def get_node_from_iter(self, iter): if iter and iter.user_data: return self.nodemap.node(iter.user_data) else: print ('Problem', iter, iter.user_data) return None def do_get_path(self, iter): """ Returns a path from a given node. """ cached, path = self.get_cached_path(iter.user_data) if cached: (treepath, pathtup) = path return treepath node = self.get_node_from_iter(iter) pathlist = [] while node.parent is not None: parent = self.nodemap.node(node.parent) index = -1 while node is not None: # Step backwards nodeid = node.next if self.__reverse else node.prev # Let's see if sibling is cached: cached, sib_path = self.get_cached_path(nodeid) if cached: (sib_treepath, sib_pathtup) = sib_path # Does it have an actual path? if sib_pathtup: # Compute path to here from sibling: # parent_path + sib_path + offset newtup = (sib_pathtup[:-1] + (sib_pathtup[-1] + index + 2, ) + tuple(reversed(pathlist))) #print("computed path:", iter.user_data, newtup) retval = Gtk.TreePath(newtup) self.set_cached_path(iter.user_data, (retval, newtup)) return retval node = nodeid and self.nodemap.node(nodeid) index += 1 pathlist.append(index) node = parent if pathlist: pathlist.reverse() #print("actual path :", iter.user_data, tuple(pathlist)) retval = Gtk.TreePath(tuple(pathlist)) else: retval = None self.set_cached_path(iter.user_data, (retval, tuple(pathlist) if pathlist else None)) return retval def do_iter_next(self, iter): """ Sets iter to the next node at this level of the tree See Gtk.TreeModel Get the next node with the same parent as the given node. """ node = self.get_node_from_iter(iter) val = node.prev if self.__reverse else node.next if val: #user_data contains the nodeid iter.user_data = val return True else: return False def do_iter_children(self, iterparent): """ Get the first child of the given node. """ if iterparent is None: nodeid = id(self.tree[None]) else: nodeparent = self.get_node_from_iter(iterparent) if nodeparent.children: nodeid = nodeparent.children[-1 if self.__reverse else 0][1] else: return False, None return True, self._new_iter(nodeid) def do_iter_has_child(self, iter): """ Find if the given node has any children. """ node = self.get_node_from_iter(iter) return True if node.children else False def do_iter_n_children(self, iter): """ Get the number of children of the given node. """ if iter is None: node = self.tree[None] else: node = self.get_node_from_iter(iter) return len(node.children) def do_iter_nth_child(self, iterparent, index): """ Get the nth child of the given node. """ if iterparent is None: node = self.tree[None] else: node = self.get_node_from_iter(iterparent) if node.children: if len(node.children) > index: _index = (-index - 1) if self.__reverse else index return True, self._new_iter(node.children[_index][1]) else: return False, None else: return False, None def do_iter_parent(self, iterchild): """ Get the parent of the given node. """ node = self.get_node_from_iter(iterchild) if node.parent: return True, self._new_iter(node.parent) else: return False, None