From 1b556586a452456f91739177c8ade1b3435c988c Mon Sep 17 00:00:00 2001 From: Benny Malengier Date: Sat, 7 Nov 2009 13:04:45 +0000 Subject: [PATCH] 3275: PageView reworking main work by Nick Hall Moving personview to the new pageview classes, and using a generic treebasemodel svn: r13515 --- src/DataViews/PersonView.py | 1044 +++++---------------- src/DisplayModels/_PeopleModel.py | 615 ++++-------- src/Selectors/_BaseSelector.py | 58 +- src/Selectors/_SelectPerson.py | 6 + src/gen/db/read.py | 6 +- src/gui/views/listview.py | 33 +- src/gui/views/treemodels/treebasemodel.py | 739 ++++++++------- 7 files changed, 865 insertions(+), 1636 deletions(-) diff --git a/src/DataViews/PersonView.py b/src/DataViews/PersonView.py index e8fbda872..356139d85 100644 --- a/src/DataViews/PersonView.py +++ b/src/DataViews/PersonView.py @@ -1,14 +1,15 @@ -# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2000-2007 Donald N. Allingham +# Copyright (C) 2008 Gary Burton +# Copyright (C) 2009 Nick Hall # # 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, +# 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. @@ -21,329 +22,151 @@ # $Id$ """ -PersonView interface. +Provide the person view. """ -#------------------------------------------------------------------------ -# -# standard python modules -# -#------------------------------------------------------------------------ - -import cPickle as pickle - #------------------------------------------------------------------------- # -# gtk +# GTK/Gnome modules # #------------------------------------------------------------------------- import gtk -import pango -from gtk.gdk import ACTION_COPY, BUTTON1_MASK #------------------------------------------------------------------------- # -# GRAMPS modules +# set up logging +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".gui.personview") + +#------------------------------------------------------------------------- +# +# gramps modules # #------------------------------------------------------------------------- import gen.lib -from DisplayModels import PeopleModel -import PageView -from BasicUtils import name_displayer +import gui.views.pageview as PageView +from gui.views.listview import ListView +import DisplayModels import Utils -from gui.utils import add_menuitem +from BasicUtils import name_displayer from QuestionDialog import ErrorDialog, QuestionDialog -import TreeTips import Errors +import Bookmarks import config -import const +from DdTargets import DdTargets +from Editors import EditPerson +from Filters.SideBar import PersonSidebarFilter +from gen.plug import CATEGORY_QR_PERSON + +#------------------------------------------------------------------------- +# +# internationalization +# +#------------------------------------------------------------------------- from TransUtils import sgettext as _ -from Editors import EditPerson -from Filters import SearchBar -from Filters.SideBar import PersonSidebarFilter -from DdTargets import DdTargets - -column_names = [ - _('Name'), - _('ID') , - _('Gender'), - _('Birth Date'), - _('Birth Place'), - _('Death Date'), - _('Death Place'), - _('Spouse'), - _('Last Change'), - ] - -class PersonView(PageView.PersonNavView): +#------------------------------------------------------------------------- +# +# PersonView +# +#------------------------------------------------------------------------- +class PersonView(ListView): """ - PersonView interface + PersonView class, derived from the ListView """ + COLUMN_NAMES = [ + _('Name'), + _('ID'), + _('Gender'), + _('Birth Date'), + _('Birth Place'), + _('Death Date'), + _('Death Place'), + _('Spouse'), + _('Last Changed'), + ] + + ADD_MSG = _("Add a new person") + EDIT_MSG = _("Edit the selected person") + DEL_MSG = _("Delete the selected person") + FILTER_TYPE = "Person" + QR_CATEGORY = CATEGORY_QR_PERSON def __init__(self, dbstate, uistate): """ - Create the new PersonView interface, with the current dbstate and uistate + Create the Person View """ - PageView.PersonNavView.__init__(self, _('People'), dbstate, uistate) - - self.inactive = False - dbstate.connect('database-changed', self.change_db) - self.handle_col = PeopleModel.COLUMN_INT_ID - self.model = None - self.generic_filter = None - - self.func_list = { - 'F2' : self.key_goto_home_person, - 'F3' : self.key_edit_selected_person, - 'BackSpace' : self.key_delete_selected_person, - 'J' : self.jump, + signal_map = { + 'person-add' : self.row_add, + 'person-update' : self.row_update, + 'person-delete' : self.row_delete, + 'person-rebuild' : self.object_build, } - self.dirty = True - config.connect("interface.filter", - self.filter_toggle) - - def change_page(self): - PageView.PersonNavView.change_page(self) - self.edit_action.set_visible(True) - self.edit_action.set_sensitive(not self.dbstate.db.readonly) - self.uistate.show_filter_results(self.dbstate, - self.model.displayed, - self.model.total) - - def set_active(self): - PageView.PersonNavView.set_active(self) - self.key_active_changed = self.dbstate.connect('active-changed', - self.goto_active_person) - self.goto_active_person() - - def set_inactive(self): - if self.active: - PageView.PersonNavView.set_inactive(self) - self.dbstate.disconnect(self.key_active_changed) + ListView.__init__( + self, _('People'), dbstate, uistate, + PersonView.COLUMN_NAMES, len(PersonView.COLUMN_NAMES), + DisplayModels.PeopleModel, + signal_map, dbstate.db.get_bookmarks(), + Bookmarks.Bookmarks, + multiple=True, + filter_class=PersonSidebarFilter, + markup=True) - def define_actions(self): - """ - Required define_actions function for PageView. Builds the action - group information required. We extend beyond the normal here, - since we want to have more than one action group for the PersonView. - Most PageViews really won't care about this. + self.func_list = { + 'J' : self.jump, + 'BackSpace' : self.key_delete, + } - Special action groups for Forward and Back are created to allow the - handling of navigation buttons. Forward and Back allow the user to - advance or retreat throughout the history, and we want to have these - be able to toggle these when you are at the end of the history or - at the beginning of the history. - """ - - PageView.PersonNavView.define_actions(self) - - self.all_action = gtk.ActionGroup(self.title + "/PersonAll") - self.edit_action = gtk.ActionGroup(self.title + "/PersonEdit") - - self.all_action.add_actions([ - ('OpenAllNodes', None, _("Expand all Nodes"), None, None, - self.open_all_nodes), - ('Edit', gtk.STOCK_EDIT, _("action|_Edit..."), "Return", - _("Edit the selected person"), self.edit), - ('CloseAllNodes', None, _("Collapse all Nodes"), None, None, - self.close_all_nodes), - ('QuickReport', None, _("Quick View"), None, None, None), - ('Dummy', None, ' ', None, None, self.dummy_report), - ]) - - self.edit_action.add_actions( - [ - ('Add', gtk.STOCK_ADD, _("_Add..."), "Insert", - _("Add a new person"), self.add), - ('Remove', gtk.STOCK_REMOVE, _("_Remove"), "Delete", - _("Remove the Selected Person"), self.remove), - ('ColumnEdit', gtk.STOCK_PROPERTIES, _('_Column Editor...'), None, - None, self._column_editor), - ('CmpMerge', None, _('Compare and _Merge...'), None, None, - self.cmp_merge), - ('FastMerge', None, _('_Fast Merge...'), None, None, - self.fast_merge), - ('ExportTab', None, _('Export View...'), None, None, self.export), - ]) - - self._add_action_group(self.edit_action) - self._add_action_group(self.all_action) - - def enable_action_group(self, obj): - PageView.PersonNavView.enable_action_group(self, obj) - self.all_action.set_visible(True) - self.edit_action.set_visible(False) - self.edit_action.set_sensitive(not self.dbstate.db.readonly) + config.connect("interface.filter", self.filter_toggle) - def disable_action_group(self): - PageView.PersonNavView.disable_action_group(self) + def column_ord_setfunc(self, clist): + self.dbstate.db.set_person_column_order(clist) - self.all_action.set_visible(False) - self.edit_action.set_visible(False) + def navigation_type(self): + return PageView.NAVIGATION_PERSON - def cmp_merge(self, obj): - mlist = self.get_selected_objects() + def get_bookmarks(self): + """ + Return the bookmark object + """ + return self.dbstate.db.get_bookmarks() - if len(mlist) != 2: - ErrorDialog( - _("Cannot merge people"), - _("Exactly two people must be selected to perform a merge. " - "A second person can be selected by holding down the " - "control key while clicking on the desired person.")) - else: - import Merge - person1 = self.db.get_person_from_handle(mlist[0]) - person2 = self.db.get_person_from_handle(mlist[1]) - if person1 and person2: - Merge.PersonCompare(self.dbstate, self.uistate, person1, - person2, self.build_tree) - else: - ErrorDialog( - _("Cannot merge people"), - _("Exactly two people must be selected to perform a " - "merge. A second person can be selected by holding " - "down the control key while clicking on the desired " - "person.")) + def drag_info(self): + """ + Specify the drag type for a single selection + """ + return DdTargets.PERSON_LINK + + def drag_list_info(self): + """ + Specify the drag type for a multiple selected rows + """ + return DdTargets.PERSON_LINK_LIST - def fast_merge(self, obj): - mlist = self.get_selected_objects() + def column_order(self): + """ + returns a tuple indicating the column order + """ + return self.dbstate.db.get_person_column_order() - if len(mlist) != 2: - ErrorDialog( - _("Cannot merge people"), - _("Exactly two people must be selected to perform a merge. " - "A second person can be selected by holding down the " - "control key while clicking on the desired person.")) - else: - import Merge - - person1 = self.db.get_person_from_handle(mlist[0]) - person2 = self.db.get_person_from_handle(mlist[1]) - if person1 and person2: - Merge.MergePeopleUI(self.dbstate, self.uistate, person1, - person2, self.build_tree) - else: - ErrorDialog( - _("Cannot merge people"), - _("Exactly two people must be selected to perform a merge. " - "A second person can be selected by holding down the " - "control key while clicking on the desired person.")) - - def _column_editor(self, obj): - import ColumnOrder - - ColumnOrder.ColumnOrder( - _('Select Person Columns'), - self.uistate, - self.dbstate.db.get_person_column_order(), - column_names, - self.set_column_order) - - def set_column_order(self, column_list): - self.dbstate.db.set_person_column_order(column_list) - self.build_columns() - self.setup_filter() + def exact_search(self): + """ + Returns a tuple indicating columns requiring an exact search + """ + return (2,) # Gender ('female' contains the string 'male') def get_stock(self): """ - Return the name of the stock icon to use for the display. - This assumes that this icon has already been registered with - GNOME as a stock icon. + Use the gramps-person stock icon """ return 'gramps-person' - def start_expand(self, *obj): - self.uistate.set_busy_cursor(True) - - def expanded(self, *obj): - self.uistate.set_busy_cursor(False) - - def build_widget(self): - """ - Builds the interface and returns a gtk.Container type that - contains the interface. This containter will be inserted into - a gtk.Notebook page. - """ - hpaned = gtk.HBox() - self.vbox = gtk.VBox() - self.vbox.set_border_width(4) - self.vbox.set_spacing(4) - - self.search_bar = SearchBar(self.dbstate, self.uistate, - self.build_tree, self.goto_active_person) - filter_box = self.search_bar.build() - - self.tree = gtk.TreeView() - self.tree.set_rules_hint(True) - self.tree.set_headers_visible(True) - self.tree.set_fixed_height_mode(True) - self.tree.connect('key-press-event', self._key_press) - self.tree.connect('start-interactive-search', self.open_all_nodes) - - scrollwindow = gtk.ScrolledWindow() - scrollwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scrollwindow.set_shadow_type(gtk.SHADOW_ETCHED_IN) - scrollwindow.add(self.tree) - scrollwindow.show_all() - - self.vbox.pack_start(filter_box, False) - self.vbox.pack_start(scrollwindow, True) - - self.renderer = gtk.CellRendererText() - self.renderer.set_property('ellipsize', pango.ELLIPSIZE_END) - self.inactive = False - - self.columns = [] - - self.setup_filter() - self.build_columns() - self.tree.connect('button-press-event', self._button_press) - self.tree.connect('drag_data_get', self.drag_data_get) - self.tree.connect('drag_begin', self.drag_begin) - - self.selection = self.tree.get_selection() - self.selection.set_mode(gtk.SELECTION_MULTIPLE) - self.selection.connect('changed', self.row_changed) - - self.filter_sidebar = PersonSidebarFilter(self.dbstate, self.uistate, - self.filter_clicked) - self.filter_pane = self.filter_sidebar.get_widget() - - hpaned.pack_start(self.vbox, True, True) - hpaned.pack_end(self.filter_pane, False, False) - self.filter_toggle(None, None, None, None) - return hpaned - - def post(self): - if config.get('interface.filter'): - self.search_bar.hide() - self.filter_pane.show() - else: - self.search_bar.show() - self.filter_pane.hide() - - def filter_clicked(self): - self.generic_filter = self.filter_sidebar.get_filter() - self.build_tree() - - def filter_toggle(self, client, cnxn_id, entry, data): - if config.get('interface.filter'): - self.search_bar.hide() - self.filter_pane.show() - else: - self.search_bar.show() - self.filter_pane.hide() - self.build_tree() - - def drag_begin(self, widget, *data): - widget.drag_source_set_icon_stock(self.get_stock()) - def ui_definition(self): """ - Specifies the UIManager XML code that defines the menus and buttons - associated with the interface. + Defines the UI string for UIManager """ return ''' @@ -412,157 +235,40 @@ class PersonView(PageView.PersonNavView): ''' - def change_db(self, db): - """ - Callback associated with DbState. Whenenver the database - changes, this task is called. In this case, we rebuild the - columns, and connect signals to the connected database. Tere - is no need to store the database, since we will get the value - from self.state.db - """ - self.setup_filter() - self.db = db - db.connect('person-add', self.person_added) - db.connect('person-update', self.person_updated) - db.connect('person-delete', self.person_removed) - db.connect('person-rebuild', self.person_rebuild) - - if self.active: - self.build_tree() + def get_handle_from_gramps_id(self, gid): + obj = self.dbstate.db.get_person_from_gramps_id(gid) + if obj: + return obj.get_handle() else: - self.dirty = True + return None - self.bookmarks.update_bookmarks(db.get_bookmarks()) - if self.active: - self.bookmarks.redraw() - - def goto_active_person(self, obj=None): + def _column_editor(self, obj): """ - Callback (and usable function) that selects the active person - in the display tree. - - We have a bit of a problem due to the nature of how GTK works. - We have unselect the previous path and select the new path. However, - these cause a row change, which calls the row_change callback, which - can end up calling change_active_person, which can call - goto_active_person, causing a bit of recusion. Confusing, huh? - - Unforunately, we row_change has to be able to call change_active_person, - because the can occur from the interface in addition to programatically. - - TO handle this, we set the self.inactive variable that we can check - in row_change to look for this particular condition. + returns a tuple indicating the column order """ + import ColumnOrder - # if there is no active person, or if we have been marked inactive, - # simply return - - if not self.dbstate.active or self.inactive: - return - - # mark inactive to prevent recusion - self.inactive = True - - self._goto() - - # disable the inactive flag - self.inactive = False - - # update history - self.handle_history(self.dbstate.active.handle) - - def _goto(self): - """ - select the active person in the person view - """ - - person = self.dbstate.active - try: - if self.model and person: - path = self.model.on_get_path(person.get_handle()) - - group_name = person.get_primary_name().get_group_name() - top_name = self.dbstate.db.get_name_group_mapping(group_name) - top_path = self.model.on_get_path(top_name) - self.tree.expand_row(top_path, 0) - - current = self.model.on_get_iter(path) - selected = self.selection.path_is_selected(path) - if current != person.get_handle() or not selected: - self.selection.unselect_all() - self.selection.select_path(path) - self.tree.scroll_to_cell(path, None, 1, 0.5, 0) - except KeyError: - self.selection.unselect_all() - self.uistate.push_message(self.dbstate, - _("Active person not visible")) - self.dbstate.active = person - - def setup_filter(self): - """ - Builds the default filters and add them to the filter menu. - """ - - cols = [] - cols.append((_("Name"), 0)) - for pair in self.dbstate.db.get_person_column_order(): - if not pair[0]: - continue - cols.append((column_names[pair[1]], pair[1])) - - self.search_bar.setup_filter(cols) - - def build_tree(self, skip=[]): - """ - Create a new PeopleModel instance. Essentially creates a complete - rebuild of the data. We need to temporarily store the active person, - since it can change when rows are unselected when the model is set. - """ - if self.active: - if config.get('interface.filter'): - filter_info = (PeopleModel.GENERIC, self.generic_filter) - else: - filter_info = (PeopleModel.SEARCH, self.search_bar.get_value()) - - self.model = PeopleModel(self.dbstate.db, filter_info, skip) - active = self.dbstate.active - self.tree.set_model(self.model) - - if const.USE_TIPS and self.model.tooltip_column is not None: - self.tooltips = TreeTips.TreeTips(self.tree, - self.model.tooltip_column, - True) - self.tree.set_model(None) - self.build_columns() - self.tree.set_model(self.model) - self.dbstate.change_active_person(active) - self._goto() - self.dirty = False - self.uistate.show_filter_results(self.dbstate, - self.model.displayed, - self.model.total) - else: - self.dirty = True + ColumnOrder.ColumnOrder( + _('Select Person Columns'), + self.uistate, + self.dbstate.db.get_person_column_order(), + PersonView.COLUMN_NAMES, + self.set_column_order) def add(self, obj): person = gen.lib.Person() # attempt to get the current surname - - (mode, paths) = self.selection.get_selected_rows() - + (model, pathlist) = self.selection.get_selected_rows() name = u"" - - if len(paths) == 1: - path = paths[0] + if len(pathlist) == 1: + path = pathlist[0] if len(path) == 1: - name = self.model.on_get_iter(path) + name = model.on_get_iter(path) else: - node = self.model.on_get_iter(path) - handle = self.model.on_get_value(node, - PeopleModel.COLUMN_INT_ID) - newp = self.dbstate.db.get_person_from_handle(handle) - name = newp.get_primary_name().get_surname() + node = model.on_get_iter(path) + name = model.on_iter_parent(node) + try: person.get_primary_name().set_surname(name) EditPerson(self.dbstate, self.uistate, [], person) @@ -570,32 +276,15 @@ class PersonView(PageView.PersonNavView): pass def edit(self, obj): - if self.dbstate.active: + for handle in self.selected_handles(): + person = self.dbstate.db.get_person_from_handle(handle) try: - handle = self.dbstate.active.handle - person = self.dbstate.db.get_person_from_handle(handle) EditPerson(self.dbstate, self.uistate, [], person) except Errors.WindowActiveError: pass - def open_all_nodes(self, obj): - self.uistate.status_text(_("Updating display...")) - self.uistate.set_busy_cursor(True) - - self.tree.expand_all() - - self.uistate.set_busy_cursor(False) - self.uistate.modify_statusbar(self.dbstate) - - def close_all_nodes(self, obj): - self.tree.collapse_all() - def remove(self, obj): - mlist = self.get_selected_objects() - if len(mlist) == 0: - return - - for sel in mlist: + for sel in self.selected_handles(): person = self.dbstate.db.get_person_from_handle(sel) self.active_person = person name = name_displayer.display(person) @@ -635,180 +324,6 @@ class PersonView(PageView.PersonNavView): self.uistate.phistory.back() self.uistate.set_busy_cursor(False) - def build_columns(self): - for column in self.columns: - self.tree.remove_column(column) - try: - column = gtk.TreeViewColumn( - _('Name'), - self.renderer, - text=0, - foreground=self.model.marker_color_column) - - except AttributeError: - column = gtk.TreeViewColumn(_('Name'), self.renderer, text=0) - - column.set_resizable(True) - column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - column.set_fixed_width(225) - self.tree.append_column(column) - self.columns = [column] - - for pair in self.dbstate.db.get_person_column_order(): - if not pair[0]: - continue - name = column_names[pair[1]] - try: - column = gtk.TreeViewColumn( - name, self.renderer, markup=pair[1], - foreground=self.model.marker_color_column) - except AttributeError: - column = gtk.TreeViewColumn( - name, self.renderer, markup=pair[1]) - - column.set_resizable(True) - column.set_fixed_width(pair[2]) - column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self.columns.append(column) - self.tree.append_column(column) - - def row_changed(self, obj): - """Called with a row is changed. Check the selected objects from - the person_tree to get the IDs of the selected objects. Set the - active person to the first person in the list. If no one is - selected, set the active person to None""" - - selected_ids = self.get_selected_objects() - if not self.inactive: - try: - if len(selected_ids) == 0: - self.dbstate.change_active_person(None) - else: - handle = selected_ids[0] - person = self.dbstate.db.get_person_from_handle(handle) - self.dbstate.change_active_person(person) - except: - pass - - if len(selected_ids) == 1: - self.tree.drag_source_set(BUTTON1_MASK, - [DdTargets.PERSON_LINK.target()], - ACTION_COPY) - elif len(selected_ids) > 1: - self.tree.drag_source_set(BUTTON1_MASK, - [DdTargets.PERSON_LINK_LIST.target()], - ACTION_COPY) - self.uistate.modify_statusbar(self.dbstate) - - def drag_data_get(self, widget, context, sel_data, info, time): - selected_ids = self.get_selected_objects() - nonempty_ids = [h for h in selected_ids if h] - if nonempty_ids: - data = (DdTargets.PERSON_LINK.drag_type, - id(self), nonempty_ids[0], 0) - sel_data.set(sel_data.target, 8, pickle.dumps(data)) - - def person_added(self, handle_list): - if not self.model: - return - if self.active: - self.dirty = False - for node in set(handle_list): - person = self.dbstate.db.get_person_from_handle(node) - pname = person.get_primary_name() - top = name_displayer.name_grouping_name(self.db, pname) - - self.model.rebuild_data(self.model.current_filter) - - if not self.model.is_visable(node): - continue - - if (not self.model.mapper.has_top_node(top) or - self.model.mapper.number_of_children(top) == 1): - path = self.model.on_get_path(top) - pnode = self.model.get_iter(path) - self.model.row_inserted(path, pnode) - path = self.model.on_get_path(node) - pnode = self.model.get_iter(path) - self.model.row_inserted(path, pnode) - else: - self.dirty = True - - def func(self, tree, path, ex_list): - ex_list.append(self.model.mapper.top_path2iter[path[0]]) - - def person_rebuild(self): - """Large change to person database""" - if self.active: - self.bookmarks.redraw() - self.build_tree() - - def person_removed(self, handle_list): - if not self.model: - return - - expand = [] - self.tree.map_expanded_rows(self.func, expand) - - self.build_tree(handle_list) - for i in expand: - path = self.model.mapper.top_iter2path.get(i) - if path: - self.tree.expand_row(path, False) - - def person_updated(self, handle_list): - if not self.model: - return - - self.model.clear_cache() - for node in handle_list: - person = self.dbstate.db.get_person_from_handle(node) - try: - oldpath = self.model.mapper.iter2path[node] - except: - return - pathval = self.model.on_get_path(node) - pnode = self.model.get_iter(pathval) - - # calculate the new data - - if person.primary_name.group_as: - surname = person.primary_name.group_as - else: - base = person.primary_name.surname - surname = self.dbstate.db.get_name_group_mapping(base) - - if oldpath[0] == surname: - try: - self.model.build_sub_entry(surname) - except: - self.model.calculate_data() - else: - self.model.calculate_data() - - # find the path of the person in the new data build - newpath = self.model.mapper.temp_iter2path[node] - - # if paths same, just issue row changed signal - - if oldpath == newpath: - self.model.row_changed(pathval, pnode) - else: - self.build_tree() - break - - self.goto_active_person() - - def get_selected_objects(self): - (mode, paths) = self.selection.get_selected_rows() - mlist = [] - for path in paths: - node = self.model.on_get_iter(path) - handle = self.model.on_get_value(node, PeopleModel.COLUMN_INT_ID) - if handle: - mlist.append(handle) - return mlist - def remove_from_person_list(self, person): """Remove the selected person from the list. A person object is expected, not an ID""" @@ -819,186 +334,6 @@ class PersonView(PageView.PersonNavView): elif row == 0 and self.model.on_get_iter(path): self.selection.select_path(path) - def _button_press(self, obj, event): - if not self.dbstate.open: - return False - if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1: - handle = self.first_selected() - person = self.dbstate.db.get_person_from_handle(handle) - if person: - try: - EditPerson(self.dbstate, self.uistate, [], person) - except Errors.WindowActiveError: - pass - return True - else: - #press on a parent node - return self.expand_collapse() - - elif event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: - - from gen.plug import CATEGORY_QR_PERSON - from QuickReports import create_quickreport_menu - - menu = self.uistate.uimanager.get_widget('/Popup') - - #add the quickreports, different for every handle - qr_menu = self.uistate.uimanager.\ - get_widget('/Popup/QuickReport').get_submenu() - if qr_menu : - self.uistate.uimanager.\ - get_widget('/Popup/QuickReport').remove_submenu() - reportactions = [] - if menu and self.dbstate.active: - (ui, reportactions) = create_quickreport_menu( - CATEGORY_QR_PERSON, - self.dbstate, - self.uistate, - self.dbstate.active.handle) - if len(reportactions) > 1 : - qr_menu = gtk.Menu() - for action in reportactions[1:] : - add_menuitem(qr_menu, action[2], None, action[5]) - self.uistate.uimanager.get_widget('/Popup/QuickReport').\ - set_submenu(qr_menu) - if menu: - menu.popup(None, None, None, event.button, event.time) - return True - return False - - def _key_press(self, obj, event): - if not self.dbstate.open: - return False - if not event.state or event.state in (gtk.gdk.MOD2_MASK, ): - if event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter): - if self.dbstate.active: - self.edit(obj) - return True - else: - return self.expand_collapse() - return False - - def expand_collapse(self): - """ - Expand or collapse the selected parent name node. - Return True if change done, False otherwise - """ - store, paths = self.selection.get_selected_rows() - if paths and len(paths[0]) == 1 : - if self.tree.row_expanded(paths[0]): - self.tree.collapse_row(paths[0]) - else: - self.tree.expand_row(paths[0], 0) - return True - return False - - def key_goto_home_person(self): - self.home(None) - self.uistate.push_message(self.dbstate, - _("Go to default person")) - - def key_edit_selected_person(self): - self.edit(None) - self.uistate.push_message(self.dbstate, - _("Edit selected person")) - - - def key_delete_selected_person(self): - self.remove(None) - self.uistate.push_message(self.dbstate, - _("Delete selected person")) - - def export(self, obj): - chooser = gtk.FileChooserDialog( - _("Export View as Spreadsheet"), - self.uistate.window, - gtk.FILE_CHOOSER_ACTION_SAVE, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, gtk.RESPONSE_OK)) - chooser.set_do_overwrite_confirmation(True) - - combobox = gtk.combo_box_new_text() - label = gtk.Label(_("Format:")) - label.set_alignment(1.0, 0.5) - box = gtk.HBox() - box.pack_start(label, True, True, padding=12) - box.pack_start(combobox, False, False) - combobox.append_text(_('CSV')) - combobox.append_text(_('Open Document Spreadsheet')) - combobox.set_active(0) - box.show_all() - chooser.set_extra_widget(box) - - while True: - value = chooser.run() - fn = chooser.get_filename() - fl = combobox.get_active() - if value == gtk.RESPONSE_OK: - if fn: - chooser.destroy() - break - else: - chooser.destroy() - return - self.write_tabbed_file(fn, fl) - - def write_tabbed_file(self, name, type): - """ - Write a tabbed file to the specified name. - - The output file type is determined by the type variable. - """ - - from docgen import CSVTab, ODSTab - ofile = None - # build the active data columns, prepending 0 for the name column, then - # derive the column names fromt the active data columns - - data_cols = [0] + [pair[1] \ - for pair in self.dbstate.db.get_person_column_order() \ - if pair[0]] - - cnames = [column_names[i] for i in data_cols] - # Select the correct output format - if type == 0: - ofile = CSVTab(len(cnames)) - else: - ofile = ODSTab(len(cnames)) - - # create the output tabbed document, and open it - ofile.open(name) - - # start the current page - ofile.start_page() - - # open the header row, write the column names, and close the row - ofile.start_row() - for name in cnames: - ofile.write_cell(name) - ofile.end_row() - - # The tree model works different from the rest of the list-based models, - # since the iterator method only works on top level nodes. So we must - # loop through based off of paths - - path = (0, ) - node = self.model.on_get_iter(path) - - # Node might be null if the surname is not known so test against None - while node is not None: - real_iter = self.model.get_iter(path) - for subindex in range(0, self.model.iter_n_children(real_iter)): - subpath = ((path[0], subindex)) - row = self.model[subpath] - ofile.start_row() - for index in data_cols: - ofile.write_cell(row[index]) - ofile.end_row() - node = self.model.on_iter_next(node) - path = (path[0]+1, ) - ofile.end_page() - ofile.close() - def dummy_report(self, obj): """ For the xml UI definition of popup to work, the submenu Quick Report must have an entry in the xml @@ -1006,3 +341,124 @@ class PersonView(PageView.PersonNavView): """ pass + def define_actions(self): + """ + Required define_actions function for PageView. Builds the action + group information required. We extend beyond the normal here, + since we want to have more than one action group for the PersonView. + Most PageViews really won't care about this. + + Special action groups for Forward and Back are created to allow the + handling of navigation buttons. Forward and Back allow the user to + advance or retreat throughout the history, and we want to have these + be able to toggle these when you are at the end of the history or + at the beginning of the history. + """ + + ListView.define_actions(self) + + self.all_action = gtk.ActionGroup(self.title + "/PersonAll") + self.edit_action = gtk.ActionGroup(self.title + "/PersonEdit") + + self.all_action.add_actions([ + ('FilterEdit', None, _('Person Filter Editor'), None, None, + self.filter_editor), + ('OpenAllNodes', None, _("Expand all Nodes"), None, None, + self.open_all_nodes), + ('Edit', gtk.STOCK_EDIT, _("action|_Edit..."), "Return", + _("Edit the selected person"), self.edit), + ('CloseAllNodes', None, _("Collapse all Nodes"), None, None, + self.close_all_nodes), + ('QuickReport', None, _("Quick View"), None, None, None), + ('Dummy', None, ' ', None, None, self.dummy_report), + ]) + + self.edit_action.add_actions( + [ + ('Add', gtk.STOCK_ADD, _("_Add..."), "Insert", + _("Add a new person"), self.add), + ('Remove', gtk.STOCK_REMOVE, _("_Remove"), "Delete", + _("Remove the Selected Person"), self.remove), + ('ColumnEdit', gtk.STOCK_PROPERTIES, _('_Column Editor...'), None, + None, self._column_editor), + ('CmpMerge', None, _('Compare and _Merge...'), None, None, + self.cmp_merge), + ('FastMerge', None, _('_Fast Merge...'), None, None, + self.fast_merge), + ('ExportTab', None, _('Export View...'), None, None, self.export), + ]) + + self._add_action_group(self.edit_action) + self._add_action_group(self.all_action) + + def enable_action_group(self, obj): + ListView.enable_action_group(self, obj) + self.all_action.set_visible(True) + self.edit_action.set_visible(False) + self.edit_action.set_sensitive(not self.dbstate.db.readonly) + + def disable_action_group(self): + ListView.disable_action_group(self) + + self.all_action.set_visible(False) + self.edit_action.set_visible(False) + + def open_all_nodes(self, obj): + self.uistate.status_text(_("Updating display...")) + self.uistate.set_busy_cursor(True) + + self.list.expand_all() + + self.uistate.set_busy_cursor(False) + self.uistate.modify_statusbar(self.dbstate) + + def close_all_nodes(self, obj): + self.list.collapse_all() + + def cmp_merge(self, obj): + mlist = self.get_selected_objects() + + if len(mlist) != 2: + ErrorDialog( + _("Cannot merge people"), + _("Exactly two people must be selected to perform a merge. " + "A second person can be selected by holding down the " + "control key while clicking on the desired person.")) + else: + import Merge + person1 = self.db.get_person_from_handle(mlist[0]) + person2 = self.db.get_person_from_handle(mlist[1]) + if person1 and person2: + Merge.PersonCompare(self.dbstate, self.uistate, person1, + person2, self.build_tree) + else: + ErrorDialog( + _("Cannot merge people"), + _("Exactly two people must be selected to perform a " + "merge. A second person can be selected by holding " + "down the control key while clicking on the desired " + "person.")) + + def fast_merge(self, obj): + mlist = self.get_selected_objects() + + if len(mlist) != 2: + ErrorDialog( + _("Cannot merge people"), + _("Exactly two people must be selected to perform a merge. " + "A second person can be selected by holding down the " + "control key while clicking on the desired person.")) + else: + import Merge + + person1 = self.db.get_person_from_handle(mlist[0]) + person2 = self.db.get_person_from_handle(mlist[1]) + if person1 and person2: + Merge.MergePeopleUI(self.dbstate, self.uistate, person1, + person2, self.build_tree) + else: + ErrorDialog( + _("Cannot merge people"), + _("Exactly two people must be selected to perform a merge. " + "A second person can be selected by holding down the " + "control key while clicking on the desired person.")) diff --git a/src/DisplayModels/_PeopleModel.py b/src/DisplayModels/_PeopleModel.py index f068f6bfe..91bbc02af 100644 --- a/src/DisplayModels/_PeopleModel.py +++ b/src/DisplayModels/_PeopleModel.py @@ -3,6 +3,8 @@ # # Copyright (C) 2000-2007 Donald N. Allingham # Copyright (C) 2009 Gary Burton +# Copyright (C) 2009 Nick Hall +# 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 @@ -34,15 +36,6 @@ from __future__ import with_statement from gettext import gettext as _ import time import cgi -import locale - -#------------------------------------------------------------------------- -# -# set up logging -# -#------------------------------------------------------------------------- -import logging -log = logging.getLogger(".") #------------------------------------------------------------------------- # @@ -51,6 +44,14 @@ log = logging.getLogger(".") #------------------------------------------------------------------------- import gtk +#------------------------------------------------------------------------- +# +# set up logging +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".") + #------------------------------------------------------------------------- # # GRAMPS modules @@ -62,412 +63,144 @@ from BasicUtils import name_displayer import DateHandler import ToolTips import GrampsLocale -import config -from gen.utils.longop import LongOpStatus -from Filters import SearchFilter, ExactSearchFilter from Lru import LRU +from gui.views.treemodels.treebasemodel import TreeBaseModel -_CACHE_SIZE = 250 -invalid_date_format = config.get('preferences.invalid-date-format') - -class NodeTreeMap(object): - - def __init__(self): - - self.sortnames = {} - self.temp_top_path2iter = [] - - self.iter2path = {} - self.path2iter = {} - self.sname_sub = {} - - self.temp_iter2path = {} - self.temp_path2iter = {} - self.temp_sname_sub = {} - - def clear_sort_names(self): - self.sortnames = {} - - def clear_temp_data(self): - self.temp_iter2path = {} - self.temp_path2iter = {} - self.temp_sname_sub = {} - - def build_toplevel(self): - self.temp_top_path2iter = sorted(self.temp_sname_sub, key=locale.strxfrm) - for name in self.temp_top_path2iter: - self.build_sub_entry(name) - - def get_group_names(self): - return self.temp_top_path2iter - - def assign_sort_name(self, handle, sorted_name, group_name): - self.sortnames[handle] = sorted_name - if group_name in self.temp_sname_sub: - self.temp_sname_sub[group_name] += [handle] - else: - self.temp_sname_sub[group_name] = [handle] - - def assign_data(self): - self.top_path2iter = self.temp_top_path2iter - self.iter2path = self.temp_iter2path - self.path2iter = self.temp_path2iter - self.sname_sub = self.temp_sname_sub - self.top_iter2path = {} - for i, item in enumerate(self.top_path2iter): - self.top_iter2path[item] = i - - def get_path(self, node): - if node in self.top_iter2path: - return (self.top_iter2path[node], ) - else: - (surname, index) = self.iter2path[node] - return (self.top_iter2path[surname], index) - - def has_entry(self, handle): - return handle in self.iter2path - - def get_iter(self, path): - try: - if len(path)==1: # Top Level - return self.top_path2iter[path[0]] - else: # Sublevel - surname = self.top_path2iter[path[0]] - return self.path2iter[(surname, path[1])] - except: - return None - - def has_top_node(self, node): - return node in self.sname_sub - - def find_next_node(self, node): - if node in self.top_iter2path: - path = self.top_iter2path[node] - if path+1 < len(self.top_path2iter): - return self.top_path2iter[path+1] - else: - return None - else: - (surname, val) = self.iter2path[node] - return self.path2iter.get((surname, val+1)) - - def first_child(self, node): - if node is None: - return self.top_path2iter[0] - else: - return self.path2iter.get((node, 0)) - - def has_child(self, node): - if node is None: - return len(self.sname_sub) - if node in self.sname_sub and self.sname_sub[node]: - return True - return False - - def number_of_children(self, node): - if node is None: - return len(self.sname_sub) - if node in self.sname_sub: - return len(self.sname_sub[node]) - return 0 - - def get_nth_child(self, node, n): - if node is None: - if n < len(self.top_path2iter): - return self.top_path2iter[n] - else: - return None - else: - return self.path2iter.get((node, n)) - - def get_parent_of(self, node): - path = self.iter2path.get(node) - if path: - return path[0] - return None - - def build_sub_entry(self, name): - slist = sorted(( (self.sortnames[x], x) \ - for x in self.temp_sname_sub[name] ), - key=lambda x: locale.strxfrm(x[0])) - - for val, (junk, person_handle) in enumerate(slist): - tpl = (name, val) - self.temp_iter2path[person_handle] = tpl - self.temp_path2iter[tpl] = person_handle +#------------------------------------------------------------------------- +# +# COLUMN constants +# +#------------------------------------------------------------------------- +COLUMN_ID = 1 +COLUMN_GENDER = 2 +COLUMN_NAME = 3 +COLUMN_DEATH = 5 +COLUMN_BIRTH = 6 +COLUMN_EVENT = 7 +COLUMN_FAMILY = 8 +COLUMN_CHANGE = 17 +COLUMN_MARKER = 18 #------------------------------------------------------------------------- # # PeopleModel # #------------------------------------------------------------------------- -class PeopleModel(gtk.GenericTreeModel): +class PeopleModel(TreeBaseModel): """ Basic GenericTreeModel interface to handle the Tree interface for the PersonView """ - - # Model types - GENERIC = 0 - SEARCH = 1 - FAST = 2 - - # Column numbers - _ID_COL = 1 - _GENDER_COL = 2 - _NAME_COL = 3 - _DEATH_COL = 5 - _BIRTH_COL = 6 - _EVENT_COL = 7 - _FAMILY_COL = 8 - _CHANGE_COL = 17 - _MARKER_COL = 18 - _GENDER = [ _(u'female'), _(u'male'), _(u'unknown') ] - # dynamic calculation of column indices, for use by various Views - COLUMN_INT_ID = 12 + # The following is accessed from PersonView - CHECK + COLUMN_INT_ID = 12 # dynamic calculation of column indices - # indices into main column definition table - COLUMN_DEF_LIST = 0 - COLUMN_DEF_HEADER = 1 - COLUMN_DEF_TYPE = 2 - - def __init__(self, db, filter_info=None, skip=[]): + def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None, + skip=set(), sort_map=None): """ Initialize the model building the initial data """ - gtk.GenericTreeModel.__init__(self) + self.lru_name = LRU(TreeBaseModel._CACHE_SIZE) + self.lru_bdate = LRU(TreeBaseModel._CACHE_SIZE) + self.lru_ddate = LRU(TreeBaseModel._CACHE_SIZE) - self.db = db - self.in_build = False - self.lru_data = LRU(_CACHE_SIZE) - self.lru_name = LRU(_CACHE_SIZE) - self.lru_bdate = LRU(_CACHE_SIZE) - self.lru_ddate = LRU(_CACHE_SIZE) + self.gen_cursor = db.get_person_cursor + self.map = db.get_raw_person_data + self.scol = scol - config.connect("preferences.todo-color", - self.update_todo) - config.connect("preferences.custom-marker-color", - self.update_custom) - config.connect("preferences.complete-color", - self.update_complete) - - self.complete_color = config.get('preferences.complete-color') - self.todo_color = config.get('preferences.todo-color') - self.custom_color = config.get('preferences.custom-marker-color') - - self.marker_color_column = 10 - self.tooltip_column = 11 - - self.mapper = NodeTreeMap() - - self.total = 0 - self.displayed = 0 - - if filter_info and filter_info != (1, (0, u'', False)): - if filter_info[0] == PeopleModel.GENERIC: - data_filter = filter_info[1] - self._build_data = self._build_filter_sub - elif filter_info[0] == PeopleModel.SEARCH: - col, text, inv = filter_info[1][:3] - func = lambda x: self.on_get_value(x, col) or u"" - - if col == self._GENDER_COL: - data_filter = ExactSearchFilter(func, text, inv) - else: - data_filter = SearchFilter(func, text, inv) - - self._build_data = self._build_search_sub - else: - data_filter = filter_info[1] - self._build_data = self._build_search_sub - else: - self._build_data = self._build_search_sub - data_filter = None - self.current_filter = data_filter - self.rebuild_data(data_filter, skip) - - def update_todo(self,client,cnxn_id,entry,data): - self.todo_color = config.get('preferences.todo-color') - - def update_custom(self,client,cnxn_id,entry,data): - self.custom_color = config.get('preferences.custom-marker-color') - - def update_complete(self,client,cnxn_id,entry,data): - self.complete_color = config.get('preferences.complete-color') - - def rebuild_data(self, data_filter=None, skip=[]): - """ - Convience function that calculates the new data and assigns it. - """ - self.calculate_data(data_filter, skip) - self.assign_data() - self.current_filter = data_filter - - def _build_search_sub(self,dfilter, skip): - ngn = name_displayer.name_grouping_data - nsn = name_displayer.raw_sorted_name - - self.mapper.clear_sort_names() - - self.total = 0 - self.displayed = 0 - with self.db.get_person_cursor() as cursor: - for handle, d in cursor: - self.total += 1 - if not (handle in skip or (dfilter and not dfilter.match(handle,self.db))): - name_data = d[PeopleModel._NAME_COL] - group_name = ngn(self.db, name_data) - sorted_name = nsn(name_data) - self.displayed += 1 - self.mapper.assign_sort_name(handle, sorted_name, group_name) - - def _build_filter_sub(self,dfilter, skip): - ngn = name_displayer.name_grouping_data - nsn = name_displayer.raw_sorted_name - handle_list = self.db.iter_person_handles() - self.total = self.db.get_number_of_people() - - if dfilter: - handle_list = dfilter.apply(self.db, handle_list) - self.displayed = len(handle_list) - else: - self.displayed = self.db.get_number_of_people() - - self.mapper.clear_sort_names() - status = LongOpStatus(msg="Loading People", - total_steps=self.displayed, - interval=self.displayed//10) - self.db.emit('long-op-start', (status,)) - for handle in handle_list: - status.heartbeat() - d = self.db.get_raw_person_data(handle) - if not handle in skip: - name_data = d[PeopleModel._NAME_COL] - group_name = ngn(self.db, name_data) - sorted_name = nsn(name_data) - - self.mapper.assign_sort_name(handle, sorted_name, group_name) - status.end() - - def calculate_data(self, dfilter=None, skip=[]): - """ - Calculate the new path to node values for the model. - """ - self.clear_cache() - self.in_build = True - - self.total = 0 - self.displayed = 0 - - if dfilter: - self.dfilter = dfilter - - self.mapper.clear_temp_data() - - if not self.db.is_open(): - return - - self._build_data(dfilter, skip) - self.mapper.build_toplevel() - - self.in_build = False + #self.group_list = [] + self.fmap = [ + self.column_name, + self.column_id, + self.column_gender, + self.column_birth_day, + self.column_birth_place, + self.column_death_day, + self.column_death_place, + self.column_spouse, + self.column_change, + self.column_marker_text, + self.column_marker_color, + self.column_tooltip, + self.column_int_id, + ] + self.smap = [ + self.sort_name, + self.column_id, + self.column_gender, + self.sort_birth_day, + self.column_birth_place, + self.sort_death_day, + self.column_death_place, + self.column_spouse, + self.column_change, + self.column_marker_text, + self.column_marker_color, + self.column_tooltip, + self.column_int_id, + ] + self.hmap = [ + self.column_header, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + TreeBaseModel.__init__(self, db, search=search, skip=skip, + tooltip_column=11, marker_column=10, + scol=scol, order=order, sort_map=sort_map) def clear_cache(self): + """ Clear the LRU cache """ + TreeBaseModel.clear_cache(self) self.lru_name.clear() - self.lru_data.clear() self.lru_bdate.clear() self.lru_ddate.clear() - - def build_sub_entry(self, name): - self.mapper.build_sub_entry(name) - - def assign_data(self): - self.mapper.assign_data() - - def on_get_flags(self): - """returns the GtkTreeModelFlags for this particular type of model""" - return gtk.TREE_MODEL_ITERS_PERSIST - + def on_get_n_columns(self): - return len(PeopleModel.COLUMN_DEFS) + """ Return the number of columns in the model """ + return len(self.fmap)+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.mapper.get_path(node) + def add_row(self, handle, data): + """ + Add nodes to the node map for a single person. - def is_visable(self, handle): - return self.mapper.has_entry(handle) + handle The handle of the gramps object. + data The object data. + """ + ngn = name_displayer.name_grouping_data + nsn = name_displayer.raw_sorted_name + + name_data = data[COLUMN_NAME] + group_name = ngn(self.db, name_data) + sort_key = self.sort_func(data, handle) - def on_get_column_type(self, index): - return PeopleModel.COLUMN_DEFS[index][PeopleModel.COLUMN_DEF_TYPE] - - def on_get_iter(self, path): - return self.mapper.get_iter(path) - - def on_get_value(self, node, col): - # test for header or data row-type - if self.mapper.has_top_node(node): - # Header rows dont get the foreground color set - if col == self.marker_color_column: - return None - # test for 'header' column being empty (most are) - if not PeopleModel.COLUMN_DEFS[col][PeopleModel.COLUMN_DEF_HEADER]: - return u'' - # return values for 'header' row, calling a function - # according to column_defs table - return (PeopleModel.COLUMN_DEFS[col] - [PeopleModel.COLUMN_DEF_HEADER](self, node) - ) - else: - # return values for 'data' row, calling a function - # according to column_defs table - try: - if node in self.lru_data: - data = self.lru_data[node] - else: - data = self.db.get_raw_person_data(str(node)) - if not self.in_build: - self.lru_data[node] = data - return (PeopleModel.COLUMN_DEFS[col] - [PeopleModel.COLUMN_DEF_LIST](self, data, node) - ) - except: - return None - - def on_iter_next(self, node): - """returns the next node at this level of the tree""" - return self.mapper.find_next_node(node) - - def on_iter_children(self, node): - """Return the first child of the node""" - return self.mapper.first_child(node) - - def on_iter_has_child(self, node): - """returns true if this node has children""" - return self.mapper.has_child(node) - - def on_iter_n_children(self, node): - return self.mapper.number_of_children(node) - - def on_iter_nth_child(self, node, n): - return self.mapper.get_nth_child(node, n) - - def on_iter_parent(self, node): - """returns the parent of this node""" - return self.mapper.get_parent_of(node) - - def column_sort_name(self, data, node): + #if group_name not in self.group_list: + #self.group_list.append(group_name) + #self.add_node(None, group_name, group_name, None) + + # add as node: parent, child, sortkey, handle; parent and child are + # nodes in the treebasemodel, and will be used as iters + self.add_node(group_name, handle, sort_key, handle) + + def sort_name(self, data, node): n = Name() - n.unserialize(data[PeopleModel._NAME_COL]) + n.unserialize(data[COLUMN_NAME]) return name_displayer.sort_string(n) def column_spouse(self, data, node): spouses_names = u"" handle = data[0] - for family_handle in data[PeopleModel._FAMILY_COL]: + for family_handle in data[COLUMN_FAMILY]: family = self.db.get_family_from_handle(family_handle) for spouse_id in [family.get_father_handle(), family.get_mother_handle()]: @@ -485,43 +218,49 @@ class PeopleModel(gtk.GenericTreeModel): if node in self.lru_name: name = self.lru_name[node] else: - name = name_displayer.raw_sorted_name(data[PeopleModel._NAME_COL]) - if not self.in_build: + name = name_displayer.raw_sorted_name(data[COLUMN_NAME]) + if not self._in_build: self.lru_name[node] = name return name def column_id(self, data, node): - return data[PeopleModel._ID_COL] - + return data[COLUMN_ID] + def column_change(self, data, node): return unicode( time.strftime('%x %X', - time.localtime(data[PeopleModel._CHANGE_COL])), + time.localtime(data[COLUMN_CHANGE])), GrampsLocale.codeset) def column_gender(self, data, node): - return PeopleModel._GENDER[data[PeopleModel._GENDER_COL]] + return PeopleModel._GENDER[data[COLUMN_GENDER]] def column_birth_day(self, data, node): if node in self.lru_bdate: value = self.lru_bdate[node] else: - value = self._get_birth_data(data, node) - if not self.in_build: + value = self._get_birth_data(data, node, False) + if not self._in_build: self.lru_bdate[node] = value return value + + def sort_birth_day(self, data, node): + return self._get_birth_data(data, node, True) - def _get_birth_data(self, data, node): - index = data[PeopleModel._BIRTH_COL] + def _get_birth_data(self, data, node, sort_mode): + index = data[COLUMN_BIRTH] if index != -1: try: - local = data[PeopleModel._EVENT_COL][index] + local = data[COLUMN_EVENT][index] b = EventRef() b.unserialize(local) birth = self.db.get_event_from_handle(b.ref) - date_str = DateHandler.get_date(birth) - if date_str != "": - retval = cgi.escape(date_str) + if sort_mode: + retval = "%09d" % birth.get_date_object().get_sort_value() + else: + date_str = DateHandler.get_date(birth) + if date_str != "": + retval = cgi.escape(date_str) if not DateHandler.get_date_valid(birth): return invalid_date_format % retval else: @@ -529,7 +268,7 @@ class PeopleModel(gtk.GenericTreeModel): except: return u'' - for event_ref in data[PeopleModel._EVENT_COL]: + for event_ref in data[COLUMN_EVENT]: er = EventRef() er.unserialize(event_ref) event = self.db.get_event_from_handle(er.ref) @@ -538,7 +277,10 @@ class PeopleModel(gtk.GenericTreeModel): if (etype in [EventType.BAPTISM, EventType.CHRISTEN] and er.get_role() == EventRoleType.PRIMARY and date_str != ""): - retval = u"%s" % cgi.escape(date_str) + if sort_mode: + retval = "%09d" % event.get_date_object().get_sort_value() + else: + retval = u"%s" % cgi.escape(date_str) if not DateHandler.get_date_valid(event): return invalid_date_format % retval else: @@ -550,22 +292,28 @@ class PeopleModel(gtk.GenericTreeModel): if node in self.lru_ddate: value = self.lru_ddate[node] else: - value = self._get_death_data(data, node) - if not self.in_build: + value = self._get_death_data(data, node, False) + if not self._in_build: self.lru_ddate[node] = value return value + + def sort_death_day(self, data, node): + return self._get_death_data(data, node, True) - def _get_death_data(self, data, node): - index = data[PeopleModel._DEATH_COL] + def _get_death_data(self, data, node, sort_mode): + index = data[COLUMN_DEATH] if index != -1: try: - local = data[PeopleModel._EVENT_COL][index] + local = data[COLUMN_EVENT][index] ref = EventRef() ref.unserialize(local) event = self.db.get_event_from_handle(ref.ref) - date_str = DateHandler.get_date(event) - if date_str != "": - retval = cgi.escape(date_str) + if sort_mode: + retval = "%09d" % event.get_date_object().get_sort_value() + else: + date_str = DateHandler.get_date(event) + if date_str != "": + retval = cgi.escape(date_str) if not DateHandler.get_date_valid(event): return invalid_date_format % retval else: @@ -573,16 +321,21 @@ class PeopleModel(gtk.GenericTreeModel): except: return u'' - for event_ref in data[PeopleModel._EVENT_COL]: + for event_ref in data[COLUMN_EVENT]: er = EventRef() er.unserialize(event_ref) event = self.db.get_event_from_handle(er.ref) etype = event.get_type() date_str = DateHandler.get_date(event) - if (etype in [EventType.BURIAL, EventType.CREMATION, EventType.CAUSE_DEATH] + if (etype in [EventType.BURIAL, + EventType.CREMATION, + EventType.CAUSE_DEATH] and er.get_role() == EventRoleType.PRIMARY and date_str): - retval = "%s" % cgi.escape(date_str) + if sort_mode: + retval = "%09d" % event.get_date_object().get_sort_value() + else: + retval = "%s" % cgi.escape(date_str) if not DateHandler.get_date_valid(event): return invalid_date_format % retval else: @@ -590,10 +343,10 @@ class PeopleModel(gtk.GenericTreeModel): return u"" def column_birth_place(self, data, node): - index = data[PeopleModel._BIRTH_COL] + index = data[COLUMN_BIRTH] if index != -1: try: - local = data[PeopleModel._EVENT_COL][index] + local = data[COLUMN_EVENT][index] br = EventRef() br.unserialize(local) event = self.db.get_event_from_handle(br.ref) @@ -607,7 +360,7 @@ class PeopleModel(gtk.GenericTreeModel): except: return u'' - for event_ref in data[PeopleModel._EVENT_COL]: + for event_ref in data[COLUMN_EVENT]: er = EventRef() er.unserialize(event_ref) event = self.db.get_event_from_handle(er.ref) @@ -625,10 +378,10 @@ class PeopleModel(gtk.GenericTreeModel): return u"" def column_death_place(self, data, node): - index = data[PeopleModel._DEATH_COL] + index = data[COLUMN_DEATH] if index != -1: try: - local = data[PeopleModel._EVENT_COL][index] + local = data[COLUMN_EVENT][index] dr = EventRef() dr.unserialize(local) event = self.db.get_event_from_handle(dr.ref) @@ -642,12 +395,13 @@ class PeopleModel(gtk.GenericTreeModel): except: return u'' - for event_ref in data[PeopleModel._EVENT_COL]: + for event_ref in data[COLUMN_EVENT]: er = EventRef() er.unserialize(event_ref) event = self.db.get_event_from_handle(er.ref) etype = event.get_type() - if (etype in [EventType.BURIAL, EventType.CREMATION, EventType.CAUSE_DEATH] + if (etype in [EventType.BURIAL, EventType.CREMATION, + EventType.CAUSE_DEATH] and er.get_role() == EventRoleType.PRIMARY): place_handle = event.get_place_handle() @@ -659,18 +413,18 @@ class PeopleModel(gtk.GenericTreeModel): return u"" def column_marker_text(self, data, node): - if PeopleModel._MARKER_COL < len(data): - return str(data[PeopleModel._MARKER_COL]) + if COLUMN_MARKER < len(data): + return str(data[COLUMN_MARKER]) return "" def column_marker_color(self, data, node): try: - if data[PeopleModel._MARKER_COL]: - if data[PeopleModel._MARKER_COL][0] == MarkerType.COMPLETE: + if data[COLUMN_MARKER]: + if data[COLUMN_MARKER][0] == MarkerType.COMPLETE: return self.complete_color - if data[PeopleModel._MARKER_COL][0] == MarkerType.TODO_TYPE: + if data[COLUMN_MARKER][0] == MarkerType.TODO_TYPE: return self.todo_color - if data[PeopleModel._MARKER_COL][0] == MarkerType.CUSTOM: + if data[COLUMN_MARKER][0] == MarkerType.CUSTOM: return self.custom_color except IndexError: pass @@ -694,24 +448,3 @@ class PeopleModel(gtk.GenericTreeModel): def column_header_view(self, node): return True - # table of column definitions - # (unless this is declared after the PeopleModel class, an error is thrown) - - COLUMN_DEFS = [ - (column_name, column_header, str), - (column_id, None, str), - (column_gender, None, str), - (column_birth_day, None, str), - (column_birth_place, None, str), - (column_death_day, None, str), - (column_death_place, None, str), - (column_spouse, None, str), - (column_change, None, str), - (column_marker_text, None, str), - (column_marker_color, None, str), - # the order of the above columns must match PeopleView.column_names - - # these columns are hidden, and must always be last in the list - (column_tooltip, None, object), - (column_int_id, None, str), - ] diff --git a/src/Selectors/_BaseSelector.py b/src/Selectors/_BaseSelector.py index 678863c33..09be75e16 100644 --- a/src/Selectors/_BaseSelector.py +++ b/src/Selectors/_BaseSelector.py @@ -37,7 +37,6 @@ import pango import const import ManagedWindow from Filters import SearchBar -from DisplayModels import PeopleModel from glade import Glade #------------------------------------------------------------------------- @@ -62,7 +61,7 @@ class BaseSelector(ManagedWindow.ManagedWindow): set of handles to skip in the view, and search_bar to show the SearchBar at the top or not. """ - self.filter = filter + self.filter = (2, filter, False) # Set window title, some selectors may set self.title in their __init__ if not hasattr(self, 'title'): @@ -231,6 +230,12 @@ class BaseSelector(ManagedWindow.ManagedWindow): """ raise NotImplementedError + def exact_search(self): + """ + Returns a tuple indicating columns requiring an exact search + """ + return () + def setup_filter(self): """ Builds the default filters and add them to the filter bar. @@ -244,23 +249,25 @@ class BaseSelector(ManagedWindow.ManagedWindow): """ Builds the selection people see in the Selector """ - #search info for the - filter_info = (False, self.search_bar.get_value(), False) + if self.filter: + filter_info = self.filter + else: + #search info for the + if self.search_bar.get_value()[0] in self.exact_search(): + filter_info = (0, self.search_bar.get_value(), True) + else: + filter_info = (0, self.search_bar.get_value(), False) + #set up cols the first time if self.setupcols : self.add_columns(self.tree) - + #reset the model with correct sorting - if self.get_model_class() is PeopleModel: - self.model = PeopleModel(self.db, - (PeopleModel.FAST, self.filter), - skip=self.skip_list) - else: - self.model = self.get_model_class()(self.db, self.sort_col, - self.sortorder, - sort_map=self.column_order(), - skip=self.skip_list, - search=filter_info) + self.model = self.get_model_class()(self.db, self.sort_col, + self.sortorder, + sort_map=self.column_order(), + skip=self.skip_list, + search=filter_info) self.tree.set_model(self.model) @@ -272,10 +279,7 @@ class BaseSelector(ManagedWindow.ManagedWindow): self.columns[self.sort_col].set_sort_order(self.sortorder) # set the search column to be the sorted column - if self.get_model_class() is PeopleModel: - search_col = 0 - else: - search_col = self.column_order()[self.sort_col][1] + search_col = self.column_order()[self.sort_col][1] self.tree.set_search_column(search_col) self.setupcols = False @@ -283,18 +287,17 @@ class BaseSelector(ManagedWindow.ManagedWindow): def column_clicked(self, obj, data): if self.sort_col != data: self.sortorder = gtk.SORT_ASCENDING + self.sort_col = data + self.build_tree() else: if (self.columns[data].get_sort_order() == gtk.SORT_DESCENDING or not self.columns[data].get_sort_indicator()): self.sortorder = gtk.SORT_ASCENDING else: self.sortorder = gtk.SORT_DESCENDING + self.model.reverse_order() - self.sort_col = data handle = self.first_selected() - - self.build_tree() - if handle: path = self.model.on_get_path(handle) self.selection.select_path(path) @@ -303,9 +306,12 @@ class BaseSelector(ManagedWindow.ManagedWindow): return True def show_toggle(self, obj): - filter = None if obj.get_active() else self.filter + filter_info = None if obj.get_active() else self.filter - self.model = PeopleModel(self.db, (PeopleModel.FAST, filter), - skip=self.skip_list) + self.model = self.get_model_class()(self.db, self.sort_col, + self.sortorder, + sort_map=self.column_order(), + skip=self.skip_list, + search=filter_info) self.tree.set_model(self.model) self.tree.grab_focus() diff --git a/src/Selectors/_SelectPerson.py b/src/Selectors/_SelectPerson.py index 66e4a8fa5..7ba68e43e 100644 --- a/src/Selectors/_SelectPerson.py +++ b/src/Selectors/_SelectPerson.py @@ -107,6 +107,12 @@ class SelectPerson(BaseSelector): ] return column_names + def exact_search(self): + """ + Returns a tuple indicating columns requiring an exact search + """ + return (2,) # Gender ('female' contains the string 'male') + def _on_row_activated(self, treeview, path, view_col): store, paths = self.selection.get_selected_rows() if paths and len(paths[0]) == 2 : diff --git a/src/gen/db/read.py b/src/gen/db/read.py index 1796251bb..948a0b298 100644 --- a/src/gen/db/read.py +++ b/src/gen/db/read.py @@ -1404,9 +1404,9 @@ class GrampsDbRead(GrampsDbBase, Callback): Return the Person display common information stored in the database's metadata. """ - default = [(1, 1, 100), (1, 2, 100), (1, 3, 150), (0, 4, 150), - (1, 5, 150), (0, 6, 150), (0, 7, 100), (0, 8, 100), - ] + default = [(1, 0, 250), (1, 1, 50), (1, 2, 75), (1, 3, 100), + (1, 4, 175), (1, 5, 100), (1, 6, 175), (1, 7, 100), + (0, 8, 100)] return self.__get_column_order(PERSON_COL_KEY, default) def __get_columns(self, key, default): diff --git a/src/gui/views/listview.py b/src/gui/views/listview.py index 3f0499f64..a119c4110 100644 --- a/src/gui/views/listview.py +++ b/src/gui/views/listview.py @@ -34,7 +34,7 @@ import cPickle as pickle import time import logging -_LOG = logging.getLogger('.listview') +_LOG = logging.getLogger('.gui.listview') #---------------------------------------------------------------- # @@ -215,14 +215,15 @@ class ListView(NavigationView): def build_tree(self): if self.active: - cput = time.clock() + cput0 = time.clock() if config.get('interface.filter'): - filter_info = (True, self.generic_filter) + filter_info = (True, self.generic_filter, False) else: - if self.search_bar.get_value()[0] in self.exact_search(): - filter_info = (False, self.search_bar.get_value(), True) + value = self.search_bar.get_value() + if value[0] in self.exact_search(): + filter_info = (False, value, True) else: - filter_info = (False, self.search_bar.get_value(), False) + filter_info = (False, value, False) if self.dirty or not self.model: self.model = self.make_model(self.dbstate.db, self.sort_col, @@ -235,8 +236,11 @@ class ListView(NavigationView): self.model.set_search(filter_info) self.model.rebuild_data() + cput1 = time.clock() self.build_columns() + cput2 = time.clock() self.list.set_model(self.model) + cput3 = time.clock() self.__display_column_sort() self.goto_active(None) @@ -244,11 +248,17 @@ class ListView(NavigationView): self.tooltips = TreeTips.TreeTips( self.list, self.model.tooltip_column, True) self.dirty = False + cput4 = time.clock() 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') + str(time.clock() - cput0) + ' sec') + _LOG.debug('parts ' + str(cput1-cput0) + ' , ' + + str(cput2-cput1) + ' , ' + + str(cput3-cput2) + ' , ' + + str(cput4-cput3) + ' , ' + + str(time.clock() - cput4)) else: self.dirty = True @@ -534,12 +544,13 @@ class ListView(NavigationView): handle = self.first_selected() if config.get('interface.filter'): - filter_info = (True, self.generic_filter) + filter_info = (True, self.generic_filter, False) else: - if self.search_bar.get_value()[0] in self.exact_search(): - filter_info = (False, self.search_bar.get_value(), True) + value = self.search_bar.get_value() + if value[0] in self.exact_search(): + filter_info = (False, value, True) else: - filter_info = (False, self.search_bar.get_value(), False) + filter_info = (False, value, False) if same_col: self.model.reverse_order() diff --git a/src/gui/views/treemodels/treebasemodel.py b/src/gui/views/treemodels/treebasemodel.py index 19d7beac6..23d626899 100644 --- a/src/gui/views/treemodels/treebasemodel.py +++ b/src/gui/views/treemodels/treebasemodel.py @@ -4,6 +4,7 @@ # Copyright (C) 2000-2007 Donald N. Allingham # Copyright (C) 2009 Gary Burton # Copyright (C) 2009 Nick Hall +# 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 @@ -49,20 +50,21 @@ import gtk # #------------------------------------------------------------------------- import config +from Utils import conv_unicode_tosrtkey_ongtk from gen.utils.longop import LongOpStatus -from Filters import SearchFilter -# from Filters import ExactSearchFilter from Lru import LRU +from bisect import bisect_right +from Filters import SearchFilter, ExactSearchFilter #------------------------------------------------------------------------- # -# TreeNodeMap +# TreeBaseModel # #------------------------------------------------------------------------- -class TreeNodeMap(object): +class TreeBaseModel(gtk.GenericTreeModel): """ - A NodeMap for a hierarchical treeview. The map defines the mapping - between a unique node and a path. Paths are defined by a tuple. + 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 @@ -75,275 +77,56 @@ class TreeNodeMap(object): is set to None if no gramps object is associated with the node. children A dictionary of parent nodes. Each entry is a list of - (child, sortkey) tuples. The list is sorted during the + (sortkey, child) tuples. The list is sorted during the build. The top node of the hierarchy is None. - path2node A dictionary of paths. Each entry is a node. - node2path A dictionary of nodes. Each entry is a path. handle2node A dictionary of gramps handles. Each entry is a node. - - Nodes are added using the add_node method. - The path2node and node2path mapping is built using build_toplevel. - A simple recursive algorithm is used. - Branches of the tree can be re-built using build_sub_entry. - """ - def __init__(self): - """ - Initialise data structures - """ - self.tree = {} - self.children = {} - self.path2node = {} - self.node2path = {} - - self.handle2node = {} - - self.__reverse = False - - def clear(self): - """ - Clear the entire map. - """ - self.tree = {} - self.children = {} - self.path2node = {} - self.node2path = {} - - self.handle2node = {} - - def clear_sub_entry(self, node): - """ - Clear a single branch of the map. - """ - if node is None: - self.path2node = {} - self.node2path = {} - else: - if node in self.children: - for child in self.children[node]: - self.clear_node(child[0]) - - def clear_node(self, node): - if node in self.node2path: - path = self.node2path[node] - del self.path2node[path] - del self.node2path[node] - if node in self.children: - for child in self.children[node]: - self.clear_node(child[0]) - - def add_node(self, parent, child, sortkey, handle): - """ - Add a node to the map. - - parent The parent node for the child. None for top level. - 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. - """ - if child in self.tree: - if handle: - self.tree[child][1] = handle - node_added = False - else: - - self.tree[child] = [parent, handle] - if parent in self.children: - self.children[parent] += [(child, sortkey)] - else: - self.children[parent] = [(child, sortkey)] - node_added = True - - if handle: - self.handle2node[handle] = child - - return node_added - - def remove_node(self, node): - if node in self.children: - self.tree[node][1] = None - node_removed = False - else: - parent = self.tree[node][0] - del self.tree[node] - new_list = [] - for child in self.children[parent]: - if child[0] != node: - new_list.append(child) - if len(new_list) == 0: - del self.children[parent] - else: - self.children[parent] = new_list - node_removed = True - - return node_removed - - def build_sub_entry(self, node, path, sort): - """ - Build the path2node and node2path maps for the children of a - given node and recursively builds the next level down. - - node The parent node. - path The path of the parent node. - """ - if sort: - self.children[node].sort(key=lambda x: locale.strxfrm(x[1])) - for i, child in enumerate(self.children[node]): - if self.__reverse: - new_path = path + [len(self.children[node]) - i - 1] - else: - new_path = path + [i] - self.path2node[tuple(new_path)] = child[0] - self.node2path[child[0]] = tuple(new_path) - if child[0] in self.children: - self.build_sub_entry(child[0], new_path, sort) - - def build_toplevel(self, sort=True): - """ - Build the complete map from the top level. - """ - if len(self.tree) == 0: - return - self.build_sub_entry(None, [], sort) - - def reverse_order(self): - self.__reverse = not self.__reverse - self.path2node = {} - self.node2path = {} - self.build_toplevel(sort=False) - - def get_handle(self, node): - """ - Get the gramps handle for a node. Return None if the node does - not correspond to a gramps object. - """ - if node in self.tree: - return self.tree[node][1] - else: - return None - - def get_node(self, handle): - """ - Get the node for a handle. - """ - if handle in self.handle2node: - return self.handle2node[handle] - else: - return None - - # The following methods support the public interface of the - # GenericTreeModel. - def get_path(self, node): - """ - Get the path for a node. - """ - # For trees without the active person a key error is thrown - return self.node2path[node] - - def get_iter(self, path): - """ - Build the complete map from the top level. - """ - if path in self.path2node: - return self.path2node[path] - else: - # Empty tree - return None - - def find_next_node(self, node): - """ - Get the next node with the same parent as the given node. - """ - path_list = list(self.node2path[node]) - path_list[len(path_list)-1] += 1 - path = tuple(path_list) - if path in self.path2node: - return self.path2node[path] - else: - return None - - def first_child(self, node): - """ - Get the first child of the given node. - """ - if node in self.children: - if self.__reverse: - size = len(self.children[node]) - return self.children[node][size - 1][0] - else: - return self.children[node][0][0] - else: - return None - - def has_child(self, node): - """ - Find if the given node has any children. - """ - if node in self.children: - return True - else: - return False - - def number_of_children(self, node): - """ - Get the number of children of the given node. - """ - if node in self.children: - return len(self.children[node]) - else: - return 0 - - def get_nth_child(self, node, index): - """ - Get the nth child of the given node. - """ - if node in self.children: - if len(self.children[node]) > index: - if self.__reverse: - size = len(self.children[node]) - return self.children[node][size - index - 1][0] - else: - return self.children[node][index][0] - else: - return None - else: - return None - - def get_parent_of(self, node): - """ - Get the parent of the given node. - """ - if node in self.tree: - return self.tree[node][0] - else: - return None - -#------------------------------------------------------------------------- -# -# TreeBaseModel -# -#------------------------------------------------------------------------- -class TreeBaseModel(gtk.GenericTreeModel): - """ - The base class for all hierarchical treeview models. - It keeps a TreeNodeMap, and obtains data from database as needed. + 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 + tooltip_column : column number of tooltip + marker_column : column number of marker + 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 """ # LRU cache size _CACHE_SIZE = 250 - # Search/Filter modes - GENERIC = 0 - SEARCH = 1 - FAST = 2 - def __init__(self, db, tooltip_column, marker_column=None, search=None, skip=set(), - scol=0, order=gtk.SORT_ASCENDING, sort_map=None): + scol=0, order=gtk.SORT_ASCENDING, sort_map=None, + nrgroups = 1, + group_can_have_handle = False): cput = time.clock() gtk.GenericTreeModel.__init__(self) + # Initialise data structures + self.tree = {} + self.children = {} + self.children[None] = [] + self.handle2node = {} + self.__reverse = (order == gtk.SORT_DESCENDING) + self.nrgroups = nrgroups + self.group_can_have_handle = group_can_have_handle + + self.set_property("leak_references", False) self.db = db #normally sort on first column, so scol=0 if sort_map: @@ -353,11 +136,12 @@ class TreeBaseModel(gtk.GenericTreeModel): #we need the model col, that corresponds with scol col = self.sort_map[scol][1] self.sort_func = self.smap[col] + self.sort_col = col else: self.sort_func = self.smap[scol] - self.sort_col = scol + self.sort_col = scol - self.in_build = False + self._in_build = False self.lru_data = LRU(TreeBaseModel._CACHE_SIZE) @@ -372,15 +156,13 @@ class TreeBaseModel(gtk.GenericTreeModel): self.todo_color = config.get('preferences.todo-color') self.custom_color = config.get('preferences.custom-marker-color') - self.mapper = TreeNodeMap() - self.set_search(search) - - self.tooltip_column = tooltip_column - self.marker_color_column = marker_column + self._tooltip_column = tooltip_column + self._marker_column = marker_column self.__total = 0 self.__displayed = 0 + self.set_search(search) self.rebuild_data(self.current_filter, skip) _LOG.debug(self.__class__.__name__ + ' __init__ ' + @@ -388,20 +170,63 @@ class TreeBaseModel(gtk.GenericTreeModel): def __update_todo(self, *args): + """ + Update the todo color when the preferences change. + """ self.todo_color = config.get('preferences.todo-color') def __update_custom(self, *args): + """ + Update the custom color when the preferences change. + """ self.custom_color = config.get('preferences.custom-marker-color') def __update_complete(self, *args): + """ + Update the complete color when the preferences change. + """ self.complete_color = config.get('preferences.complete-color') 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 tooltip_column(self): + """ + Return the tooltip column. + """ + return self._tooltip_column + + def marker_column(self): + """ + Return the marker color column. + """ + return self._marker_column + + def clear_cache(self): + """ + Clear the LRU cache. + """ + self.lru_data.clear() + + def clear(self): + """ + Clear the data map. + """ + self.tree = {} + self.children = {} + self.children[None] = [] + self.handle2node = {} + self.__reverse = False + def set_search(self, search): """ Change the search function that filters the data in the model. @@ -412,24 +237,28 @@ class TreeBaseModel(gtk.GenericTreeModel): with the new entries """ if search: - if search[0]: + if search[0] == 1: # Filter #following is None if no data given in filter sidebar self.search = search[1] - self._build_data = self._build_filter_sub - else: + self._build_data = self._rebuild_filter + elif search[0] == 0: # Search if search[1]: # we have search[1] = (index, text_unicode, inversion) - col = search[1][0] - text = search[1][1] - inv = search[1][2] + col, text, inv = search[1] func = lambda x: self._get_value(x, col) or u"" - self.search = SearchFilter(func, text, inv) + if search[2]: + self.search = ExactSearchFilter(func, text, inv) + else: + self.search = SearchFilter(func, text, inv) else: self.search = None - self._build_data = self._build_search_sub + self._build_data = self._rebuild_search + else: # Fast filter + self.search = search[1] + self._build_data = self._rebuild_search else: self.search = None - self._build_data = self._build_search_sub + self._build_data = self._rebuild_search self.current_filter = self.search @@ -437,23 +266,28 @@ class TreeBaseModel(gtk.GenericTreeModel): """ Rebuild the data map. """ + cput = time.clock() self.clear_cache() - self.in_build = True - - self.mapper.clear() + self._in_build = True if not self.db.is_open(): return - #self._build_data(data_filter, skip) + self.clear() self._build_data(self.current_filter, skip) - self.mapper.build_toplevel() + self.sort_data() - self.in_build = False + self._in_build = False self.current_filter = data_filter + + _LOG.debug(self.__class__.__name__ + ' rebuild_data ' + + str(time.clock() - cput) + ' sec') - def _build_search_sub(self, dfilter, skip): + def _rebuild_search(self, dfilter, skip): + """ + Rebuild the data map where a search condition is applied. + """ self.__total = 0 self.__displayed = 0 with self.gen_cursor() as cursor: @@ -464,8 +298,10 @@ class TreeBaseModel(gtk.GenericTreeModel): self.__displayed += 1 self.add_row(handle, data) - def _build_filter_sub(self, dfilter, skip): - + def _rebuild_filter(self, dfilter, skip): + """ + Rebuild the data map where a filter is applied. + """ with self.gen_cursor() as cursor: handle_list = [key for key, data in cursor] self.__total = len(handle_list) @@ -487,54 +323,160 @@ class TreeBaseModel(gtk.GenericTreeModel): self.add_row(handle, data) status.end() - def reverse_order(self): - self.mapper.reverse_order() + def add_node(self, parent, child, sortkey, handle, add_parent=True): + """ + 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 + """ + sortkey = conv_unicode_tosrtkey_ongtk(sortkey) + 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, + self._add_dup_node(parent, child, sortkey, handle) + else: + self.tree[child] = (parent, handle) + if parent in self.children: + if self._in_build: + self.children[parent].append((sortkey, child)) + else: + index = bisect_right(self.children[parent], (sortkey, child)) + self.children[parent].insert(index, (sortkey, child)) + else: + self.children[parent] = [(sortkey, child)] - def clear_cache(self): - self.lru_data.clear() + if not self._in_build: + # emit row_inserted signal + path = self.on_get_path(child) + node = self.get_iter(path) + self.row_inserted(path, node) - def build_sub_entry(self, node): - self.mapper.clear_sub_entry(node) - if node is None: - self.mapper.build_toplevel(sort=True) + if handle: + self.handle2node[handle] = child + + def _add_dup_node(self, parent, child, sortkey, handle): + """ + 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: + raise ValueError, 'attempt to add twice a node to the model %s' % \ + str(parent) + ' ' + str(child) + ' ' + sortkey + present_val = self.tree[child] + if handle and present_val[1] is None: + self.tree[child][1] = handle + elif handle is None: + pass + else: + #handle given, and present handle is not None + raise ValueError, 'attempt to add twice a node to the model' + + def sort_data(self): + """ + Sort the data in the map according to the value of the sort key. + """ + for node in self.children: + self.children[node].sort() + + def remove_node(self, node): + """ + Remove a node from the map. + """ + if node in self.children: + self.tree[node][1] = None else: path = self.on_get_path(node) - self.mapper.build_sub_entry(node, list(path), sort=True) + parent = self.tree[node][0] + del self.tree[node] + new_list = [] + for child in self.children[parent]: + if child[1] != node: + new_list.append(child) + if len(new_list) == 0: + del self.children[parent] + else: + self.children[parent] = new_list + + # emit row_deleted signal + self.row_deleted(path) + + def reverse_order(self): + """ + Reverse the order of the map. + """ + cput = time.clock() + self.__reverse = not self.__reverse + self._reverse_level(None) + _LOG.debug(self.__class__.__name__ + ' reverse_order ' + + str(time.clock() - cput) + ' sec') + + def _reverse_level(self, node): + """ + Reverse the order of a single level in the map. + """ + if node in self.children: + rows = range(len(self.children[node])) + rows.reverse() + if node is None: + path = iter = None + else: + path = self.on_get_path(node) + iter = self.get_iter(path) + self.rows_reordered(path, iter, rows) + for child in self.children[node]: + self._reverse_level(child[1]) def add_row(self, handle, data): - pass + """ + Add a row to the model. In general this will add more then one node. + """ + raise NotImplementedError def add_row_by_handle(self, handle): """ Add a row to the model. """ + cput = time.clock() data = self.map(handle) - top_node = self.add_row(handle, data) - parent_node = self.on_iter_parent(top_node) - - self.build_sub_entry(parent_node) + self.add_row(handle, data) - path = self.on_get_path(top_node) - node = self.get_iter(path) - # only one row_inserted and row_has_child_toggled is needed? - self.row_inserted(path, node) - self.row_has_child_toggled(path, node) + _LOG.debug(self.__class__.__name__ + ' add_row_by_handle ' + + str(time.clock() - cput) + ' sec') def delete_row_by_handle(self, handle): """ Delete a row from the model. """ + cput = time.clock() self.clear_cache() + node = self.get_node(handle) parent = self.on_iter_parent(node) - while node and self.mapper.remove_node(node): - path = self.on_get_path(node) - node = parent - parent = self.on_iter_parent(parent) - - self.build_sub_entry(node) + self.remove_node(node) - self.row_deleted(path) + while parent is not None: + next_parent = self.on_iter_parent(parent) + if parent not in self.children: + self.remove_node(parent) + parent = next_parent + + _LOG.debug(self.__class__.__name__ + ' delete_row_by_handle ' + + str(time.clock() - cput) + ' sec') def update_row_by_handle(self, handle): """ @@ -543,31 +485,26 @@ class TreeBaseModel(gtk.GenericTreeModel): self.delete_row_by_handle(handle) self.add_row_by_handle(handle) - # If the node hasn't moved all we need is to call row_changed. + # If the node hasn't moved, all we need is to call row_changed. #self.row_changed(path, node) - def get_node(self, handle): - return self.mapper.get_node(handle) - def get_handle(self, node): - return self.mapper.get_handle(node) - - def _get_value(self, handle, col): """ - Returns the contents of a given column of a gramps object + Get the gramps handle for a node. Return None if the node does + not correspond to a gramps object. """ - try: - if handle in self.lru_data: - data = self.lru_data[handle] - else: - data = self.map(handle) - if not self.in_build: - self.lru_data[handle] = data - return (self.fmap[col](data, handle)) - except: - return None + ret = self.tree.get(node) + if ret: + return ret[1] + return ret + + def get_node(self, handle): + """ + Get the node for a handle. + """ + return self.handle2node.get(handle) - # The following define the public interface of gtk.GenericTreeModel + # The following implement the public interface of gtk.GenericTreeModel def on_get_flags(self): """ @@ -582,34 +519,22 @@ class TreeBaseModel(gtk.GenericTreeModel): """ raise NotImplementedError - def on_get_path(self, node): - """ - See gtk.GenericTreeModel - """ - return self.mapper.get_path(node) - def on_get_column_type(self, index): """ See gtk.GenericTreeModel """ - if index == self.tooltip_column: + if index == self._tooltip_column: return object return str - def on_get_iter(self, path): - """ - See gtk.GenericTreeModel - """ - return self.mapper.get_iter(path) - def on_get_value(self, node, col): """ See gtk.GenericTreeModel """ - handle = self.mapper.get_handle(node) + handle = self.get_handle(node) if handle is None: # Header rows dont get the foreground color set - if col == self.marker_color_column: + if col == self._marker_column: return None # Look for header fuction for column and call it @@ -623,39 +548,131 @@ class TreeBaseModel(gtk.GenericTreeModel): # return values for 'data' row, calling a function # according to column_defs table return self._get_value(handle, col) + + def _get_value(self, handle, col): + """ + Returns the contents of a given column of a gramps object + """ + try: + if handle in self.lru_data: + data = self.lru_data[handle] + else: + data = self.map(handle) + if not self._in_build: + self.lru_data[handle] = data + return (self.fmap[col](data, handle)) + except: + return None - def on_iter_next(self, node): + def on_get_iter(self, path): """ - See gtk.GenericTreeModel + Returns a node from a given path. """ - return self.mapper.find_next_node(node) + if not self.tree: + return None + node = None + pathlist = list(path) + for index in pathlist: + if self.__reverse: + size = len(self.children[node]) + node = self.children[node][size - index - 1][1] + else: + node = self.children[node][index][1] + return node + + def on_get_path(self, node): + """ + Returns a path from a given node. + """ + pathlist = [] + while node is not None: + parent = self.tree[node][0] + for index, value in enumerate(self.children[parent]): + if value[1] == node: + break + if self.__reverse: + size = len(self.children[parent]) + pathlist.append(size - index - 1) + else: + pathlist.append(index) + node = parent + if pathlist is not None: + pathlist.reverse() + return tuple(pathlist) + else: + return None + + def on_iter_next(self, node): + """ + Get the next node with the same parent as the given node. + """ + parent = self.tree[node][0] + for index, child in enumerate(self.children[parent]): + if child[1] == node: + break + + if self.__reverse: + index -= 1 + else: + index += 1 + + if index >= 0 and index < len(self.children[parent]): + return self.children[parent][index][1] + else: + return None def on_iter_children(self, node): """ - See gtk.GenericTreeModel + Get the first child of the given node. """ - return self.mapper.first_child(node) - + if node in self.children: + if self.__reverse: + size = len(self.children[node]) + return self.children[node][size - 1][1] + else: + return self.children[node][0][1] + else: + return None + def on_iter_has_child(self, node): """ - See gtk.GenericTreeModel + Find if the given node has any children. """ - return self.mapper.has_child(node) + if node in self.children: + return True + else: + return False def on_iter_n_children(self, node): """ - See gtk.GenericTreeModel + Get the number of children of the given node. """ - return self.mapper.number_of_children(node) + if node in self.children: + return len(self.children[node]) + else: + return 0 def on_iter_nth_child(self, node, index): """ - See gtk.GenericTreeModel + Get the nth child of the given node. """ - return self.mapper.get_nth_child(node, index) + if node in self.children: + if len(self.children[node]) > index: + if self.__reverse: + size = len(self.children[node]) + return self.children[node][size - index - 1][1] + else: + return self.children[node][index][1] + else: + return None + else: + return None def on_iter_parent(self, node): """ - See gtk.GenericTreeModel + Get the parent of the given node. """ - return self.mapper.get_parent_of(node) + if node in self.tree: + return self.tree[node][0] + else: + return None