optimization and cleaning of the base model for flat treeviews

svn: r12723
This commit is contained in:
Benny Malengier 2009-06-28 21:15:10 +00:00
parent db5458a507
commit fb6967d000
16 changed files with 636 additions and 384 deletions

View File

@ -127,6 +127,7 @@ src/gen/plug/docgen/Makefile
src/gen/plug/menu/Makefile
src/gui/Makefile
src/gui/views/Makefile
src/gui/views/treemodels/Makefile
src/Config/Makefile
src/FilterEditor/Makefile
src/Mime/Makefile

View File

@ -190,6 +190,10 @@ src/gui/viewmanager.py
# gui/views - the GUI views package
src/gui/views/__init__.py
# gui/views/treemodels - the GUI views package
src/gui/views/treemodels/__init__.py
src/gui/views/treemodels/flatbasemodel.py
# Simple API
src/Simple/_SimpleTable.py
@ -250,7 +254,6 @@ src/docgen/SpreadSheetDoc.py
src/docgen/TextBufDoc.py
# DisplayModels package
src/DisplayModels/_BaseModel.py
src/DisplayModels/_EventModel.py
src/DisplayModels/_FamilyModel.py
src/DisplayModels/_MediaModel.py

View File

@ -4,7 +4,6 @@ pkgdatadir = $(datadir)/@PACKAGE@/DisplayModels
pkgdata_PYTHON = \
__init__.py \
_BaseModel.py \
_EventModel.py \
_FamilyModel.py \
_MediaModel.py \

View File

