From d7b4ca2d9c0e1de9db152c88447e5f2614600f39 Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Tue, 17 Nov 2009 00:13:03 +0000 Subject: [PATCH] Add hierarchical place view. Reorganise existing place view and model. svn: r13601 --- src/gui/views/Makefile.am | 5 +- src/gui/views/placebaseview.py | 381 +++++++++++++++++++++ src/gui/views/treemodels/Makefile.am | 2 + src/gui/views/treemodels/__init__.py | 2 + src/gui/views/treemodels/peoplemodel.py | 71 ++-- src/gui/views/treemodels/placebasemodel.py | 208 +++++++++++ src/gui/views/treemodels/placemodel.py | 136 +------- src/gui/views/treemodels/placetreemodel.py | 122 +++++++ src/gui/views/treemodels/treebasemodel.py | 4 +- src/plugins/view/Makefile.am | 2 + src/plugins/view/placetreeview.gpr.py | 12 + src/plugins/view/placetreeview.py | 205 +++++++++++ src/plugins/view/placeview.py | 349 +------------------ 13 files changed, 999 insertions(+), 500 deletions(-) create mode 100644 src/gui/views/placebaseview.py create mode 100644 src/gui/views/treemodels/placebasemodel.py create mode 100644 src/gui/views/treemodels/placetreemodel.py create mode 100644 src/plugins/view/placetreeview.gpr.py create mode 100644 src/plugins/view/placetreeview.py diff --git a/src/gui/views/Makefile.am b/src/gui/views/Makefile.am index f06e69de2..f07075b8c 100644 --- a/src/gui/views/Makefile.am +++ b/src/gui/views/Makefile.am @@ -11,8 +11,9 @@ pkgdatadir = $(datadir)/@PACKAGE@/gui/views pkgdata_PYTHON = \ __init__.py \ listview.py \ - navigationview.py \ - pageview.py + navigationview.py \ + pageview.py \ + placebaseview.py pkgpyexecdir = @pkgpyexecdir@/gui/views pkgpythondir = @pkgpythondir@/gui/views diff --git a/src/gui/views/placebaseview.py b/src/gui/views/placebaseview.py new file mode 100644 index 000000000..4ac2477be --- /dev/null +++ b/src/gui/views/placebaseview.py @@ -0,0 +1,381 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2001-2006 Donald N. Allingham +# Copyright (C) 2008 Gary Burton +# +# 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$ + +""" +Base view for Place Views +""" + +#------------------------------------------------------------------------- +# +# Global modules +# +#------------------------------------------------------------------------- + + +#------------------------------------------------------------------------- +# +# GTK/Gnome modules +# +#------------------------------------------------------------------------- +import gtk + +#------------------------------------------------------------------------- +# +# gramps modules +# +#------------------------------------------------------------------------- +import gen.lib +from gui.views.listview import ListView +from gui.utils import add_menuitem +import Errors +import Bookmarks +import config +from QuestionDialog import ErrorDialog +from gui.pluginmanager import GuiPluginManager +from DdTargets import DdTargets +from Editors import EditPlace, DeletePlaceQuery +from Filters.SideBar import PlaceSidebarFilter + +#------------------------------------------------------------------------- +# +# internationalization +# +#------------------------------------------------------------------------- +from gettext import gettext as _ + + +#------------------------------------------------------------------------- +# +# PlaceBaseView +# +#------------------------------------------------------------------------- +class PlaceBaseView(ListView): + + COLUMN_NAMES = [ + _('Place Name'), + _('ID'), + _('Church Parish'), + _('ZIP/Postal Code'), + _('City'), + _('County'), + _('State'), + _('Country'), + _('Latitude'), + _('Longitude'), + _('Last Changed'), + _('Street'), + ] + + ADD_MSG = _("Add a new place") + EDIT_MSG = _("Edit the selected place") + DEL_MSG = _("Delete the selected place") + FILTER_TYPE = "Place" + + def __init__(self, dbstate, uistate, title, model): + + signal_map = { + 'place-add' : self.row_add, + 'place-update' : self.row_update, + 'place-delete' : self.row_delete, + 'place-rebuild' : self.object_build, + } + + self.func_list = { + 'J' : self.jump, + 'BackSpace' : self.key_delete, + } + + self.mapservice = config.get('interface.mapservice') + self.mapservicedata = {} + + ListView.__init__( + self, title, dbstate, uistate, PlaceBaseView.COLUMN_NAMES, + len(PlaceBaseView.COLUMN_NAMES), + model, signal_map, + dbstate.db.get_place_bookmarks(), + Bookmarks.PlaceBookmarks, + multiple=True, + filter_class=PlaceSidebarFilter) + + config.connect("interface.filter", + self.filter_toggle) + + def column_ord_setfunc(self, clist): + self.dbstate.db.set_place_column_order(clist) + + def get_bookmarks(self): + return self.dbstate.db.get_place_bookmarks() + + def define_actions(self): + ListView.define_actions(self) + self._add_action('ColumnEdit', gtk.STOCK_PROPERTIES, + _('_Column Editor'), callback=self._column_editor) + self._add_action('FastMerge', None, _('_Merge...'), + callback=self.fast_merge) + self._add_toolmenu_action('MapsList', _('Loading...'), + _("Attempt to see selected locations with a Map " + "Service (OpenstreetMap, Google Maps, ...)"), + self.gotomap, + _('Select a Map Service')) + self._add_action('GotoMap', gtk.STOCK_JUMP_TO, + _('_Look up with Map Service'), + callback=self.gotomap, + tip=_("Attempt to see this location with a Map " + "Service (OpenstreetMap, Google Maps, ...)")) + self._add_action('FilterEdit', None, _('Place Filter Editor'), + callback=self.filter_editor) + + def change_page(self): + """ + Called by viewmanager at end of realization when arriving on the page + At this point the Toolbar is created. We need to: + 1. get the menutoolbutton + 2. add all possible map services in the drop down menu + 3. add the actions that correspond to clicking in this drop down menu + 4. set icon and label of the menutoolbutton now that it is realized + 5. store label so it can be changed when selection changes + """ + ListView.change_page(self) + #menutoolbutton actions are stored in PageView class, + # obtain the widgets where we need to add to menu + actionservices = self.action_toolmenu['MapsList'] + widgets = actionservices.get_proxies() + mmenu = self.__create_maps_menu_actions() + + if not self.mapservicedata: + return + + self.mapslistlabel = [] + if not self.mapservice in self.mapservicedata: + #stored val no longer exists, use the first key instead + self.set_mapservice(self.mapservicedata.keys()[0]) + + #store all gtk labels to be able to update label on selection change + for widget in widgets : + if isinstance(widget, gtk.MenuToolButton): + widget.set_menu(mmenu) + if gtk.pygtk_version >= (2, 12, 0): + widget.set_arrow_tooltip_text(actionservices.arrowtooltip) + lbl = gtk.Label(self.mapservice_label()) + lbl.show() + self.mapslistlabel.append(lbl) + widget.set_label_widget(self.mapslistlabel[-1]) + widget.set_stock_id(gtk.STOCK_JUMP_TO) + if self.drag_info(): + self.list.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, + [('text/plain', 0, 0), self.drag_info().target()], + gtk.gdk.ACTION_COPY) + + def __create_maps_menu_actions(self): + """ + Function creating a menu and actions that are used as dropdown menu + from the menutoolbutton + """ + menu = gtk.Menu() + + #select the map services to show + self.mapservicedata = {} + servlist = GuiPluginManager.get_instance().get_reg_mapservices() + for i, pdata in zip(range(len(servlist)), servlist): + key = pdata.id.replace(' ', '-') + add_menuitem(menu, pdata.name, None, + make_callback(self.set_mapservice, key)) + self.mapservicedata[key] = pdata + + return menu + + def set_mapservice(self, mapkey): + """ + change the service that runs on click of the menutoolbutton + used as callback menu on menu clicks + """ + self.mapservice = mapkey + for label in self.mapslistlabel: + label.set_label(self.mapservice_label()) + label.show() + config.set('interface.mapservice', mapkey) + config.save() + + def mapservice_label(self): + """ + return the current label for the menutoolbutton + """ + return self.mapservicedata[self.mapservice].name + + def gotomap(self, obj): + """ + Run the map service + """ + #First test if any map service is available + if not len(self.mapservicedata): + msg = _("No map service is available.") + msg2 = _("Check your installation.") + ErrorDialog(msg, msg2) + return + + place_handles = self.selected_handles() + try: + place_handle = self.selected_handles()[0] + except IndexError: + msg = _("No place selected.") + msg2 = _("You need to select a place to be able to view it" + " on a map. Some Map Services might support multiple" + " selections.") + ErrorDialog(msg, msg2) + return + + #TODO: support for descriptions in some cases. For now, pass None + #TODO: Later this might be 'Birth of William' .... + places = [(x, None) for x in place_handles] + + #run the mapservice: + pmgr = GuiPluginManager.get_instance() + serv = self.mapservicedata[self.mapservice] + mod = pmgr.load_plugin(serv) + if mod: + servfunc = eval('mod.' + serv.mapservice) + servfunc()(self.dbstate.db, places) + else: + print 'Failed to load map plugin, see Plugin Status' + + def drag_info(self): + return DdTargets.PLACE_LINK + + def _column_editor(self, obj): + import ColumnOrder + + ColumnOrder.ColumnOrder( + _('Select Place Columns'), + self.uistate, + self.dbstate.db.get_place_column_order(), + PlaceBaseView.COLUMN_NAMES, + self.set_column_order) + + def column_order(self): + return self.dbstate.db.get_place_column_order() + + def get_stock(self): + return 'gramps-place' + + def ui_definition(self): + return ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + + def add(self, obj): + try: + EditPlace(self.dbstate, self.uistate, [], gen.lib.Place()) + except Errors.WindowActiveError: + pass + + def remove(self, obj): + self.remove_selected_objects() + + def remove_object_from_handle(self, handle): + person_list = [ + item[1] for item in + self.dbstate.db.find_backlink_handles(handle,['Person'])] + + family_list = [ + item[1] for item in + self.dbstate.db.find_backlink_handles(handle,['Family'])] + + event_list = [ + item[1] for item in + self.dbstate.db.find_backlink_handles(handle,['Event'])] + + object = self.dbstate.db.get_place_from_handle(handle) + query = DeletePlaceQuery(self.dbstate, self.uistate, object, + person_list, family_list, event_list) + + is_used = len(person_list) + len(family_list) + len(event_list) > 0 + return (query, is_used, object) + + def edit(self, obj): + for handle in self.selected_handles(): + place = self.dbstate.db.get_place_from_handle(handle) + try: + EditPlace(self.dbstate, self.uistate, [], place) + except Errors.WindowActiveError: + pass + + def fast_merge(self, obj): + mlist = self.selected_handles() + + if len(mlist) != 2: + msg = _("Cannot merge places.") + msg2 = _("Exactly two places must be selected to perform a merge. " + "A second place can be selected by holding down the " + "control key while clicking on the desired place.") + ErrorDialog(msg, msg2) + else: + import Merge + Merge.MergePlaces(self.dbstate, self.uistate, mlist[0], mlist[1]) + + def get_handle_from_gramps_id(self, gid): + obj = self.dbstate.db.get_place_from_gramps_id(gid) + if obj: + return obj.get_handle() + else: + return None + +def make_callback(func, val): + return lambda x: func(val) diff --git a/src/gui/views/treemodels/Makefile.am b/src/gui/views/treemodels/Makefile.am index 8499b308c..090d1e38e 100644 --- a/src/gui/views/treemodels/Makefile.am +++ b/src/gui/views/treemodels/Makefile.am @@ -13,7 +13,9 @@ pkgdata_PYTHON = \ mediamodel.py \ notemodel.py \ peoplemodel.py \ + placebasemodel.py \ placemodel.py \ + placetreemodel.py \ repomodel.py \ sourcemodel.py \ treebasemodel.py diff --git a/src/gui/views/treemodels/__init__.py b/src/gui/views/treemodels/__init__.py index 32c48e93b..f59fcd5f0 100644 --- a/src/gui/views/treemodels/__init__.py +++ b/src/gui/views/treemodels/__init__.py @@ -27,7 +27,9 @@ from peoplemodel import PeopleModel from familymodel import FamilyModel from eventmodel import EventModel from sourcemodel import SourceModel +from placebasemodel import PlaceBaseModel from placemodel import PlaceModel +from placetreemodel import PlaceTreeModel from mediamodel import MediaModel from repomodel import RepositoryModel from notemodel import NoteModel diff --git a/src/gui/views/treemodels/peoplemodel.py b/src/gui/views/treemodels/peoplemodel.py index 91bbc02af..78a5770a6 100644 --- a/src/gui/views/treemodels/peoplemodel.py +++ b/src/gui/views/treemodels/peoplemodel.py @@ -182,7 +182,7 @@ class PeopleModel(TreeBaseModel): name_data = data[COLUMN_NAME] group_name = ngn(self.db, name_data) - sort_key = self.sort_func(data, handle) + sort_key = self.sort_func(data) #if group_name not in self.group_list: #self.group_list.append(group_name) @@ -192,12 +192,12 @@ class PeopleModel(TreeBaseModel): # 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): + def sort_name(self, data): n = Name() n.unserialize(data[COLUMN_NAME]) return name_displayer.sort_string(n) - def column_spouse(self, data, node): + def column_spouse(self, data): spouses_names = u"" handle = data[0] for family_handle in data[COLUMN_FAMILY]: @@ -214,40 +214,43 @@ class PeopleModel(TreeBaseModel): spouses_names += name_displayer.display(spouse) return spouses_names - def column_name(self, data, node): - if node in self.lru_name: - name = self.lru_name[node] + def column_name(self, data): + handle = data[0] + if handle in self.lru_name: + name = self.lru_name[handle] else: name = name_displayer.raw_sorted_name(data[COLUMN_NAME]) if not self._in_build: - self.lru_name[node] = name + self.lru_name[handle] = name return name - def column_id(self, data, node): + def column_id(self, data): return data[COLUMN_ID] - def column_change(self, data, node): + def column_change(self, data): return unicode( time.strftime('%x %X', time.localtime(data[COLUMN_CHANGE])), GrampsLocale.codeset) - def column_gender(self, data, node): + def column_gender(self, data): return PeopleModel._GENDER[data[COLUMN_GENDER]] - def column_birth_day(self, data, node): - if node in self.lru_bdate: - value = self.lru_bdate[node] + def column_birth_day(self, data): + handle = data[0] + if handle in self.lru_bdate: + value = self.lru_bdate[handle] else: - value = self._get_birth_data(data, node, False) + value = self._get_birth_data(data, False) if not self._in_build: - self.lru_bdate[node] = value + self.lru_bdate[handle] = value return value - def sort_birth_day(self, data, node): - return self._get_birth_data(data, node, True) + def sort_birth_day(self, data): + handle = data[0] + return self._get_birth_data(data, True) - def _get_birth_data(self, data, node, sort_mode): + def _get_birth_data(self, data, sort_mode): index = data[COLUMN_BIRTH] if index != -1: try: @@ -288,19 +291,21 @@ class PeopleModel(TreeBaseModel): return u"" - def column_death_day(self, data, node): - if node in self.lru_ddate: - value = self.lru_ddate[node] + def column_death_day(self, data): + handle = data[0] + if handle in self.lru_ddate: + value = self.lru_ddate[handle] else: - value = self._get_death_data(data, node, False) + value = self._get_death_data(data, False) if not self._in_build: - self.lru_ddate[node] = value + self.lru_ddate[handle] = value return value - def sort_death_day(self, data, node): - return self._get_death_data(data, node, True) + def sort_death_day(self, data): + handle = data[0] + return self._get_death_data(data, True) - def _get_death_data(self, data, node, sort_mode): + def _get_death_data(self, data, sort_mode): index = data[COLUMN_DEATH] if index != -1: try: @@ -342,7 +347,7 @@ class PeopleModel(TreeBaseModel): return retval return u"" - def column_birth_place(self, data, node): + def column_birth_place(self, data): index = data[COLUMN_BIRTH] if index != -1: try: @@ -377,7 +382,7 @@ class PeopleModel(TreeBaseModel): return u"" - def column_death_place(self, data, node): + def column_death_place(self, data): index = data[COLUMN_DEATH] if index != -1: try: @@ -412,12 +417,12 @@ class PeopleModel(TreeBaseModel): return "" + cgi.escape(place_title) + "" return u"" - def column_marker_text(self, data, node): + def column_marker_text(self, data): if COLUMN_MARKER < len(data): return str(data[COLUMN_MARKER]) return "" - def column_marker_color(self, data, node): + def column_marker_color(self, data): try: if data[COLUMN_MARKER]: if data[COLUMN_MARKER][0] == MarkerType.COMPLETE: @@ -430,7 +435,7 @@ class PeopleModel(TreeBaseModel): pass return None - def column_tooltip(self, data, node): + def column_tooltip(self, data): if const.USE_TIPS: return ToolTips.TipFromFunction( self.db, @@ -439,8 +444,8 @@ class PeopleModel(TreeBaseModel): else: return u'' - def column_int_id(self, data, node): - return node + def column_int_id(self, data): + return data[0] def column_header(self, node): return node diff --git a/src/gui/views/treemodels/placebasemodel.py b/src/gui/views/treemodels/placebasemodel.py new file mode 100644 index 000000000..0fdae697d --- /dev/null +++ b/src/gui/views/treemodels/placebasemodel.py @@ -0,0 +1,208 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2000-2006 Donald N. Allingham +# 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, +# 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 time +import logging +log = logging.getLogger(".") + +#------------------------------------------------------------------------- +# +# GNOME/GTK modules +# +#------------------------------------------------------------------------- +import gtk + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +import const +import ToolTips +import GrampsLocale + +#------------------------------------------------------------------------- +# +# internationalization +# +#------------------------------------------------------------------------- +from gettext import gettext as _ + +#------------------------------------------------------------------------- +# +# PlaceBaseModel +# +#------------------------------------------------------------------------- +class PlaceBaseModel(): + + HANDLE_COL = 12 + + def __init__(self, db): + self.gen_cursor = db.get_place_cursor + self.map = db.get_raw_place_data + self.fmap = [ + self.column_name, + self.column_id, + self.column_parish, + self.column_postal_code, + self.column_city, + self.column_county, + self.column_state, + self.column_country, + self.column_latitude, + self.column_longitude, + self.column_change, + self.column_street, + self.column_handle, + self.column_tooltip + ] + self.smap = [ + self.column_name, + self.column_id, + self.column_parish, + self.column_postal_code, + self.column_city, + self.column_county, + self.column_state, + self.column_country, + self.sort_latitude, + self.sort_longitude, + self.sort_change, + self.column_street, + self.column_handle, + ] + + def on_get_n_columns(self): + return len(self.fmap)+1 + + def column_handle(self, data): + return unicode(data[0]) + + def column_name(self, data): + return unicode(data[2]) + + def __format_degrees(self, angle, sign_str): + """ + Format a decimal as degrees, minutes and seconds. + If the value is not a decimal leave it unformatted. + """ + try: + angle = float(angle) + except ValueError: + return angle + + if angle >= 0: + sign = sign_str[0] + else: + sign = sign_str[1] + seconds = abs(int(angle * 60 * 60)) + minutes = seconds / 60 + seconds %= 60 + degrees = minutes / 60 + minutes %= 60 + + string = unicode(degrees) + u'\u00b0 ' + \ + unicode(minutes) + u'\u2032 ' + \ + unicode(seconds) + u'\u2033 ' + unicode(sign) + + return string + + def column_longitude(self, data): + return self.__format_degrees(data[3], _('EW')) + + def column_latitude(self, data): + return self.__format_degrees(data[4], _('NS')) + + def sort_longitude(self, data): + return unicode(data[3]) + + def sort_latitude(self, data): + return unicode(data[4]) + + def column_id(self, data): + return unicode(data[1]) + + def column_parish(self, data): + try: + return data[5][1] + except: + return u'' + + def column_street(self, data): + try: + return data[5][0][0] + except: + return u'' + + def column_city(self, data): + try: + return data[5][0][1] + except: + return u'' + + def column_county(self, data): + try: + return data[5][0][2] + except: + return u'' + + def column_state(self, data): + try: + return data[5][0][3] + except: + return u'' + + def column_country(self, data): + try: + return data[5][0][4] + except: + return u'' + + def column_postal_code(self, data): + try: + return data[5][0][5] + except: + return u'' + + def sort_change(self, data): + return "%012x" % data[11] + + def column_change(self, data, node): + return unicode(time.strftime('%x %X',time.localtime(data[11])), + GrampsLocale.codeset) + + def column_tooltip(self, data): + if const.USE_TIPS: + try: + t = ToolTips.TipFromFunction( + self.db, lambda: + self.db.get_place_from_handle(data[0])) + except: + log.error("Failed to create tooltip.", exc_info=True) + return t + else: + return u'' diff --git a/src/gui/views/treemodels/placemodel.py b/src/gui/views/treemodels/placemodel.py index 10ca5268b..1b6053494 100644 --- a/src/gui/views/treemodels/placemodel.py +++ b/src/gui/views/treemodels/placemodel.py @@ -1,7 +1,7 @@ # # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2000-2006 Donald N. Allingham +# 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 @@ -19,14 +19,16 @@ # # $Id:_PlaceModel.py 9912 2008-01-22 09:17:46Z acraphae $ +""" +Place Model. +""" #------------------------------------------------------------------------- # # python modules # #------------------------------------------------------------------------- -import time import logging -log = logging.getLogger(".") +_LOG = logging.getLogger(".gui.views.treemodels.placemodel") #------------------------------------------------------------------------- # @@ -40,9 +42,7 @@ import gtk # GRAMPS modules # #------------------------------------------------------------------------- -import const -import ToolTips -import GrampsLocale +from gui.views.treemodels.placebasemodel import PlaceBaseModel from gui.views.treemodels.flatbasemodel import FlatBaseModel #------------------------------------------------------------------------- @@ -50,123 +50,13 @@ from gui.views.treemodels.flatbasemodel import FlatBaseModel # PlaceModel # #------------------------------------------------------------------------- -class PlaceModel(FlatBaseModel): - - HANDLE_COL = 12 - - def __init__(self,db,scol=0, order=gtk.SORT_ASCENDING,search=None, +class PlaceModel(PlaceBaseModel, FlatBaseModel): + """ + Flat place model. (Original code in PlaceBaseModel). + """ + def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None, skip=set(), sort_map=None): - self.gen_cursor = db.get_place_cursor - self.map = db.get_raw_place_data - self.fmap = [ - self.column_name, - self.column_id, - self.column_parish, - self.column_postal_code, - self.column_city, - self.column_county, - self.column_state, - self.column_country, - self.column_latitude, - self.column_longitude, - self.column_change, - self.column_street, - self.column_handle, - self.column_tooltip - ] - self.smap = [ - self.column_name, - self.column_id, - self.column_parish, - self.column_postal_code, - self.column_city, - self.column_county, - self.column_state, - self.column_country, - self.column_latitude, - self.column_longitude, - self.column_change, - self.column_street, - self.column_handle, - ] + + PlaceBaseModel.__init__(self, db) FlatBaseModel.__init__(self, db, scol, order, tooltip_column=13, search=search, skip=skip, sort_map=sort_map) - - def on_get_n_columns(self): - return len(self.fmap)+1 - - def column_handle(self,data): - return unicode(data[0]) - - def column_name(self,data): - return unicode(data[2]) - - def column_longitude(self,data): - return unicode(data[3]) - - def column_latitude(self,data): - return unicode(data[4]) - - def column_id(self,data): - return unicode(data[1]) - - def column_parish(self,data): - try: - return data[5][1] - except: - return u'' - - def column_street(self,data): - try: - return data[5][0][0] - except: - return u'' - - def column_city(self,data): - try: - return data[5][0][1] - except: - return u'' - - def column_county(self,data): - try: - return data[5][0][2] - except: - return u'' - - def column_state(self,data): - try: - return data[5][0][3] - except: - return u'' - - def column_country(self,data): - try: - return data[5][0][4] - except: - return u'' - - def column_postal_code(self,data): - try: - return data[5][0][5] - except: - return u'' - - def sort_change(self,data): - return "%012x" % data[11] - - def column_change(self,data): - return unicode(time.strftime('%x %X',time.localtime(data[11])), - GrampsLocale.codeset) - - def column_tooltip(self,data): - if const.USE_TIPS: - try: - t = ToolTips.TipFromFunction( - self.db, lambda: - self.db.get_place_from_handle(data[0])) - except: - log.error("Failed to create tooltip.", exc_info=True) - return t - else: - return u'' diff --git a/src/gui/views/treemodels/placetreemodel.py b/src/gui/views/treemodels/placetreemodel.py new file mode 100644 index 000000000..9d2f7b2e9 --- /dev/null +++ b/src/gui/views/treemodels/placetreemodel.py @@ -0,0 +1,122 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# 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, +# 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$ + +""" +Place tree model. +""" +#------------------------------------------------------------------------- +# +# python modules +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".gui.views.treemodels.placetreemodel") + +#------------------------------------------------------------------------- +# +# GNOME/GTK modules +# +#------------------------------------------------------------------------- +import gtk + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gui.views.treemodels.placebasemodel import PlaceBaseModel +from gui.views.treemodels.treebasemodel import TreeBaseModel + +#------------------------------------------------------------------------- +# +# Internationalization +# +#------------------------------------------------------------------------- +from gettext import gettext as _ + +#------------------------------------------------------------------------- +# +# PlaceTreeModel +# +#------------------------------------------------------------------------- +class PlaceTreeModel(PlaceBaseModel, TreeBaseModel): + """ + Hierarchical place model. + """ + def __init__(self, db, scol=0, order=gtk.SORT_ASCENDING, search=None, + skip=set(), sort_map=None): + + self.hmap = [ + self.column_header, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + + PlaceBaseModel.__init__(self, db) + TreeBaseModel.__init__(self, db, scol=scol, order=order, + tooltip_column=13, + search=search, skip=skip, sort_map=sort_map, + nrgroups = 3, + group_can_have_handle = True) + + def add_row(self, handle, data): + """ + Add nodes to the node map for a single place. + + handle The handle of the gramps object. + data The object data. + """ + level1 = data[5][0][4] + if not level1: + level1 = _('Unknown level1') + level2 = data[5][0][3] + if not level2: + level2 = _('Unknown level2') + level3 = data[5][0][2] + if not level3: + level3 = _('Unknown level3') + + node1 = (level1, ) + node2 = (level2, level1) + node3 = (level3, level2) + sort_key = self.sort_func(data) + + self.add_node(None, node1, level1, None, add_parent=False) + self.add_node(node1, node2, level2, None, add_parent=False) + self.add_node(node2, node3, level3, None, add_parent=False) + self.add_node(node3, handle, sort_key, handle, add_parent=False) + + def column_header(self, node): + """ + Return a column heading. This is called for nodes with no associated + Gramps handle. + """ + return node[0] diff --git a/src/gui/views/treemodels/treebasemodel.py b/src/gui/views/treemodels/treebasemodel.py index 23d626899..f3d86dfb7 100644 --- a/src/gui/views/treemodels/treebasemodel.py +++ b/src/gui/views/treemodels/treebasemodel.py @@ -280,7 +280,7 @@ class TreeBaseModel(gtk.GenericTreeModel): self._in_build = False self.current_filter = data_filter - + _LOG.debug(self.__class__.__name__ + ' rebuild_data ' + str(time.clock() - cput) + ' sec') @@ -560,7 +560,7 @@ class TreeBaseModel(gtk.GenericTreeModel): data = self.map(handle) if not self._in_build: self.lru_data[handle] = data - return (self.fmap[col](data, handle)) + return (self.fmap[col](data)) except: return None diff --git a/src/plugins/view/Makefile.am b/src/plugins/view/Makefile.am index f7122708a..d8a8a3e5d 100644 --- a/src/plugins/view/Makefile.am +++ b/src/plugins/view/Makefile.am @@ -16,6 +16,8 @@ pkgdata_PYTHON = \ noteview.py \ pedigreeview.py \ personview.py \ + placetreeview.gpr.py \ + placetreeview.py \ placeview.py \ relview.py \ repoview.py \ diff --git a/src/plugins/view/placetreeview.gpr.py b/src/plugins/view/placetreeview.gpr.py new file mode 100644 index 000000000..73f456d74 --- /dev/null +++ b/src/plugins/view/placetreeview.gpr.py @@ -0,0 +1,12 @@ +register(VIEW, + id = 'placetreeview', + name = _("Place Tree View"), + description = _("A view displaying places in a tree format."), + version = '1.0', + status = UNSTABLE, + fname = 'placetreeview.py', + authors = [u"Donald N. Allingham", u"Gary Burton", u"Nick Hall"], + authors_email = [""], + category = VIEW_PLACE, + viewclass = 'PlaceTreeView', + ) diff --git a/src/plugins/view/placetreeview.py b/src/plugins/view/placetreeview.py new file mode 100644 index 000000000..2aca19179 --- /dev/null +++ b/src/plugins/view/placetreeview.py @@ -0,0 +1,205 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# 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, +# 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$ + +""" +Place Tree View +""" + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from gui.views.placebaseview import PlaceBaseView +from gui.views.treemodels import PlaceTreeModel +import gen.lib +import Errors +from Editors import EditPlace + +#------------------------------------------------------------------------- +# +# Internationalization +# +#------------------------------------------------------------------------- +from gettext import gettext as _ + +#------------------------------------------------------------------------- +# +# PlaceTreeView +# +#------------------------------------------------------------------------- +class PlaceTreeView(PlaceBaseView): + """ + A hierarchical view of the top three levels of places. + """ + + def __init__(self, dbstate, uistate): + PlaceBaseView.__init__(self, dbstate, uistate, + _('Tree'), PlaceTreeModel) + + def get_viewtype_stock(self): + """ + Override the default icon. Set for hierarchical view. + """ + return 'gramps-tree-group' + + def define_actions(self): + """ + Define actions for the popup menu specific to the tree view. + """ + PlaceBaseView.define_actions(self) + + self._add_action('OpenBranch', None, _("Expand Rows"), + callback=self.open_branch) + self._add_action('CloseBranch', None, _("Collapse Rows"), + callback=self.close_branch) + self._add_action('OpenAllNodes', None, _("Expand all Nodes"), + callback=self.open_all_nodes) + self._add_action('CloseAllNodes', None, _("Collapse all Nodes"), + callback=self.close_all_nodes) + + def ui_definition(self): + """ + A user interface definition including tree specific actions. + """ + return ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + + def add(self, obj): + """ + Add a new place. Attempt to get the top three levels of hierarchy from + the currently selected row. + """ + place = gen.lib.Place() + + model, pathlist = self.selection.get_selected_rows() + level1 = level2 = level3 = u"" + if len(pathlist) == 1: + path = pathlist[0] + if len(path) == 1: + level1 = model.on_get_iter(path)[0] + elif len(path) == 2: + level2 = model.on_get_iter(path)[0] + level1 = model.on_get_iter(path)[1] + elif len(path) == 3: + node = model.on_get_iter(path) + level3 = node[0] + level2 = node[1] + level1 = model.on_iter_parent(node)[1] + else: + node = model.on_iter_parent(model.on_get_iter(path)) + level3 = node[0] + level2 = node[1] + level1 = model.on_iter_parent(node)[1] + + try: + place.get_main_location().set_country(level1) + place.get_main_location().set_state(level2) + place.get_main_location().set_county(level3) + EditPlace(self.dbstate, self.uistate, [], place) + except Errors.WindowActiveError: + pass + + def open_branch(self, obj): + """ + Expand the selected branches and all children. + """ + self.uistate.status_text(_("Updating display...")) + self.uistate.set_busy_cursor(True) + + selected = self.selection.get_selected_rows() + for path in selected[1]: + self.list.expand_row(path, True) + + self.uistate.set_busy_cursor(False) + self.uistate.modify_statusbar(self.dbstate) + + def close_branch(self, obj): + """ + Collapse the selected branches. + """ + selected = self.selection.get_selected_rows() + for path in selected[1]: + self.list.collapse_row(path) + + def open_all_nodes(self, obj): + """ + Expand the entire tree. + """ + 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): + """ + Collapse the entire tree. + """ + self.list.collapse_all() diff --git a/src/plugins/view/placeview.py b/src/plugins/view/placeview.py index 3ecaca569..c9ab2c77e 100644 --- a/src/plugins/view/placeview.py +++ b/src/plugins/view/placeview.py @@ -1,7 +1,6 @@ # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2001-2006 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 @@ -26,357 +25,27 @@ Place View #------------------------------------------------------------------------- # -# Global modules +# Gramps modules # #------------------------------------------------------------------------- - - -#------------------------------------------------------------------------- -# -# GTK/Gnome modules -# -#------------------------------------------------------------------------- -import gtk - -#------------------------------------------------------------------------- -# -# gramps modules -# -#------------------------------------------------------------------------- -import gen.lib -from gui.views.listview import ListView +from gui.views.placebaseview import PlaceBaseView from gui.views.treemodels import PlaceModel -from gui.utils import add_menuitem -import Errors -import Bookmarks -import config -from QuestionDialog import ErrorDialog -from gui.pluginmanager import GuiPluginManager -from DdTargets import DdTargets -from Editors import EditPlace, DeletePlaceQuery -from Filters.SideBar import PlaceSidebarFilter #------------------------------------------------------------------------- # -# internationalization +# Internationalization # #------------------------------------------------------------------------- from gettext import gettext as _ - #------------------------------------------------------------------------- # # PlaceView # #------------------------------------------------------------------------- -class PlaceView(ListView): - - COLUMN_NAMES = [ - _('Place Name'), - _('ID'), - _('Church Parish'), - _('ZIP/Postal Code'), - _('City'), - _('County'), - _('State'), - _('Country'), - _('Latitude'), - _('Longitude'), - _('Last Changed'), - _('Street'), - ] - - ADD_MSG = _("Add a new place") - EDIT_MSG = _("Edit the selected place") - DEL_MSG = _("Delete the selected place") - FILTER_TYPE = "Place" - +class PlaceView(PlaceBaseView): + """ + Flat place view. (Original code in PlaceBaseView). + """ def __init__(self, dbstate, uistate): - - signal_map = { - 'place-add' : self.row_add, - 'place-update' : self.row_update, - 'place-delete' : self.row_delete, - 'place-rebuild' : self.object_build, - } - - self.func_list = { - 'J' : self.jump, - 'BackSpace' : self.key_delete, - } - - self.mapservice = config.get('interface.mapservice') - self.mapservicedata = {} - - ListView.__init__( - self, _('Places'), dbstate, uistate, PlaceView.COLUMN_NAMES, - len(PlaceView.COLUMN_NAMES), - PlaceModel, signal_map, - dbstate.db.get_place_bookmarks(), - Bookmarks.PlaceBookmarks, - multiple=True, - filter_class=PlaceSidebarFilter) - - config.connect("interface.filter", - self.filter_toggle) - - def column_ord_setfunc(self, clist): - self.dbstate.db.set_place_column_order(clist) - - def get_bookmarks(self): - return self.dbstate.db.get_place_bookmarks() - - def define_actions(self): - ListView.define_actions(self) - self._add_action('ColumnEdit', gtk.STOCK_PROPERTIES, - _('_Column Editor'), callback=self._column_editor) - self._add_action('FastMerge', None, _('_Merge...'), - callback=self.fast_merge) - self._add_toolmenu_action('MapsList', _('Loading...'), - _("Attempt to see selected locations with a Map " - "Service (OpenstreetMap, Google Maps, ...)"), - self.gotomap, - _('Select a Map Service')) - self._add_action('GotoMap', gtk.STOCK_JUMP_TO, - _('_Look up with Map Service'), - callback=self.gotomap, - tip=_("Attempt to see this location with a Map " - "Service (OpenstreetMap, Google Maps, ...)")) - self._add_action('FilterEdit', None, _('Place Filter Editor'), - callback=self.filter_editor) - - def change_page(self): - """ - Called by viewmanager at end of realization when arriving on the page - At this point the Toolbar is created. We need to: - 1. get the menutoolbutton - 2. add all possible map services in the drop down menu - 3. add the actions that correspond to clicking in this drop down menu - 4. set icon and label of the menutoolbutton now that it is realized - 5. store label so it can be changed when selection changes - """ - ListView.change_page(self) - #menutoolbutton actions are stored in PageView class, - # obtain the widgets where we need to add to menu - actionservices = self.action_toolmenu['MapsList'] - widgets = actionservices.get_proxies() - mmenu = self.__create_maps_menu_actions() - - if not self.mapservicedata: - return - - self.mapslistlabel = [] - if not self.mapservice in self.mapservicedata: - #stored val no longer exists, use the first key instead - self.set_mapservice(self.mapservicedata.keys()[0]) - - #store all gtk labels to be able to update label on selection change - for widget in widgets : - if isinstance(widget, gtk.MenuToolButton): - widget.set_menu(mmenu) - if gtk.pygtk_version >= (2, 12, 0): - widget.set_arrow_tooltip_text(actionservices.arrowtooltip) - lbl = gtk.Label(self.mapservice_label()) - lbl.show() - self.mapslistlabel.append(lbl) - widget.set_label_widget(self.mapslistlabel[-1]) - widget.set_stock_id(gtk.STOCK_JUMP_TO) - if self.drag_info(): - self.list.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, - [('text/plain', 0, 0), self.drag_info().target()], - gtk.gdk.ACTION_COPY) - - def __create_maps_menu_actions(self): - """ - Function creating a menu and actions that are used as dropdown menu - from the menutoolbutton - """ - menu = gtk.Menu() - - #select the map services to show - self.mapservicedata = {} - servlist = GuiPluginManager.get_instance().get_reg_mapservices() - for i, pdata in zip(range(len(servlist)), servlist): - key = pdata.id.replace(' ', '-') - add_menuitem(menu, pdata.name, None, - make_callback(self.set_mapservice, key)) - self.mapservicedata[key] = pdata - - return menu - - def set_mapservice(self, mapkey): - """ - change the service that runs on click of the menutoolbutton - used as callback menu on menu clicks - """ - self.mapservice = mapkey - for label in self.mapslistlabel: - label.set_label(self.mapservice_label()) - label.show() - config.set('interface.mapservice', mapkey) - config.save() - - def mapservice_label(self): - """ - return the current label for the menutoolbutton - """ - return self.mapservicedata[self.mapservice].name - - def gotomap(self, obj): - """ - Run the map service - """ - #First test if any map service is available - if not len(self.mapservicedata): - msg = _("No map service is available.") - msg2 = _("Check your installation.") - ErrorDialog(msg, msg2) - return - - place_handles = self.selected_handles() - try: - place_handle = self.selected_handles()[0] - except IndexError: - msg = _("No place selected.") - msg2 = _("You need to select a place to be able to view it" - " on a map. Some Map Services might support multiple" - " selections.") - ErrorDialog(msg, msg2) - return - - #TODO: support for descriptions in some cases. For now, pass None - #TODO: Later this might be 'Birth of William' .... - places = [(x, None) for x in place_handles] - - #run the mapservice: - pmgr = GuiPluginManager.get_instance() - serv = self.mapservicedata[self.mapservice] - mod = pmgr.load_plugin(serv) - if mod: - servfunc = eval('mod.' + serv.mapservice) - servfunc()(self.dbstate.db, places) - else: - print 'Failed to load map plugin, see Plugin Status' - - def drag_info(self): - return DdTargets.PLACE_LINK - - def _column_editor(self, obj): - import ColumnOrder - - ColumnOrder.ColumnOrder( - _('Select Place Columns'), - self.uistate, - self.dbstate.db.get_place_column_order(), - PlaceView.COLUMN_NAMES, - self.set_column_order) - - def column_order(self): - return self.dbstate.db.get_place_column_order() - - def get_stock(self): - return 'gramps-place' - - def ui_definition(self): - return ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ''' - - def add(self, obj): - try: - EditPlace(self.dbstate, self.uistate, [], gen.lib.Place()) - except Errors.WindowActiveError: - pass - - def remove(self, obj): - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - person_list = [ - item[1] for item in - self.dbstate.db.find_backlink_handles(handle,['Person'])] - - family_list = [ - item[1] for item in - self.dbstate.db.find_backlink_handles(handle,['Family'])] - - event_list = [ - item[1] for item in - self.dbstate.db.find_backlink_handles(handle,['Event'])] - - object = self.dbstate.db.get_place_from_handle(handle) - query = DeletePlaceQuery(self.dbstate, self.uistate, object, - person_list, family_list, event_list) - - is_used = len(person_list) + len(family_list) + len(event_list) > 0 - return (query, is_used, object) - - def edit(self, obj): - for handle in self.selected_handles(): - place = self.dbstate.db.get_place_from_handle(handle) - try: - EditPlace(self.dbstate, self.uistate, [], place) - except Errors.WindowActiveError: - pass - - def fast_merge(self, obj): - mlist = self.selected_handles() - - if len(mlist) != 2: - msg = _("Cannot merge places.") - msg2 = _("Exactly two places must be selected to perform a merge. " - "A second place can be selected by holding down the " - "control key while clicking on the desired place.") - ErrorDialog(msg, msg2) - else: - import Merge - Merge.MergePlaces(self.dbstate, self.uistate, mlist[0], mlist[1]) - - def get_handle_from_gramps_id(self, gid): - obj = self.dbstate.db.get_place_from_gramps_id(gid) - if obj: - return obj.get_handle() - else: - return None - -def make_callback(func, val): - return lambda x: func(val) + PlaceBaseView.__init__(self, dbstate, uistate, _('Places'), PlaceModel)