Files
gramps/src/gui/widgets/fanchart.py
Benny Malengier 29ea43cfe4 Fanchart: less default transparency
svn: r20345
2012-09-06 22:06:12 +00:00

1421 lines
59 KiB
Python

#
# 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
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)
#-------------------------------------------------------------------------
#
# Functions
#
#-------------------------------------------------------------------------
def gender_code(is_male):
"""
Given boolean is_male (means position in FanChart) return code.
"""
if is_male:
return gen.lib.Person.MALE
else:
return gen.lib.Person.FEMALE
#-------------------------------------------------------------------------
#
# FanChartWidget
#
#-------------------------------------------------------------------------
class FanChartWidget(Gtk.DrawingArea):
"""
Interactive Fan Chart Widget.
"""
PIXELS_PER_GENERATION = 50 # size of radius for generation
BORDER_EDGE_WIDTH = 10
CHILDRING_WIDTH = 12
TRANSLATE_PX = 10
BACKGROUND_SCHEME1 = 0
BACKGROUND_SCHEME2 = 1
BACKGROUND_GENDER = 2
BACKGROUND_WHITE = 3
BACKGROUND_GRAD_GEN = 4
BACKGROUND_GRAD_AGE = 5
GENCOLOR = {
BACKGROUND_SCHEME1: ((255, 63, 0),
(255,175, 15),
(255,223, 87),
(255,255,111),
(159,255,159),
(111,215,255),
( 79,151,255),
(231, 23,255),
(231, 23,121),
(210,170,124),
(189,153,112)),
BACKGROUND_SCHEME2: ((229,191,252),
(191,191,252),
(191,222,252),
(183,219,197),
(206,246,209)),
BACKGROUND_WHITE: ((255,255,255),
(255,255,255),),
}
MAX_AGE = 100
COLLAPSED = 0
NORMAL = 1
EXPANDED = 2
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.
"""
GObject.GObject.__init__(self)
self.dbstate = dbstate
self.translating = False
self.goto = None
self.on_popup = callback_popup
self.last_x, self.last_y = None, None
self.fontdescr = "Sans"
self.fontsize = 8
self.connect("button_release_event", self.on_mouse_up)
self.connect("motion_notify_event", self.on_mouse_move)
self.connect("button-press-event", self.on_mouse_down)
self.connect("draw", self.on_draw)
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK |
Gdk.EventMask.POINTER_MOTION_MASK)
# Enable drag
self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
[],
Gdk.DragAction.COPY)
tglist = Gtk.TargetList.new([])
tglist.add(DdTargets.PERSON_LINK.atom_drag_type,
DdTargets.PERSON_LINK.target_flags,
DdTargets.PERSON_LINK.app_id)
#allow drag to a text document, info on drag_get will be 0L !
tglist.add_text_targets(0L)
self.drag_source_set_target_list(tglist)
self.connect("drag_data_get", self.on_drag_data_get)
self.connect("drag_begin", self.on_drag_begin)
self.connect("drag_end", self.on_drag_end)
# Enable drop
self.drag_dest_set(Gtk.DestDefaults.MOTION |
Gtk.DestDefaults.DROP,
[],
Gdk.DragAction.COPY)
tglist = Gtk.TargetList.new([])
tglist.add(DdTargets.PERSON_LINK.atom_drag_type,
DdTargets.PERSON_LINK.target_flags,
DdTargets.PERSON_LINK.app_id)
self.drag_dest_set_target_list(tglist)
self.connect('drag_data_received', self.on_drag_data_received)
self._mouse_click = False
self.rotate_value = 90 # degrees, initially, 1st gen male on right half
self.center_xy = [0, 0] # distance from center (x, y)
self.center = 50 # pixel radius of center
#default values
self.reset(None, 9, self.BACKGROUND_GRAD_GEN, True, True, 'Sans', '#0000FF',
'#FF0000', None, 0.5)
self.set_size_request(120, 120)
def reset(self, root_person_handle, maxgen, background, childring,
radialtext, fontdescr, grad_start, grad_end,
filter, alpha_filter):
"""
Reset all of the data:
root_person_handle = person to show
maxgen = maximum generations to show
background = config setting of which background procedure to use (int)
childring = to show the center ring with children or not
radialtext = try to use radial text or not
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
"""
self.cache_fontcolor = {}
self.radialtext = radialtext
self.childring = childring
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.set_generations(maxgen)
# fill the data structure: self.data, self.childrenroot, self.angle
self._fill_data_structures(root_person_handle)
# prepare the colors for the boxes
self.prepare_background_box()
def _fill_data_structures(self, root_person_handle):
person = self.dbstate.db.get_person_from_handle(root_person_handle)
if not person:
name = None
else:
name = name_displayer.display(person)
parents = self._have_parents(person)
child = self._have_children(person)
# our data structure is the text, the person object, parents, child and
# list for userdata which we might fill in later.
self.data[0][0] = (name, person, parents, child, [])
self.childrenroot = []
if child:
childlist = find_children(self.dbstate.db, person)
for child_handle in childlist:
child = self.dbstate.db.get_person_from_handle(child_handle)
if not child:
continue
else:
self.childrenroot.append((child_handle, child.get_gender(),
self._have_children(child), []))
for current in range(1, self.generations):
parent = 0
# name, person, parents, children
for (n, p, q, c, d) in self.data[current - 1]:
# Get father's details:
person = self._get_parent(p, True)
if person:
name = name_displayer.display(person)
else:
name = None
if current == self.generations - 1:
parents = self._have_parents(person)
else:
parents = None
self.data[current][parent] = (name, person, parents, None, [])
if person is None:
# start,stop,male/right,state
self.angle[current][parent][3] = self.COLLAPSED
parent += 1
# Get mother's details:
person = self._get_parent(p, False)
if person:
name = name_displayer.display(person)
else:
name = None
if current == self.generations - 1:
parents = self._have_parents(person)
else:
parents = None
self.data[current][parent] = (name, person, parents, None, [])
if person is None:
# start,stop,male/right,state
self.angle[current][parent][3] = self.COLLAPSED
parent += 1
def _have_parents(self, person):
"""
Returns True if a person has parents.
TODO: is there no util function for this
"""
if person:
m = self._get_parent(person, False)
f = self._get_parent(person, True)
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
"""
if person:
parent_handle_list = person.get_parent_family_handle_list()
if parent_handle_list:
family_id = parent_handle_list[0]
family = self.dbstate.db.get_family_from_handle(family_id)
if family:
if father:
person_handle = gen.lib.Family.get_father_handle(family)
else:
person_handle = gen.lib.Family.get_mother_handle(family)
if person_handle:
return self.dbstate.db.get_person_from_handle(person_handle)
return None
def set_generations(self, generations):
"""
Set the generations to max, and fill data structures with initial data.
"""
self.generations = generations
self.angle = {}
self.data = {}
self.childrenroot = []
for i in range(self.generations):
# name, person, parents?, children?
self.data[i] = [(None,) * 5] * 2 ** i
self.angle[i] = []
angle = 0
slice = 360.0 / (2 ** i)
gender = True
for count in range(len(self.data[i])):
# start, stop, male, state
self.angle[i].append([angle, angle + slice, gender, self.NORMAL])
angle += slice
gender = not gender
def do_size_request(self, requisition):
"""
Overridden method to handle size request events.
"""
requisition.width = 2 * self.halfdist()
requisition.height = requisition.width
def do_get_preferred_width(self):
""" GTK3 uses width for height sizing model. This method will
override the virtual method
"""
req = Gtk.Requisition()
self.do_size_request(req)
return req.width, req.width
def do_get_preferred_height(self):
""" GTK3 uses width for height sizing model. This method will
override the virtual method
"""
req = Gtk.Requisition()
self.do_size_request(req)
return req.height, req.height
def nrgen(self):
#compute the number of generations present
nrgen = None
for generation in range(self.generations - 1, 0, -1):
for p in range(len(self.data[generation])):
(text, person, parents, child, userdata) = self.data[generation][p]
if person:
nrgen = generation
break
if nrgen is not None:
break
if nrgen is None:
nrgen = 1
return nrgen
def halfdist(self):
"""
Compute the half radius of the circle
"""
nrgen = self.nrgen()
return self.PIXELS_PER_GENERATION * nrgen + self.center \
+ self.BORDER_EDGE_WIDTH
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
nrgen = self.nrgen()
halfdist = self.PIXELS_PER_GENERATION * nrgen + self.center
self.set_size_request(2 * halfdist, 2 * halfdist)
#obtain the allocation
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cr.scale(scale, scale)
if widget:
cr.translate(w/2. - self.center_xy[0], h/2. - self.center_xy[1])
else:
cr.translate(halfdist - self.center_xy[0], halfdist - self.center_xy[1])
cr.save()
cr.rotate(self.rotate_value * math.pi/180)
for generation in range(self.generations - 1, 0, -1):
for p in range(len(self.data[generation])):
(text, person, parents, child, userdata) = self.data[generation][p]
if person:
start, stop, male, state = self.angle[generation][p]
if state in [self.NORMAL, self.EXPANDED]:
self.draw_person(cr, gender_code(male),
text, start, stop,
generation, state, parents, child,
person, userdata)
cr.set_source_rgb(1, 1, 1) # white
cr.move_to(0,0)
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.move_to(0,0)
cr.fill()
cr.set_source_rgb(0, 0, 0) # black
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.stroke()
cr.restore()
# Draw center person:
(text, person, parents, child, userdata) = self.data[0][0]
if person:
r, g, b, a = self.background_box(person, person.gender, 0, userdata)
cr.arc(0, 0, self.center, 0, 2 * math.pi)
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 - 10, 95, 455, False,
self.fontcolor(r,g,b))
cr.restore()
#draw center to move chart
cr.set_source_rgb(0, 0, 0) # black
cr.move_to(self.TRANSLATE_PX, 0)
cr.arc(0, 0, self.TRANSLATE_PX, 0, 2 * math.pi)
if child: # has at least one child
cr.fill()
else:
cr.stroke()
if child and self.childring:
self.drawchildring(cr)
if self.background in [self.BACKGROUND_GRAD_AGE]:
self.draw_gradient(cr, widget, halfdist)
def draw_person(self, cr, gender, name, start, stop, generation,
state, parents, child, person, userdata):
"""
Display the piece of pie for a given person. start and stop
are in degrees. Gender is indication of father position or mother
position in the chart
"""
cr.save()
start_rad = start * math.pi/180
stop_rad = stop * math.pi/180
r, g, b, a = self.background_box(person, gender, generation, userdata)
radius = generation * self.PIXELS_PER_GENERATION + self.center
# If max generation, and they have parents:
if generation == self.generations - 1 and parents:
# draw an indicator
radmax = radius + self.BORDER_EDGE_WIDTH
cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad))
cr.arc(0, 0, radius + self.BORDER_EDGE_WIDTH, start_rad, stop_rad)
cr.line_to(radius*math.cos(stop_rad), radius*math.sin(stop_rad))
cr.arc_negative(0, 0, radius, stop_rad, start_rad)
cr.close_path()
path = cr.copy_path()
cr.set_source_rgb(255, 255, 255) # white
cr.fill()
#and again for the border
cr.append_path(path)
cr.set_source_rgb(0, 0, 0) # black
cr.stroke()
# now draw the person
cr.move_to(radius * math.cos(start_rad), radius * math.sin(start_rad))
cr.arc(0, 0, radius, start_rad, stop_rad)
radmin = radius - self.PIXELS_PER_GENERATION
cr.line_to(radmin * math.cos(stop_rad), radmin * math.sin(stop_rad))
cr.arc_negative(0, 0, radmin, stop_rad, start_rad)
cr.close_path()
path = cr.copy_path()
cr.set_source_rgba(r/255., g/255., b/255., a)
cr.fill()
#and again for the border
cr.append_path(path)
cr.set_source_rgb(0, 0, 0) # black
if state == self.NORMAL: # normal
cr.set_line_width(1)
else: # EXPANDED
cr.set_line_width(3)
cr.stroke()
cr.set_line_width(1)
if self.last_x is None or self.last_y is None:
#we are not in a move, so draw text
radial = False
radstart = radius - self.PIXELS_PER_GENERATION/2
if self.radialtext and generation >= 6:
spacepolartext = radstart * (stop-start)*math.pi/180
if spacepolartext < self.PIXELS_PER_GENERATION * 1.1:
# more space to print it radial
radial = True
radstart = radius - self.PIXELS_PER_GENERATION + 4
self.draw_text(cr, name, radstart, start, stop, radial,
self.fontcolor(r, g, b))
cr.restore()
def drawchildring(self, cr):
cr.move_to(self.TRANSLATE_PX + self.CHILDRING_WIDTH, 0)
cr.set_source_rgb(0, 0, 0) # black
cr.set_line_width(1)
cr.arc(0, 0, self.TRANSLATE_PX + self.CHILDRING_WIDTH, 0, 2 * math.pi)
cr.stroke()
nrchild = len(self.childrenroot)
#Y axis is downward. positve angles are hence clockwise
startangle = math.pi
if nrchild <= 4:
angleinc = math.pi/2
else:
angleinc = 2 * math.pi / nrchild
self.angle[-2] = []
for child in self.childrenroot:
self.drawchild(cr, child, startangle, angleinc)
startangle += angleinc
def drawchild(self, cr, childdata, start, inc):
child_handle, child_gender, has_child, userdata = childdata
# in polar coordinates what is to draw
rmin = self.TRANSLATE_PX
rmax = self.TRANSLATE_PX + self.CHILDRING_WIDTH
thetamin = start
thetamax = start + inc
# add child to angle storage
self.angle[-2].append([thetamin, thetamax, child_gender, None])
#draw child now
cr.move_to(rmin*math.cos(thetamin), rmin*math.sin(thetamin))
cr.arc(0, 0, rmin, thetamin, thetamax)
cr.line_to(rmax*math.cos(thetamax), rmax*math.sin(thetamax))
cr.arc_negative(0, 0, rmax, thetamax, thetamin)
cr.line_to(rmin*math.cos(thetamin), rmin*math.sin(thetamin))
path = cr.copy_path()
cr.set_source_rgb(0, 0, 0) # black
cr.set_line_width(1)
cr.stroke()
#now again to fill
person = self.dbstate.db.get_person_from_handle(child_handle)
r, g, b, a = self.background_box(person, person.gender, -1, userdata)
cr.append_path(path)
cr.set_source_rgba(r/255., g/255., b/255., a)
cr.fill()
def draw_text(self, cr, text, radius, start, stop, radial=False,
fontcolor=(0, 0, 0)):
"""
Display text at a particular radius, between start and stop
degrees.
"""
cr.save()
font = Pango.FontDescription(self.fontdescr)
fontsize = self.fontsize
font.set_size(fontsize * Pango.SCALE)
cr.set_source_rgb(fontcolor[0], fontcolor[1], fontcolor[2])
if radial and self.radialtext:
cr.save()
layout = self.create_pango_layout(text)
layout.set_font_description(font)
w, h = layout.get_size()
w = w / Pango.SCALE + 5 # 5 pixel padding
h = h / Pango.SCALE + 4 # 4 pixel padding
#first we check if height is ok
degneedheight = h / radius * (180 / math.pi)
degavailheight = stop-start
degoffsetheight = 0
if degneedheight > degavailheight:
#reduce height
fontsize = degavailheight / degneedheight * fontsize / 2
font.set_size(fontsize * Pango.SCALE)
layout = self.create_pango_layout(text)
layout.set_font_description(font)
w, h = layout.get_size()
w = w / Pango.SCALE + 5 # 5 pixel padding
h = h / Pango.SCALE + 4 # 4 pixel padding
#first we check if height is ok
degneedheight = h / radius * (180 / math.pi)
degavailheight = stop-start
if degneedheight > degavailheight:
#we could not fix it, no text
text = ""
if text:
#spread rest
degoffsetheight = (degavailheight - degneedheight) / 2
txlen = len(text)
if w > self.PIXELS_PER_GENERATION:
txlen = int(w/self.PIXELS_PER_GENERATION * txlen)
cont = True
while cont:
layout = self.create_pango_layout(text[:txlen])
layout.set_font_description(font)
w, h = layout.get_size()
w = w / Pango.SCALE + 5 # 5 pixel padding
h = h / Pango.SCALE + 4 # 4 pixel padding
if w > self.PIXELS_PER_GENERATION:
if txlen <= 1:
cont = False
txlen = 0
else:
txlen -= 1
else:
cont = False
# offset for cairo-font system is 90
rotval = self.rotate_value % 360 - 90
if (start + rotval) % 360 > 179:
pos = start + degoffsetheight + 90 - 90
else:
pos = stop - degoffsetheight + 180
cr.rotate(pos * math.pi / 180)
layout.context_changed()
if (start + rotval) % 360 > 179:
cr.move_to(radius+2, 0)
else:
cr.move_to(-radius-self.PIXELS_PER_GENERATION+6, 0)
PangoCairo.show_layout(cr, layout)
cr.restore()
else:
# center text:
# 1. determine degrees of the text we can draw
degpadding = 5 / radius * (180 / math.pi) # degrees for 5 pixel padding
degneed = degpadding
maxlen = len(text)
for i in range(len(text)):
layout = self.create_pango_layout(text[i])
layout.set_font_description(font)
w, h = layout.get_size()
w = w / Pango.SCALE + 2 # 2 pixel padding after letter
h = h / Pango.SCALE + 2 # 2 pixel padding
degneed += w / radius * (180 / math.pi)
if degneed > stop - start:
#outside of the box
maxlen = i
break
# 2. determine degrees over we can distribute before and after
if degneed > stop - start:
degover = 0
else:
degover = stop - start - degneed - degpadding
# 3. now draw this text, letter per letter
text = text[:maxlen]
# offset for cairo-font system is 90, padding used is 5:
pos = start + 90 + degpadding + degover / 2
# Create a PangoLayout, set the font and text
# Draw the layout N_WORDS times in a circle
for i in range(len(text)):
layout = self.create_pango_layout(text[i])
layout.set_font_description(font)
w, h = layout.get_size()
w = w / Pango.SCALE + 2 # 4 pixel padding after word
h = h / Pango.SCALE + 2 # 4 pixel padding
degneed = w / radius * (180 / math.pi)
if pos+degneed > stop + 90:
#failsafe, outside of the box, redo
break
cr.save()
cr.rotate(pos * math.pi / 180)
pos = pos + degneed
# Inform Pango to re-layout the text with the new transformation
layout.context_changed()
#width, height = layout.get_size()
#r.move_to(- (width / Pango.SCALE) / 2.0, - radius)
cr.move_to(0, - radius)
PangoCairo.show_layout(cr, layout)
cr.restore()
cr.restore()
def draw_gradient(self, cr, widget, halfdist):
gradwidth = 10
gradheight = 10
starth = 25
startw = 5
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cr.save()
if widget:
cr.translate(-w/2. + self.center_xy[0], -h/2. + self.center_xy[1])
else:
cr.translate(-halfdist + self.center_xy[0], -halfdist + self.center_xy[1])
font = Pango.FontDescription(self.fontdescr)
fontsize = self.fontsize
font.set_size(fontsize * Pango.SCALE)
for color, text in zip(self.gradcol, self.gradval):
cr.move_to(startw, starth)
cr.rectangle(startw, starth, gradwidth, gradheight)
cr.set_source_rgb(color[0], color[1], color[2])
cr.fill()
layout = self.create_pango_layout(text)
layout.set_font_description(font)
cr.move_to(startw+gradwidth+4, starth)
cr.set_source_rgb(0, 0, 0) #black
PangoCairo.show_layout(cr, layout)
starth = starth+gradheight
cr.restore()
def prepare_background_box(self):
"""
Method that is called every reset of the chart, to precomputed values
needed for the background of the boxes
"""
maxgen = self.generations
cstart = gui.utils.hex_to_rgb(self.grad_start)
cend = gui.utils.hex_to_rgb(self.grad_end)
cstart_hsv = colorsys.rgb_to_hsv(cstart[0]/255, cstart[1]/255,
cstart[2]/255)
cend_hsv = colorsys.rgb_to_hsv(cend[0]/255, cend[1]/255,
cend[2]/255)
if self.background == self.BACKGROUND_GENDER:
# nothing to precompute
self.colors = None
elif self.background == self.BACKGROUND_GRAD_GEN:
#compute the colors, -1, 0, ..., maxgen
divs = [x/(maxgen-1) for x in range(maxgen)]
rgb_colors = [colorsys.hsv_to_rgb(
(1-x) * cstart_hsv[0] + x * cend_hsv[0],
(1-x) * cstart_hsv[1] + x * cend_hsv[1],
(1-x) * cstart_hsv[2] + x * cend_hsv[2],
) for x in divs]
self.colors = [(255*r, 255*g, 255*b) for r, g, b in rgb_colors]
elif self.background == self.BACKGROUND_GRAD_AGE:
# we fill in in the data structure what the age is, None if no age
for generation in range(self.generations):
for p in range(len(self.data[generation])):
agecol = (255, 255, 255) # white
(text, person, parents, child, userdata) = self.data[generation][p]
if person:
age = get_age(self.dbstate.db, person)
if age is not None:
age = age[0]
if age < 0:
age = 0
#now determine fraction for gradient
agefrac = age / self.MAX_AGE
agecol = colorsys.hsv_to_rgb(
(1-agefrac) * cstart_hsv[0] + agefrac * cend_hsv[0],
(1-agefrac) * cstart_hsv[1] + agefrac * cend_hsv[1],
(1-agefrac) * cstart_hsv[2] + agefrac * cend_hsv[2],
)
userdata.append((agecol[0]*255, agecol[1]*255, agecol[2]*255))
# same for child
for childdata in self.childrenroot:
agecol = (255, 255, 255) # white
child_handle, child_gender, has_child, userdata = childdata
child = self.dbstate.db.get_person_from_handle(child_handle)
age = get_age(self.dbstate.db, child)
if age is not None:
age = age[0]
if age < 0:
age = 0
#now determine fraction for gradient
agefrac = age / self.MAX_AGE
agecol = colorsys.hsv_to_rgb(
(1-agefrac) * cstart_hsv[0] + agefrac * cend_hsv[0],
(1-agefrac) * cstart_hsv[1] + agefrac * cend_hsv[1],
(1-agefrac) * cstart_hsv[2] + agefrac * cend_hsv[2],
)
userdata.append((agecol[0]*255, agecol[1]*255, agecol[2]*255))
#now create gradient data, 5 values from 0 to max
steps = 5
divs = [x/steps for x in range(steps+1)]
self.gradval = ['%d' % int(x*self.MAX_AGE) for x in divs]
self.gradcol = [colorsys.hsv_to_rgb(
(1-div) * cstart_hsv[0] + div * cend_hsv[0],
(1-div) * cstart_hsv[1] + div * cend_hsv[1],
(1-div) * cstart_hsv[2] + div * cend_hsv[2],
) for div in divs]
else:
# known colors per generation, set or compute them
self.colors = self.GENCOLOR[self.background]
def background_box(self, person, gender, generation, userdata):
"""
determine red, green, blue value of background of the box of person,
which has gender gender, and is in ring generation
"""
if generation == 0 and self.background in [self.BACKGROUND_GENDER,
self.BACKGROUND_GRAD_GEN, self.BACKGROUND_SCHEME1,
self.BACKGROUND_SCHEME2]:
# white for center person:
color = (255, 255, 255)
elif self.background == self.BACKGROUND_GENDER:
try:
alive = probably_alive(person, self.dbstate.db)
except RuntimeError:
alive = False
backgr, border = gui.utils.color_graph_box(alive, person.gender)
color = gui.utils.hex_to_rgb(backgr)
elif self.background == self.BACKGROUND_GRAD_AGE:
color = userdata[0]
else:
if self.background == self.BACKGROUND_GRAD_GEN and generation < 0:
generation = 0
color = self.colors[generation % len(self.colors)]
if gender == gen.lib.Person.MALE:
color = [x*.9 for x in color]
# now we set transparency data
if self.filter and not self.filter.match(person.handle, self.dbstate.db):
alpha = self.alpha_filter
else:
alpha = 1.
return color[0], color[1], color[2], alpha
def fontcolor(self, r, g, b):
"""
return the font color based on the r, g, b of the background
"""
try:
return self.cache_fontcolor[(r, g, b)]
except KeyError:
hls = colorsys.rgb_to_hls(r/255, g/255, b/255)
# we use the lightness value to determine white or black font
if hls[1] > 0.4:
self.cache_fontcolor[(r, g, b)] = (0, 0, 0)
else:
self.cache_fontcolor[(r, g, b)] = (255, 255, 255)
return self.cache_fontcolor[(r, g, b)]
def expand_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected+1, current)
def show_parents(self, generation, selected, angle, slice):
if generation >= self.generations: return
selected *= 2
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle + slice
self.angle[generation][selected][3] = self.NORMAL
self.show_parents(generation+1, selected, angle, slice/2.0)
self.angle[generation][selected+1][0] = angle + slice
self.angle[generation][selected+1][1] = angle + slice + slice
self.angle[generation][selected+1][3] = self.NORMAL
self.show_parents(generation+1, selected + 1, angle + slice, slice/2.0)
def hide_parents(self, generation, selected, angle):
if generation >= self.generations: return
selected = 2 * selected
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle
self.angle[generation][selected][3] = self.COLLAPSED
self.hide_parents(generation + 1, selected, angle)
self.angle[generation][selected+1][0] = angle
self.angle[generation][selected+1][1] = angle
self.angle[generation][selected+1][3] = self.COLLAPSED
self.hide_parents(generation + 1, selected+1, angle)
def shrink_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected] = [current, current + slice,
male,state]
self.shrink_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.shrink_parents(generation + 1, selected+1, current)
def change_slice(self, generation, selected):
if generation < 1:
return
gstart, gstop, gmale, gstate = self.angle[generation][selected]
if gstate == self.NORMAL: # let's expand
if gmale:
# go to right
stop = gstop + (gstop - gstart)
self.angle[generation][selected] = [gstart,stop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, gstart)
start,stop,male,state = self.angle[generation][selected+1]
self.angle[generation][selected+1] = [stop,stop,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected+1, stop)
else:
# go to left
start = gstart - (gstop - gstart)
self.angle[generation][selected] = [start,gstop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected-1, start)
elif gstate == self.EXPANDED: # let's shrink
if gmale:
# shrink from right
slice = (gstop - gstart)/2.0
stop = gstop - slice
self.angle[generation][selected] = [gstart,stop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, gstart)
self.angle[generation][selected+1][0] = stop # start
self.angle[generation][selected+1][1] = stop + slice # stop
self.angle[generation][selected+1][3] = self.NORMAL
self.show_parents(generation+1, selected+1, stop, slice/2.0)
else:
# shrink from left
slice = (gstop - gstart)/2.0
start = gstop - slice
self.angle[generation][selected] = [start,gstop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start+slice,male,
self.NORMAL]
self.show_parents(generation+1, selected-1, start, slice/2.0)
def on_mouse_move(self, widget, event):
self._mouse_click = False
if self.last_x is None or self.last_y is None:
# while mouse is moving, we must update the tooltip based on person
generation, selected = self.person_under_cursor(event.x, event.y)
tooltip = ""
person = None
if selected is not None and generation >= 0:
text, person, parents, child, userdata = \
self.data[generation][selected]
elif selected is not None and generation == -2:
child_handle, child_gender, has_child, userdata = \
self.childrenroot[selected]
person = self.dbstate.db.get_person_from_handle(child_handle)
if person:
tooltip = self.format_helper.format_person(person, 11)
self.set_tooltip_text(tooltip)
return False
#translate or rotate should happen
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
if self.translating:
self.center_xy = w/2 - event.x, h/2 - event.y
else:
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
# get the angles of the two points from the center:
start_angle = math.atan2(event.y - cy, event.x - cx)
end_angle = math.atan2(self.last_y - cy, self.last_x - cx)
if start_angle < 0: # second half of unit circle
start_angle = math.pi + (math.pi + start_angle)
if end_angle < 0: # second half of unit circle
end_angle = math.pi + (math.pi + end_angle)
# now look at change in angle:
diff_angle = (end_angle - start_angle) % (math.pi * 2.0)
self.rotate_value -= diff_angle * 180.0/ math.pi
self.last_x, self.last_y = event.x, event.y
self.queue_draw()
return True
def person_under_cursor(self, curx, cury):
"""
Determine the generation and the position in the generation at
position x and y.
generation = -1 on center black dot
generation >= self.generations outside of diagram
"""
# compute angle, radius, find out who would be there (rotated)
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
radius = math.sqrt((curx - cx) ** 2 + (cury - cy) ** 2)
if radius < self.TRANSLATE_PX:
generation = -1
elif (self.childring and self.childrenroot and
radius < self.TRANSLATE_PX + self.CHILDRING_WIDTH):
generation = -2 # indication of one of the children
elif radius < self.center:
generation = 0
else:
generation = int((radius - self.center) /
self.PIXELS_PER_GENERATION) + 1
rads = math.atan2( (cury - cy), (curx - cx) )
if rads < 0: # second half of unit circle
rads = math.pi + (math.pi + rads)
pos = ((rads/(math.pi * 2) - self.rotate_value/360.) * 360.0) % 360
#children are in cairo angle (clockwise) from pi to 3 pi
#rads however is clock 0 to 2 pi
if rads < math.pi:
rads += 2 * math.pi
# if generation is in expand zone:
# FIXME: add a way of expanding
# find what person is in this position:
selected = None
if (0 < generation < self.generations):
for p in range(len(self.angle[generation])):
if self.data[generation][p][1]: # there is a person there
start, stop, male, state = self.angle[generation][p]
if state == self.COLLAPSED: continue
if start <= pos <= stop:
selected = p
break
elif generation == 0:
selected = 0
elif generation == -2:
for p in range(len(self.angle[generation])):
start, stop, male, state = self.angle[generation][p]
if start <= rads <= stop:
selected = p
break
return generation, selected
def on_mouse_down(self, widget, event):
self.translating = False # keep track of up/down/left/right movement
generation, selected = self.person_under_cursor(event.x, event.y)
# left mouse on center dot, we translate on left click
if generation == -1:
if event.button == 1: # left mouse
# save the mouse location for movements
self.translating = True
self.last_x, self.last_y = event.x, event.y
return True
#click in open area, prepare for a rotate
if selected is None:
# save the mouse location for movements
self.last_x, self.last_y = event.x, event.y
return True
#left click on person, prepare for expand/collapse or drag
if event.button == 1:
self._mouse_click = True
self._mouse_click_gen = generation
self._mouse_click_sel = selected
return False
#right click on person, context menu
# Do things based on state, event.get_state(), or button, event.button
if gui.utils.is_right_click(event):
if generation == -2:
child_handle, child_gender, has_child, userdata = \
self.childrenroot[selected]
person = self.dbstate.db.get_person_from_handle(child_handle)
else:
text, person, parents, child, userdata = \
self.data[generation][selected]
if person and self.on_popup:
self.on_popup(widget, event, person.handle)
return True
return False
def on_mouse_up(self, widget, event):
if self._mouse_click:
# no drag occured, expand or collapse the section
self.change_slice(self._mouse_click_gen, self._mouse_click_sel)
self._mouse_click = False
self.queue_draw()
return True
if self.last_x is None or self.last_y is None:
# No translate or rotate
return True
if self.translating:
self.translating = False
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
self.center_xy = w/2 - event.x, h/2 - event.y
self.last_x, self.last_y = None, None
self.queue_draw()
return True
def on_drag_begin(self, widget, data):
"""Set up some inital conditions for drag. Set up icon."""
self.in_drag = True
self.drag_source_set_icon_stock('gramps-person')
def on_drag_end(self, widget, data):
"""Set up some inital conditions for drag. Set up icon."""
self.in_drag = False
def on_drag_data_get(self, widget, context, sel_data, info, time):
"""
Returned parameters after drag.
Specified for 'person-link', for others return text info about person.
"""
tgs = [x.name() for x in context.list_targets()]
if self._mouse_click_gen == -2:
#children
child_handle, child_gender, has_child, userdata = \
self.childrenroot[self._mouse_click_sel]
person = self.dbstate.db.get_person_from_handle(child_handle)
else:
text, person, parents, child, userdata \
= self.data[self._mouse_click_gen][self._mouse_click_sel]
if info == DdTargets.PERSON_LINK.app_id:
data = (DdTargets.PERSON_LINK.drag_type,
id(self), person.get_handle(), 0)
sel_data.set(sel_data.get_target(), 8, pickle.dumps(data))
elif ('TEXT' in tgs or 'text/plain' in tgs) and info == 0L:
sel_data.set_text(self.format_helper.format_person(person, 11), -1)
def on_drag_data_received(self, widget, context, x, y, sel_data, info, time):
"""
Handle the standard gtk interface for drag_data_received.
If the selection data is defined, extract the value from sel_data.data
"""
gen, persatcurs = self.person_under_cursor(x, y)
if gen == -1 or gen == 0:
if sel_data and sel_data.get_data():
(drag_type, idval, handle, val) = pickle.loads(sel_data.get_data())
self.goto(self, handle)
class FanChartGrampsGUI(object):
""" class for functions fanchart GUI elements will need in Gramps
"""
def __init__(self, maxgen, background, childring, radialtext, font,
on_childmenu_changed):
"""
Common part of GUI that shows Fan Chart, needs to know what to do if
one moves via Fan Ch def set_fan(self, fan):art to a new person
on_childmenu_changed: in popup, function called on moving to a new person
"""
self.fan = None
self.on_childmenu_changed = on_childmenu_changed
self.format_helper = FormattingHelper(self.dbstate)
self.maxgen = maxgen
self.background = background
self.childring = childring
self.radialtext = radialtext
self.fonttype = font
self.grad_start = '#0000FF'
self.grad_end = '#FF0000'
self.generic_filter = None # the filter to use. Named as in PageView
self.alpha_filter = 0.2 # transparency of filtered out values
def set_fan(self, fan):
"""
Set the fanchartwidget to work on
"""
self.fan = fan
self.fan.format_helper = self.format_helper
self.fan.goto = self.on_childmenu_changed
def main(self):
"""
Fill the data structures with the active data. This initializes all
data.
"""
root_person_handle = self.get_active('Person')
self.fan.reset(root_person_handle, self.maxgen, self.background, self.childring,
self.radialtext, self.fonttype,
self.grad_start, self.grad_end,
self.generic_filter, self.alpha_filter)
self.fan.queue_draw()
def on_popup(self, obj, event, person_handle):
"""
Builds the full menu (including Siblings, Spouses, Children,
and Parents) with navigation. Copied from PedigreeView.
"""
#store menu for GTK3 to avoid it being destroyed before showing
self.menu = Gtk.Menu()
menu = self.menu
menu.set_title(_('People Menu'))
person = self.dbstate.db.get_person_from_handle(person_handle)
if not person:
return 0
go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO,Gtk.IconSize.MENU)
go_image.show()
go_item = Gtk.ImageMenuItem(name_displayer.display(person))
go_item.set_image(go_image)
go_item.connect("activate", self.on_childmenu_changed, person_handle)
go_item.show()
menu.append(go_item)
edit_item = Gtk.ImageMenuItem.new_from_stock(stock_id=Gtk.STOCK_EDIT, accel_group=None)
edit_item.connect("activate", self.edit_person_cb, person_handle)
edit_item.show()
menu.append(edit_item)
clipboard_item = Gtk.ImageMenuItem.new_from_stock(stock_id=Gtk.STOCK_COPY, accel_group=None)
clipboard_item.connect("activate", self.copy_person_to_clipboard_cb,
person_handle)
clipboard_item.show()
menu.append(clipboard_item)
# collect all spouses, parents and children
linked_persons = []
# Go over spouses and build their menu
item = Gtk.MenuItem(label=_("Spouses"))
fam_list = person.get_family_handle_list()
no_spouses = 1
for fam_id in fam_list:
family = self.dbstate.db.get_family_from_handle(fam_id)
if family.get_father_handle() == person.get_handle():
sp_id = family.get_mother_handle()
else:
sp_id = family.get_father_handle()
spouse = self.dbstate.db.get_person_from_handle(sp_id)
if not spouse:
continue
if no_spouses:
no_spouses = 0
item.set_submenu(Gtk.Menu())
sp_menu = item.get_submenu()
go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU)
go_image.show()
sp_item = Gtk.ImageMenuItem(name_displayer.display(spouse))
sp_item.set_image(go_image)
linked_persons.append(sp_id)
sp_item.connect("activate", self.on_childmenu_changed, sp_id)
sp_item.show()
sp_menu.append(sp_item)
if no_spouses:
item.set_sensitive(0)
item.show()
menu.append(item)
# Go over siblings and build their menu
item = Gtk.MenuItem(label=_("Siblings"))
pfam_list = person.get_parent_family_handle_list()
no_siblings = 1
for f in pfam_list:
fam = self.dbstate.db.get_family_from_handle(f)
sib_list = fam.get_child_ref_list()
for sib_ref in sib_list:
sib_id = sib_ref.ref
if sib_id == person.get_handle():
continue
sib = self.dbstate.db.get_person_from_handle(sib_id)
if not sib:
continue
if no_siblings:
no_siblings = 0
item.set_submenu(Gtk.Menu())
sib_menu = item.get_submenu()
if find_children(self.dbstate.db,sib):
label = Gtk.Label(label='<b><i>%s</i></b>' % escape(name_displayer.display(sib)))
else:
label = Gtk.Label(label=escape(name_displayer.display(sib)))
go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU)
go_image.show()
sib_item = Gtk.ImageMenuItem(None)
sib_item.set_image(go_image)
label.set_use_markup(True)
label.show()
label.set_alignment(0,0)
sib_item.add(label)
linked_persons.append(sib_id)
sib_item.connect("activate", self.on_childmenu_changed, sib_id)
sib_item.show()
sib_menu.append(sib_item)
if no_siblings:
item.set_sensitive(0)
item.show()
menu.append(item)
# Go over children and build their menu
item = Gtk.MenuItem(label=_("Children"))
no_children = 1
childlist = find_children(self.dbstate.db, person)
for child_handle in childlist:
child = self.dbstate.db.get_person_from_handle(child_handle)
if not child:
continue
if no_children:
no_children = 0
item.set_submenu(Gtk.Menu())
child_menu = item.get_submenu()
if find_children(self.dbstate.db,child):
label = Gtk.Label(label='<b><i>%s</i></b>' % escape(name_displayer.display(child)))
else:
label = Gtk.Label(label=escape(name_displayer.display(child)))
go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU)
go_image.show()
child_item = Gtk.ImageMenuItem(None)
child_item.set_image(go_image)
label.set_use_markup(True)
label.show()
label.set_alignment(0,0)
child_item.add(label)
linked_persons.append(child_handle)
child_item.connect("activate", self.on_childmenu_changed, child_handle)
child_item.show()
child_menu.append(child_item)
if no_children:
item.set_sensitive(0)
item.show()
menu.append(item)
# Go over parents and build their menu
item = Gtk.MenuItem(label=_("Parents"))
no_parents = 1
par_list = find_parents(self.dbstate.db,person)
for par_id in par_list:
par = self.dbstate.db.get_person_from_handle(par_id)
if not par:
continue
if no_parents:
no_parents = 0
item.set_submenu(Gtk.Menu())
par_menu = item.get_submenu()
if find_parents(self.dbstate.db,par):
label = Gtk.Label(label='<b><i>%s</i></b>' % escape(name_displayer.display(par)))
else:
label = Gtk.Label(label=escape(name_displayer.display(par)))
go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU)
go_image.show()
par_item = Gtk.ImageMenuItem(None)
par_item.set_image(go_image)
label.set_use_markup(True)
label.show()
label.set_alignment(0,0)
par_item.add(label)
linked_persons.append(par_id)
par_item.connect("activate",self.on_childmenu_changed, par_id)
par_item.show()
par_menu.append(par_item)
if no_parents:
item.set_sensitive(0)
item.show()
menu.append(item)
# Go over parents and build their menu
item = Gtk.MenuItem(label=_("Related"))
no_related = 1
for p_id in find_witnessed_people(self.dbstate.db,person):
#if p_id in linked_persons:
# continue # skip already listed family members
per = self.dbstate.db.get_person_from_handle(p_id)
if not per:
continue
if no_related:
no_related = 0
item.set_submenu(Gtk.Menu())
per_menu = item.get_submenu()
label = Gtk.Label(label=escape(name_displayer.display(per)))
go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU)
go_image.show()
per_item = Gtk.ImageMenuItem(None)
per_item.set_image(go_image)
label.set_use_markup(True)
label.show()
label.set_alignment(0, 0)
per_item.add(label)
per_item.connect("activate", self.on_childmenu_changed, p_id)
per_item.show()
per_menu.append(per_item)
if no_related:
item.set_sensitive(0)
item.show()
menu.append(item)
menu.popup(None, None, None, None, event.button, event.time)
return 1
def edit_person_cb(self, obj,person_handle):
person = self.dbstate.db.get_person_from_handle(person_handle)
if person:
try:
EditPerson(self.dbstate, self.uistate, [], person)
except WindowActiveError:
pass
return True
return False
def copy_person_to_clipboard_cb(self, obj,person_handle):
"""Renders the person data into some lines of text and puts that into the clipboard"""
person = self.dbstate.db.get_person_from_handle(person_handle)
if person:
cb = Gtk.Clipboard.get_for_display(Gdk.Display.get_default(),
Gdk.SELECTION_CLIPBOARD)
cb.set_text( self.format_helper.format_person(person,11))
return True
return False