From 9fd107741ff573b93e4a668c078f9484bbe1d039 Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Fri, 13 Nov 2009 01:59:30 +0000 Subject: [PATCH] A Fan chart view (unstable) svn: r13565 --- src/plugins/view/fanchartview.gpr.py | 12 + src/plugins/view/fanchartview.py | 945 +++++++++++++++++++++++++++ 2 files changed, 957 insertions(+) create mode 100644 src/plugins/view/fanchartview.gpr.py create mode 100644 src/plugins/view/fanchartview.py diff --git a/src/plugins/view/fanchartview.gpr.py b/src/plugins/view/fanchartview.gpr.py new file mode 100644 index 000000000..f3ae098a4 --- /dev/null +++ b/src/plugins/view/fanchartview.gpr.py @@ -0,0 +1,12 @@ +register(VIEW, + id = 'fanchartview', + name = _("Fan Chart View"), + description = _("The view showing relations through a fanchart"), + version = '1.0', + status = UNSTABLE, + fname = 'fanchartview.py', + authors = [u"Douglas S. Blank"], + authors_email = ["doug.blank@gmail.com"], + category = VIEW_REL, + viewclass = 'FanChartView', + ) diff --git a/src/plugins/view/fanchartview.py b/src/plugins/view/fanchartview.py new file mode 100644 index 000000000..9135ef07c --- /dev/null +++ b/src/plugins/view/fanchartview.py @@ -0,0 +1,945 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch +# Copyright (C) 2009 Douglas S. Blank +# +# 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$ + +## Based on the paper: +## http://www.cs.utah.edu/~draperg/research/fanchart/draperg_FHT08.pdf +## and the applet: +## http://www.cs.utah.edu/~draperg/research/fanchart/demo/ + +## Found by redwood: +## http://www.gramps-project.org/bugs/view.php?id=2611 + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +import pygtk +pygtk.require('2.0') +import pango +import gtk +import math +from gtk import gdk +from cgi import escape +try: + import cairo +except ImportError: + pass + +if gtk.pygtk_version < (2,3,93): + raise Exception("PyGtk 2.3.93 or later required") + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gui.views.navigationview import NavigationView +from BasicUtils import name_displayer +from gettext import gettext as _ +from gen.plug import Gramplet +from DataViews.PedigreeView import (find_children, find_parents, + find_witnessed_people, FormattingHelper) +import gen.lib +import Errors +import Bookmarks +from Editors import EditPerson, EditFamily + +#------------------------------------------------------------------------- +# +# Functions +# +#------------------------------------------------------------------------- +def gender_code(is_male): + """ + Given boolean is_male (means position in FanChart) return code. + """ + if is_male: + return gen.lib.Person.MALE + else: + return gen.lib.Person.FEMALE + +class AttachList(object): + def __init__(self): + self.list = [] + self.max_x = 0 + self.max_y = 0 + + def attach(self, widget, x0, x1, y0, y1, xoptions=gtk.EXPAND|gtk.FILL, + yoptions=gtk.EXPAND|gtk.FILL): + assert(widget) + assert(x1>x0) + self.list.append((widget, x0, x1, y0, y1, xoptions, yoptions)) + self.max_x = max(self.max_x, x1) + self.max_y = max(self.max_y, y1) + +#------------------------------------------------------------------------- +# +# FanChartWidget +# +#------------------------------------------------------------------------- +class FanChartWidget(gtk.Widget): + """ + Interactive Fan Chart Widget. + """ + BORDER_WIDTH = 10 + __gsignals__ = { 'realize': 'override', + 'expose-event' : 'override', + 'size-allocate': 'override', + 'size-request': 'override', + } + GENCOLOR = ((229,191,252), + (191,191,252), + (191,222,252), + (183,219,197), + (206,246,209)) + + COLLAPSED = 0 + NORMAL = 1 + EXPANDED = 2 + + def __init__(self, generations, context_popup_callback=None): + """ + Fan Chart Widget. Handles visualization of data in self.data. + See main() of FanChartGramplet for example of model format. + """ + gtk.Widget.__init__(self) + self.translating = False + self.last_x, self.last_y = None, None + self.connect("button_release_event", self.on_mouse_up) + self.connect("motion_notify_event", self.on_mouse_move) + self.connect("button-press-event", self.on_mouse_down) + self.context_popup_callback = context_popup_callback + self.add_events(gdk.BUTTON_PRESS_MASK | + gdk.BUTTON_RELEASE_MASK | + gdk.POINTER_MOTION_MASK) + self.pixels_per_generation = 50 # size of radius for generation + ## gotten from experiments with "sans serif 8": + self.degrees_per_radius = .80 + ## Other fonts will have different settings. Can you compute that + ## from the font size? I have no idea. + self.generations = generations + self.rotate_value = 90 # degrees, initially, 1st gen male on right half + self.center_xy = [0, 0] # distance from center (x, y) + self.set_generations(self.generations) + self.center = 50 # pixel radius of center + self.layout = self.create_pango_layout('cairo') + self.layout.set_font_description(pango.FontDescription("sans serif 8")) + + def reset_generations(self): + """ + Reset all of the data on where slices appear, and if they are expanded. + """ + self.set_generations(self.generations) + + def set_generations(self, generations): + """ + Set the generations to max, and fill data structures with initial data. + """ + self.generations = generations + self.angle = {} + self.data = {} + for i in range(self.generations): + # name, person, parents?, children? + self.data[i] = [(None, None, None, None) for j in range(2 ** i)] + self.angle[i] = [] + angle = 0 + slice = 360.0 / (2 ** i) + gender = True + for count in range(len(self.data[i])): + # start, stop, male, state + self.angle[i].append([angle, angle + slice,gender,self.NORMAL]) + angle += slice + gender = not gender + + def do_realize(self): + """ + Overriden method to handle the realize event. + """ + self.set_flags(self.flags() | gtk.REALIZED) + self.window = gdk.Window(self.get_parent_window(), + width=self.allocation.width, + height=self.allocation.height, + window_type=gdk.WINDOW_CHILD, + wclass=gdk.INPUT_OUTPUT, + event_mask=self.get_events() | gdk.EXPOSURE_MASK) + if not hasattr(self.window, "cairo_create"): + self.draw_gc = gdk.GC(self.window, + line_width=5, + line_style=gdk.SOLID, + join_style=gdk.JOIN_ROUND) + + self.window.set_user_data(self) + self.style.attach(self.window) + self.style.set_background(self.window, gtk.STATE_NORMAL) + self.window.move_resize(*self.allocation) + + def do_size_request(self, requisition): + """ + Overridden method to handle size request events. + """ + width, height = self.layout.get_size() + requisition.width = (width // pango.SCALE + self.BORDER_WIDTH*4)* 1.45 + requisition.height = (3 * height // pango.SCALE + self.BORDER_WIDTH*4) * 1.2 + + def do_size_allocate(self, allocation): + """ + Overridden method to handle size allocation events. + """ + self.allocation = allocation + if self.flags() & gtk.REALIZED: + self.window.move_resize(*allocation) + + def _expose_gdk(self, event): + x, y, w, h = self.allocation + self.layout = self.create_pango_layout('no cairo') + fontw, fonth = self.layout.get_pixel_size() + self.style.paint_layout(self.window, self.state, False, + event.area, self, "label", + (w - fontw) / 2, (h - fonth) / 2, + self.layout) + + def do_expose_event(self, event): + """ + Overridden method to handle expose events. + """ + try: + cr = self.window.cairo_create() + except AttributeError: + return self._expose_gdk(event) + return self._expose_cairo(event, cr) + + def _expose_cairo(self, event, cr): + """ + The main method to do the drawing. + """ + x, y, w, h = self.allocation + cr.translate(w/2. - self.center_xy[0], h/2. - self.center_xy[1]) + cr.save() + cr.rotate(self.rotate_value * math.pi/180) + for generation in range(self.generations - 1, 0, -1): + for p in range(len(self.data[generation])): + (text, person, parents, child) = self.data[generation][p] + if person: + start, stop, male, state = self.angle[generation][p] + if state in [self.NORMAL, self.EXPANDED]: + self.draw_person(cr, gender_code(male), + text, start, stop, + generation, state, parents, child) + cr.set_source_rgb(1, 1, 1) # white + cr.move_to(0,0) + cr.arc(0, 0, self.center, 0, 2 * math.pi) + cr.move_to(0,0) + cr.fill() + cr.set_source_rgb(0, 0, 0) # black + cr.arc(0, 0, self.center, 0, 2 * math.pi) + cr.stroke() + # Draw center person: + (text, person, parents, child) = self.data[0][0] + cr.restore() + if person: + cr.save() + name = name_displayer.display(person) + self.draw_text(cr, name, self.center - 10, 95, 455) + cr.restore() + if child: # has at least one child + cr.set_source_rgb(0, 0, 0) # black + cr.move_to(0,0) + cr.arc(0, 0, 10, 0, 2 * math.pi) + cr.move_to(0,0) + cr.fill() + fontw, fonth = self.layout.get_pixel_size() + cr.move_to((w - fontw - 4), (h - fonth )) + cr.update_layout(self.layout) + cr.show_layout(self.layout) + + def draw_person(self, cr, gender, name, start, stop, generation, + state, parents, child): + """ + Display the piece of pie for a given person. start and stop + are in degrees. + """ + x, y, w, h = self.allocation + start_rad = start * math.pi/180 + stop_rad = stop * math.pi/180 + r,g,b = self.GENCOLOR[generation % len(self.GENCOLOR)] + if gender == gen.lib.Person.MALE: + r *= .9 + g *= .9 + b *= .9 + radius = generation * self.pixels_per_generation + self.center + # If max generation, and they have parents: + if generation == self.generations - 1 and parents: + # draw an indicator + cr.move_to(0, 0) + cr.set_source_rgb(255, 255, 255) # white + cr.arc(0, 0, radius + 10, start_rad, stop_rad) + cr.fill() + cr.move_to(0, 0) + cr.set_source_rgb(0, 0, 0) # black + cr.arc(0, 0, radius + 10, start_rad, stop_rad) + cr.line_to(0, 0) + cr.stroke() + cr.set_source_rgb(r/255., g/255., b/255.) + cr.move_to(0, 0) + cr.arc(0, 0, radius, start_rad, stop_rad) + cr.move_to(0, 0) + cr.fill() + cr.set_source_rgb(0, 0, 0) # black + cr.arc(0, 0, radius, start_rad, stop_rad) + cr.line_to(0, 0) + cr.arc(0, 0, radius, start_rad, stop_rad) + cr.line_to(0, 0) + if state == self.NORMAL: # normal + cr.set_line_width(1) + else: # EXPANDED + cr.set_line_width(3) + cr.stroke() + cr.set_line_width(1) + if self.last_x is None or self.last_y is None: + self.draw_text(cr, name, radius - self.pixels_per_generation/2, + start, stop) + + def text_degrees(self, text, radius): + """ + Returns the number of degrees of text at a given radius. + """ + return 360.0 * len(text)/(radius * self.degrees_per_radius) + + def text_limit(self, text, degrees, radius): + """ + Trims the text to fit a given angle at a given radius. Probably + a better way to do this. + """ + while self.text_degrees(text, radius) > degrees: + text = text[:-1] + return text + + def draw_text(self, cr, text, radius, start, stop): + """ + Display text at a particular radius, between start and stop + degrees. + """ + # trim to fit: + text = self.text_limit(text, stop - start, radius - 15) + # center text: + # offset for cairo-font system is 90: + pos = start + ((stop - start) - self.text_degrees(text,radius))/2.0 + 90 + x, y, w, h = self.allocation + cr.save() + # Create a PangoLayout, set the font and text + # Draw the layout N_WORDS times in a circle + for i in range(len(text)): + cr.save() + layout = self.create_pango_layout(text[i]) + layout.set_font_description(pango.FontDescription("sans serif 8")) + angle = 360.0 * i / (radius * self.degrees_per_radius) + pos + cr.set_source_rgb(0, 0, 0) # black + cr.rotate(angle * (math.pi / 180)); + # Inform Pango to re-layout the text with the new transformation + cr.update_layout(layout) + width, height = layout.get_size() + cr.move_to(- (width / pango.SCALE) / 2.0, - radius) + cr.show_layout(layout) + cr.restore() + cr.restore() + + def expand_parents(self, generation, selected, current): + if generation >= self.generations: return + selected = 2 * selected + start,stop,male,state = self.angle[generation][selected] + if state in [self.NORMAL, self.EXPANDED]: + slice = (stop - start) * 2.0 + self.angle[generation][selected] = [current,current+slice, + male,state] + self.expand_parents(generation + 1, selected, current) + current += slice + start,stop,male,state = self.angle[generation][selected+1] + if state in [self.NORMAL, self.EXPANDED]: + slice = (stop - start) * 2.0 + self.angle[generation][selected+1] = [current,current+slice, + male,state] + self.expand_parents(generation + 1, selected+1, current) + + def show_parents(self, generation, selected, angle, slice): + if generation >= self.generations: return + selected *= 2 + self.angle[generation][selected][0] = angle + self.angle[generation][selected][1] = angle + slice + self.angle[generation][selected][3] = self.NORMAL + self.show_parents(generation+1, selected, angle, slice/2.0) + self.angle[generation][selected+1][0] = angle + slice + self.angle[generation][selected+1][1] = angle + slice + slice + self.angle[generation][selected+1][3] = self.NORMAL + self.show_parents(generation+1, selected + 1, angle + slice, slice/2.0) + + def hide_parents(self, generation, selected, angle): + if generation >= self.generations: return + selected = 2 * selected + self.angle[generation][selected][0] = angle + self.angle[generation][selected][1] = angle + self.angle[generation][selected][3] = self.COLLAPSED + self.hide_parents(generation + 1, selected, angle) + self.angle[generation][selected+1][0] = angle + self.angle[generation][selected+1][1] = angle + self.angle[generation][selected+1][3] = self.COLLAPSED + self.hide_parents(generation + 1, selected+1, angle) + + def shrink_parents(self, generation, selected, current): + if generation >= self.generations: return + selected = 2 * selected + start,stop,male,state = self.angle[generation][selected] + if state in [self.NORMAL, self.EXPANDED]: + slice = (stop - start) / 2.0 + self.angle[generation][selected] = [current, current + slice, + male,state] + self.shrink_parents(generation + 1, selected, current) + current += slice + start,stop,male,state = self.angle[generation][selected+1] + if state in [self.NORMAL, self.EXPANDED]: + slice = (stop - start) / 2.0 + self.angle[generation][selected+1] = [current,current+slice, + male,state] + self.shrink_parents(generation + 1, selected+1, current) + + def change_slice(self, generation, selected): + gstart, gstop, gmale, gstate = self.angle[generation][selected] + if gstate == self.NORMAL: # let's expand + if gmale: + # go to right + stop = gstop + (gstop - gstart) + self.angle[generation][selected] = [gstart,stop,gmale, + self.EXPANDED] + self.expand_parents(generation + 1, selected, gstart) + start,stop,male,state = self.angle[generation][selected+1] + self.angle[generation][selected+1] = [stop,stop,male, + self.COLLAPSED] + self.hide_parents(generation+1, selected+1, stop) + else: + # go to left + start = gstart - (gstop - gstart) + self.angle[generation][selected] = [start,gstop,gmale, + self.EXPANDED] + self.expand_parents(generation + 1, selected, start) + start,stop,male,state = self.angle[generation][selected-1] + self.angle[generation][selected-1] = [start,start,male, + self.COLLAPSED] + self.hide_parents(generation+1, selected-1, start) + elif gstate == self.EXPANDED: # let's shrink + if gmale: + # shrink from right + slice = (gstop - gstart)/2.0 + stop = gstop - slice + self.angle[generation][selected] = [gstart,stop,gmale, + self.NORMAL] + self.shrink_parents(generation+1, selected, gstart) + self.angle[generation][selected+1][0] = stop # start + self.angle[generation][selected+1][1] = stop + slice # stop + self.angle[generation][selected+1][3] = self.NORMAL + self.show_parents(generation+1, selected+1, stop, slice/2.0) + else: + # shrink from left + slice = (gstop - gstart)/2.0 + start = gstop - slice + self.angle[generation][selected] = [start,gstop,gmale, + self.NORMAL] + self.shrink_parents(generation+1, selected, start) + start,stop,male,state = self.angle[generation][selected-1] + self.angle[generation][selected-1] = [start,start+slice,male, + self.NORMAL] + self.show_parents(generation+1, selected-1, start, slice/2.0) + + def on_mouse_up(self, widget, event): + # Done with mouse movement + if self.last_x is None or self.last_y is None: return True + if self.translating: + self.translating = False + x, y, w, h = self.allocation + self.center_xy = w/2 - event.x, h/2 - event.y + self.last_x, self.last_y = None, None + self.queue_draw() + return True + + def on_mouse_move(self, widget, event): + if self.last_x is None or self.last_y is None: return False + x, y, w, h = self.allocation + if self.translating: + self.center_xy = w/2 - event.x, h/2 - event.y + self.queue_draw() + return True + cx = w/2 - self.center_xy[0] + cy = h/2 - self.center_xy[1] + # get the angles of the two points from the center: + start_angle = math.atan2(event.y - cy, event.x - cx) + end_angle = math.atan2(self.last_y - cy, self.last_x - cx) + if start_angle < 0: # second half of unit circle + start_angle = math.pi + (math.pi + start_angle) + if end_angle < 0: # second half of unit circle + end_angle = math.pi + (math.pi + end_angle) + # now look at change in angle: + diff_angle = (end_angle - start_angle) % (math.pi * 2.0) + self.rotate_value -= diff_angle * 180.0/ math.pi + self.queue_draw() + self.last_x, self.last_y = event.x, event.y + return True + + def on_mouse_down(self, widget, event): + # compute angle, radius, find out who would be there (rotated) + x, y, w, h = self.allocation + self.translating = False # keep track of up/down/left/right movement + cx = w/2 - self.center_xy[0] + cy = h/2 - self.center_xy[1] + radius = math.sqrt((event.x - cx) ** 2 + (event.y - cy) ** 2) + if radius < self.center: + generation = 0 + else: + generation = int((radius - self.center) / + self.pixels_per_generation) + 1 + rads = math.atan2( (event.y - cy), (event.x - cx) ) + if rads < 0: # second half of unit circle + rads = math.pi + (math.pi + rads) + pos = ((rads/(math.pi * 2) - self.rotate_value/360.) * 360.0) % 360 + # if generation is in expand zone: + # FIXME: add a way of expanding + # find what person is in this position: + selected = None + if (0 < generation < self.generations): + for p in range(len(self.angle[generation])): + if self.data[generation][p][1]: # there is a person there + start, stop, male, state = self.angle[generation][p] + if state == self.COLLAPSED: continue + if start <= pos <= stop: + selected = p + break + # Handle the click: + if generation == 0: + # left mouse on center: + if event.button == 1: # left mouse + # save the mouse location for movements + self.translating = True + self.last_x, self.last_y = event.x, event.y + return True + if selected is None: # clicked in open area, or center + if radius < self.center: + # right mouse + if event.button == 3 and self.context_popup_callback: + if self.data[0][0][1]: + self.context_popup_callback(widget, event, + self.data[0][0][1].handle) + return True + else: + return False + # else, what to do on left click? + else: + # save the mouse location for movements + self.last_x, self.last_y = event.x, event.y + return True + # Do things based on state, event.state, or button, event.button + if event.button == 1: # left mouse + self.change_slice(generation, selected) + elif event.button == 3: # right mouse + text, person, parents, child = self.data[generation][selected] + if person and self.context_popup_callback: + self.context_popup_callback(widget, event, person.handle) + self.queue_draw() + return True + +class FanChartView(NavigationView): + """ + The Gramplet code that realizes the FanChartWidget. + """ + def __init__(self, dbstate, uistate): + NavigationView.__init__(self, _('Fan Chart'), + dbstate, uistate, + dbstate.db.get_bookmarks(), + Bookmarks.Bookmarks) + + dbstate.connect('active-changed', self.active_changed) + self.dbstate = dbstate + self.uistate = uistate + self.generations = 9 + self.format_helper = FormattingHelper(self.dbstate) + + def build_widget(self): + self.fan = FanChartWidget(self.generations, + context_popup_callback=self.on_popup) + return self.fan + + def ui_definition(self): + return ''' + + + + + + + + + ''' + + def build_tree(self): + self.main() + + def active_changed(self, handle): + """ + Method called when active person changes. + """ + # Reset everything but rotation angle (leave it as is) + self.update() + + def update(self): + self.main() + + def have_parents(self, person): + """ + Returns True if a person has parents. + """ + if person: + m = self.get_parent(person, "female") + f = self.get_parent(person, "male") + return not m is f is None + return False + + def have_children(self, person): + """ + Returns True if a person has children. + """ + if person: + for family_handle in person.get_family_handle_list(): + family = self.dbstate.db.get_family_from_handle(family_handle) + if family and len(family.get_child_ref_list()) > 0: + return True + return False + + def get_parent(self, person, gender): + """ + Get the father if gender == "male", or get mother otherwise. + """ + if person: + parent_handle_list = person.get_parent_family_handle_list() + if parent_handle_list: + family_id = parent_handle_list[0] + family = self.dbstate.db.get_family_from_handle(family_id) + if family: + if gender == "male": + person_handle = gen.lib.Family.get_father_handle(family) + else: + person_handle = gen.lib.Family.get_mother_handle(family) + if person_handle: + return self.dbstate.db.get_person_from_handle(person_handle) + return None + + def main(self): + """ + Fill the data structures with the active data. This initializes all + data. + """ + self.fan.reset_generations() + person = self.dbstate.get_active_person() + if not person: + name = None + else: + name = name_displayer.display(person) + parents = self.have_parents(person) + child = self.have_children(person) + self.fan.data[0][0] = (name, person, parents, child) + for current in range(1, self.generations): + parent = 0 + # name, person, parents, children + for (n,p,q,c) in self.fan.data[current - 1]: + # Get father's details: + person = self.get_parent(p, "male") + if person: + name = name_displayer.display(person) + else: + name = None + if current == self.generations - 1: + parents = self.have_parents(person) + else: + parents = None + self.fan.data[current][parent] = (name, person, parents, None) + if person is None: + # start,stop,male/right,state + self.fan.angle[current][parent][3] = self.fan.COLLAPSED + parent += 1 + # Get mother's details: + person = self.get_parent(p, "female") + if person: + name = name_displayer.display(person) + else: + name = None + if current == self.generations - 1: + parents = self.have_parents(person) + else: + parents = None + self.fan.data[current][parent] = (name, person, parents, None) + if person is None: + # start,stop,male/right,state + self.fan.angle[current][parent][3] = self.fan.COLLAPSED + parent += 1 + self.fan.queue_draw() + + def on_childmenu_changed(self, obj,person_handle): + """Callback for the pulldown menu selection, changing to the person + attached with menu item.""" + self.dbstate.change_active_handle(person_handle) + return True + + def edit_person_cb(self, obj,person_handle): + person = self.dbstate.db.get_person_from_handle(person_handle) + if person: + try: + EditPerson(self.dbstate, self.uistate, [], person) + except Errors.WindowActiveError: + pass + return True + return False + + def copy_person_to_clipboard_cb(self, obj,person_handle): + """Renders the person data into some lines of text and puts that into the clipboard""" + person = self.dbstate.db.get_person_from_handle(person_handle) + if person: + cb = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD) + cb.set_text( self.format_helper.format_person(person,11)) + return True + return False + + def on_popup(self, obj, event, person_handle): + """ + Builds the full menu (including Siblings, Spouses, Children, + and Parents) with navigation. Copied from PedigreeView. + """ + + menu = gtk.Menu() + menu.set_title(_('People Menu')) + + person = self.dbstate.db.get_person_from_handle(person_handle) + if not person: + return 0 + + go_image = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,gtk.ICON_SIZE_MENU) + go_image.show() + go_item = gtk.ImageMenuItem(name_displayer.display(person)) + go_item.set_image(go_image) + go_item.connect("activate",self.on_childmenu_changed,person_handle) + go_item.show() + menu.append(go_item) + + edit_item = gtk.ImageMenuItem(gtk.STOCK_EDIT) + edit_item.connect("activate",self.edit_person_cb,person_handle) + edit_item.show() + menu.append(edit_item) + + clipboard_item = gtk.ImageMenuItem(gtk.STOCK_COPY) + clipboard_item.connect("activate",self.copy_person_to_clipboard_cb,person_handle) + clipboard_item.show() + menu.append(clipboard_item) + + # collect all spouses, parents and children + linked_persons = [] + + # Go over spouses and build their menu + item = gtk.MenuItem(_("Spouses")) + fam_list = person.get_family_handle_list() + no_spouses = 1 + for fam_id in fam_list: + family = self.dbstate.db.get_family_from_handle(fam_id) + if family.get_father_handle() == person.get_handle(): + sp_id = family.get_mother_handle() + else: + sp_id = family.get_father_handle() + spouse = self.dbstate.db.get_person_from_handle(sp_id) + if not spouse: + continue + + if no_spouses: + no_spouses = 0 + item.set_submenu(gtk.Menu()) + sp_menu = item.get_submenu() + + go_image = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,gtk.ICON_SIZE_MENU) + go_image.show() + sp_item = gtk.ImageMenuItem(name_displayer.display(spouse)) + sp_item.set_image(go_image) + linked_persons.append(sp_id) + sp_item.connect("activate",self.on_childmenu_changed,sp_id) + sp_item.show() + sp_menu.append(sp_item) + + if no_spouses: + item.set_sensitive(0) + + item.show() + menu.append(item) + + # Go over siblings and build their menu + item = gtk.MenuItem(_("Siblings")) + pfam_list = person.get_parent_family_handle_list() + no_siblings = 1 + for f in pfam_list: + fam = self.dbstate.db.get_family_from_handle(f) + sib_list = fam.get_child_ref_list() + for sib_ref in sib_list: + sib_id = sib_ref.ref + if sib_id == person.get_handle(): + continue + sib = self.dbstate.db.get_person_from_handle(sib_id) + if not sib: + continue + + if no_siblings: + no_siblings = 0 + item.set_submenu(gtk.Menu()) + sib_menu = item.get_submenu() + + if find_children(self.dbstate.db,sib): + label = gtk.Label('%s' % escape(name_displayer.display(sib))) + else: + label = gtk.Label(escape(name_displayer.display(sib))) + + go_image = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,gtk.ICON_SIZE_MENU) + go_image.show() + sib_item = gtk.ImageMenuItem(None) + sib_item.set_image(go_image) + label.set_use_markup(True) + label.show() + label.set_alignment(0,0) + sib_item.add(label) + linked_persons.append(sib_id) + sib_item.connect("activate",self.on_childmenu_changed,sib_id) + sib_item.show() + sib_menu.append(sib_item) + + if no_siblings: + item.set_sensitive(0) + item.show() + menu.append(item) + + # Go over children and build their menu + item = gtk.MenuItem(_("Children")) + no_children = 1 + childlist = find_children(self.dbstate.db,person) + for child_handle in childlist: + child = self.dbstate.db.get_person_from_handle(child_handle) + if not child: + continue + + if no_children: + no_children = 0 + item.set_submenu(gtk.Menu()) + child_menu = item.get_submenu() + + if find_children(self.dbstate.db,child): + label = gtk.Label('%s' % escape(name_displayer.display(child))) + else: + label = gtk.Label(escape(name_displayer.display(child))) + + go_image = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,gtk.ICON_SIZE_MENU) + go_image.show() + child_item = gtk.ImageMenuItem(None) + child_item.set_image(go_image) + label.set_use_markup(True) + label.show() + label.set_alignment(0,0) + child_item.add(label) + linked_persons.append(child_handle) + child_item.connect("activate",self.on_childmenu_changed,child_handle) + child_item.show() + child_menu.append(child_item) + + if no_children: + item.set_sensitive(0) + item.show() + menu.append(item) + + # Go over parents and build their menu + item = gtk.MenuItem(_("Parents")) + no_parents = 1 + par_list = find_parents(self.dbstate.db,person) + for par_id in par_list: + par = self.dbstate.db.get_person_from_handle(par_id) + if not par: + continue + + if no_parents: + no_parents = 0 + item.set_submenu(gtk.Menu()) + par_menu = item.get_submenu() + + if find_parents(self.dbstate.db,par): + label = gtk.Label('%s' % escape(name_displayer.display(par))) + else: + label = gtk.Label(escape(name_displayer.display(par))) + + go_image = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,gtk.ICON_SIZE_MENU) + go_image.show() + par_item = gtk.ImageMenuItem(None) + par_item.set_image(go_image) + label.set_use_markup(True) + label.show() + label.set_alignment(0,0) + par_item.add(label) + linked_persons.append(par_id) + par_item.connect("activate",self.on_childmenu_changed,par_id) + par_item.show() + par_menu.append(par_item) + + if no_parents: + item.set_sensitive(0) + item.show() + menu.append(item) + + # Go over parents and build their menu + item = gtk.MenuItem(_("Related")) + no_related = 1 + for p_id in find_witnessed_people(self.dbstate.db,person): + #if p_id in linked_persons: + # continue # skip already listed family members + + per = self.dbstate.db.get_person_from_handle(p_id) + if not per: + continue + + if no_related: + no_related = 0 + item.set_submenu(gtk.Menu()) + per_menu = item.get_submenu() + + label = gtk.Label(escape(name_displayer.display(per))) + + go_image = gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,gtk.ICON_SIZE_MENU) + go_image.show() + per_item = gtk.ImageMenuItem(None) + per_item.set_image(go_image) + label.set_use_markup(True) + label.show() + label.set_alignment(0,0) + per_item.add(label) + per_item.connect("activate",self.on_childmenu_changed,p_id) + per_item.show() + per_menu.append(per_item) + + if no_related: + item.set_sensitive(0) + item.show() + menu.append(item) + menu.popup(None,None,None,event.button,event.time) + return 1