Feature: a fanchart for descendants. Futher to test with duplicates

svn: r20402
This commit is contained in:
Benny Malengier 2012-09-16 23:05:37 +00:00
parent e6864165ee
commit 1c6df8ad46
7 changed files with 1309 additions and 39 deletions

View File

@ -489,6 +489,7 @@ src/gui/views/treemodels/treebasemodel.py
src/gui/widgets/buttons.py
src/gui/widgets/expandcollapsearrow.py
src/gui/widgets/fanchart.py
src/gui/widgets/fanchartdesc.py
src/gui/widgets/grampletpane.py
src/gui/widgets/labels.py
src/gui/widgets/monitoredwidgets.py
@ -693,6 +694,7 @@ src/plugins/view/citationlistview.py
src/plugins/view/eventview.py
src/plugins/view/familyview.py
src/plugins/view/fanchartview.py
src/plugins/view/fanchartdescview.py
src/plugins/view/geography.gpr.py
src/plugins/view/geoclose.py
src/plugins/view/geoevents.py

View File

@ -12,6 +12,7 @@ pkgpython_PYTHON = \
buttons.py \
expandcollapsearrow.py \
fanchart.py \
fanchartdesc.py \
grampletpane.py \
labels.py \
linkbox.py \

View File

@ -86,7 +86,6 @@ def gender_code(is_male):
PIXELS_PER_GENERATION = 50 # size of radius for generation
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
TRANSLATE_PX = 10 # size of the central circle, used to move the chart
PAD_PX = 4 # padding with edges
@ -142,9 +141,11 @@ TYPE_BOX_FAMILY = 1
class FanChartBaseWidget(Gtk.DrawingArea):
""" a base widget for fancharts"""
CENTER = 50 # pixel radius of center, changes per fanchart
def __init__(self, dbstate, callback_popup=None):
GObject.GObject.__init__(self)
self.radialtext = True
st_cont = self.get_style_context()
col = st_cont.lookup_color('text_color')
if col[0]:
@ -227,9 +228,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
requisition.height = requisition.width
elif self.form == FORM_HALFCIRCLE:
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:
requisition.width = self.halfdist() + CENTER + PAD_PX
requisition.width = self.halfdist() + self.CENTER + PAD_PX
requisition.height = requisition.width
def do_get_preferred_width(self):
@ -293,7 +294,7 @@ class FanChartBaseWidget(Gtk.DrawingArea):
userdata.append(period)
def set_userdata_age(self, person, userdata):
agecol = (255, 255, 255) # white
agecol = (1, 1, 1) # white
if person:
age = get_age(self.dbstate.db, person)
if age is not None:
@ -472,6 +473,31 @@ class FanChartBaseWidget(Gtk.DrawingArea):
return True
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):
"""
Procedure to draw a person in the inner ring position
@ -682,10 +708,10 @@ class FanChartBaseWidget(Gtk.DrawingArea):
elif (self.angle[-2] and
radius < TRANSLATE_PX + CHILDRING_WIDTH):
generation = -2 # indication of one of the children
elif radius < CENTER:
elif radius < self.CENTER:
generation = 0
else:
generation = int((radius - CENTER)/self.gen_pixels()) + 1
generation = int((radius - self.CENTER)/self.gen_pixels()) + 1
btype = self.boxtype(radius)
rads = math.atan2( (cury - cy), (curx - cx) )
@ -730,6 +756,18 @@ class FanChartBaseWidget(Gtk.DrawingArea):
"""
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):
self.translating = False # keep track of up/down/left/right movement
generation, selected, btype = self.person_under_cursor(event.x, event.y)
@ -785,9 +823,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
if self.form == FORM_CIRCLE:
self.center_xy = w/2 - event.x, h/2 - event.y
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:
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:
cx = w/2 - self.center_xy[0]
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
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:
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.queue_draw()
@ -1013,18 +1051,6 @@ class FanChartWidget(FanChartBaseWidget):
return not m is f is None
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):
"""
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
"""
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):
"""
@ -1101,9 +1127,9 @@ class FanChartWidget(FanChartBaseWidget):
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 + CENTER + PAD_PX)
self.set_size_request(2 * halfdist, halfdist + self.CENTER + PAD_PX)
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
alloc = self.get_allocation()
@ -1117,10 +1143,10 @@ class FanChartWidget(FanChartBaseWidget):
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 - CENTER - PAD_PX- self.center_xy[1]
self.center_y = h - self.CENTER - PAD_PX- self.center_xy[1]
elif self.form == FORM_QUADRANT:
self.center_x = CENTER + PAD_PX - self.center_xy[0]
self.center_y = h - CENTER - PAD_PX - self.center_xy[1]
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()
@ -1137,17 +1163,17 @@ class FanChartWidget(FanChartBaseWidget):
person, userdata)
cr.set_source_rgb(1, 1, 1) # white
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.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.restore()
# Draw center person:
(text, person, parents, child, userdata) = self.data[0][0]
if person:
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:
cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 2 * math.pi, 0)
cr.close_path()
@ -1155,8 +1181,8 @@ class FanChartWidget(FanChartBaseWidget):
cr.fill()
cr.save()
name = name_displayer.display(person)
self.draw_text(cr, name, CENTER -
(CENTER - (CHILDRING_WIDTH + TRANSLATE_PX))/2, 95, 455,
self.draw_text(cr, name, self.CENTER -
(self.CENTER - (CHILDRING_WIDTH + TRANSLATE_PX))/2, 95, 455,
10, False,
self.fontcolor(r, g, b, a), self.fontbold(a))
cr.restore()
@ -1184,7 +1210,7 @@ class FanChartWidget(FanChartBaseWidget):
start_rad = start * math.pi/180
stop_rad = stop * math.pi/180
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 generation == self.generations - 1 and parents:
# draw an indicator

View 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()

View File

@ -12,6 +12,7 @@ pkgpython_PYTHON = \
eventview.py \
familyview.py \
fanchartview.py \
fanchartdescview.py \
geoclose.py \
geoevents.py \
geoplaces.py \

View 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()

View File

@ -140,17 +140,32 @@ register(VIEW,
id = 'fanchartview',
name = _("Fan Chart View"),
category = ("Ancestry", _("Ancestry")),
description = _("The view showing relations through a fanchart"),
description = _("A view showing parents through a fanchart"),
version = '1.0',
gramps_target_version = '4.0',
status = STABLE,
fname = 'fanchartview.py',
authors = [u"Douglas S. Blank"],
authors_email = ["doug.blank@gmail.com"],
authors = [u"Douglas S. Blank", u"B. Malengier"],
authors_email = ["doug.blank@gmail.com", "benny.malengier@gmail.com"],
viewclass = 'FanChartView',
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,
id = 'personview',
name = _("Person Tree View"),