Feature: a fanchart for descendants. Futher to test with duplicates
svn: r20402
This commit is contained in:
@ -489,6 +489,7 @@ src/gui/views/treemodels/treebasemodel.py
|
|||||||
src/gui/widgets/buttons.py
|
src/gui/widgets/buttons.py
|
||||||
src/gui/widgets/expandcollapsearrow.py
|
src/gui/widgets/expandcollapsearrow.py
|
||||||
src/gui/widgets/fanchart.py
|
src/gui/widgets/fanchart.py
|
||||||
|
src/gui/widgets/fanchartdesc.py
|
||||||
src/gui/widgets/grampletpane.py
|
src/gui/widgets/grampletpane.py
|
||||||
src/gui/widgets/labels.py
|
src/gui/widgets/labels.py
|
||||||
src/gui/widgets/monitoredwidgets.py
|
src/gui/widgets/monitoredwidgets.py
|
||||||
@ -693,6 +694,7 @@ src/plugins/view/citationlistview.py
|
|||||||
src/plugins/view/eventview.py
|
src/plugins/view/eventview.py
|
||||||
src/plugins/view/familyview.py
|
src/plugins/view/familyview.py
|
||||||
src/plugins/view/fanchartview.py
|
src/plugins/view/fanchartview.py
|
||||||
|
src/plugins/view/fanchartdescview.py
|
||||||
src/plugins/view/geography.gpr.py
|
src/plugins/view/geography.gpr.py
|
||||||
src/plugins/view/geoclose.py
|
src/plugins/view/geoclose.py
|
||||||
src/plugins/view/geoevents.py
|
src/plugins/view/geoevents.py
|
||||||
|
@ -12,6 +12,7 @@ pkgpython_PYTHON = \
|
|||||||
buttons.py \
|
buttons.py \
|
||||||
expandcollapsearrow.py \
|
expandcollapsearrow.py \
|
||||||
fanchart.py \
|
fanchart.py \
|
||||||
|
fanchartdesc.py \
|
||||||
grampletpane.py \
|
grampletpane.py \
|
||||||
labels.py \
|
labels.py \
|
||||||
linkbox.py \
|
linkbox.py \
|
||||||
|
@ -86,7 +86,6 @@ def gender_code(is_male):
|
|||||||
|
|
||||||
PIXELS_PER_GENERATION = 50 # size of radius for generation
|
PIXELS_PER_GENERATION = 50 # size of radius for generation
|
||||||
BORDER_EDGE_WIDTH = 10 # empty white box size at edge to indicate parents
|
BORDER_EDGE_WIDTH = 10 # empty white box size at edge to indicate parents
|
||||||
CENTER = 50 # pixel radius of center
|
|
||||||
CHILDRING_WIDTH = 12 # width of the children ring inside the person
|
CHILDRING_WIDTH = 12 # width of the children ring inside the person
|
||||||
TRANSLATE_PX = 10 # size of the central circle, used to move the chart
|
TRANSLATE_PX = 10 # size of the central circle, used to move the chart
|
||||||
PAD_PX = 4 # padding with edges
|
PAD_PX = 4 # padding with edges
|
||||||
@ -142,9 +141,11 @@ TYPE_BOX_FAMILY = 1
|
|||||||
|
|
||||||
class FanChartBaseWidget(Gtk.DrawingArea):
|
class FanChartBaseWidget(Gtk.DrawingArea):
|
||||||
""" a base widget for fancharts"""
|
""" a base widget for fancharts"""
|
||||||
|
CENTER = 50 # pixel radius of center, changes per fanchart
|
||||||
|
|
||||||
def __init__(self, dbstate, callback_popup=None):
|
def __init__(self, dbstate, callback_popup=None):
|
||||||
GObject.GObject.__init__(self)
|
GObject.GObject.__init__(self)
|
||||||
|
self.radialtext = True
|
||||||
st_cont = self.get_style_context()
|
st_cont = self.get_style_context()
|
||||||
col = st_cont.lookup_color('text_color')
|
col = st_cont.lookup_color('text_color')
|
||||||
if col[0]:
|
if col[0]:
|
||||||
@ -227,9 +228,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
requisition.height = requisition.width
|
requisition.height = requisition.width
|
||||||
elif self.form == FORM_HALFCIRCLE:
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
requisition.width = 2 * self.halfdist()
|
requisition.width = 2 * self.halfdist()
|
||||||
requisition.height = requisition.width / 2 + CENTER + PAD_PX
|
requisition.height = requisition.width / 2 + self.CENTER + PAD_PX
|
||||||
elif self.form == FORM_QUADRANT:
|
elif self.form == FORM_QUADRANT:
|
||||||
requisition.width = self.halfdist() + CENTER + PAD_PX
|
requisition.width = self.halfdist() + self.CENTER + PAD_PX
|
||||||
requisition.height = requisition.width
|
requisition.height = requisition.width
|
||||||
|
|
||||||
def do_get_preferred_width(self):
|
def do_get_preferred_width(self):
|
||||||
@ -293,7 +294,7 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
userdata.append(period)
|
userdata.append(period)
|
||||||
|
|
||||||
def set_userdata_age(self, person, userdata):
|
def set_userdata_age(self, person, userdata):
|
||||||
agecol = (255, 255, 255) # white
|
agecol = (1, 1, 1) # white
|
||||||
if person:
|
if person:
|
||||||
age = get_age(self.dbstate.db, person)
|
age = get_age(self.dbstate.db, person)
|
||||||
if age is not None:
|
if age is not None:
|
||||||
@ -472,6 +473,31 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def draw_radbox(self, cr, radiusin, radiusout, start_rad, stop_rad, color,
|
||||||
|
thick=False):
|
||||||
|
cr.move_to(radiusout * math.cos(start_rad), radiusout * math.sin(start_rad))
|
||||||
|
cr.arc(0, 0, radiusout, start_rad, stop_rad)
|
||||||
|
cr.line_to(radiusin * math.cos(stop_rad), radiusin * math.sin(stop_rad))
|
||||||
|
cr.arc_negative(0, 0, radiusin, stop_rad, start_rad)
|
||||||
|
cr.close_path()
|
||||||
|
##path = cr.copy_path() # not working correct
|
||||||
|
cr.set_source_rgba(color[0], color[1], color[2], color[3])
|
||||||
|
cr.fill()
|
||||||
|
#and again for the border
|
||||||
|
cr.move_to(radiusout * math.cos(start_rad), radiusout * math.sin(start_rad))
|
||||||
|
cr.arc(0, 0, radiusout, start_rad, stop_rad)
|
||||||
|
cr.line_to(radiusin * math.cos(stop_rad), radiusin * math.sin(stop_rad))
|
||||||
|
cr.arc_negative(0, 0, radiusin, stop_rad, start_rad)
|
||||||
|
cr.close_path()
|
||||||
|
##cr.append_path(path) # not working correct
|
||||||
|
cr.set_source_rgb(0, 0, 0) # black
|
||||||
|
if thick:
|
||||||
|
cr.set_line_width(3)
|
||||||
|
else:
|
||||||
|
cr.set_line_width(1)
|
||||||
|
cr.stroke()
|
||||||
|
cr.set_line_width(1)
|
||||||
|
|
||||||
def draw_innerring(self, cr, person, userdata, start, inc):
|
def draw_innerring(self, cr, person, userdata, start, inc):
|
||||||
"""
|
"""
|
||||||
Procedure to draw a person in the inner ring position
|
Procedure to draw a person in the inner ring position
|
||||||
@ -682,10 +708,10 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
elif (self.angle[-2] and
|
elif (self.angle[-2] and
|
||||||
radius < TRANSLATE_PX + CHILDRING_WIDTH):
|
radius < TRANSLATE_PX + CHILDRING_WIDTH):
|
||||||
generation = -2 # indication of one of the children
|
generation = -2 # indication of one of the children
|
||||||
elif radius < CENTER:
|
elif radius < self.CENTER:
|
||||||
generation = 0
|
generation = 0
|
||||||
else:
|
else:
|
||||||
generation = int((radius - CENTER)/self.gen_pixels()) + 1
|
generation = int((radius - self.CENTER)/self.gen_pixels()) + 1
|
||||||
btype = self.boxtype(radius)
|
btype = self.boxtype(radius)
|
||||||
|
|
||||||
rads = math.atan2( (cury - cy), (curx - cx) )
|
rads = math.atan2( (cury - cy), (curx - cx) )
|
||||||
@ -730,6 +756,18 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _have_children(self, person):
|
||||||
|
"""
|
||||||
|
Returns True if a person has children.
|
||||||
|
TODO: is there no util function for this
|
||||||
|
"""
|
||||||
|
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 on_mouse_down(self, widget, event):
|
def on_mouse_down(self, widget, event):
|
||||||
self.translating = False # keep track of up/down/left/right movement
|
self.translating = False # keep track of up/down/left/right movement
|
||||||
generation, selected, btype = self.person_under_cursor(event.x, event.y)
|
generation, selected, btype = self.person_under_cursor(event.x, event.y)
|
||||||
@ -785,9 +823,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
if self.form == FORM_CIRCLE:
|
if self.form == FORM_CIRCLE:
|
||||||
self.center_xy = w/2 - event.x, h/2 - event.y
|
self.center_xy = w/2 - event.x, h/2 - event.y
|
||||||
elif self.form == FORM_HALFCIRCLE:
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
self.center_xy = w/2 - event.x, h - CENTER - PAD_PX - event.y
|
self.center_xy = w/2 - event.x, h - self.CENTER - PAD_PX - event.y
|
||||||
elif self.form == FORM_QUADRANT:
|
elif self.form == FORM_QUADRANT:
|
||||||
self.center_xy = CENTER + PAD_PX - event.x, h - CENTER - PAD_PX - event.y
|
self.center_xy = self.CENTER + PAD_PX - event.x, h - self.CENTER - PAD_PX - event.y
|
||||||
else:
|
else:
|
||||||
cx = w/2 - self.center_xy[0]
|
cx = w/2 - self.center_xy[0]
|
||||||
cy = h/2 - self.center_xy[1]
|
cy = h/2 - self.center_xy[1]
|
||||||
@ -826,9 +864,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
self.center_xy = w/2 - event.x, h/2 - event.y
|
self.center_xy = w/2 - event.x, h/2 - event.y
|
||||||
self.center_xy = w/2 - event.x, h/2 - event.y
|
self.center_xy = w/2 - event.x, h/2 - event.y
|
||||||
elif self.form == FORM_HALFCIRCLE:
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
self.center_xy = w/2 - event.x, h - CENTER - PAD_PX - event.y
|
self.center_xy = w/2 - event.x, h - self.CENTER - PAD_PX - event.y
|
||||||
elif self.form == FORM_QUADRANT:
|
elif self.form == FORM_QUADRANT:
|
||||||
self.center_xy = CENTER + PAD_PX - event.x, h - CENTER - PAD_PX - event.y
|
self.center_xy = self.CENTER + PAD_PX - event.x, h - self.CENTER - PAD_PX - event.y
|
||||||
|
|
||||||
self.last_x, self.last_y = None, None
|
self.last_x, self.last_y = None, None
|
||||||
self.queue_draw()
|
self.queue_draw()
|
||||||
@ -1013,18 +1051,6 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
return not m is f is None
|
return not m is f is None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _have_children(self, person):
|
|
||||||
"""
|
|
||||||
Returns True if a person has children.
|
|
||||||
TODO: is there no util function for this
|
|
||||||
"""
|
|
||||||
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, father):
|
def _get_parent(self, person, father):
|
||||||
"""
|
"""
|
||||||
Get the father of the family if father == True, otherwise mother
|
Get the father of the family if father == True, otherwise mother
|
||||||
@ -1069,7 +1095,7 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
Compute the half radius of the circle
|
Compute the half radius of the circle
|
||||||
"""
|
"""
|
||||||
nrgen = self.nrgen()
|
nrgen = self.nrgen()
|
||||||
return PIXELS_PER_GENERATION * nrgen + CENTER + BORDER_EDGE_WIDTH
|
return PIXELS_PER_GENERATION * nrgen + self.CENTER + BORDER_EDGE_WIDTH
|
||||||
|
|
||||||
def people_generator(self):
|
def people_generator(self):
|
||||||
"""
|
"""
|
||||||
@ -1101,9 +1127,9 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
if self.form == FORM_CIRCLE:
|
if self.form == FORM_CIRCLE:
|
||||||
self.set_size_request(2 * halfdist, 2 * halfdist)
|
self.set_size_request(2 * halfdist, 2 * halfdist)
|
||||||
elif self.form == FORM_HALFCIRCLE:
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
self.set_size_request(2 * halfdist, halfdist + CENTER + PAD_PX)
|
self.set_size_request(2 * halfdist, halfdist + self.CENTER + PAD_PX)
|
||||||
elif self.form == FORM_QUADRANT:
|
elif self.form == FORM_QUADRANT:
|
||||||
self.set_size_request(halfdist + CENTER + PAD_PX, halfdist + CENTER + PAD_PX)
|
self.set_size_request(halfdist + self.CENTER + PAD_PX, halfdist + self.CENTER + PAD_PX)
|
||||||
|
|
||||||
#obtain the allocation
|
#obtain the allocation
|
||||||
alloc = self.get_allocation()
|
alloc = self.get_allocation()
|
||||||
@ -1117,10 +1143,10 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
self.center_y = h/2 - self.center_xy[1]
|
self.center_y = h/2 - self.center_xy[1]
|
||||||
elif self.form == FORM_HALFCIRCLE:
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
self.center_x = w/2. - self.center_xy[0]
|
self.center_x = w/2. - self.center_xy[0]
|
||||||
self.center_y = h - CENTER - PAD_PX- self.center_xy[1]
|
self.center_y = h - self.CENTER - PAD_PX- self.center_xy[1]
|
||||||
elif self.form == FORM_QUADRANT:
|
elif self.form == FORM_QUADRANT:
|
||||||
self.center_x = CENTER + PAD_PX - self.center_xy[0]
|
self.center_x = self.CENTER + PAD_PX - self.center_xy[0]
|
||||||
self.center_y = h - CENTER - PAD_PX - self.center_xy[1]
|
self.center_y = h - self.CENTER - PAD_PX - self.center_xy[1]
|
||||||
cr.translate(self.center_x, self.center_y)
|
cr.translate(self.center_x, self.center_y)
|
||||||
|
|
||||||
cr.save()
|
cr.save()
|
||||||
@ -1137,17 +1163,17 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
person, userdata)
|
person, userdata)
|
||||||
cr.set_source_rgb(1, 1, 1) # white
|
cr.set_source_rgb(1, 1, 1) # white
|
||||||
cr.move_to(0,0)
|
cr.move_to(0,0)
|
||||||
cr.arc(0, 0, CENTER, 0, 2 * math.pi)
|
cr.arc(0, 0, self.CENTER, 0, 2 * math.pi)
|
||||||
cr.fill()
|
cr.fill()
|
||||||
cr.set_source_rgb(0, 0, 0) # black
|
cr.set_source_rgb(0, 0, 0) # black
|
||||||
cr.arc(0, 0, CENTER, 0, 2 * math.pi)
|
cr.arc(0, 0, self.CENTER, 0, 2 * math.pi)
|
||||||
cr.stroke()
|
cr.stroke()
|
||||||
cr.restore()
|
cr.restore()
|
||||||
# Draw center person:
|
# Draw center person:
|
||||||
(text, person, parents, child, userdata) = self.data[0][0]
|
(text, person, parents, child, userdata) = self.data[0][0]
|
||||||
if person:
|
if person:
|
||||||
r, g, b, a = self.background_box(person, 0, userdata)
|
r, g, b, a = self.background_box(person, 0, userdata)
|
||||||
cr.arc(0, 0, CENTER, 0, 2 * math.pi)
|
cr.arc(0, 0, self.CENTER, 0, 2 * math.pi)
|
||||||
if self.childring and child:
|
if self.childring and child:
|
||||||
cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 2 * math.pi, 0)
|
cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 2 * math.pi, 0)
|
||||||
cr.close_path()
|
cr.close_path()
|
||||||
@ -1155,8 +1181,8 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
cr.fill()
|
cr.fill()
|
||||||
cr.save()
|
cr.save()
|
||||||
name = name_displayer.display(person)
|
name = name_displayer.display(person)
|
||||||
self.draw_text(cr, name, CENTER -
|
self.draw_text(cr, name, self.CENTER -
|
||||||
(CENTER - (CHILDRING_WIDTH + TRANSLATE_PX))/2, 95, 455,
|
(self.CENTER - (CHILDRING_WIDTH + TRANSLATE_PX))/2, 95, 455,
|
||||||
10, False,
|
10, False,
|
||||||
self.fontcolor(r, g, b, a), self.fontbold(a))
|
self.fontcolor(r, g, b, a), self.fontbold(a))
|
||||||
cr.restore()
|
cr.restore()
|
||||||
@ -1184,7 +1210,7 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
start_rad = start * math.pi/180
|
start_rad = start * math.pi/180
|
||||||
stop_rad = stop * math.pi/180
|
stop_rad = stop * math.pi/180
|
||||||
r, g, b, a = self.background_box(person, generation, userdata)
|
r, g, b, a = self.background_box(person, generation, userdata)
|
||||||
radius = generation * PIXELS_PER_GENERATION + CENTER
|
radius = generation * PIXELS_PER_GENERATION + self.CENTER
|
||||||
# If max generation, and they have parents:
|
# If max generation, and they have parents:
|
||||||
if generation == self.generations - 1 and parents:
|
if generation == self.generations - 1 and parents:
|
||||||
# draw an indicator
|
# draw an indicator
|
||||||
|
704
src/gui/widgets/fanchartdesc.py
Normal file
704
src/gui/widgets/fanchartdesc.py
Normal file
@ -0,0 +1,704 @@
|
|||||||
|
#
|
||||||
|
# Gramps - a GTK+/GNOME based genealogy program
|
||||||
|
#
|
||||||
|
# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
|
||||||
|
# Copyright (C) 2009 Douglas S. Blank
|
||||||
|
# Copyright (C) 2012 Benny Malengier
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Python modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
from gi.repository import Pango
|
||||||
|
from gi.repository import GObject
|
||||||
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import PangoCairo
|
||||||
|
import cairo
|
||||||
|
import math
|
||||||
|
import colorsys
|
||||||
|
import cPickle as pickle
|
||||||
|
from cgi import escape
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# GRAMPS modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
from gen.display.name import displayer as name_displayer
|
||||||
|
from gen.errors import WindowActiveError
|
||||||
|
from gui.editors import EditPerson, EditFamily
|
||||||
|
import gen.lib
|
||||||
|
import gui.utils
|
||||||
|
from gui.ddtargets import DdTargets
|
||||||
|
from gen.utils.alive import probably_alive
|
||||||
|
from gen.utils.libformatting import FormattingHelper
|
||||||
|
from gen.utils.db import (find_children, find_parents, find_witnessed_people,
|
||||||
|
get_age, get_timeperiod)
|
||||||
|
from gen.plug.report.utils import find_spouse
|
||||||
|
from gui.widgets.fanchart import *
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Constants
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
pi = math.pi
|
||||||
|
|
||||||
|
PIXELS_PER_GENPERSON = 30 # size of radius for generation of children
|
||||||
|
PIXELS_PER_GENFAMILY = 20 # size of radius for family
|
||||||
|
PIXELS_PER_RECLAIM = 4 # size of the radius of pixels taken from family to reclaim space
|
||||||
|
PARENTRING_WIDTH = 12 # width of the parent ring inside the person
|
||||||
|
|
||||||
|
ANGLE_CHEQUI = 0 #Algorithm with homogeneous children distribution
|
||||||
|
ANGLE_WEIGHT = 1 #Algorithm for angle computation based on nr of descendants
|
||||||
|
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# FanChartDescWidget
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class FanChartDescWidget(FanChartBaseWidget):
|
||||||
|
"""
|
||||||
|
Interactive Fan Chart Widget.
|
||||||
|
"""
|
||||||
|
CENTER = 60 # we require a larger center
|
||||||
|
|
||||||
|
def __init__(self, dbstate, callback_popup=None):
|
||||||
|
"""
|
||||||
|
Fan Chart Widget. Handles visualization of data in self.data.
|
||||||
|
See main() of FanChartGramplet for example of model format.
|
||||||
|
"""
|
||||||
|
self.set_values(None, 9, BACKGROUND_GRAD_GEN, 'Sans', '#0000FF',
|
||||||
|
'#FF0000', None, 0.5, FORM_CIRCLE, ANGLE_WEIGHT)
|
||||||
|
FanChartBaseWidget.__init__(self, dbstate, callback_popup)
|
||||||
|
|
||||||
|
def set_values(self, root_person_handle, maxgen, background,
|
||||||
|
fontdescr, grad_start, grad_end,
|
||||||
|
filter, alpha_filter, form, angle_algo):
|
||||||
|
"""
|
||||||
|
Reset the values to be used:
|
||||||
|
root_person_handle = person to show
|
||||||
|
maxgen = maximum generations to show
|
||||||
|
background = config setting of which background procedure to use (int)
|
||||||
|
fontdescr = string describing the font to use
|
||||||
|
grad_start, grad_end: colors to use for background procedure
|
||||||
|
filter = the person filter to apply to the people in the chart
|
||||||
|
alpha = the alpha transparency value (0-1) to apply to filtered out data
|
||||||
|
form = the FORM_ constant for the fanchart
|
||||||
|
"""
|
||||||
|
self.rootpersonh = root_person_handle
|
||||||
|
self.generations = maxgen
|
||||||
|
self.background = background
|
||||||
|
self.fontdescr = fontdescr
|
||||||
|
self.grad_start = grad_start
|
||||||
|
self.grad_end = grad_end
|
||||||
|
self.filter = filter
|
||||||
|
self.alpha_filter = alpha_filter
|
||||||
|
self.form = form
|
||||||
|
self.anglealgo = angle_algo
|
||||||
|
|
||||||
|
def gen_pixels(self):
|
||||||
|
"""
|
||||||
|
how many pixels a generation takes up in the fanchart
|
||||||
|
"""
|
||||||
|
return PIXELS_PER_GENPERSON + PIXELS_PER_GENFAMILY
|
||||||
|
|
||||||
|
def set_generations(self):
|
||||||
|
"""
|
||||||
|
Set the generations to max, and fill data structures with initial data.
|
||||||
|
"""
|
||||||
|
self.handle2desc = {}
|
||||||
|
self.famhandle2desc = {}
|
||||||
|
self.handle2fam = {}
|
||||||
|
self.gen2people = {}
|
||||||
|
self.gen2fam = {}
|
||||||
|
self.parentsroot = []
|
||||||
|
self.gen2people[0] = [(None, False, 0, 2*pi, '', 0, 0, [], NORMAL)] #no center person
|
||||||
|
self.gen2fam[0] = [] #no families
|
||||||
|
self.angle = {}
|
||||||
|
self.angle[-2] = []
|
||||||
|
for i in range(1, self.generations-1):
|
||||||
|
self.gen2fam[i] = []
|
||||||
|
self.gen2people[i] = []
|
||||||
|
self.gen2people[self.generations-1] = [] #indication of more children
|
||||||
|
self.rotfactor = 1
|
||||||
|
self.rotstartangle = 0
|
||||||
|
if self.form == FORM_HALFCIRCLE:
|
||||||
|
self.rotfactor = 1/2
|
||||||
|
self.rotangle = 90
|
||||||
|
elif self.form == FORM_QUADRANT:
|
||||||
|
self.rotangle = 180
|
||||||
|
self.rotfactor = 1/4
|
||||||
|
|
||||||
|
def _fill_data_structures(self):
|
||||||
|
self.set_generations()
|
||||||
|
person = self.dbstate.db.get_person_from_handle(self.rootpersonh)
|
||||||
|
if not person:
|
||||||
|
#nothing to do, just return
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
name = name_displayer.display(person)
|
||||||
|
|
||||||
|
# person, duplicate or not, start angle, slice size,
|
||||||
|
# text, parent pos in fam, nrfam, userdata, status
|
||||||
|
self.gen2people[0] = [[person, False, 0, 2*pi, name, 0, 0, [], NORMAL]]
|
||||||
|
self.handle2desc[self.rootpersonh] = 0
|
||||||
|
# fill in data for the parents
|
||||||
|
self.parentsroot = []
|
||||||
|
handleparents = []
|
||||||
|
family_handle_list = person.get_parent_family_handle_list()
|
||||||
|
if family_handle_list:
|
||||||
|
for family_handle in family_handle_list:
|
||||||
|
family = self.dbstate.db.get_family_from_handle(family_handle)
|
||||||
|
if not family:
|
||||||
|
continue
|
||||||
|
hfather = family.get_father_handle()
|
||||||
|
if hfather and hfather not in handleparents:
|
||||||
|
father = self.dbstate.db.get_person_from_handle(hfather)
|
||||||
|
if father:
|
||||||
|
self.parentsroot.append((father, []))
|
||||||
|
handleparents.append(hfather)
|
||||||
|
hmother = family.get_mother_handle()
|
||||||
|
if hmother and hmother not in handleparents:
|
||||||
|
mother = self.dbstate.db.get_person_from_handle(hmother)
|
||||||
|
if mother:
|
||||||
|
self.parentsroot.append((mother, []))
|
||||||
|
handleparents.append(hmother)
|
||||||
|
|
||||||
|
#recursively fill in the datastructures:
|
||||||
|
nrdesc = self.__rec_fill_data(0, person, 0)
|
||||||
|
self.handle2desc[person.handle] += nrdesc
|
||||||
|
self.__compute_angles()
|
||||||
|
|
||||||
|
def __rec_fill_data(self, gen, person, pos):
|
||||||
|
"""
|
||||||
|
Recursively fill in the data
|
||||||
|
"""
|
||||||
|
totdesc = 0
|
||||||
|
nrfam = len(person.get_family_handle_list())
|
||||||
|
self.gen2people[gen][pos][6] = nrfam
|
||||||
|
for family_handle in person.get_family_handle_list():
|
||||||
|
totdescfam = 0
|
||||||
|
family = self.dbstate.db.get_family_from_handle(family_handle)
|
||||||
|
|
||||||
|
spouse_handle = find_spouse(person, family)
|
||||||
|
if spouse_handle:
|
||||||
|
spouse = self.dbstate.db.get_person_from_handle(spouse_handle)
|
||||||
|
spname = name_displayer.display(spouse)
|
||||||
|
else:
|
||||||
|
spname = ''
|
||||||
|
if family_handle in self.famhandle2desc:
|
||||||
|
#family occurs via father and via mother in the chart, only
|
||||||
|
#first to show and count.
|
||||||
|
famdup = True
|
||||||
|
else:
|
||||||
|
famdup = False
|
||||||
|
# family, duplicate or not, start angle, slice size,
|
||||||
|
# text, spouse pos in gen, nrchildren, userdata, parnter, status
|
||||||
|
self.gen2fam[gen].append([family, famdup, 0, 0, spname, pos, 0, [],
|
||||||
|
spouse, NORMAL])
|
||||||
|
posfam = len(self.gen2fam[gen]) - 1
|
||||||
|
|
||||||
|
if not famdup:
|
||||||
|
nrchild = len(family.get_child_ref_list())
|
||||||
|
self.gen2fam[gen][-1][6] = nrchild
|
||||||
|
for child_ref in family.get_child_ref_list():
|
||||||
|
child = self.dbstate.db.get_person_from_handle(child_ref.ref)
|
||||||
|
chname = name_displayer.display(child)
|
||||||
|
if child_ref.ref in self.handle2desc:
|
||||||
|
dup = True
|
||||||
|
else:
|
||||||
|
dup = False
|
||||||
|
self.handle2desc[child_ref.ref] = 0
|
||||||
|
# person, duplicate or not, start angle, slice size,
|
||||||
|
# text, parent pos in fam, nrfam, userdata, status
|
||||||
|
self.gen2people[gen+1].append([child, dup, 0, 0, chname,
|
||||||
|
posfam, 0, [], NORMAL])
|
||||||
|
totdescfam += 1 #add this person as descendant
|
||||||
|
pospers = len(self.gen2people[gen+1]) - 1
|
||||||
|
if not dup and not(self.generations == gen+2):
|
||||||
|
nrdesc = self.__rec_fill_data(gen+1, child, pospers)
|
||||||
|
self.handle2desc[child_ref.ref] += nrdesc
|
||||||
|
totdescfam += nrdesc # add children of him as descendants
|
||||||
|
self.famhandle2desc[family_handle] = totdescfam
|
||||||
|
totdesc += totdescfam
|
||||||
|
return totdesc
|
||||||
|
|
||||||
|
def __compute_angles(self):
|
||||||
|
"""
|
||||||
|
Compute the angles of the boxes
|
||||||
|
"""
|
||||||
|
#first we compute the size of the slice.
|
||||||
|
nrgen = self.nrgen()
|
||||||
|
#set angles root person
|
||||||
|
if self.form == FORM_CIRCLE:
|
||||||
|
slice = 2*pi
|
||||||
|
start = 0.
|
||||||
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
|
slice = pi
|
||||||
|
start = pi/2
|
||||||
|
elif self.form == FORM_QUADRANT:
|
||||||
|
slice = pi/2
|
||||||
|
start = pi
|
||||||
|
gen = 0
|
||||||
|
data = self.gen2people[gen][0]
|
||||||
|
data[2] = start
|
||||||
|
data[3] = slice
|
||||||
|
for gen in range(1, nrgen):
|
||||||
|
nrpeople = len(self.gen2people[gen])
|
||||||
|
prevpartnerdatahandle = None
|
||||||
|
offset = 0
|
||||||
|
for data in self.gen2fam[gen-1]:
|
||||||
|
#obtain start and stop of partner
|
||||||
|
partnerdata = self.gen2people[gen-1][data[5]]
|
||||||
|
nrdescfam = self.famhandle2desc[data[0].handle]
|
||||||
|
nrdescpartner = self.handle2desc[partnerdata[0].handle]
|
||||||
|
nrfam = partnerdata[6]
|
||||||
|
partstart = partnerdata[2]
|
||||||
|
partslice = partnerdata[3]
|
||||||
|
if prevpartnerdatahandle != partnerdata[0].handle:
|
||||||
|
#reset the offset
|
||||||
|
offset = 0
|
||||||
|
prevpartnerdatahandle = partnerdata[0].handle
|
||||||
|
slice = partslice/(nrdescpartner+nrfam)*(nrdescfam+1)
|
||||||
|
if data[9] == COLLAPSED:
|
||||||
|
slice = 0
|
||||||
|
elif data[9] == EXPANDED:
|
||||||
|
slice = partslice
|
||||||
|
|
||||||
|
data[2] = partstart + offset
|
||||||
|
data[3] = slice
|
||||||
|
offset += slice
|
||||||
|
|
||||||
|
## if nrdescpartner == 0:
|
||||||
|
## #no offspring, draw as large as fraction of
|
||||||
|
## #nr families
|
||||||
|
## nrfam = partnerdata[6]
|
||||||
|
## slice = partslice/nrfam
|
||||||
|
## data[2] = partstart + offset
|
||||||
|
## data[3] = slice
|
||||||
|
## offset += slice
|
||||||
|
## elif nrdescfam == 0:
|
||||||
|
## #no offspring this family, but there is another
|
||||||
|
## #family. We draw this as a weight of 1
|
||||||
|
## nrfam = partnerdata[6]
|
||||||
|
## slice = partslice/(nrdescpartner + nrfam - 1)*(nrdescfam+1)
|
||||||
|
## data[2] = partstart + offset
|
||||||
|
## data[3] = slice
|
||||||
|
## offset += slice
|
||||||
|
## else:
|
||||||
|
## #this family has offspring. We give it space for it's
|
||||||
|
## #weight in offspring
|
||||||
|
## nrfam = partnerdata[6]
|
||||||
|
## slice = partslice/(nrdescpartner + nrfam - 1)*(nrdescfam+1)
|
||||||
|
## data[2] = partstart + offset
|
||||||
|
## data[3] = slice
|
||||||
|
## offset += slice
|
||||||
|
|
||||||
|
prevfamdatahandle = None
|
||||||
|
offset = 0
|
||||||
|
for data in self.gen2people[gen]:
|
||||||
|
#obtain start and stop of family this is child of
|
||||||
|
parentfamdata = self.gen2fam[gen-1][data[5]]
|
||||||
|
nrdescfam = self.famhandle2desc[parentfamdata[0].handle]
|
||||||
|
nrdesc = self.handle2desc[data[0].handle]
|
||||||
|
famstart = parentfamdata[2]
|
||||||
|
famslice = parentfamdata[3]
|
||||||
|
nrchild = parentfamdata[6]
|
||||||
|
#now we divide this slice to the weight of children,
|
||||||
|
#adding one for every child
|
||||||
|
if self.anglealgo == ANGLE_CHEQUI:
|
||||||
|
slice = famslice / nrchild
|
||||||
|
elif self.anglealgo == ANGLE_WEIGHT:
|
||||||
|
slice = famslice/(nrdescfam) * (nrdesc + 1)
|
||||||
|
else:
|
||||||
|
print self.anglealgo == ANGLE_WEIGHT,self.anglealgo, ANGLE_WEIGHT
|
||||||
|
raise NotImplementedError, 'Unknown angle algorithm %d' % self.anglealgo
|
||||||
|
if prevfamdatahandle != parentfamdata[0].handle:
|
||||||
|
#reset the offset
|
||||||
|
offset = 0
|
||||||
|
prevfamdatahandle = parentfamdata[0].handle
|
||||||
|
if data[8] == COLLAPSED:
|
||||||
|
slice = 0
|
||||||
|
elif data[8] == EXPANDED:
|
||||||
|
slice = famslice
|
||||||
|
data[2] = famstart + offset
|
||||||
|
data[3] = slice
|
||||||
|
offset += slice
|
||||||
|
|
||||||
|
def nrgen(self):
|
||||||
|
#compute the number of generations present
|
||||||
|
nrgen = None
|
||||||
|
for gen in range(self.generations - 1, 0, -1):
|
||||||
|
if len(self.gen2people[gen]) > 0:
|
||||||
|
nrgen = gen + 1
|
||||||
|
break
|
||||||
|
if nrgen is None:
|
||||||
|
nrgen = 1
|
||||||
|
return nrgen
|
||||||
|
|
||||||
|
def halfdist(self):
|
||||||
|
"""
|
||||||
|
Compute the half radius of the circle
|
||||||
|
"""
|
||||||
|
nrgen = self.nrgen()
|
||||||
|
ringpxs = (PIXELS_PER_GENPERSON + PIXELS_PER_GENFAMILY) * (nrgen - 1)
|
||||||
|
return ringpxs + self.CENTER + BORDER_EDGE_WIDTH
|
||||||
|
|
||||||
|
def people_generator(self):
|
||||||
|
"""
|
||||||
|
a generator over all people outside of the core person
|
||||||
|
"""
|
||||||
|
for generation in range(self.generations):
|
||||||
|
for data in self.gen2people[generation]:
|
||||||
|
yield (data[0], data[7])
|
||||||
|
for generation in range(self.generations-1):
|
||||||
|
for data in self.gen2fam[generation]:
|
||||||
|
yield (data[8], data[7])
|
||||||
|
|
||||||
|
def innerpeople_generator(self):
|
||||||
|
"""
|
||||||
|
a generator over all people inside of the core person
|
||||||
|
"""
|
||||||
|
for parentdata in self.parentsroot:
|
||||||
|
parent, userdata = parentdata
|
||||||
|
yield (parent, userdata)
|
||||||
|
|
||||||
|
def on_draw(self, widget, cr, scale=1.):
|
||||||
|
"""
|
||||||
|
The main method to do the drawing.
|
||||||
|
If widget is given, we assume we draw in GTK3 and use the allocation.
|
||||||
|
To draw raw on the cairo context cr, set widget=None.
|
||||||
|
"""
|
||||||
|
# first do size request of what we will need
|
||||||
|
halfdist = self.halfdist()
|
||||||
|
if widget:
|
||||||
|
if self.form == FORM_CIRCLE:
|
||||||
|
self.set_size_request(2 * halfdist, 2 * halfdist)
|
||||||
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
|
self.set_size_request(2 * halfdist, halfdist + self.CENTER
|
||||||
|
+ PAD_PX)
|
||||||
|
elif self.form == FORM_QUADRANT:
|
||||||
|
self.set_size_request(halfdist + self.CENTER + PAD_PX,
|
||||||
|
halfdist + self.CENTER + PAD_PX)
|
||||||
|
|
||||||
|
#obtain the allocation
|
||||||
|
alloc = self.get_allocation()
|
||||||
|
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
|
||||||
|
|
||||||
|
cr.scale(scale, scale)
|
||||||
|
# when printing, we need not recalculate
|
||||||
|
if widget:
|
||||||
|
if self.form == FORM_CIRCLE:
|
||||||
|
self.center_x = w/2 - self.center_xy[0]
|
||||||
|
self.center_y = h/2 - self.center_xy[1]
|
||||||
|
elif self.form == FORM_HALFCIRCLE:
|
||||||
|
self.center_x = w/2. - self.center_xy[0]
|
||||||
|
self.center_y = h - self.CENTER - PAD_PX- self.center_xy[1]
|
||||||
|
elif self.form == FORM_QUADRANT:
|
||||||
|
self.center_x = self.CENTER + PAD_PX - self.center_xy[0]
|
||||||
|
self.center_y = h - self.CENTER - PAD_PX - self.center_xy[1]
|
||||||
|
cr.translate(self.center_x, self.center_y)
|
||||||
|
|
||||||
|
cr.save()
|
||||||
|
#draw center
|
||||||
|
cr.set_source_rgb(1, 1, 1) # white
|
||||||
|
cr.move_to(0,0)
|
||||||
|
cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi)
|
||||||
|
cr.fill()
|
||||||
|
cr.set_source_rgb(0, 0, 0) # black
|
||||||
|
cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi)
|
||||||
|
cr.stroke()
|
||||||
|
cr.restore()
|
||||||
|
# Draw center person:
|
||||||
|
(person, dup, start, slice, text, parentfampos, nrfam, userdata, status) \
|
||||||
|
= self.gen2people[0][0]
|
||||||
|
if person:
|
||||||
|
r, g, b, a = self.background_box(person, 0, userdata)
|
||||||
|
cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi)
|
||||||
|
if self.parentsroot:
|
||||||
|
cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH,
|
||||||
|
2 * math.pi, 0)
|
||||||
|
cr.close_path()
|
||||||
|
cr.set_source_rgba(r/255, g/255, b/255, a)
|
||||||
|
cr.fill()
|
||||||
|
cr.save()
|
||||||
|
name = name_displayer.display(person)
|
||||||
|
self.draw_text(cr, name, self.CENTER - PIXELS_PER_GENFAMILY
|
||||||
|
- (self.CENTER - PIXELS_PER_GENFAMILY
|
||||||
|
- (CHILDRING_WIDTH + TRANSLATE_PX))/2,
|
||||||
|
95, 455, 10, False,
|
||||||
|
self.fontcolor(r, g, b, a), self.fontbold(a))
|
||||||
|
cr.restore()
|
||||||
|
#draw center to move chart
|
||||||
|
cr.set_source_rgb(0, 0, 0) # black
|
||||||
|
cr.move_to(TRANSLATE_PX, 0)
|
||||||
|
cr.arc(0, 0, TRANSLATE_PX, 0, 2 * math.pi)
|
||||||
|
if self.parentsroot: # has at least one parent
|
||||||
|
cr.fill()
|
||||||
|
self.draw_parentring(cr)
|
||||||
|
else:
|
||||||
|
cr.stroke()
|
||||||
|
#now write all the families and children
|
||||||
|
cr.save()
|
||||||
|
cr.rotate(self.rotate_value * math.pi/180)
|
||||||
|
radstart = self.CENTER - PIXELS_PER_GENFAMILY - PIXELS_PER_GENPERSON
|
||||||
|
for gen in range(self.generations-1):
|
||||||
|
radstart += PIXELS_PER_GENPERSON
|
||||||
|
for famdata in self.gen2fam[gen]:
|
||||||
|
# family, duplicate or not, start angle, slice size,
|
||||||
|
# text, spouse pos in gen, nrchildren, userdata, status
|
||||||
|
fam, dup, start, slice, text, posfam, nrchild, userdata,\
|
||||||
|
partner, status = famdata
|
||||||
|
if status != COLLAPSED:
|
||||||
|
self.draw_person(cr, text, start, slice, radstart,
|
||||||
|
radstart + PIXELS_PER_GENFAMILY, gen, dup,
|
||||||
|
partner, userdata, family=True, thick=status != NORMAL)
|
||||||
|
radstart += PIXELS_PER_GENFAMILY
|
||||||
|
for pdata in self.gen2people[gen+1]:
|
||||||
|
# person, duplicate or not, start angle, slice size,
|
||||||
|
# text, parent pos in fam, nrfam, userdata, status
|
||||||
|
pers, dup, start, slice, text, pospar, nrfam, userdata, status = \
|
||||||
|
pdata
|
||||||
|
if status != COLLAPSED:
|
||||||
|
self.draw_person(cr, text, start, slice, radstart,
|
||||||
|
radstart + PIXELS_PER_GENPERSON, gen+1, dup,
|
||||||
|
pers, userdata, thick=status != NORMAL)
|
||||||
|
cr.restore()
|
||||||
|
|
||||||
|
if self.background in [BACKGROUND_GRAD_AGE, BACKGROUND_GRAD_PERIOD]:
|
||||||
|
self.draw_gradient(cr, widget, halfdist)
|
||||||
|
|
||||||
|
def draw_person(self, cr, name, start_rad, slice, radius, radiusend,
|
||||||
|
generation, dup, person, userdata, family=False, thick=False):
|
||||||
|
"""
|
||||||
|
Display the piece of pie for a given person. start_rad and slice
|
||||||
|
are in radial.
|
||||||
|
"""
|
||||||
|
if slice == 0:
|
||||||
|
return
|
||||||
|
cr.save()
|
||||||
|
full = False
|
||||||
|
if abs(slice - 2*pi) < 1e-6:
|
||||||
|
full = True
|
||||||
|
stop_rad = start_rad + slice
|
||||||
|
if not dup:
|
||||||
|
r, g, b, a = self.background_box(person, generation, userdata)
|
||||||
|
else:
|
||||||
|
#duplicate color
|
||||||
|
a = 1
|
||||||
|
r, g, b = (0.2, 0.2, 0.2)
|
||||||
|
# If max generation, and they have children:
|
||||||
|
if (not family and generation == self.generations - 1
|
||||||
|
and self._have_children(person)):
|
||||||
|
# draw an indicator
|
||||||
|
radmax = radiusend + BORDER_EDGE_WIDTH
|
||||||
|
cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad))
|
||||||
|
cr.arc(0, 0, radmax, start_rad, stop_rad)
|
||||||
|
cr.line_to(radiusend*math.cos(stop_rad), radiusend*math.sin(stop_rad))
|
||||||
|
cr.arc_negative(0, 0, radiusend, stop_rad, start_rad)
|
||||||
|
cr.close_path()
|
||||||
|
##path = cr.copy_path() # not working correct
|
||||||
|
cr.set_source_rgb(1, 1, 1) # white
|
||||||
|
cr.fill()
|
||||||
|
#and again for the border
|
||||||
|
cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad))
|
||||||
|
cr.arc(0, 0, radmax, start_rad, stop_rad)
|
||||||
|
cr.line_to(radiusend*math.cos(stop_rad), radiusend*math.sin(stop_rad))
|
||||||
|
cr.arc_negative(0, 0, radiusend, stop_rad, start_rad)
|
||||||
|
cr.close_path()
|
||||||
|
##cr.append_path(path) # not working correct
|
||||||
|
cr.set_source_rgb(0, 0, 0) # black
|
||||||
|
cr.stroke()
|
||||||
|
# now draw the person
|
||||||
|
self.draw_radbox(cr, radius, radiusend, start_rad, stop_rad,
|
||||||
|
(r/255, g/255, b/255, a), thick)
|
||||||
|
if self.last_x is None or self.last_y is None:
|
||||||
|
#we are not in a move, so draw text
|
||||||
|
radial = False
|
||||||
|
width = radiusend-radius
|
||||||
|
radstart = radius + width/2
|
||||||
|
spacepolartext = radstart * (stop_rad-start_rad)
|
||||||
|
if spacepolartext < width * 1.1:
|
||||||
|
# more space to print it radial
|
||||||
|
radial = True
|
||||||
|
radstart = radius + 4
|
||||||
|
self.draw_text(cr, name, radstart, start_rad/ math.pi*180,
|
||||||
|
stop_rad/ math.pi*180, width, radial,
|
||||||
|
self.fontcolor(r, g, b, a), self.fontbold(a))
|
||||||
|
cr.restore()
|
||||||
|
|
||||||
|
def boxtype(self, radius):
|
||||||
|
"""
|
||||||
|
default is only one type of box type
|
||||||
|
"""
|
||||||
|
if radius <= self.CENTER:
|
||||||
|
if radius >= self.CENTER - PIXELS_PER_GENFAMILY:
|
||||||
|
return TYPE_BOX_FAMILY
|
||||||
|
else:
|
||||||
|
return TYPE_BOX_NORMAL
|
||||||
|
else:
|
||||||
|
gen = int((radius - self.CENTER)/self.gen_pixels()) + 1
|
||||||
|
radius = (radius - self.CENTER) % PIXELS_PER_GENERATION
|
||||||
|
if radius >= PIXELS_PER_GENPERSON:
|
||||||
|
if gen < self.generations - 1:
|
||||||
|
return TYPE_BOX_FAMILY
|
||||||
|
else:
|
||||||
|
# the last generation has no family boxes
|
||||||
|
None
|
||||||
|
else:
|
||||||
|
return TYPE_BOX_NORMAL
|
||||||
|
|
||||||
|
def draw_parentring(self, cr):
|
||||||
|
cr.move_to(TRANSLATE_PX + CHILDRING_WIDTH, 0)
|
||||||
|
cr.set_source_rgb(0, 0, 0) # black
|
||||||
|
cr.set_line_width(1)
|
||||||
|
cr.arc(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 0, 2 * math.pi)
|
||||||
|
cr.stroke()
|
||||||
|
nrparent = len(self.parentsroot)
|
||||||
|
#Y axis is downward. positve angles are hence clockwise
|
||||||
|
startangle = math.pi
|
||||||
|
if nrparent <= 2:
|
||||||
|
angleinc = math.pi
|
||||||
|
elif nrparent <= 4:
|
||||||
|
angleinc = math.pi/2
|
||||||
|
else:
|
||||||
|
angleinc = 2 * math.pi / nrchild
|
||||||
|
for data in self.parentsroot:
|
||||||
|
self.draw_innerring(cr, data[0], data[1], startangle, angleinc)
|
||||||
|
startangle += angleinc
|
||||||
|
|
||||||
|
def personpos_at_angle(self, generation, angledeg, btype):
|
||||||
|
"""
|
||||||
|
returns the person in generation generation at angle.
|
||||||
|
"""
|
||||||
|
angle = angledeg / 360 * 2 * pi
|
||||||
|
selected = None
|
||||||
|
if btype == TYPE_BOX_NORMAL:
|
||||||
|
for p, pdata in enumerate(self.gen2people[generation]):
|
||||||
|
# person, duplicate or not, start angle, slice size,
|
||||||
|
# text, parent pos in fam, nrfam, userdata, status
|
||||||
|
start = pdata[2]
|
||||||
|
stop = start + pdata[3]
|
||||||
|
if start <= angle <= stop:
|
||||||
|
selected = p
|
||||||
|
break
|
||||||
|
elif btype == TYPE_BOX_FAMILY:
|
||||||
|
for p, pdata in enumerate(self.gen2fam[generation]):
|
||||||
|
# person, duplicate or not, start angle, slice size,
|
||||||
|
# text, parent pos in fam, nrfam, userdata, status
|
||||||
|
start = pdata[2]
|
||||||
|
stop = start + pdata[3]
|
||||||
|
if start <= angle <= stop:
|
||||||
|
selected = p
|
||||||
|
break
|
||||||
|
return selected
|
||||||
|
|
||||||
|
def person_at(self, generation, pos, btype):
|
||||||
|
"""
|
||||||
|
returns the person at generation, pos, btype
|
||||||
|
"""
|
||||||
|
if pos is None:
|
||||||
|
return None
|
||||||
|
if generation == -2:
|
||||||
|
person, userdata = self.parentsroot[pos]
|
||||||
|
elif btype == TYPE_BOX_NORMAL:
|
||||||
|
# person, duplicate or not, start angle, slice size,
|
||||||
|
# text, parent pos in fam, nrfam, userdata, status
|
||||||
|
person = self.gen2people[generation][pos][0]
|
||||||
|
elif btype == TYPE_BOX_FAMILY:
|
||||||
|
# family, duplicate or not, start angle, slice size,
|
||||||
|
# text, spouse pos in gen, nrchildren, userdata, person, status
|
||||||
|
person = self.gen2fam[generation][pos][8]
|
||||||
|
return person
|
||||||
|
|
||||||
|
def do_mouse_click(self):
|
||||||
|
# no drag occured, expand or collapse the section
|
||||||
|
self.change_slice(self._mouse_click_gen, self._mouse_click_sel,
|
||||||
|
self._mouse_click_btype)
|
||||||
|
self._mouse_click = False
|
||||||
|
self.queue_draw()
|
||||||
|
|
||||||
|
def change_slice(self, generation, selected, btype):
|
||||||
|
if generation < 1:
|
||||||
|
return
|
||||||
|
if btype == TYPE_BOX_NORMAL:
|
||||||
|
data = self.gen2people[generation][selected]
|
||||||
|
parpos = data[5]
|
||||||
|
status = data[8]
|
||||||
|
if status == NORMAL:
|
||||||
|
#should be expanded, rest collapsed
|
||||||
|
for entry in self.gen2people[generation]:
|
||||||
|
if entry[5] == parpos:
|
||||||
|
entry[8] = COLLAPSED
|
||||||
|
data[8] = EXPANDED
|
||||||
|
else:
|
||||||
|
#is expanded, set back to normal
|
||||||
|
for entry in self.gen2people[generation]:
|
||||||
|
if entry[5] == parpos:
|
||||||
|
entry[8] = NORMAL
|
||||||
|
if btype == TYPE_BOX_FAMILY:
|
||||||
|
data = self.gen2fam[generation][selected]
|
||||||
|
parpos = data[5]
|
||||||
|
status = data[9]
|
||||||
|
if status == NORMAL:
|
||||||
|
#should be expanded, rest collapsed
|
||||||
|
for entry in self.gen2fam[generation]:
|
||||||
|
if entry[5] == parpos:
|
||||||
|
entry[9] = COLLAPSED
|
||||||
|
data[9] = EXPANDED
|
||||||
|
else:
|
||||||
|
#is expanded, set back to normal
|
||||||
|
for entry in self.gen2fam[generation]:
|
||||||
|
if entry[5] == parpos:
|
||||||
|
entry[9] = NORMAL
|
||||||
|
|
||||||
|
self.__compute_angles()
|
||||||
|
|
||||||
|
class FanChartDescGrampsGUI(FanChartGrampsGUI):
|
||||||
|
""" class for functions fanchart GUI elements will need in Gramps
|
||||||
|
"""
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
"""
|
||||||
|
Fill the data structures with the active data. This initializes all
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
root_person_handle = self.get_active('Person')
|
||||||
|
self.fan.set_values(root_person_handle, self.maxgen, self.background,
|
||||||
|
self.fonttype, self.grad_start, self.grad_end,
|
||||||
|
self.generic_filter, self.alpha_filter, self.form,
|
||||||
|
self.angle_algo)
|
||||||
|
self.fan.reset()
|
||||||
|
self.fan.queue_draw()
|
@ -12,6 +12,7 @@ pkgpython_PYTHON = \
|
|||||||
eventview.py \
|
eventview.py \
|
||||||
familyview.py \
|
familyview.py \
|
||||||
fanchartview.py \
|
fanchartview.py \
|
||||||
|
fanchartdescview.py \
|
||||||
geoclose.py \
|
geoclose.py \
|
||||||
geoevents.py \
|
geoevents.py \
|
||||||
geoplaces.py \
|
geoplaces.py \
|
||||||
|
521
src/plugins/view/fanchartdescview.py
Normal file
521
src/plugins/view/fanchartdescview.py
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
# 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
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import Gtk
|
||||||
|
import cairo
|
||||||
|
from gen.ggettext import gettext as _
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# GRAMPS modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
import gen.lib
|
||||||
|
import gui.widgets.fanchart as fanchart
|
||||||
|
import gui.widgets.fanchartdesc as fanchartdesc
|
||||||
|
from gui.views.navigationview import NavigationView
|
||||||
|
from gui.views.bookmarks import PersonBookmarks
|
||||||
|
from gui.utils import SystemFonts
|
||||||
|
|
||||||
|
# the print settings to remember between print sessions
|
||||||
|
PRINT_SETTINGS = None
|
||||||
|
|
||||||
|
class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
|
||||||
|
"""
|
||||||
|
The Gramplet code that realizes the FanChartWidget.
|
||||||
|
"""
|
||||||
|
#settings in the config file
|
||||||
|
CONFIGSETTINGS = (
|
||||||
|
('interface.fanview-maxgen', 9),
|
||||||
|
('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN),
|
||||||
|
('interface.fanview-font', 'Sans'),
|
||||||
|
('interface.fanview-form', fanchart.FORM_CIRCLE),
|
||||||
|
('interface.color-start-grad', '#ef2929'),
|
||||||
|
('interface.color-end-grad', '#3d37e9'),
|
||||||
|
('interface.angle-algorithm', fanchartdesc.ANGLE_WEIGHT),
|
||||||
|
)
|
||||||
|
def __init__(self, pdata, dbstate, uistate, nav_group=0):
|
||||||
|
self.dbstate = dbstate
|
||||||
|
self.uistate = uistate
|
||||||
|
|
||||||
|
NavigationView.__init__(self, _('Descendant Fan Chart'),
|
||||||
|
pdata, dbstate, uistate,
|
||||||
|
dbstate.db.get_bookmarks(),
|
||||||
|
PersonBookmarks,
|
||||||
|
nav_group)
|
||||||
|
fanchartdesc.FanChartDescGrampsGUI.__init__(self, self.on_childmenu_changed)
|
||||||
|
#set needed values
|
||||||
|
self.maxgen = self._config.get('interface.fanview-maxgen')
|
||||||
|
self.background = self._config.get('interface.fanview-background')
|
||||||
|
self.fonttype = self._config.get('interface.fanview-font')
|
||||||
|
|
||||||
|
self.grad_start = self._config.get('interface.color-start-grad')
|
||||||
|
self.grad_end = self._config.get('interface.color-end-grad')
|
||||||
|
self.form = self._config.get('interface.fanview-form')
|
||||||
|
self.angle_algo = self._config.get('interface.angle-algorithm')
|
||||||
|
self.generic_filter = None
|
||||||
|
self.alpha_filter = 0.2
|
||||||
|
|
||||||
|
dbstate.connect('active-changed', self.active_changed)
|
||||||
|
dbstate.connect('database-changed', self.change_db)
|
||||||
|
|
||||||
|
self.additional_uis.append(self.additional_ui())
|
||||||
|
self.allfonts = [x for x in enumerate(SystemFonts().get_system_fonts())]
|
||||||
|
|
||||||
|
def navigation_type(self):
|
||||||
|
return 'Person'
|
||||||
|
|
||||||
|
def build_widget(self):
|
||||||
|
self.set_fan(fanchartdesc.FanChartDescWidget(self.dbstate, self.on_popup))
|
||||||
|
self.scrolledwindow = Gtk.ScrolledWindow(None, None)
|
||||||
|
self.scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC,
|
||||||
|
Gtk.PolicyType.AUTOMATIC)
|
||||||
|
self.fan.show_all()
|
||||||
|
self.scrolledwindow.add_with_viewport(self.fan)
|
||||||
|
|
||||||
|
return self.scrolledwindow
|
||||||
|
|
||||||
|
def get_stock(self):
|
||||||
|
"""
|
||||||
|
The category stock icon
|
||||||
|
"""
|
||||||
|
return 'gramps-pedigree'
|
||||||
|
|
||||||
|
def get_viewtype_stock(self):
|
||||||
|
"""Type of view in category
|
||||||
|
"""
|
||||||
|
return 'gramps-fanchart'
|
||||||
|
|
||||||
|
def additional_ui(self):
|
||||||
|
return '''<ui>
|
||||||
|
<menubar name="MenuBar">
|
||||||
|
<menu action="GoMenu">
|
||||||
|
<placeholder name="CommonGo">
|
||||||
|
<menuitem action="Back"/>
|
||||||
|
<menuitem action="Forward"/>
|
||||||
|
<separator/>
|
||||||
|
<menuitem action="HomePerson"/>
|
||||||
|
<separator/>
|
||||||
|
</placeholder>
|
||||||
|
</menu>
|
||||||
|
<menu action="EditMenu">
|
||||||
|
<placeholder name="CommonEdit">
|
||||||
|
<menuitem action="PrintView"/>
|
||||||
|
</placeholder>
|
||||||
|
</menu>
|
||||||
|
<menu action="BookMenu">
|
||||||
|
<placeholder name="AddEditBook">
|
||||||
|
<menuitem action="AddBook"/>
|
||||||
|
<menuitem action="EditBook"/>
|
||||||
|
</placeholder>
|
||||||
|
</menu>
|
||||||
|
</menubar>
|
||||||
|
<toolbar name="ToolBar">
|
||||||
|
<placeholder name="CommonNavigation">
|
||||||
|
<toolitem action="Back"/>
|
||||||
|
<toolitem action="Forward"/>
|
||||||
|
<toolitem action="HomePerson"/>
|
||||||
|
</placeholder>
|
||||||
|
<placeholder name="CommonEdit">
|
||||||
|
<toolitem action="PrintView"/>
|
||||||
|
</placeholder>
|
||||||
|
</toolbar>
|
||||||
|
</ui>
|
||||||
|
'''
|
||||||
|
|
||||||
|
def define_actions(self):
|
||||||
|
"""
|
||||||
|
Required define_actions function for PageView. Builds the action
|
||||||
|
group information required.
|
||||||
|
"""
|
||||||
|
NavigationView.define_actions(self)
|
||||||
|
|
||||||
|
self._add_action('PrintView', Gtk.STOCK_PRINT, _("_Print/Save View..."),
|
||||||
|
accel="<PRIMARY>P",
|
||||||
|
tip=_("Print or save the Fan Chart View"),
|
||||||
|
callback=self.printview)
|
||||||
|
def build_tree(self):
|
||||||
|
"""
|
||||||
|
Generic method called by PageView to construct the view.
|
||||||
|
Here the tree builds when active person changes or db changes or on
|
||||||
|
callbacks like person_rebuild, so build will be double sometimes.
|
||||||
|
However, change in generic filter also triggers build_tree ! So we
|
||||||
|
need to reset.
|
||||||
|
"""
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def active_changed(self, handle):
|
||||||
|
"""
|
||||||
|
Method called when active person changes.
|
||||||
|
"""
|
||||||
|
# Reset everything but rotation angle (leave it as is)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def _connect_db_signals(self):
|
||||||
|
"""
|
||||||
|
Connect database signals.
|
||||||
|
"""
|
||||||
|
self._add_db_signal('person-add', self.person_rebuild)
|
||||||
|
self._add_db_signal('person-update', self.person_rebuild)
|
||||||
|
self._add_db_signal('person-delete', self.person_rebuild)
|
||||||
|
self._add_db_signal('person-rebuild', self.person_rebuild_bm)
|
||||||
|
self._add_db_signal('family-update', self.person_rebuild)
|
||||||
|
self._add_db_signal('family-add', self.person_rebuild)
|
||||||
|
self._add_db_signal('family-delete', self.person_rebuild)
|
||||||
|
self._add_db_signal('family-rebuild', self.person_rebuild)
|
||||||
|
|
||||||
|
def change_db(self, db):
|
||||||
|
self._change_db(db)
|
||||||
|
self.bookmarks.update_bookmarks(self.dbstate.db.get_bookmarks())
|
||||||
|
if self.active:
|
||||||
|
self.bookmarks.redraw()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.main()
|
||||||
|
|
||||||
|
def goto_handle(self, handle):
|
||||||
|
self.change_active(handle)
|
||||||
|
self.main()
|
||||||
|
|
||||||
|
def get_active(self, object):
|
||||||
|
"""overrule get_active, to support call as in Gramplets
|
||||||
|
"""
|
||||||
|
return NavigationView.get_active(self)
|
||||||
|
|
||||||
|
def person_rebuild(self, *args):
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def person_rebuild_bm(self, *args):
|
||||||
|
"""Large change to person database"""
|
||||||
|
self.person_rebuild()
|
||||||
|
if self.active:
|
||||||
|
self.bookmarks.redraw()
|
||||||
|
|
||||||
|
def printview(self, obj):
|
||||||
|
"""
|
||||||
|
Print or save the view that is currently shown
|
||||||
|
"""
|
||||||
|
widthpx = 2 * self.fan.halfdist()
|
||||||
|
heightpx = widthpx
|
||||||
|
if self.form == fanchart.FORM_HALFCIRCLE:
|
||||||
|
heightpx = heightpx / 2 + fanchart.CENTER + fanchart.PAD_PX
|
||||||
|
elif self.form == fanchart.FORM_QUADRANT:
|
||||||
|
heightpx = heightpx / 2 + fanchart.CENTER + fanchart.PAD_PX
|
||||||
|
widthpx = heightpx
|
||||||
|
|
||||||
|
prt = CairoPrintSave(widthpx, heightpx, self.fan.on_draw, self.uistate.window)
|
||||||
|
prt.run()
|
||||||
|
|
||||||
|
def on_childmenu_changed(self, obj, person_handle):
|
||||||
|
"""Callback for the pulldown menu selection, changing to the person
|
||||||
|
attached with menu item."""
|
||||||
|
self.change_active(person_handle)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_configure(self):
|
||||||
|
"""
|
||||||
|
See :class:`~gui.views.pageview.PageView
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_configure_page_funcs(self):
|
||||||
|
"""
|
||||||
|
Return a list of functions that create gtk elements to use in the
|
||||||
|
notebook pages of the Configure dialog
|
||||||
|
|
||||||
|
:return: list of functions
|
||||||
|
"""
|
||||||
|
return [self.config_panel]
|
||||||
|
|
||||||
|
def config_panel(self, configdialog):
|
||||||
|
"""
|
||||||
|
Function that builds the widget in the configuration dialog
|
||||||
|
"""
|
||||||
|
nrentry = 7
|
||||||
|
table = Gtk.Table(6, 3)
|
||||||
|
table.set_border_width(12)
|
||||||
|
table.set_col_spacings(6)
|
||||||
|
table.set_row_spacings(6)
|
||||||
|
|
||||||
|
configdialog.add_spinner(table, _("Max generations"), 0,
|
||||||
|
'interface.fanview-maxgen', (1, 11),
|
||||||
|
callback=self.cb_update_maxgen)
|
||||||
|
configdialog.add_combo(table,
|
||||||
|
_('Text Font'),
|
||||||
|
1, 'interface.fanview-font',
|
||||||
|
self.allfonts, callback=self.cb_update_font, valueactive=True)
|
||||||
|
backgrvals = (
|
||||||
|
(fanchart.BACKGROUND_GENDER, _('Gender colors')),
|
||||||
|
(fanchart.BACKGROUND_GRAD_GEN, _('Generation based gradient')),
|
||||||
|
(fanchart.BACKGROUND_GRAD_AGE, _('Age (0-100) based gradient')),
|
||||||
|
(fanchart.BACKGROUND_SINGLE_COLOR,
|
||||||
|
_('Single main (filter) color')),
|
||||||
|
(fanchart.BACKGROUND_GRAD_PERIOD, _('Time period based gradient')),
|
||||||
|
(fanchart.BACKGROUND_WHITE, _('White')),
|
||||||
|
(fanchart.BACKGROUND_SCHEME1, _('Color scheme classic report')),
|
||||||
|
(fanchart.BACKGROUND_SCHEME2, _('Color scheme classic view')),
|
||||||
|
)
|
||||||
|
curval = self._config.get('interface.fanview-background')
|
||||||
|
nrval = 0
|
||||||
|
for nr, val in backgrvals:
|
||||||
|
if curval == nr:
|
||||||
|
break
|
||||||
|
nrval += 1
|
||||||
|
configdialog.add_combo(table,
|
||||||
|
_('Background'),
|
||||||
|
2, 'interface.fanview-background',
|
||||||
|
backgrvals,
|
||||||
|
callback=self.cb_update_background, valueactive=False,
|
||||||
|
setactive=nrval
|
||||||
|
)
|
||||||
|
#colors, stored as hex values
|
||||||
|
configdialog.add_color(table, _('Start gradient/Main color'), 3,
|
||||||
|
'interface.color-start-grad', col=1)
|
||||||
|
configdialog.add_color(table, _('End gradient/2nd color'), 4,
|
||||||
|
'interface.color-end-grad', col=1)
|
||||||
|
# form of the fan
|
||||||
|
configdialog.add_combo(table, _('Fan chart type'), 5,
|
||||||
|
'interface.fanview-form',
|
||||||
|
((fanchart.FORM_CIRCLE, _('Full Circle')),
|
||||||
|
(fanchart.FORM_HALFCIRCLE, _('Half Circle')),
|
||||||
|
(fanchart.FORM_QUADRANT, _('Quadrant'))),
|
||||||
|
callback=self.cb_update_form)
|
||||||
|
# algo for the fan angle distribution
|
||||||
|
configdialog.add_combo(table, _('Fan chart distribution'), 6,
|
||||||
|
'interface.angle-algorithm',
|
||||||
|
((fanchartdesc.ANGLE_CHEQUI,
|
||||||
|
_('Homogeneous children distribution')),
|
||||||
|
(fanchartdesc.ANGLE_WEIGHT,
|
||||||
|
_('Size proportional to number of descendants')),
|
||||||
|
),
|
||||||
|
callback=self.cb_update_anglealgo)
|
||||||
|
|
||||||
|
return _('Layout'), table
|
||||||
|
|
||||||
|
def config_connect(self):
|
||||||
|
"""
|
||||||
|
Overwriten from :class:`~gui.views.pageview.PageView method
|
||||||
|
This method will be called after the ini file is initialized,
|
||||||
|
use it to monitor changes in the ini file
|
||||||
|
"""
|
||||||
|
self._config.connect('interface.color-start-grad',
|
||||||
|
self.cb_update_color)
|
||||||
|
self._config.connect('interface.color-end-grad',
|
||||||
|
self.cb_update_color)
|
||||||
|
|
||||||
|
def cb_update_maxgen(self, spinbtn, constant):
|
||||||
|
self.maxgen = spinbtn.get_value_as_int()
|
||||||
|
self._config.set(constant, self.maxgen)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def cb_update_background(self, obj, constant):
|
||||||
|
entry = obj.get_active()
|
||||||
|
Gtk.TreePath.new_from_string('%d' % entry)
|
||||||
|
val = int(obj.get_model().get_value(
|
||||||
|
obj.get_model().get_iter_from_string('%d' % entry), 0))
|
||||||
|
self._config.set(constant, val)
|
||||||
|
self.background = val
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def cb_update_form(self, obj, constant):
|
||||||
|
entry = obj.get_active()
|
||||||
|
self._config.set(constant, entry)
|
||||||
|
self.form = entry
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def cb_update_anglealgo(self, obj, constant):
|
||||||
|
entry = obj.get_active()
|
||||||
|
self._config.set(constant, entry)
|
||||||
|
self.angle_algo = entry
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def cb_update_color(self, client, cnxn_id, entry, data):
|
||||||
|
"""
|
||||||
|
Called when the configuration menu changes the childrenring setting.
|
||||||
|
"""
|
||||||
|
self.grad_start = self._config.get('interface.color-start-grad')
|
||||||
|
self.grad_end = self._config.get('interface.color-end-grad')
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def cb_update_font(self, obj, constant):
|
||||||
|
entry = obj.get_active()
|
||||||
|
self._config.set(constant, self.allfonts[entry][1])
|
||||||
|
self.fonttype = self.allfonts[entry][1]
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def get_default_gramplets(self):
|
||||||
|
"""
|
||||||
|
Define the default gramplets for the sidebar and bottombar.
|
||||||
|
"""
|
||||||
|
return (("Person Filter",),
|
||||||
|
())
|
||||||
|
|
||||||
|
#------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# CairoPrintSave class
|
||||||
|
#
|
||||||
|
#------------------------------------------------------------------------
|
||||||
|
class CairoPrintSave():
|
||||||
|
"""Act as an abstract document that can render onto a cairo context.
|
||||||
|
|
||||||
|
It can render the model onto cairo context pages, according to the received
|
||||||
|
page style.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, widthpx, heightpx, drawfunc, parent):
|
||||||
|
"""
|
||||||
|
This class provides the things needed so as to dump a cairo drawing on
|
||||||
|
a context to output
|
||||||
|
"""
|
||||||
|
self.widthpx = widthpx
|
||||||
|
self.heightpx = heightpx
|
||||||
|
self.drawfunc = drawfunc
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Create the physical output from the meta document.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global PRINT_SETTINGS
|
||||||
|
|
||||||
|
# set up a print operation
|
||||||
|
operation = Gtk.PrintOperation()
|
||||||
|
operation.connect("draw_page", self.on_draw_page)
|
||||||
|
operation.connect("preview", self.on_preview)
|
||||||
|
operation.connect("paginate", self.on_paginate)
|
||||||
|
operation.set_n_pages(1)
|
||||||
|
#paper_size = Gtk.PaperSize.new(name="iso_a4")
|
||||||
|
## WHY no Gtk.Unit.PIXEL ?? Is there a better way to convert
|
||||||
|
## Pixels to MM ??
|
||||||
|
paper_size = Gtk.PaperSize.new_custom("custom",
|
||||||
|
"Custom Size",
|
||||||
|
round(self.widthpx * 0.2646),
|
||||||
|
round(self.heightpx * 0.2646),
|
||||||
|
Gtk.Unit.MM)
|
||||||
|
page_setup = Gtk.PageSetup()
|
||||||
|
page_setup.set_paper_size(paper_size)
|
||||||
|
#page_setup.set_orientation(Gtk.PageOrientation.PORTRAIT)
|
||||||
|
operation.set_default_page_setup(page_setup)
|
||||||
|
#operation.set_use_full_page(True)
|
||||||
|
|
||||||
|
if PRINT_SETTINGS is not None:
|
||||||
|
operation.set_print_settings(PRINT_SETTINGS)
|
||||||
|
|
||||||
|
# run print dialog
|
||||||
|
while True:
|
||||||
|
self.preview = None
|
||||||
|
res = operation.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.parent)
|
||||||
|
if self.preview is None: # cancel or print
|
||||||
|
break
|
||||||
|
# set up printing again; can't reuse PrintOperation?
|
||||||
|
operation = Gtk.PrintOperation()
|
||||||
|
operation.set_default_page_setup(page_setup)
|
||||||
|
operation.connect("draw_page", self.on_draw_page)
|
||||||
|
operation.connect("preview", self.on_preview)
|
||||||
|
operation.connect("paginate", self.on_paginate)
|
||||||
|
# set print settings if it was stored previously
|
||||||
|
if PRINT_SETTINGS is not None:
|
||||||
|
operation.set_print_settings(PRINT_SETTINGS)
|
||||||
|
|
||||||
|
# store print settings if printing was successful
|
||||||
|
if res == Gtk.PrintOperationResult.APPLY:
|
||||||
|
PRINT_SETTINGS = operation.get_print_settings()
|
||||||
|
|
||||||
|
def on_draw_page(self, operation, context, page_nr):
|
||||||
|
"""Draw a page on a Cairo context.
|
||||||
|
"""
|
||||||
|
cr = context.get_cairo_context()
|
||||||
|
pxwidth = round(context.get_width())
|
||||||
|
pxheight = round(context.get_height())
|
||||||
|
scale = min(pxwidth/self.widthpx, pxheight/self.heightpx)
|
||||||
|
if scale > 1:
|
||||||
|
scale = 1
|
||||||
|
self.drawfunc(None, cr, scale=scale)
|
||||||
|
|
||||||
|
def on_paginate(self, operation, context):
|
||||||
|
"""Paginate the whole document in chunks.
|
||||||
|
We don't need this as there is only one page, however,
|
||||||
|
we provide a dummy holder here, because on_preview crashes if no
|
||||||
|
default application is set with gir 3.3.2 (typically evince not installed)!
|
||||||
|
It will provide the start of the preview dialog, which cannot be
|
||||||
|
started in on_preview
|
||||||
|
"""
|
||||||
|
finished = True
|
||||||
|
# update page number
|
||||||
|
operation.set_n_pages(1)
|
||||||
|
|
||||||
|
# start preview if needed
|
||||||
|
if self.preview:
|
||||||
|
self.preview.run()
|
||||||
|
|
||||||
|
return finished
|
||||||
|
|
||||||
|
def on_preview(self, operation, preview, context, parent):
|
||||||
|
"""Implement custom print preview functionality.
|
||||||
|
We provide a dummy holder here, because on_preview crashes if no
|
||||||
|
default application is set with gir 3.3.2 (typically evince not installed)!
|
||||||
|
"""
|
||||||
|
dlg = Gtk.MessageDialog(parent,
|
||||||
|
flags=Gtk.DialogFlags.MODAL,
|
||||||
|
type=Gtk.MessageType.WARNING,
|
||||||
|
buttons=Gtk.ButtonsType.CLOSE,
|
||||||
|
message_format=_('No preview available'))
|
||||||
|
self.preview = dlg
|
||||||
|
self.previewopr = operation
|
||||||
|
#dlg.format_secondary_markup(msg2)
|
||||||
|
dlg.set_title("Fan Chart Preview - Gramps")
|
||||||
|
dlg.connect('response', self.previewdestroy)
|
||||||
|
|
||||||
|
# give a dummy cairo context to Gtk.PrintContext,
|
||||||
|
try:
|
||||||
|
width = int(round(context.get_width()))
|
||||||
|
except ValueError:
|
||||||
|
width = 0
|
||||||
|
try:
|
||||||
|
height = int(round(context.get_height()))
|
||||||
|
except ValueError:
|
||||||
|
height = 0
|
||||||
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||||
|
cr = cairo.Context(surface)
|
||||||
|
context.set_cairo_context(cr, 72.0, 72.0)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def previewdestroy(self, dlg, res):
|
||||||
|
self.preview.destroy()
|
||||||
|
self.previewopr.end_preview()
|
@ -140,17 +140,32 @@ register(VIEW,
|
|||||||
id = 'fanchartview',
|
id = 'fanchartview',
|
||||||
name = _("Fan Chart View"),
|
name = _("Fan Chart View"),
|
||||||
category = ("Ancestry", _("Ancestry")),
|
category = ("Ancestry", _("Ancestry")),
|
||||||
description = _("The view showing relations through a fanchart"),
|
description = _("A view showing parents through a fanchart"),
|
||||||
version = '1.0',
|
version = '1.0',
|
||||||
gramps_target_version = '4.0',
|
gramps_target_version = '4.0',
|
||||||
status = STABLE,
|
status = STABLE,
|
||||||
fname = 'fanchartview.py',
|
fname = 'fanchartview.py',
|
||||||
authors = [u"Douglas S. Blank"],
|
authors = [u"Douglas S. Blank", u"B. Malengier"],
|
||||||
authors_email = ["doug.blank@gmail.com"],
|
authors_email = ["doug.blank@gmail.com", "benny.malengier@gmail.com"],
|
||||||
viewclass = 'FanChartView',
|
viewclass = 'FanChartView',
|
||||||
stock_icon = 'gramps-fanchart',
|
stock_icon = 'gramps-fanchart',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
register(VIEW,
|
||||||
|
id = 'fanchartdescview',
|
||||||
|
name = _("Descendants Fan Chart View"),
|
||||||
|
category = ("Ancestry", _("Ancestry")),
|
||||||
|
description = _("Showing descendants through a fanchart"),
|
||||||
|
version = '1.0',
|
||||||
|
gramps_target_version = '4.0',
|
||||||
|
status = STABLE,
|
||||||
|
fname = 'fanchartdescview.py',
|
||||||
|
authors = [u"B. Malengier"],
|
||||||
|
authors_email = ["benny.malengier@gmail.com"],
|
||||||
|
viewclass = 'FanChartDescView',
|
||||||
|
stock_icon = 'gramps-fanchart',
|
||||||
|
)
|
||||||
|
|
||||||
register(VIEW,
|
register(VIEW,
|
||||||
id = 'personview',
|
id = 'personview',
|
||||||
name = _("Person Tree View"),
|
name = _("Person Tree View"),
|
||||||
|
Reference in New Issue
Block a user