From fb6967d000e7ba4734dbd1c997025952ba24cd8c Mon Sep 17 00:00:00 2001 From: Benny Malengier Date: Sun, 28 Jun 2009 21:15:10 +0000 Subject: [PATCH] optimization and cleaning of the base model for flat treeviews svn: r12723 --- configure.in | 1 + po/POTFILES.in | 5 +- src/DisplayModels/Makefile.am | 1 - src/DisplayModels/_BaseModel.py | 357 -------------- src/DisplayModels/_EventModel.py | 6 +- src/DisplayModels/_FamilyModel.py | 6 +- src/DisplayModels/_MediaModel.py | 6 +- src/DisplayModels/_NoteModel.py | 6 +- src/DisplayModels/_PlaceModel.py | 6 +- src/DisplayModels/_RepositoryModel.py | 6 +- src/DisplayModels/_SourceModel.py | 6 +- src/PageView.py | 31 +- src/gui/views/Makefile.am | 3 + src/gui/views/__init__.py | 4 + src/gui/views/treemodels/__init__.py | 24 + src/gui/views/treemodels/flatbasemodel.py | 552 ++++++++++++++++++++++ 16 files changed, 636 insertions(+), 384 deletions(-) delete mode 100644 src/DisplayModels/_BaseModel.py create mode 100644 src/gui/views/treemodels/__init__.py create mode 100644 src/gui/views/treemodels/flatbasemodel.py diff --git a/configure.in b/configure.in index cb716256d..56afc9cbd 100644 --- a/configure.in +++ b/configure.in @@ -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 diff --git a/po/POTFILES.in b/po/POTFILES.in index 0f8d416a4..48e363f27 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/src/DisplayModels/Makefile.am b/src/DisplayModels/Makefile.am index 225319005..039d4d147 100644 --- a/src/DisplayModels/Makefile.am +++ b/src/DisplayModels/Makefile.am @@ -4,7 +4,6 @@ pkgdatadir = $(datadir)/@PACKAGE@/DisplayModels pkgdata_PYTHON = \ __init__.py \ - _BaseModel.py \ _EventModel.py \ _FamilyModel.py \ _MediaModel.py \ diff --git a/src/DisplayModels/_BaseModel.py b/src/DisplayModels/_BaseModel.py deleted file mode 100644 index 82e155fe2..000000000 --- a/src/DisplayModels/_BaseModel.py +++ /dev/null @@ -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 diff --git a/src/DisplayModels/_EventModel.py b/src/DisplayModels/_EventModel.py index 42de67bcc..4eaf95391 100644 --- a/src/DisplayModels/_EventModel.py +++ b/src/DisplayModels/_EventModel.py @@ -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): diff --git a/src/DisplayModels/_FamilyModel.py b/src/DisplayModels/_FamilyModel.py index d90c553d3..5cc19b7ef 100644 --- a/src/DisplayModels/_FamilyModel.py +++ b/src/DisplayModels/_FamilyModel.py @@ -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): diff --git a/src/DisplayModels/_MediaModel.py b/src/DisplayModels/_MediaModel.py index 62a5e0392..4b7193052 100644 --- a/src/DisplayModels/_MediaModel.py +++ b/src/DisplayModels/_MediaModel.py @@ -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): diff --git a/src/DisplayModels/_NoteModel.py b/src/DisplayModels/_NoteModel.py index fd08cd1a7..041b4d641 100644 --- a/src/DisplayModels/_NoteModel.py +++ b/src/DisplayModels/_NoteModel.py @@ -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): diff --git a/src/DisplayModels/_PlaceModel.py b/src/DisplayModels/_PlaceModel.py index 1e11ee1ec..10ca5268b 100644 --- a/src/DisplayModels/_PlaceModel.py +++ b/src/DisplayModels/_PlaceModel.py @@ -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): diff --git a/src/DisplayModels/_RepositoryModel.py b/src/DisplayModels/_RepositoryModel.py index f2e7c99f2..0a0ab5f7c 100644 --- a/src/DisplayModels/_RepositoryModel.py +++ b/src/DisplayModels/_RepositoryModel.py @@ -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): diff --git a/src/DisplayModels/_SourceModel.py b/src/DisplayModels/_SourceModel.py index 89db8984e..c985acf55 100644 --- a/src/DisplayModels/_SourceModel.py +++ b/src/DisplayModels/_SourceModel.py @@ -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): diff --git a/src/PageView.py b/src/PageView.py index 1707ba5f4..d084b7098 100644 --- a/src/PageView.py +++ b/src/PageView.py @@ -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 diff --git a/src/gui/views/Makefile.am b/src/gui/views/Makefile.am index 820dbdf54..fb8626e6b 100644 --- a/src/gui/views/Makefile.am +++ b/src/gui/views/Makefile.am @@ -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 = \ diff --git a/src/gui/views/__init__.py b/src/gui/views/__init__.py index 2406cbaf8..622330914 100644 --- a/src/gui/views/__init__.py +++ b/src/gui/views/__init__.py @@ -22,3 +22,7 @@ """ Package init for the views package. """ + +# DO NOT IMPORT METHODS/CLASSES FROM src/gui/views HERE ! Only __all__ + +__all__ = [ "treemodels" ] diff --git a/src/gui/views/treemodels/__init__.py b/src/gui/views/treemodels/__init__.py new file mode 100644 index 000000000..617ba3923 --- /dev/null +++ b/src/gui/views/treemodels/__init__.py @@ -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. +""" diff --git a/src/gui/views/treemodels/flatbasemodel.py b/src/gui/views/treemodels/flatbasemodel.py new file mode 100644 index 000000000..213e5cd94 --- /dev/null +++ b/src/gui/views/treemodels/flatbasemodel.py @@ -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