diff --git a/gramps2/ChangeLog b/gramps2/ChangeLog index ebb7722e6..1c4de818a 100644 --- a/gramps2/ChangeLog +++ b/gramps2/ChangeLog @@ -1,3 +1,11 @@ +2005-08-11 Martin Hawlisch + * src/PedView.py: More work on it. + * src/MapView.py: New View showing locations on a map. Early unfinished version. + * src/gramps_main.py: Enable MapView + * src/land_shallow_topo_2048.jpg: + * src/land_shallow_topo_350.jpg: Two new map images downloaded from NASA + so the are assumed to be public domain. + 2005-08-10 Don Allingham * src/DbState.py: separate database class from display class * src/EditPerson.py: separate database class from display class diff --git a/gramps2/src/MapView.py b/gramps2/src/MapView.py new file mode 100644 index 000000000..d48d60381 --- /dev/null +++ b/gramps2/src/MapView.py @@ -0,0 +1,356 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2001-2005 Donald N. Allingham +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +from gettext import gettext as _ + +#------------------------------------------------------------------------- +# +# GTK/Gnome modules +# +#------------------------------------------------------------------------- +import gobject +import gtk +import gtk.gdk +import gc + +#------------------------------------------------------------------------- +# +# Gramps Modules +# +#------------------------------------------------------------------------- +import RelLib +import PageView + + +data = (("_Center", 0,0), + ("_North",0,80), + ("_South",0,-80), + ("_West",-170,0), + ("_East",170,0), + ("Chicago",-87.75,41.83), + ("Berlin",13.42,52.53), + ("Honolulu",-157.83,21.32), + ("Madrid",-3.72,40.42), + ("Moscow",37.70,55.75), + ("Vienna",16.37,48.22), + ("Sidney",151.17,-33.92), + ("Rio de Janeiro",-43.28,-22.88), + ("Tokyo",139.75,35.67), + ("Cape Town",18.47,-33.93), + ("Anchorage",-150.00,61.17)) + + +# Draws a map image and tries to allocate space in the correct aspect ratio +class GuideMap(gtk.DrawingArea): + def __init__(self, map_pixbuf): + gtk.DrawingArea.__init__(self) + self.map_pixbuf = map_pixbuf + self.connect("expose-event", self.expose_cb) + self.connect("size-allocate", self.size_allocate_cb) + #self.connect("size-request", self.size_request_cb) + self.gc = None + self.current_area = None + self.old_size = (-1,-1) + + # Set hightlight region + def hightlight_area( self, area): + print "GuideMap.hightlight_area" + print area + self.current_area = area + self.queue_draw() + + # Redraw the image + def expose_cb(self,widget,event): + a = widget.get_allocation() + print "GuideMap.expose_cb (%dx%d)" % (a.width,a.height) + if not self.gc: + self.gc = self.window.new_gc() + self.gc.set_foreground( self.get_colormap().alloc_color("red")) + self.gc.set_background( self.get_colormap().alloc_color("blue")) + if self.backbuf and self.gc: + self.window.draw_pixbuf( self.gc, self.backbuf, 0,0, 0,0, -1,-1) + if self.current_area: + r = self.map_to_screen(self.current_area[0],self.current_area[1],self.current_area[2],self.current_area[3]) + self.window.draw_rectangle( self.gc, False, r[0],r[1], r[2],r[3]) + + # Scale backbuffer + def size_allocate_cb(self,widget,allocation): + a = allocation + print "GuideMap.size_allocate_cb (%dx%d)" % (a.width,a.height) + # Always request a height, that is half of the width + w = max( 128,allocation.width) + self.set_size_request(-1,w/2) + + # only create new backbuffer if size is different + new_size = (allocation.width,allocation.height) + if new_size is not self.old_size: + self.old_size = new_size + self.backbuf = self.map_pixbuf.scale_simple(self.old_size[0],self.old_size[1],gtk.gdk.INTERP_BILINEAR) + gc.collect() + + def map_to_screen( self, x,y,w,h): + px = int((float(x) + 180.0) / 360.0 * self.backbuf.get_width()) + py = int((90-float(y)) / 180.0 * self.backbuf.get_height()) + pw = int(float(w) / 360.0 * self.backbuf.get_width()) + ph = int(float(h) / 180.0 * self.backbuf.get_height()) + + return (px,py,pw,ph) + +# Zoomable map image +class ZoomMap( gtk.DrawingArea): + def __init__(self, map_pixbuf, place_marker_pixbuf, hightlight_marker_pixbuf): + gtk.DrawingArea.__init__(self) + self.map_pixbuf = map_pixbuf + self.place_marker_pixbuf = place_marker_pixbuf + self.hightlight_marker_pixbuf = hightlight_marker_pixbuf + self.connect("expose-event", self.expose_cb) + self.connect("size-allocate", self.size_allocate_cb) + self.gc = None + self.old_size = (-1,-1) + self.zoom_pos = (0,0) + self.current_area = (0,0,0,0) + self.magnifer = 0.5 + self.guide = None + + # Set the guide map that should follow the zoom area + def set_guide( self, guide): + self.guide = guide + + # Redraw the image + def expose_cb(self,widget,event): + a = widget.get_allocation() + print "GuideMap.expose_cb (%dx%d)" % (a.width,a.height) + if not self.gc: + self.gc = self.window.new_gc() + self.gc.set_foreground( self.get_colormap().alloc_color("red")) + self.gc.set_background( self.get_colormap().alloc_color("blue")) + if not self.backbuf: + self.size_allocate_cb( self,self.get_allocation()) + if self.backbuf and self.gc: + self.window.draw_pixbuf( self.gc, self.backbuf, 0,0, 0,0, -1,-1) + px = int((float(self.zoom_pos[1]) + 180.0) / 360.0 * self.backbuf.get_width()) + py = int((90-float(self.zoom_pos[0])) / 180.0 * self.backbuf.get_height()) + self.window.draw_pixbuf( self.gc, self.hightlight_marker_pixbuf, 0,0, px-self.hightlight_marker_pixbuf.get_width()/2,py-self.hightlight_marker_pixbuf.get_height()/2, -1,-1) + self.window.draw_rectangle( self.gc, False, px-3,py-3, 6,6) + + # Scale backbuffer + def size_allocate_cb(self,widget,allocation): + a = allocation + print "GuideMap.size_allocate_cb (%dx%d)" % (a.width,a.height) + + # only create new backbuffer if size is different + new_size = (allocation.width,allocation.height) + if new_size is not self.old_size or not self.backbuf: + self.old_size = new_size + + # Desired midpoint in map + pw = int(self.old_size[0]*self.magnifer) + ph = int(self.old_size[1]*self.magnifer) + + px = int((float(self.zoom_pos[1]) + 180.0) / 360.0 * self.map_pixbuf.get_width()) + py = int((90-float(self.zoom_pos[0])) / 180.0 * self.map_pixbuf.get_height()) + + px = max( pw/2, px) + py = max( ph/2, py) + + px = min( px, self.map_pixbuf.get_width()-pw/2) + py = min( py, self.map_pixbuf.get_height()-ph/2) + + zoomebuf = self.map_pixbuf.subpixbuf( int(px-pw/2), int(py-ph/2), pw,ph) + print ( px-pw/2, py-ph/2, pw,ph) + + self.backbuf = zoomebuf.scale_simple(self.old_size[0],self.old_size[1],gtk.gdk.INTERP_BILINEAR) + gc.collect() + if self.guide: + mx = 360.0 / self.map_pixbuf.get_width() * (px-pw/2.0) - 180.0 + my = 90.0 - 180.0 / self.map_pixbuf.get_height() * (py-ph/2.0) + mw = 360.0 / self.map_pixbuf.get_width() * pw + mh = 180.0 / self.map_pixbuf.get_height() * ph + self.guide.hightlight_area( (mx,my,mw,mh)) + self.current_area = (px-pw/2, py-ph/2, pw,ph) + + # Scroll to requested position + def scroll_to( self, lat, lon): + self.zoom_pos = ( min(90,(max(-90,lon))), min(180,(max(-180,lat)))) + self.backbuf = None + self.queue_draw() + + def zoom_in(self): + self.magnifer = min( 10, self.magnifer * 1.5) + self.backbuf = None + self.queue_draw() + + def zoom_out(self): + self.magnifer = max( 0.1, self.magnifer * 0.75) + self.backbuf = None + self.queue_draw() + + def zoom_normal(self): + self.magnifer = 1 + self.backbuf = None + self.queue_draw() + + def zoom_fit(self): + self.magnifer = 2 + self.backbuf = None + self.queue_draw() + + +# Place list widget +class MapPlacesList(gtk.TreeView): + def __init__(self, data): + lstore = gtk.ListStore( + gobject.TYPE_STRING, + gobject.TYPE_FLOAT, + gobject.TYPE_FLOAT) + + for item in data: + iter = lstore.append() + lstore.set(iter, + 0, item[0], + 1, item[1], + 2, item[2]) + + gtk.TreeView.__init__(self, lstore) + self.set_rules_hint(True) + self.set_search_column(0) + + column = gtk.TreeViewColumn('Place', gtk.CellRendererText(), text=0) + column.set_sort_column_id(0) + self.append_column(column) + + column = gtk.TreeViewColumn('Lat', gtk.CellRendererText(), text=1) + column.set_sort_column_id(1) + self.append_column(column) + + column = gtk.TreeViewColumn('Long', gtk.CellRendererText(),text=2) + column.set_sort_column_id(2) + self.append_column(column) + + + +# Map View main class +class MapView(PageView.PageView): + def __init__(self,dbstate,uistate): + PageView.PageView.__init__(self,'Pedigree View',dbstate,uistate) + dbstate.connect('database-changed',self.change_db) + self.current_marker = None + + def navigation_type(self): + print "MapView.navigation_type" + return PageView.NAVIGATION_NONE + + def define_actions(self): + print "MapView.define_actions" + self.add_action('ZoomIn', gtk.STOCK_ZOOM_IN, "Zoom _In", callback=self.zoom_in_cb) + self.add_action('ZoomOut', gtk.STOCK_ZOOM_OUT, "Zoom _Out", callback=self.zoom_out_cb) + self.add_action('ZoomNormal',gtk.STOCK_ZOOM_100,"_Normal Size", callback=self.zoom_100_cb) + self.add_action('ZoomFit', gtk.STOCK_ZOOM_FIT, "Best _Fit", callback=self.zoom_fit_cb) + + def get_stock(self): + """ + Returns 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. + """ + return 'gramps-place' + + def build_widget(self): + hbox = gtk.HBox( False, 4) + hbox.set_border_width( 4) + + # The large zoomable map + self.zoom_map = ZoomMap( + gtk.gdk.pixbuf_new_from_file("land_shallow_topo_2048.jpg"), + gtk.gdk.pixbuf_new_from_file("bad.png"), + gtk.gdk.pixbuf_new_from_file("good.png")) + self.zoom_map.set_size_request(300,300) + hbox.pack_start( self.zoom_map, True, True, 0) + + # On the right side + vbox = gtk.VBox( False, 4) + hbox.pack_start( vbox, False, True, 0) + + # The small guide map + self.guide_map = GuideMap( gtk.gdk.pixbuf_new_from_file("land_shallow_topo_350.jpg")) + self.guide_map.set_size_request(128,64) + vbox.pack_start( self.guide_map, False, True, 0) + + self.zoom_map.set_guide(self.guide_map) + + # And the place list + self.place_list_view = MapPlacesList( data) + vbox.pack_start( self.place_list_view,True,True,0) + + self.place_list_view.connect("cursor-changed", self.entry_select_cb) + + return hbox + + def ui_definition(self): + """ + Specifies the UIManager XML code that defines the menus and buttons + associated with the interface. + """ + return ''' + + + + + + + ''' + + def change_db(self,db): + print "MapView.change_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.db = db + + def entry_select_cb(self,treeview): + s = treeview.get_selection() + (model,sel) = s.get_selected_rows() + for path in sel: + iter = model.get_iter(path) + self.zoom_map.scroll_to(model.get_value(iter,1), model.get_value(iter,2)) + break + + def zoom_in_cb(self,obj): + self.zoom_map.zoom_in() + + def zoom_out_cb(self,obj): + self.zoom_map.zoom_out() + + def zoom_100_cb(self,obj): + self.zoom_map.zoom_normal() + + def zoom_fit_cb(self,obj): + self.zoom_map.zoom_fit() diff --git a/gramps2/src/PedView.py b/gramps2/src/PedView.py index 6b06ce899..3c964b08f 100644 --- a/gramps2/src/PedView.py +++ b/gramps2/src/PedView.py @@ -41,12 +41,10 @@ import gtk.gdk # Gramps Modules # #------------------------------------------------------------------------- -import PageView -import const -import GrampsCfg -import Relationship -import NameDisplay import RelLib +import PageView +import EditPerson +import NameDisplay import Utils import DateHandler @@ -55,9 +53,7 @@ import DateHandler # Constants # #------------------------------------------------------------------------- -_PAD = 3 -_CANVASPAD = 3 -_PERSON = "p" +_PERSON = "p" _BORN = _('b.') _DIED = _('d.') _BAPT = _('bap.') @@ -74,18 +70,98 @@ _CREM = _('crem.') class PedView(PageView.PageView): def __init__(self,dbstate,uistate): + print "PedView.__init__" PageView.PageView.__init__(self,'Pedigree View',dbstate,uistate) - self.inactive = False dbstate.connect('database-changed',self.change_db) dbstate.connect('active-changed',self.goto_active_person) self.force_size = 0 # Automatic resize def navigation_type(self): + print "PedView.navigation_type" return PageView.NAVIGATION_PERSON + def init_parent_signals_cb(self, widget, event): + print "PedView.init_parent_signals_cb" + self.notebook.disconnect(self.bootstrap_handler) + self.notebook.parent.connect("size-allocate", self.size_request_cb) + self.size_request_cb(widget.parent,event) + + def add_table_to_notebook( self, table): + frame = gtk.ScrolledWindow(None,None) + frame.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) + frame.add_with_viewport(table) + try: + self.notebook.append_page(frame,None) + except: + # for PyGtk < 2.4 + self.notebook.append_page(frame,gtk.Label("")) + + def define_actions(self): + print "PedView.define_actions" + self.add_action('Forward',gtk.STOCK_GO_FORWARD,"_Forward", callback=self.fwd_clicked) + self.add_action('Back', gtk.STOCK_GO_BACK, "_Back", callback=self.back_clicked) + self.add_action('HomePerson', gtk.STOCK_HOME, "_Home", callback=self.home) + + # add the Forward action group to handle the Forward button + self.fwd_action = gtk.ActionGroup(self.title + '/Forward') + self.fwd_action.add_actions([ + ('Forward',gtk.STOCK_GO_FORWARD,"_Forward", None, None, self.fwd_clicked) + ]) + + # add the Backward action group to handle the Forward button + self.back_action = gtk.ActionGroup(self.title + '/Backward') + self.back_action.add_actions([ + ('Back',gtk.STOCK_GO_BACK,"_Back", None, None, self.back_clicked) + ]) + + self.add_action_group(self.back_action) + self.add_action_group(self.fwd_action) + + def disable_action_group(self): + print "PedView.disable_action_group" + """ + Normally, this would not be overridden from the base class. However, + in this case, we have additional action groups that need to be + handled correctly. + """ + PageView.PageView.disable_action_group(self) + + self.fwd_action.set_visible(False) + self.back_action.set_visible(False) + + def enable_action_group(self,obj): + print "PedView.enable_action_group" + """ + Normally, this would not be overridden from the base class. However, + in this case, we have additional action groups that need to be + handled correctly. + """ + PageView.PageView.enable_action_group(self,obj) + + self.fwd_action.set_visible(True) + self.back_action.set_visible(True) + hobj = self.uistate.phistory + self.fwd_action.set_sensitive(not hobj.at_end()) + self.back_action.set_sensitive(not hobj.at_front()) + + def get_stock(self): + """ + Returns 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. + """ + return 'gramps-pedigree' + def build_widget(self): + print "PedView.build_widget" + """ + Builds the interface and returns a gtk.Container type that + contains the interface. This containter will be inserted into + a gtk.Notebook page. + """ self.notebook = gtk.Notebook() self.notebook.connect("button-press-event", self.on_show_option_menu_cb) + self.bootstrap_handler = self.notebook.connect("expose-event", self.init_parent_signals_cb) self.notebook.set_show_border(False) self.notebook.set_show_tabs(False) @@ -105,75 +181,82 @@ class PedView(PageView.PageView): self.table_5.connect("button-press-event", self.on_show_option_menu_cb) self.add_table_to_notebook( self.table_5) - #self.parent_container.connect("size-allocate", self.size_request_cb) + self.rebuild_trees(None) return self.notebook - def add_table_to_notebook( self, table): - frame = gtk.ScrolledWindow(None,None) - frame.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) - frame.add_with_viewport(table) - try: - self.notebook.append_page(frame,None) - except: - # for PyGtk < 2.4 - self.notebook.append_page(frame,gtk.Label("")) - - def define_actions(self): - #self.add_action('Add', gtk.STOCK_ADD, "_Add", callback=self.add) - #self.add_action('Edit', gtk.STOCK_EDIT, "_Edit", callback=self.edit) - #self.add_action('Remove', gtk.STOCK_REMOVE, "_Remove", callback=self.remove) - #self.add_action('Forward',gtk.STOCK_GO_FORWARD,"_Forward", callback=self.fwd_clicked) - #self.add_action('Back', gtk.STOCK_GO_BACK, "_Back", callback=self.back_clicked) - self.add_action('HomePerson', gtk.STOCK_HOME, "_Home", callback=self.home) - #self.add_toggle_action('Filter', None, '_Filter', callback=self.filter_toggle) - def ui_definition(self): + """ + Specifies the UIManager XML code that defines the menus and buttons + associated with the interface. + """ return ''' + + + + + + ''' - def get_stock(self): - return 'gramps-pedigree' - def change_db(self,db): - # Reconnect signals + print "PedView.change_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.db = db db.connect('person-add', self.person_updated_cb) db.connect('person-update', self.person_updated_cb) db.connect('person-delete', self.person_updated_cb) db.connect('person-rebuild', self.person_rebuild) - self.active_person = None - - def person_updated_cb(self,handle_list): - self.rebuild_trees(self.active_person) - - def person_rebuild(self): - self.rebuild_trees(self.active_person) - - def goto_active_person(self,handle): + self.rebuild_trees(None) + + def goto_active_person(self,handle=None): print "PedView.goto_active_person" if handle: - self.active_person = self.db.get_person_from_handle(handle) - self.rebuild_trees(self.active_person) + self.rebuild_trees(self.db.get_person_from_handle(handle)) else: self.rebuild_trees(None) + def fwd_clicked(self,obj,step=1): + pass + + def back_clicked(self,obj,step=1): + pass + + def person_updated_cb(self,handle_list): + print "PedView.person_updated_cb" + self.rebuild_trees(self.dbstate.active) + + def person_rebuild(self): + print "PedView.person_rebuild" + self.rebuild_trees(self.dbstate.active) + + def person_edited_cb(self, p1=None, p2=None): + print "PedView.person_edited_cb" + def request_resize(self): + print "PedView.request_resize" self.size_request_cb(self.notebook.parent,None,None) def size_request_cb(self, widget, event, data=None): + print "PedView.size_request_cb" if self.force_size == 0: v = widget.get_allocation() page_list = range(0,self.notebook.get_n_pages()) @@ -248,13 +331,14 @@ class PedView(PageView.PageView): self.rebuild( self.table_4, pos_4, person) self.rebuild( self.table_5, pos_5, person) - gobject.idle_add(self.request_resize) + #gobject.idle_add(self.request_resize) + def rebuild( self, table_widget, positions, active_person): print "PedView.rebuild" # Build ancestor tree lst = [None]*31 - self.find_tree(self.active_person,0,1,lst) + self.find_tree(active_person,0,1,lst) # Purge current table content for child in table_widget.get_children(): @@ -389,9 +473,9 @@ class PedView(PageView.PageView): person_handle = obj.get_data(_PERSON) person = self.db.get_person_from_handle(person_handle) if person: - self.edit_person(person) + EditPerson.EditPerson(self.dbstate, person, self.person_edited_cb) return True - return 0 + return False def on_show_option_menu_cb(self,obj,data=None): myMenu = gtk.Menu() @@ -402,16 +486,16 @@ class PedView(PageView.PageView): def on_show_child_menu(self,obj): """User clicked button to move to child of active person""" - if self.active_person: + if self.dbstate.active: # Build and display the menu attached to the left pointing arrow # button. The menu consists of the children of the current root # person of the tree. Attach a child to each menu item. - childlist = find_children(self.db,self.active_person) + childlist = find_children(self.db,self.dbstate.active) if len(childlist) == 1: child = self.db.get_person_from_handle(childlist[0]) if child: - self.parent.change_active_person(child) + self.dbstate.change_active_person(child) elif len(childlist) > 1: myMenu = gtk.Menu() for child_handle in childlist: diff --git a/gramps2/src/gramps_main.py b/gramps2/src/gramps_main.py index dbdca8c15..003ee2281 100755 --- a/gramps2/src/gramps_main.py +++ b/gramps2/src/gramps_main.py @@ -24,6 +24,7 @@ import gtk import ViewManager import PersonView import PedView +import MapView import ArgHandler import DisplayTrace import GrampsKeys @@ -190,6 +191,7 @@ class Gramps: a = ViewManager.ViewManager(state) a.register_view(PersonView.PersonView) a.register_view(PedView.PedView) + a.register_view(MapView.MapView) a.init_interface() if GrampsKeys.get_usetips(): diff --git a/gramps2/src/land_shallow_topo_2048.jpg b/gramps2/src/land_shallow_topo_2048.jpg new file mode 100644 index 000000000..0e872b8e5 Binary files /dev/null and b/gramps2/src/land_shallow_topo_2048.jpg differ diff --git a/gramps2/src/land_shallow_topo_350.jpg b/gramps2/src/land_shallow_topo_350.jpg new file mode 100644 index 000000000..20ed8996d Binary files /dev/null and b/gramps2/src/land_shallow_topo_350.jpg differ