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 #------------------------------------------------------------------------- #