@ -1,357 +0,0 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2006 Donald N. Allingham
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
#-------------------------------------------------------------------------
#
# python modules
#
#-------------------------------------------------------------------------
import locale
#-------------------------------------------------------------------------
#
# GNOME/GTK modules
#
#-------------------------------------------------------------------------
import gtk
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from Filters import SearchFilter
import Config
#-------------------------------------------------------------------------
#
# NodeMap
#
#-------------------------------------------------------------------------
class NodeMap(object):
"""
Provide the Path to Iter mappings for a TreeView model. The implementation
provides a list of nodes and a dictionary of handles. The datalist provides
the path (index) to iter (handle) mapping, while the the indexmap provides
the handle to index mappings
"""
def __init__(self):
"""
Create a new instance, clearing the datalist and indexmap
"""
self.data_list = []
self.index_map = {}
def set_path_map(self, dlist):
"""
Takes a list of handles and builds the index map from it.
"""
self.data_list = dlist
i = 0
self.index_map = {}
for key in self.data_list:
self.index_map[key] = i
i +=1
def clear_map(self):
"""
Clears out the data_list and the index_map
"""
self.data_list = []
self.index_map = {}
def get_path(self, handle):
"""
Return the path from the passed handle. This is accomplished by
indexing into the index_map to get the index (path)
"""
return self.index_map.get(handle)
def get_handle(self, path):
"""
Return the handle from the path. The path is assumed to be an integer.
This is accomplished by indexing into the data_list
"""
return self.data_list[path]
def delete_by_index(self, index):
"""
Deletes the item at the specified path, then rebuilds the index_map,
subtracting one from each item greater than the deleted index.
"""
handle = self.data_list[index]
del self.data_list[index]
del self.index_map[handle]
for key in self.index_map:
if self.index_map[key] > index:
self.index_map[key] -= 1
def find_next_handle(self, handle):
"""
Finds the next handle based off the passed handle. This is accomplished
by finding the index of associated with the handle, adding one to find
the next index, then finding the handle associated with the next index.
"""
try:
return self.data_list[self.index_map.get(handle)+1]
except IndexError:
return None
def __len__(self):
"""
Return the number of entries in the map.
"""
return len(self.data_list)
def get_first_handle(self):
"""
Return the first handle in the map.
"""
return self.data_list[0]
#-------------------------------------------------------------------------
#
# BaseModel
#
#-------------------------------------------------------------------------
class BaseModel(gtk.GenericTreeModel):
def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING,
tooltip_column=None, search=None, skip=set(),
sort_map=None):
gtk.GenericTreeModel.__init__(self)
self.prev_handle = None
self.prev_data = None
self.set_property("leak_references",False)
self.db = db
if sort_map:
self.sort_map = [ f for f in sort_map if f[0]]
col = self.sort_map[scol][1]
self.sort_func = self.smap[col]
else:
self.sort_func = self.smap[scol]
self.sort_col = scol
self.skip = skip
self.total = 0
self.displayed = 0
self.node_map = NodeMap()
if search:
if search[0]:
self.search = search[1]
self.rebuild_data = self._rebuild_filter
else:
if search[1]:
# we have search[1] = (index, text_unicode, inversion)
col = search[1][0]
text = search[1][1]
inv = search[1][2]
func = lambda x: self.on_get_value(x, col) or u""
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
self.reverse = (order == gtk.SORT_DESCENDING)
self.tooltip_column = tooltip_column
Config.client.notify_add("/apps/gramps/preferences/todo-color",
self.update_todo)
Config.client.notify_add("/apps/gramps/preferences/custom-marker-color",
self.update_custom)
Config.client.notify_add("/apps/gramps/preferences/complete-color",
self.update_complete)
self.complete_color = Config.get(Config.COMPLETE_COLOR)
self.todo_color = Config.get(Config.TODO_COLOR)
self.custom_color = Config.get(Config.CUSTOM_MARKER_COLOR)
self.rebuild_data()
def update_todo(self,client,cnxn_id,entry,data):
self.todo_color = Config.get(Config.TODO_COLOR)
def update_custom(self,client,cnxn_id,entry,data):
self.custom_color = Config.get(Config.CUSTOM_MARKER_COLOR)
def update_complete(self,client,cnxn_id,entry,data):
self.complete_color = Config.get(Config.COMPLETE_COLOR)
def set_sort_column(self,col):
self.sort_func = self.smap[col]
def sort_keys(self):
cursor = self.gen_cursor()
self.sort_data = []
data = cursor.next()
self.total = 0
while data:
self.sort_data.append((self.sort_func(data[1]),data[0]))
self.total += 1
data = cursor.next()
cursor.close()
self.sort_data.sort(lambda x, y: locale.strcoll(x[0], y[0]),
reverse=self.reverse)
return [ x[1] for x in self.sort_data ]
def _rebuild_search(self,ignore=None):
""" function called when view must be build, given a search text
in the top search bar
"""
self.total = 0
if self.db.is_open():
if self.search and self.search.text:
dlist = [h for h in self.sort_keys()\
if self.search.match(h,self.db) and \
h not in self.skip and h != ignore]
else:
dlist = [h for h in self.sort_keys() \
if h not in self.skip and h != ignore]
self.displayed = len(dlist)
self.node_map.set_path_map(dlist)
else:
self.displayed = 0
self.node_map.clear_map()
def _rebuild_filter(self, ignore=None):
""" function called when view must be build, given filter options
in the filter sidebar
"""
self.total = 0
if self.db.is_open():
if self.search:
dlist = self.search.apply(self.db,
[ k for k in self.sort_keys()\
if k != ignore])
else:
dlist = [ k for k in self.sort_keys() \
if k != ignore ]
self.displayed = len(dlist)
self.node_map.set_path_map(dlist)
else:
self.displayed = 0
self.node_map.clear_map()
def add_row_by_handle(self, handle):
if not self.search or \
(self.search and self.search.match(handle, self.db)):
data = self.map(handle)
self.sort_data.append((self.sort_func(data), handle))
self.sort_data.sort(lambda x, y: locale.strcoll(x[0], y[0]),
reverse=self.reverse)
self.node_map.set_path_map([ x[1] for x in self.sort_data ])
index = self.node_map.get_path(handle)
if index is not None:
node = self.get_iter(index)
self.row_inserted(index, node)
def delete_row_by_handle(self, handle):
index = self.node_map.get_path(handle)
# remove from sort array
i = 0
for (key, node) in self.sort_data:
if handle == node:
del self.sort_data[i]
break
i += 1
self.node_map.delete_by_index(index)
self.row_deleted(index)
def update_row_by_handle(self, handle):
index = self.node_map.get_path(handle)
node = self.get_iter(index)
self.row_changed(index, node)
def on_get_flags(self):
"""returns the GtkTreeModelFlags for this particular type of model"""
return gtk.TREE_MODEL_LIST_ONLY | gtk.TREE_MODEL_ITERS_PERSIST
def on_get_n_columns(self):
return 1
def on_get_path(self, node):
"""returns the tree path (a tuple of indices at the various
levels) for a particular node."""
return self.node_map.get_path(node)
def on_get_column_type(self,index):
if index == self.tooltip_column:
return object
return str
def on_get_iter(self, path):
try:
return self.node_map.get_handle(path[0])
except:
return None
def on_get_value(self, node, col):
try:
if node != self.prev_handle:
self.prev_data = self.map(str(node))
self.prev_handle = node
return self.fmap[col](self.prev_data)
except:
return u''
def on_iter_next(self, node):
"""returns the next node at this level of the tree"""
return self.node_map.find_next_handle(node)
def on_iter_children(self, node):
"""Return the first child of the node"""
if node is None and len(self.node_map):
return self.node_map.get_first_handle()
return None
def on_iter_has_child(self, node):
"""returns true if this node has children"""
if node is None:
return len(self.node_map) > 0
return False
def on_iter_n_children(self, node):
if node is None:
return len(self.node_map)
return 0
def on_iter_nth_child(self, node, n):
if node is None:
return self.node_map.get_handle(n)
return None
def on_iter_parent(self, node):
"""returns the parent of this node"""
return None

