Beispiel #1
0
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
Beispiel #2
0
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