diff --git a/src/DataViews/GrampletView.py b/src/DataViews/GrampletView.py
index d35a01fd6..0c33360a9 100644
--- a/src/DataViews/GrampletView.py
+++ b/src/DataViews/GrampletView.py
@@ -219,6 +219,7 @@ class Gramplet(object):
self.gui = gui # plugin gramplet has link to gui
gui.pui = self # gui has link to plugin ui
self.dbstate = gui.dbstate
+ self.uistate = gui.uistate
self.init()
self.on_load()
self.build_options()
@@ -400,7 +401,7 @@ class Gramplet(object):
from PluginUtils import make_gui_option
#tooltips, dbstate, uistate, track
widget, label = make_gui_option(option, None, self.dbstate,
- self.gui.uistate,None)
+ self.uistate,None)
self.option_dict.update({option.get_label(): (widget, option)})
self.option_order.append(option.get_label())
diff --git a/src/plugins/gramplet/FanChartGramplet.py b/src/plugins/gramplet/FanChartGramplet.py
index 112afe77d..4c8551088 100644
--- a/src/plugins/gramplet/FanChartGramplet.py
+++ b/src/plugins/gramplet/FanChartGramplet.py
@@ -1,5 +1,6 @@
# 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
@@ -26,13 +27,6 @@
## Found by redwood:
## http://www.gramps-project.org/bugs/view.php?id=2611
-## TODO:
-## 1) add arrows to show rotation ability (click on background)
-## 2) add center popup to pick center's children
-## 3) perhaps right-click shows choice to edit, or make active, quick views,
-## etc
-## 4) add animations
-
#-------------------------------------------------------------------------
#
# Python modules
@@ -44,6 +38,7 @@ import pango
import gtk
import math
from gtk import gdk
+from cgi import escape
try:
import cairo
except ImportError:
@@ -60,11 +55,25 @@ if gtk.pygtk_version < (2,3,93):
from BasicUtils import name_displayer
from gettext import gettext as _
from DataViews import Gramplet, register
+from DataViews.PedigreeView import (find_children, find_parents,
+ find_witnessed_people, FormattingHelper)
import gen.lib
+import Errors
+from Editors import EditPerson, EditFamily
+#-------------------------------------------------------------------------
+#
+# Functions
+#
+#-------------------------------------------------------------------------
def gender_code(is_male):
- if is_male: return 1
- return 0
+ """
+ Given boolean is_male (means position in FanChart) return code.
+ """
+ if is_male:
+ return gen.lib.Person.MALE
+ else:
+ return gen.lib.Person.FEMALE
#-------------------------------------------------------------------------
#
@@ -91,16 +100,17 @@ class FanChartWidget(gtk.Widget):
NORMAL = 1
EXPANDED = 2
- def __init__(self, generations, right_click_callback=None):
+ def __init__(self, generations, context_popup_callback=None):
"""
- Highly experimental... documents forthcoming...
+ Fan Chart Widget. Handles visualization of data in self.data.
+ See main() of FanChartGramplet for example of model format.
"""
gtk.Widget.__init__(self)
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.right_click_callback = right_click_callback
+ self.context_popup_callback = context_popup_callback
self.add_events(gdk.BUTTON_PRESS_MASK |
gdk.BUTTON_RELEASE_MASK |
gdk.POINTER_MOTION_MASK)
@@ -130,12 +140,13 @@ class FanChartWidget(gtk.Widget):
self.angle = {}
self.data = {}
for i in range(self.generations):
- self.data[i] = [(None, None, None) for j in range(2 ** i)]
+ # 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 a in range(len(self.data[i])):
+ for count in range(len(self.data[i])):
# start, stop, male, state
self.angle[i].append([angle, angle + slice,gender,self.NORMAL])
angle += slice
@@ -208,13 +219,13 @@ class FanChartWidget(gtk.Widget):
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) = self.data[generation][p]
+ (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)
+ 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)
@@ -224,20 +235,26 @@ class FanChartWidget(gtk.Widget):
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.stroke()
# Draw center person:
- (text, person, parents) = self.data[0][0]
+ (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):
+ state, parents, child):
"""
Display the piece of pie for a given person. start and stop
are in degrees.
@@ -485,11 +502,16 @@ class FanChartWidget(gtk.Widget):
selected = p
break
# Handle the click:
- if selected == None: # clicked in open area
+ if selected == None: # clicked in open area, or center
if radius < self.center:
- print "TODO: select child, spouse"
- self.queue_draw()
- return True
+ # right mouse
+ if event.button == 3 and self.context_popup_callback:
+ 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
@@ -498,9 +520,9 @@ class FanChartWidget(gtk.Widget):
if event.button == 1: # left mouse
self.change_slice(generation, selected)
elif event.button == 3: # right mouse
- text, person, parents = self.data[generation][selected]
- if person and self.right_click_callback:
- self.right_click_callback(person)
+ 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
@@ -509,10 +531,11 @@ class FanChartGramplet(Gramplet):
The Gramplet code that realizes the FanChartWidget.
"""
def init(self):
- self.set_tooltip("Click to expand/contract person\nRight-click to make person active")
+ self.set_tooltip("Click to expand/contract person\nRight-click for options\nClick and drag in open area to rotate")
self.generations = 6
+ self.format_helper = FormattingHelper(self.dbstate)
self.gui.fan = FanChartWidget(self.generations,
- right_click_callback=self.dbstate.change_active_person)
+ context_popup_callback=self.on_popup)
# Replace the standard textview with the fan chart widget:
self.gui.get_container_widget().remove(self.gui.textview)
self.gui.get_container_widget().add_with_viewport(self.gui.fan)
@@ -536,6 +559,17 @@ class FanChartGramplet(Gramplet):
return m != None or f != 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.
@@ -566,11 +600,12 @@ class FanChartGramplet(Gramplet):
else:
name = name_displayer.display(person)
parents = self.have_parents(person)
- self.gui.fan.data[0][0] = (name, person, parents)
+ child = self.have_children(person)
+ self.gui.fan.data[0][0] = (name, person, parents, child)
for current in range(1, self.generations):
parent = 0
- # name, person, parents
- for (n,p,q) in self.gui.fan.data[current - 1]:
+ # name, person, parents, children
+ for (n,p,q,c) in self.gui.fan.data[current - 1]:
# Get father's details:
person = self.get_parent(p, "male")
if person:
@@ -581,7 +616,7 @@ class FanChartGramplet(Gramplet):
parents = self.have_parents(person)
else:
parents = None
- self.gui.fan.data[current][parent] = (name, person, parents)
+ self.gui.fan.data[current][parent] = (name, person, parents, None)
if person is None:
# start,stop,male/right,state
self.gui.fan.angle[current][parent][3] = self.gui.fan.COLLAPSED
@@ -592,14 +627,263 @@ class FanChartGramplet(Gramplet):
name = name_displayer.display(person)
else:
name = None
- parents = self.have_parents(person)
- self.gui.fan.data[current][parent] = (name, person, parents)
+ if current == self.generations - 1:
+ parents = self.have_parents(person)
+ else:
+ parents = None
+ self.gui.fan.data[current][parent] = (name, person, parents, None)
if person is None:
# start,stop,male/right,state
self.gui.fan.angle[current][parent][3] = self.gui.fan.COLLAPSED
parent += 1
self.gui.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
#-------------------------------------------------------------------------
#