View File

@ -44,7 +44,7 @@ import ToolTips
import GrampsLocale
import DateHandler
import gen.lib
from _BaseModel import BaseModel
from gui.views.treemodels.flatbasemodel import FlatBaseModel
#-------------------------------------------------------------------------
#
@ -64,7 +64,7 @@ COLUMN_CHANGE = 10
# EventModel
#
#-------------------------------------------------------------------------
class EventModel(BaseModel):
class EventModel(FlatBaseModel):
def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None,
skip=set(), sort_map=None):
@ -91,7 +91,7 @@ class EventModel(BaseModel):
self.column_handle,
self.column_tooltip,
]
BaseModel.__init__(self, db, scol, order, tooltip_column=8,
FlatBaseModel.__init__(self, db, scol, order, tooltip_column=8,
search=search, skip=skip, sort_map=sort_map)
def on_get_n_columns(self):

View File

@ -48,14 +48,14 @@ from BasicUtils import name_displayer
import gen.lib
import gen.utils
from _BaseModel import BaseModel
from gui.views.treemodels.flatbasemodel import FlatBaseModel
#-------------------------------------------------------------------------
#
# FamilyModel
#
#-------------------------------------------------------------------------
class FamilyModel(BaseModel):
class FamilyModel(FlatBaseModel):
_MARKER_COL = 13
@ -88,7 +88,7 @@ class FamilyModel(BaseModel):
self.column_marker_color,
]
self.marker_color_column = 9
BaseModel.__init__(self, db, scol, order, tooltip_column=6,
FlatBaseModel.__init__(self, db, scol, order, tooltip_column=6,
search=search, skip=skip, sort_map=sort_map)
def on_get_n_columns(self):

View File

@ -46,14 +46,14 @@ import ToolTips
import GrampsLocale
import DateHandler
import gen.lib
from _BaseModel import BaseModel
from gui.views.treemodels.flatbasemodel import FlatBaseModel
#-------------------------------------------------------------------------
#
# MediaModel
#
#-------------------------------------------------------------------------
class MediaModel(BaseModel):
class MediaModel(FlatBaseModel):
def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None,
skip=set(), sort_map=None):
@ -80,7 +80,7 @@ class MediaModel(BaseModel):
self.sort_date,
self.column_handle,
]
BaseModel.__init__(self, db, scol, order, tooltip_column=7,
FlatBaseModel.__init__(self, db, scol, order, tooltip_column=7,
search=search, skip=skip, sort_map=sort_map)
def on_get_n_columns(self):

View File

@ -39,7 +39,7 @@ import gtk
# GRAMPS modules
#
#-------------------------------------------------------------------------
from _BaseModel import BaseModel
from gui.views.treemodels.flatbasemodel import FlatBaseModel
from gen.lib import (Note, NoteType, MarkerType, StyledText)
#-------------------------------------------------------------------------
@ -47,7 +47,7 @@ from gen.lib import (Note, NoteType, MarkerType, StyledText)
# NoteModel
#
#-------------------------------------------------------------------------
class NoteModel(BaseModel):
class NoteModel(FlatBaseModel):
"""
"""
def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None,
@ -72,7 +72,7 @@ class NoteModel(BaseModel):
self.column_marker_color
]
self.marker_color_column = 5
BaseModel.__init__(self, db, scol, order, search=search,
FlatBaseModel.__init__(self, db, scol, order, search=search,
skip=skip, sort_map=sort_map)
def on_get_n_columns(self):

View File

@ -43,14 +43,14 @@ import gtk
import const
import ToolTips
import GrampsLocale
from _BaseModel import BaseModel
from gui.views.treemodels.flatbasemodel import FlatBaseModel
#-------------------------------------------------------------------------
#
# PlaceModel
#
#-------------------------------------------------------------------------
class PlaceModel(BaseModel):
class PlaceModel(FlatBaseModel):
HANDLE_COL = 12
@ -89,7 +89,7 @@ class PlaceModel(BaseModel):
self.column_street,
self.column_handle,
]
BaseModel.__init__(self, db, scol, order, tooltip_column=13,
FlatBaseModel.__init__(self, db, scol, order, tooltip_column=13,
search=search, skip=skip, sort_map=sort_map)
def on_get_n_columns(self):

View File

@ -42,14 +42,14 @@ import gtk
#-------------------------------------------------------------------------
import gen.lib
import GrampsLocale
from _BaseModel import BaseModel
from gui.views.treemodels.flatbasemodel import FlatBaseModel
#-------------------------------------------------------------------------
#
# RepositoryModel
#
#-------------------------------------------------------------------------
class RepositoryModel(BaseModel):
class RepositoryModel(FlatBaseModel):
def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None,
skip=set(), sort_map=None):
@ -91,7 +91,7 @@ class RepositoryModel(BaseModel):
self.column_handle,
]
BaseModel.__init__(self, db, scol, order, tooltip_column=14,
FlatBaseModel.__init__(self, db, scol, order, tooltip_column=14,
search=search, skip=skip, sort_map=sort_map)
def on_get_n_columns(self):

View File

@ -43,14 +43,14 @@ import gtk
import const
import ToolTips
import GrampsLocale
from _BaseModel import BaseModel
from gui.views.treemodels.flatbasemodel import FlatBaseModel
#-------------------------------------------------------------------------
#
# SourceModel
#
#-------------------------------------------------------------------------
class SourceModel(BaseModel):
class SourceModel(FlatBaseModel):
def __init__(self,db,scol=0, order=gtk.SORT_ASCENDING,search=None,
skip=set(), sort_map=None):
@ -74,7 +74,7 @@ class SourceModel(BaseModel):
self.column_pubinfo,
self.sort_change,
]
BaseModel.__init__(self,db,scol, order,tooltip_column=7,search=search,
FlatBaseModel.__init__(self,db,scol, order,tooltip_column=7,search=search,
skip=skip, sort_map=sort_map)
def on_get_n_columns(self):

View File

@ -30,6 +30,10 @@ Provide the base classes for GRAMPS' DataView classes
#
#----------------------------------------------------------------
import cPickle as pickle
import time
import logging
_LOG = logging.getLogger('.pageview')
#----------------------------------------------------------------
#
@ -835,9 +839,12 @@ class ListView(BookMarkView):
self.inactive = False
def column_clicked(self, obj, data):
cput = time.clock()
same_col = False
if self.sort_col != data:
order = gtk.SORT_ASCENDING
else:
same_col = True
if (self.columns[data].get_sort_order() == gtk.SORT_DESCENDING
or not self.columns[data].get_sort_indicator()):
order = gtk.SORT_ASCENDING
@ -852,16 +859,17 @@ class ListView(BookMarkView):
else:
search = (False, self.search_bar.get_value())
self.model = self.make_model(self.dbstate.db, self.sort_col, order,
if same_col:
self.model.reverse_order()
else:
self.model = self.make_model(self.dbstate.db, self.sort_col, order,
search=search,
sort_map=self.column_order())
self.list.set_model(self.model)
if handle:
path = self.model.on_get_path(handle)
self.selection.select_path(path)
self.list.scroll_to_cell(path, None, 1, 0.5, 0)
self.goto_handle(handle)
for i in xrange(len(self.columns)):
enable_sort_flag = (i==self.sort_col)
self.columns[i].set_sort_indicator(enable_sort_flag)
@ -870,6 +878,8 @@ class ListView(BookMarkView):
# set the search column to be the sorted column
search_col = self.column_order()[data][1]
self.list.set_search_column(search_col)
_LOG.debug(' ' + self.__class__.__name__ + ' column_clicked ' +
str(time.clock() - cput) + ' sec')
def build_columns(self):
for column in self.columns:
@ -899,6 +909,7 @@ class ListView(BookMarkView):
def build_tree(self):
if self.active:
cput = time.clock()
if Config.get(Config.FILTER):
filter_info = (True, self.generic_filter)
else:
@ -916,6 +927,9 @@ class ListView(BookMarkView):
self.uistate.show_filter_results(self.dbstate,
self.model.displayed,
self.model.total)
_LOG.debug(self.__class__.__name__ + ' build_tree ' +
str(time.clock() - cput) + ' sec')
else:
self.dirty = True
@ -959,8 +973,11 @@ class ListView(BookMarkView):
def row_add(self, handle_list):
if self.active:
cput = time.clock()
for handle in handle_list:
self.model.add_row_by_handle(handle)
_LOG.debug(' ' + self.__class__.__name__ + ' row_add ' +
str(time.clock() - cput) + ' sec')
else:
self.dirty = True
@ -968,15 +985,21 @@ class ListView(BookMarkView):
if self.model:
self.model.prev_handle = None
if self.active:
cput = time.clock()
for handle in handle_list:
self.model.update_row_by_handle(handle)
_LOG.debug(' ' + self.__class__.__name__ + ' row_update ' +
str(time.clock() - cput) + ' sec')
else:
self.dirty = True
def row_delete(self, handle_list):
if self.active:
cput = time.clock()
for handle in handle_list:
self.model.delete_row_by_handle(handle)
_LOG.debug(' ' + self.__class__.__name__ + ' row_delete ' +
str(time.clock() - cput) + ' sec')
else:
self.dirty = True

View File

@ -3,6 +3,9 @@
# but that is not necessarily portable.
# If not using GNU make, then list all .py files individually
SUBDIRS = \
treemodels
pkgdatadir = $(datadir)/@PACKAGE@/views
pkgdata_PYTHON = \

View File

@ -22,3 +22,7 @@
"""
Package init for the views package.
"""
# DO NOT IMPORT METHODS/CLASSES FROM src/gui/views HERE ! Only __all__
__all__ = [ "treemodels" ]

View File

@ -0,0 +1,24 @@
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2009 Benny Malengier
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id: __init__.py 11943 2009-02-09 23:37:40Z acraphae $
"""
Package init for the treemodels package.
"""

View File

@ -0,0 +1,552 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2006 Donald N. Allingham
# Copyright (C) 2009 Benny Malengier
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id: _BaseModel.py 12559 2009-05-21 17:19:50Z gbritton $
#-------------------------------------------------------------------------
#
# python modules
#
#-------------------------------------------------------------------------
from __future__ import with_statement
"""
This module provides the flat treemodel that is used for all flat treeviews.
For performance, GRAMPS does not use gtk.TreeStore, as that would mean keeping
the entire database table of an object in memory.
Instead, it suffices to keep in memory the sortkey and the matching handle,
as well as a map of sortkey,handle to treeview path, and vice versa.
For a flat view, the index of sortkey,handle will be the path, so it suffices
to keep in memory a map that given a sortkey,handle returns the path.
As we need to be able to insert/delete/update objects, and for that the handle
is all we know initially, and as sortkey,handle is uniquely determined by
handle, instead of keeping a map of sortkey,handle to path, we keep a map of
handle to path
As a user selects another column to sort, the sortkey must be rebuild, and the
map remade.
The class FlatNodeMap keeps a sortkeyhandle list with (sortkey, handle) entries,
and a handle2path dictionary. As the Map is flat, the index in sortkeyhandle
corresponds to the path.
The class FlatBaseModel, is the base class for all flat treeview models.
It keeps a FlatNodeMap, and obtains data from database as needed
"""
import locale
import logging
import bisect
_LOG = logging.getLogger(".gui.basetreemodel")
#-------------------------------------------------------------------------
#
# GNOME/GTK modules
#
#-------------------------------------------------------------------------
import gtk
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from Filters import SearchFilter
import Config
import time
#-------------------------------------------------------------------------
#
# FlatNodeMap
#
#-------------------------------------------------------------------------
class FlatNodeMap(object):
"""
A NodeMap for a flat treeview. In such a TreeView, the paths possible are
0, 1, 2, ..., n-1, where n is the number of items to show. For the model
it is needed to keep the Path to Iter mappings of the TreeView in memory
The order of what is shown is based on the unique key: (sortkey, handle)
Naming:
* srtkey : key on which to sort
* hndl : handle of the object, makes it possible to retrieve the
object from the database. As handle is unique, it is used
as the iter for the TreeView
* index : the index in the internal lists. When a view is in reverse,
this is not kept physically, but instead via an offset
* path : integer path in the TreeView. This will be index if view is
ascending, but will begin at back of list if view shows
the entries in reverse.
* index2hndl : list of (srtkey, hndl) tuples. The index gives the
(srtkey, hndl) it belongs to
* hndl2index : dictionary of *hndl: index* values
The implementation provides a list of (srtkey, hndl) of which the index is
the path, and a dictionary mapping hndl to index.
To obtain index given a path, method real_index() is available
..Note: If a string sortkey is used, apply locale.strxfrm on it , so as
to have localized sort
"""
def __init__(self):
"""
Create a new instance.
"""
self._index2hndl = []
self._hndl2index = {}
self._reverse = False
self.__corr = (0, 1)
def set_path_map(self, index2hndllist, reverse=False):
"""
This is the core method to set up the FlatNodeMap
Input is a list of (srtkey, handle), of which the index is the path
Calling this method sets the index2hndllist, and creates the hndl2index
map.
:param index2hndllist: the ascending sorted (sortkey, handle) values
as they will appear in the flat treeview.
:type index2hndllist: a list of (sortkey, handle) tuples
"""
self._index2hndl = index2hndllist
self._hndl2index = {}
self._reverse = reverse
self.reverse_order()
def reverse_order(self):
"""
This method keeps the index2hndl map, but sets it up the index in
reverse order. If the hndl2index map does not exist yet, it is created
in the acending order as given in index2hndl
The result is always a hndl2index map wich is correct, so or ascending
order, or reverse order.
"""
if self._hndl2index:
#if hndl2index is build already, invert order, otherwise keep
# requested order
self._reverse = not self._reverse
if self._reverse:
self.__corr = (len(self._index2hndl) - 1, -1)
else:
self.__corr = (0, 1)
if not self._hndl2index:
for index, key in enumerate(self._index2hndl):
#the handle is key[1]
self._hndl2index[key[1]] = index
def real_path(self, index):
"""
Given the index in the maps, return the real path.
If reverse = False, then index is path, otherwise however, the
path must be calculated so that the last index is the first path
"""
return self.__corr[0] + self.__corr[1] * index
def real_index(self, path):
"""
Given the path in the view, return the real index.
If reverse = False, then path is index, otherwise however, the
index must be calculated so that the last index is the first path
"""
return self.__corr[0] + self.__corr[1] * path
def clear_map(self):
"""
Clears out the index2hndl and the hndl2index
"""
self._index2hndl = []
self._hndl2index = {}
def get_path(self, handle):
"""
Return the path from the passed handle.
:param handle: the key of the object for which the path in the treeview
is needed
:param type: an object handle
"""
return self.real_path(self._hndl2index.get(handle))
def get_handle(self, path):
"""
Return the handle from the path. The path is assumed to be an integer.
This is accomplished by indexing into the index2hndl
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: path as it appears in the treeview
:type path: integer
"""
return self._index2hndl[self.real_index(path)][1]
def find_next_handle(self, handle):
"""
Finds the next handle based off the passed handle. This is accomplished
by finding the index associated with the handle, adding or substracting
one to find the next index, then finding the handle associated with
that.
:param handle: the key of the object for which the next handle shown
in the treeview is needed
:param type: an object handle
"""
index = self._hndl2index.get(handle)
if self._reverse :
index -= 1
if index < 0:
# -1 does not raise IndexError, as -1 is last element. Catch.
return None
else:
index += 1
try:
return self._index2hndl[index][1]
except IndexError:
return None
def get_first_handle(self):
"""
Return the first handle that must be shown (corresponding to path 0)
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
"""
return self._index2hndl[self.real_index(0)][1]
def __len__(self):
"""
Return the number of entries in the map.
"""
return len(self._index2hndl)
def insert(self, srtkey_hndl):
"""
Insert a node. Given is a tuple (sortkey, handle), and this is added
in the correct place, while the hndl2index map is updated.
Returns the path of the inserted row
:param srtkey_hndl: the (sortkey, handle) tuple that must be inserted
:Returns: path of the row inserted in the treeview
:Returns type: integer
"""
insert_pos = bisect.bisect_left(self._index2hndl, srtkey_hndl)
self._index2hndl.insert(insert_pos, srtkey_hndl)
#make sure the index map is updated
for hndl, index in self._hndl2index.iteritems():
if index >= insert_pos:
self._hndl2index[hndl] += 1
self._hndl2index[srtkey_hndl[1]] = insert_pos
#update self.__corr so it remains correct
if self._reverse:
self.__corr = (len(self._index2hndl) - 1, -1)
return self.real_path(insert_pos)
def delete(self, handle):
"""
Delete the row with handle.
This then rebuilds the hndl2index, subtracting one from each item
greater than the deleted index.
:param handle: the handle that must be removed
:type handle: an object handle
:Returns: path of the row deleted from the treeview
:Returns type: integer
"""
index = self._hndl2index[handle]
del self._index2hndl[index]
del self._hndl2index[handle]
#update self.__corr so it remains correct
if self._reverse:
self.__corr = (len(self._index2hndl) - 1, -1)
#update the handle2path map so it remains correct
for key, val in self._hndl2index.iteritems():
if val > index:
self._hndl2index[key] -= 1
return self.real_path(index)
#-------------------------------------------------------------------------
#
# FlatBaseModel
#
#-------------------------------------------------------------------------
class FlatBaseModel(gtk.GenericTreeModel):
"""
The base class for all flat treeview models.
It keeps a FlatNodeMap, and obtains data from database as needed
"""
def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING,
tooltip_column=None, search=None, skip=set(),
sort_map=None):
cput = time.clock()
gtk.GenericTreeModel.__init__(self)
self.prev_handle = None
self.prev_data = None
self.set_property("leak_references",False)
self.db = db
if sort_map:
self.sort_map = [ f for f in sort_map if f[0]]
col = self.sort_map[scol][1]
self.sort_func = self.smap[col]
else:
self.sort_func = self.smap[scol]
self.sort_col = scol
self.skip = skip
self.total = 0
self.displayed = 0
self.node_map = FlatNodeMap()
if search:
if search[0]:
self.search = search[1]
self.rebuild_data = self._rebuild_filter
else:
if search[1]:
# we have search[1] = (index, text_unicode, inversion)
col = search[1][0]
text = search[1][1]
inv = search[1][2]
func = lambda x: self.on_get_value(x, col) or u""
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
self._reverse = (order == gtk.SORT_DESCENDING)
self.tooltip_column = tooltip_column
Config.client.notify_add("/apps/gramps/preferences/todo-color",
self.update_todo)
Config.client.notify_add("/apps/gramps/preferences/custom-marker-color",
self.update_custom)
Config.client.notify_add("/apps/gramps/preferences/complete-color",
self.update_complete)
self.complete_color = Config.get(Config.COMPLETE_COLOR)
self.todo_color = Config.get(Config.TODO_COLOR)
self.custom_color = Config.get(Config.CUSTOM_MARKER_COLOR)
self.rebuild_data()
_LOG.debug(self.__class__.__name__ + ' __init__ ' +
str(time.clock() - cput) + ' sec')
def update_todo(self,client,cnxn_id,entry,data):
self.todo_color = Config.get(Config.TODO_COLOR)
def update_custom(self,client,cnxn_id,entry,data):
self.custom_color = Config.get(Config.CUSTOM_MARKER_COLOR)
def update_complete(self,client,cnxn_id,entry,data):
self.complete_color = Config.get(Config.COMPLETE_COLOR)
def set_sort_column(self, col):
self.sort_func = self.smap[col]
def reverse_order(self):
self._reverse = not self._reverse
self.node_map.reverse_order()
def sort_keys(self):
sort_data = []
self.total = 0
with self.gen_cursor() as cursor: # use cursor as a context manager
#loop over database and store the sort field, and the handle
for key, data in cursor:
## as per locale doc, use strxfrm for frequent compare.
## apparently broken in Win --> they should fix base lib !!
#add to sort_data in such a way that bisect module can be
# used on the result later on.
sort_data.append((locale.strxfrm(self.sort_func(data)),
key))
#bisect.insort_left(sort_data,
# (locale.strxfrm(self.sort_func(data)), key))
sort_data.sort()
self.total = len(sort_data)
return sort_data
def _rebuild_search(self, ignore=None):
""" function called when view must be build, given a search text
in the top search bar
"""
self.total = 0
if self.db.is_open():
if self.search and self.search.text:
dlist = [h for h in self.sort_keys()\
if self.search.match(h[1],self.db) and \
h[1] not in self.skip and h[1] != ignore]
else:
dlist = [h for h in self.sort_keys() \
if h[1] not in self.skip and h[1] != ignore]
self.displayed = len(dlist)
self.node_map.set_path_map(dlist, reverse=self._reverse)
else:
self.displayed = 0
self.node_map.clear_map()
def _rebuild_filter(self, ignore=None):
""" function called when view must be build, given filter options
in the filter sidebar
"""
self.total = 0
if self.db.is_open():
if self.search:
dlist = self.search.apply(self.db,
[ k for k in self.sort_keys()\
if k[1] != ignore])
else:
dlist = [ k for k in self.sort_keys() \
if k[1] != ignore ]
self.displayed = len(dlist)
self.node_map.set_path_map(dlist, reverse=self._reverse)
else:
self.displayed = 0
self.node_map.clear_map()
def add_row_by_handle(self, handle):
if not self.search or \
(self.search and self.search.match(handle, self.db)):
#row needs to be added to the model
data = self.map(handle)
insert_val = (locale.strxfrm(self.sort_func(data)), handle)
insert_path = self.node_map.insert(insert_val)
if insert_path is not None:
node = self.get_iter(insert_path)
self.row_inserted(insert_path, node)
def delete_row_by_handle(self, handle):
delete_path = self.node_map.delete(handle)
self.row_deleted(delete_path)
def update_row_by_handle(self, handle):
## TODO: if sort key changes, this is not updated correctly ....
path = self.node_map.get_path(handle)
node = self.get_iter(path)
self.row_changed(path, node)
def on_get_flags(self):
"""
Returns the GtkTreeModelFlags for this particular type of model
See gtk.TreeModel
"""
return gtk.TREE_MODEL_LIST_ONLY | gtk.TREE_MODEL_ITERS_PERSIST
def on_get_n_columns(self):
"""
Return the number of columns. Must be implemented in the child objects
See gtk.TreeModel
"""
raise NotImplementedError
def on_get_path(self, handle):
"""
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
"""
return self.node_map.get_path(handle)
def on_get_column_type(self, index):
"""
See gtk.TreeModel
"""
if index == self.tooltip_column:
return object
return str
def on_get_iter(self, path):
"""
See gtk.TreeModel
"""
try:
return self.node_map.get_handle(path[0])
except:
return None
def on_get_value(self, handle, col):
"""
See gtk.TreeModel
"""
try:
if handle != self.prev_handle:
self.prev_data = self.map(str(handle))
self.prev_handle = handle
return self.fmap[col](self.prev_data)
except:
return u''
def on_iter_next(self, handle):
"""
Returns the next node at this level of the tree
See gtk.TreeModel
"""
return self.node_map.find_next_handle(handle)
def on_iter_children(self, handle):
"""
Return the first child of the node
See gtk.TreeModel
"""
if handle is None and len(self.node_map):
return self.node_map.get_first_handle()
return None
def on_iter_has_child(self, handle):
"""
Returns true if this node has children
See gtk.TreeModel
"""
if handle is None:
return len(self.node_map) > 0
return False
def on_iter_n_children(self, handle):
"""
See gtk.TreeModel
"""
if handle is None:
return len(self.node_map)
return 0
def on_iter_nth_child(self, handle, nth):
"""
See gtk.TreeModel
"""
if handle is None:
return self.node_map.get_handle(nth)
return None
def on_iter_parent(self, handle):
"""
Returns the parent of this node
See gtk.TreeModel
"""
return None