1097 lines
39 KiB
Python
1097 lines
39 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2000-2007 Donald N. Allingham
|
|
# Copyright (C) 2009 Gary Burton
|
|
# Copyright (C) 2009-2011 Nick Hall
|
|
# Copyright (C) 2009-2012 Benny Malengier
|
|
# Copyright (C) 2011 Tim G L lyons
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
# gui/views/treemodels/treebasemodel.py
|
|
|
|
"""
|
|
This module provides the model that is used for all hierarchical treeviews.
|
|
"""
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Standard python modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
import time
|
|
import logging
|
|
|
|
_LOG = logging.getLogger(".gui.treebasemodel")
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# GTK modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
from gi.repository import GObject
|
|
from gi.repository import Gtk
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Gramps modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
from gramps.gen.const import GRAMPS_LOCALE as glocale
|
|
_ = glocale.translation.gettext
|
|
import gramps.gui.widgets.progressdialog as progressdlg
|
|
from ...user import User
|
|
from bisect import bisect_right
|
|
from gramps.gen.filters import SearchFilter, ExactSearchFilter
|
|
from .basemodel import BaseModel
|
|
from gramps.gen.proxy.cache import CacheProxyDb
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Node
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
class Node:
|
|
"""
|
|
This class defines an individual node of a tree in the model. The node
|
|
stores the following data:
|
|
|
|
name Textual description of the node.
|
|
sortkey A key which defines the sort order of the node.
|
|
ref Reference to this node in the tree dictionary.
|
|
handle A Gramps handle. Can be None if no Gramps object is
|
|
associated with the node.
|
|
parent id of the parent node.
|
|
prev Link to the previous sibling via id.
|
|
next Link to the next sibling via id.
|
|
|
|
children A list of (sortkey, nodeid) tuples for the children of the node.
|
|
This list is always kept sorted.
|
|
"""
|
|
__slots__ = ('name', 'sortkey', 'ref', 'handle', 'secondary', 'parent',
|
|
'prev', 'next', 'children')#, '__weakref__')
|
|
|
|
def __init__(self, ref, parent, sortkey, handle, secondary):
|
|
if sortkey:
|
|
self.name = sortkey
|
|
#sortkey must be localized sort, so
|
|
self.sortkey = glocale.sort_key(sortkey)
|
|
if not self.sortkey:
|
|
self.sortkey = glocale.sort_key('')
|
|
else:
|
|
self.name = ''
|
|
self.sortkey = glocale.sort_key('')
|
|
self.ref = ref
|
|
self.handle = handle
|
|
self.secondary = secondary
|
|
self.parent = parent
|
|
self.prev = None
|
|
self.next = None
|
|
self.children = []
|
|
|
|
def set_handle(self, handle, secondary=False):
|
|
"""
|
|
Assign the handle of a Gramps object to this node.
|
|
"""
|
|
if not self.handle or handle is None:
|
|
self.handle = handle
|
|
self.secondary = secondary
|
|
else:
|
|
print ('WARNING: Attempt to add handle twice to the node (%s)' %
|
|
handle)
|
|
|
|
def add_child(self, node, nodemap):
|
|
"""
|
|
Add a node to the list of children for this node using the id's in
|
|
nodemap.
|
|
"""
|
|
nodeid = id(node)
|
|
if len(self.children):
|
|
index = bisect_right(self.children, (node.sortkey, nodeid))
|
|
if index == 0:
|
|
node.prev = None
|
|
next_nodeid = self.children[0][1]
|
|
next_node = nodemap.node(next_nodeid)
|
|
next_node.prev = nodeid
|
|
node.next = next_nodeid
|
|
elif index == len(self.children):
|
|
prev_nodeid = self.children[-1][1]
|
|
prev_node = nodemap.node(prev_nodeid)
|
|
prev_node.next = nodeid
|
|
node.prev = prev_nodeid
|
|
node.next = None
|
|
else:
|
|
prev_nodeid = self.children[index - 1][1]
|
|
next_nodeid = self.children[index][1]
|
|
prev_node = nodemap.node(prev_nodeid)
|
|
next_node = nodemap.node(next_nodeid)
|
|
prev_node.next = nodeid
|
|
next_node.prev = nodeid
|
|
node.prev = prev_nodeid
|
|
node.next = next_nodeid
|
|
|
|
self.children.insert(index, (node.sortkey, nodeid))
|
|
|
|
else:
|
|
self.children.append((node.sortkey, nodeid))
|
|
|
|
def remove_child(self, node, nodemap):
|
|
"""
|
|
Remove a node from the list of children for this node, using nodemap.
|
|
"""
|
|
nodeid = id(node)
|
|
index = bisect_right(self.children, (node.sortkey, nodeid)) - 1
|
|
if not (self.children[index] == (node.sortkey, nodeid)):
|
|
raise ValueError(str(node.name) + \
|
|
' not present in self.children: ' + str(self.children)\
|
|
+ ' at index ' + str(index))
|
|
if index == 0:
|
|
if len(self.children) > 1:
|
|
nodemap.node(self.children[index+1][1]).prev = None
|
|
elif index == len(self.children)-1:
|
|
nodemap.node(self.children[index - 1][1]).next = None
|
|
else:
|
|
nodemap.node(self.children[index - 1][1]).next = \
|
|
self.children[index + 1][1]
|
|
nodemap.node(self.children[index + 1][1]).prev = \
|
|
self.children[index - 1][1]
|
|
|
|
self.children.pop(index)
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# NodeMap
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
class NodeMap:
|
|
"""
|
|
Map of id of Node classes to real object
|
|
"""
|
|
def __init__(self):
|
|
self.id2node = {}
|
|
|
|
def destroy(self):
|
|
"""
|
|
Unset all elements that can prevent garbage collection
|
|
"""
|
|
## for key, item in self.id2node.iteritems():
|
|
## item.prev = None
|
|
## item.next = None
|
|
## item.parent = None
|
|
## item.children = []
|
|
self.id2node.clear()
|
|
|
|
def add_node(self, node):
|
|
"""
|
|
Add a Node object to the map and return id of this node
|
|
"""
|
|
nodeid = id(node)
|
|
self.id2node[nodeid] = node
|
|
return nodeid
|
|
|
|
def del_node(self, node):
|
|
"""
|
|
Remove a Node object from the map and return nodeid
|
|
"""
|
|
nodeid = id(node)
|
|
del self.id2node[nodeid]
|
|
return nodeid
|
|
|
|
def del_nodeid(self, nodeid):
|
|
"""
|
|
Remove Node with id nodeid from the map
|
|
"""
|
|
del self.id2node[nodeid]
|
|
|
|
def node(self, nodeid):
|
|
"""
|
|
Obtain the node object from it's id
|
|
"""
|
|
return self.id2node[nodeid]
|
|
|
|
def clear(self):
|
|
"""
|
|
clear the map
|
|
"""
|
|
self.id2node.clear()
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# TreeBaseModel
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
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:
|
|
cdb = CacheProxyDb(self.db)
|
|
for handle in dfilter.apply(cdb,
|
|
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
|