Feature: Gep-030 FanChart2Way

...New FanChart consisting of both ascendants and descendants.

It can be checked-out there :
https://sourceforge.net/u/bubblegum00/gramps/ci/geps/gep-030-FanChart2Way/~/tree/

<snip>

I find it quite handy, so please let me know if you have any comments or suggestions.

NB: I heavily cleaned up the FanChart code on my way.

Regards,

Bastien Jacquet

https://sourceforge.net/p/gramps/mailman/message/32908110/
........................................................
Refactor fanchart for further modification

Fix radial text pos_start radian alignment

Simplify father's and mother's details getter

Rename draw_gradient to draw_gradient_legend

refactor prepare_background_box

Add option not to flip names

Add a display_format(self, person, num) function as proposed by Benny Malengier on 2012-12-13
It just returns name_display.name_formats[num][_F_FN](person.get_primary_name())

Add option to show the name on two lines

Allow for variable radius depending on generation

Switch to WORD_CHAR wrapping of name (ie word, and char if 0-length word)

Move rescaling tentative inside wrap_truncate_layout

Fix person_under_cursor bugs

Refactor root angle computation

Refactor code positionning the fan

Refactor personpos_at_angle

move implementation of person_under_cursor outside of FanChartBaseWidget class

Change draw_person to take angles in radians

Use same structure for innerring as for outerring

Uses cursor_to_polar and cursor_on_tranlation_dot

Slightly change person_under_cursor logic to return an "address" in the fan

Uses radian_in_bounds to compare angles modulo 2 PI

Fixup test on cursor over inner ring

Fix Center size for FanchartDesc

Fixup fanchart check up to last generation

Give same signature to draw_person

Refactor the common code of self.draw_person in a single function in Base class

Fix center box comment

Refactoring inside celladdress

Remove manual central box drawing since done with draw_person

Fixup draw_person color for duplicates

Use draw_person for central person too

Make __compute_angle and __rec_fill_data public for use in FanChart2Way

Add 2Way View

Rewrite create_map_rect_to_sector to allow bottom-outside-oriented text-arc

Allow to automatically right upside-up bottom arc-text

Correct icons for Fanchart2Way

Small code refactoring

Refactor code of fanchartdesc to use self.rootangle_rad

Rename change_slice to toggle_cell_state

Fanchart2Way code formating and changes

Small refactoring of fanchartdesc innerring fill data

Remove the name from the local temporary data structure

Remove the name from the local temporary data structure (in Fanchart2Way)

Change background gradient to follow the user-selected gradient colors

rename parentsroot to innerring

Some renaming for clearer code

Show last generation of partners in descendant fanchart

Show last partner in Fanchart2Way

Fanchart2Way : Add option to disable gradient on the background

Fixup flipupsidedownname parameter for gramplet usage of fancharts

Fixup twolinename parameter for gramplet usage of fancharts

Add FanChart2Way in available gramplets

Tentative fix for last view on Fanchart2Way

Show step-sibling in Fancharts context-menu

Fix overestimation of descendant halfdist

(SM) Trailing White spaces removed

(SM) Fix config box Table Grid

(SM) Move Icons gramps-fanchart2way to new location

(SM) Add Copyright for Bastien Jacquet

(SM) Fix BSDDB AttributeError NoneType object has no attr

(SM) Update patch to account for bug 9771; fix missing right-click menu items

(Nick Hall/eno93) Fix set_text method takes the length of the utf-8, not the length of the unicode as the second parameter ((Gramps.py:3697): Pango-WARNING **: Invalid UTF-8 string passed to pango_layout_set_text())
This commit is contained in:
Bastien Jacquet 2016-11-25 16:15:25 +11:00 committed by Nick Hall
parent 3d854ba944
commit fec5d532d1
16 changed files with 3700 additions and 815 deletions

View File

@ -892,6 +892,24 @@ class NameDisplay:
name = person.get_primary_name()
return self.display_name(name)
def display_format(self, person, num):
"""
Return a text string representing the L{gen.lib.Person} instance's
L{Name} using num format.
@param person: L{gen.lib.Person} instance that contains the
L{Name} that is to be displayed. The primary name is used for
the display.
@type person: L{gen.lib.Person}
@param num: num of the format to be used, as return by
name_displayer.add_name_format('name','format')
@type num: int
@returns: Returns the L{gen.lib.Person} instance's name
@rtype: str
"""
name = person.get_primary_name()
return self.name_formats[num][_F_FN](name)
def display_formal(self, person):
"""
Return a text string representing the :class:`~.person.Person`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,625 @@
#
# 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
# Copyright (C) 2014 Bastien Jacquet
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
## 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 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 sys
import pickle
from cgi import escape
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from gramps.gen.display.name import displayer as name_displayer
from gramps.gen.errors import WindowActiveError
from ..editors import EditPerson, EditFamily
from ..utils import hex_to_rgb
from ..ddtargets import DdTargets
from gramps.gen.utils.alive import probably_alive
from gramps.gen.utils.libformatting import FormattingHelper
from gramps.gen.utils.db import (find_children, find_parents, find_witnessed_people,
get_age, get_timeperiod)
from gramps.gen.plug.report.utils import find_spouse
from .fanchart import *
from .fanchartdesc import *
#-------------------------------------------------------------------------
#
# Constants
#
#-------------------------------------------------------------------------
PIXELS_PER_GENPERSON_RATIO = 0.55 # ratio of generation radius for person (rest for partner)
PIXELS_PER_GEN_SMALL = 80
PIXELS_PER_GEN_LARGE = 160
N_GEN_SMALL = 4
PIXELS_PER_GENFAMILY = 25 # size of radius for family
PIXELS_PER_RECLAIM = 4 # size of the radius of pixels taken from family to reclaim space
PIXELS_PARTNER_GAP = 0 # Padding between someone and his partner
PIXELS_CHILDREN_GAP = 5 # Padding between generations
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
TYPE_ASCENDANCE = 0
TYPE_DESCENDANCE = 1
#-------------------------------------------------------------------------
#
# FanChart2WayWidget
#
#-------------------------------------------------------------------------
class FanChart2WayWidget(FanChartWidget, FanChartDescWidget):
"""
Interactive Fan Chart Widget.
"""
CENTER = 50 # we require a larger center
def __init__(self, dbstate, uistate, 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, 6, 5, True, True, BACKGROUND_GRAD_GEN, True, 'Sans', '#0000FF',
'#FF0000', None, 0.5, ANGLE_WEIGHT, '#888a85')
FanChartBaseWidget.__init__(self, dbstate, uistate, callback_popup)
def reset(self):
"""
Reset the fan chart. This should trigger computation of all data
structures needed
"""
self.cache_fontcolor = {}
# fill the data structure
self._fill_data_structures()
# prepare the colors for the boxes
self.prepare_background_box(self.generations_asc + self.generations_desc - 1)
def set_values(self, root_person_handle, maxgen_asc, maxgen_desc, flipupsidedownname, twolinename, background,
background_gradient, fontdescr, grad_start, grad_end,
filter, alpha_filter, angle_algo, dupcolor):
"""
Reset the values to be used:
:param root_person_handle: person to show
:param maxgen_asc: maximum of ascendant generations to show
:param maxgen_desc: maximum of descendant generations to show
:param flipupsidedownname: flip name on the left of the fanchart for the display of person's name
:param background: config setting of which background procedure to use
:type background: int
:param background_gradient: option to add an overall gradient for distinguishing Asc/Desc
:param fontdescr: string describing the font to use
:param grad_start: colors to use for background procedure
:param grad_end: colors to use for background procedure
:param filter: the person filter to apply to the people in the chart
:param alpha_filter: the alpha transparency value (0-1) to apply to
filtered out data
:param angle_algo: alorithm to use to calculate the sizes of the boxes
:param dupcolor: color to use for people or families that occur a second
or more time
"""
self.rootpersonh = root_person_handle
self.generations_asc = maxgen_asc
self.generations_desc = maxgen_desc
self.background = background
self.background_gradient = background_gradient
self.fontdescr = fontdescr
self.grad_start = grad_start
self.grad_end = grad_end
self.filter = filter
self.form = FORM_CIRCLE
self.alpha_filter = alpha_filter
self.anglealgo = angle_algo
self.dupcolor = hex_to_rgb(dupcolor)
self.childring = False
self.flipupsidedownname = flipupsidedownname
self.twolinename = twolinename
def set_generations(self):
"""
Set the generations to max, and fill data structures with initial data.
"""
self.rootangle_rad_desc = [math.radians(275), math.radians(275 + 170)]
self.rootangle_rad_asc = [math.radians(90), math.radians(270)]
self.handle2desc = {}
self.famhandle2desc = {}
self.handle2fam = {}
self.gen2people = {}
self.gen2fam = {}
self.gen2people[0] = [(None, False, 0, 2 * math.pi, 0, 0, [], NORMAL)] # no center person
self.gen2fam[0] = [] # no families
for i in range(1, self.generations_desc):
self.gen2fam[i] = []
self.gen2people[i] = []
self.gen2people[self.generations_desc] = [] # indication of more children
# Ascendance part
self.angle = {}
self.data = {}
for i in range(self.generations_asc):
# name, person, parents?, children?
self.data[i] = [(None,) * 4] * 2 ** i
self.angle[i] = []
angle = self.rootangle_rad_asc[0]
slice = 1 / (2 ** i) * (self.rootangle_rad_asc[1] - self.rootangle_rad_asc[0])
for count in range(len(self.data[i])):
# start, stop, state
self.angle[i].append([angle, angle + slice, NORMAL])
angle += slice
def _fill_data_structures(self):
self.set_generations()
if not self.rootpersonh:
return
person = self.dbstate.db.get_person_from_handle(self.rootpersonh)
if not person:
# nothing to do, just return
return
# Descendance part
# person, duplicate or not, start angle, slice size,
# text, parent pos in fam, nrfam, userdata, status
self.gen2people[0] = [[person, False, 0, 2 * math.pi, 0, 0, [], NORMAL]]
self.handle2desc[self.rootpersonh] = 0
# recursively fill in the datastructures:
nrdesc = self._rec_fill_data(0, person, 0, self.generations_desc)
self.handle2desc[person.handle] += nrdesc
self._compute_angles(*self.rootangle_rad_desc)
# Ascendance part
parents = self._have_parents(person)
child = self._have_children(person)
# Ascendance data structure is the person object, parents, child and
# list for userdata which we might fill in later.
self.data[0][0] = (person, parents, child, [])
for current in range(1, self.generations_asc):
parent = 0
# name, person, parents, children
for (p, q, c, d) in self.data[current - 1]:
# Get father's and mother's details:
for person in [self._get_parent(p, True), self._get_parent(p, False)]:
if current == self.generations_asc - 1:
parents = self._have_parents(person)
else:
parents = None
self.data[current][parent] = (person, parents, None, [])
if person is None:
# start,stop,male/right,state
self.angle[current][parent][2] = COLLAPSED
parent += 1
def nrgen_desc(self):
# compute the number of generations present
for gen in range(self.generations_desc - 1, 0, -1):
if len(self.gen2people[gen]) > 0:
return gen + 1
return 1
def nrgen_asc(self):
# compute the number of generations present
for generation in range(self.generations_asc - 1, 0, -1):
for p in range(len(self.data[generation])):
(person, parents, child, userdata) = self.data[generation][p]
if person:
return generation
return 1
def maxradius_asc(self, generation):
"""
Compute the current half radius of the ascendant circle
"""
radiusin, radius_asc = self.get_radiusinout_for_generation_asc(generation)
return radius_asc + BORDER_EDGE_WIDTH
def maxradius_desc(self,generation):
"""
Compute the current radius of the descendant circle
"""
radiusin_pers, radiusout_pers, radiusin_partner, radius_desc = self.get_radiusinout_for_generation_pair(generation-1)
return radius_desc + BORDER_EDGE_WIDTH
def halfdist(self):
"""
Compute the current max half radius of the circle
"""
return max(self.maxradius_desc(self.nrgen_desc()), self.maxradius_asc(self.nrgen_asc()))
def get_radiusinout_for_generation_desc(self, generation):
"""
Get the in and out radius for descendant generation (starting with center pers = 0)
"""
radius_first_gen = self.CENTER - (1 - PIXELS_PER_GENPERSON_RATIO) * PIXELS_PER_GEN_SMALL
if generation < N_GEN_SMALL:
radius_start = PIXELS_PER_GEN_SMALL * generation + radius_first_gen
return (radius_start, radius_start + PIXELS_PER_GEN_SMALL)
else:
radius_start = PIXELS_PER_GEN_SMALL * N_GEN_SMALL + PIXELS_PER_GEN_LARGE \
* (generation - N_GEN_SMALL) + radius_first_gen
return (radius_start, radius_start + PIXELS_PER_GEN_LARGE)
def get_radiusinout_for_generation_asc(self, generation):
"""
Get the in and out radius for ascendant generation (starting with center pers = 0)
"""
radiusin, radius_first_gen = self.get_radiusinout_for_generation_desc(0)
outerradius = generation * PIXELS_PER_GENERATION + radius_first_gen
innerradius = (generation - 1) * PIXELS_PER_GENERATION + radius_first_gen
if generation == 0:
innerradius = CHILDRING_WIDTH + TRANSLATE_PX
return (innerradius, outerradius)
def get_radiusinout_for_generation_pair(self, generation):
"""
Get the in and out radius for descendant generation pair (starting with center pers = 0)
:return: (radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner)
"""
radiusin, radiusout = self.get_radiusinout_for_generation_desc(generation)
radius_spread = radiusout - radiusin - PIXELS_CHILDREN_GAP - PIXELS_PARTNER_GAP
radiusin_pers = radiusin + PIXELS_CHILDREN_GAP
radiusout_pers = radiusin_pers + PIXELS_PER_GENPERSON_RATIO * radius_spread
radiusin_partner = radiusout_pers + PIXELS_PARTNER_GAP
radiusout_partner = radiusout
return (radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner)
def people_generator(self):
"""
a generator over all people outside of the core person
"""
for generation in range(self.generations_desc):
for data in self.gen2people[generation]:
yield (data[0], data[6])
for generation in range(self.generations_desc):
for data in self.gen2fam[generation]:
yield (data[7], data[6])
for generation in range(self.generations_asc):
for p in range(len(self.data[generation])):
(person, parents, child, userdata) = self.data[generation][p]
yield (person, userdata)
def innerpeople_generator(self):
"""
a generator over all people inside of the core person
"""
if False:
yield
def draw_background(self, cr):
cr.save()
cr.rotate(math.radians(self.rotate_value))
delta = (self.rootangle_rad_asc[0] - self.rootangle_rad_desc[1]) / 2.0 % math.pi
cr.move_to(0, 0)
radius_gradient_asc = 1.5 * self.maxradius_asc(self.generations_asc)
gradient_asc = cairo.RadialGradient(0, 0, self.CENTER, 0, 0, radius_gradient_asc)
color = hex_to_rgb(self.grad_end)
gradient_asc.add_color_stop_rgba(0.0, color[0]/255, color[1]/255, color[2]/255, 0.5)
gradient_asc.add_color_stop_rgba(1.0, 1, 1, 1, 0.0)
start_rad, stop_rad = self.rootangle_rad_asc[0] - delta, self.rootangle_rad_asc[1] + delta
cr.set_source(gradient_asc)
cr.arc(0, 0, radius_gradient_asc, start_rad, stop_rad)
cr.fill()
cr.move_to(0, 0)
radius_gradient_desc = 1.5 * self.maxradius_desc(self.generations_desc)
gradient_desc = cairo.RadialGradient(0, 0, self.CENTER, 0, 0, radius_gradient_desc)
color = hex_to_rgb(self.grad_start)
gradient_desc.add_color_stop_rgba(0.0, color[0]/255, color[1]/255, color[2]/255, 0.5)
gradient_desc.add_color_stop_rgba(1.0, 1, 1, 1, 0.0)
start_rad, stop_rad = self.rootangle_rad_desc[0] - delta, self.rootangle_rad_desc[1] + delta
cr.set_source(gradient_desc)
cr.arc(0, 0, radius_gradient_desc, start_rad, stop_rad)
cr.fill()
cr.restore()
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:
self.set_size_request(2 * halfdist, 2 * halfdist)
cr.scale(scale, scale)
if widget:
self.center_xy = self.center_xy_from_delta()
cr.translate(*self.center_xy)
cr.save()
# Draw background
if self.background_gradient:
self.draw_background(cr)
# Draw center person:
(person, dup, start, slice, parentfampos, nrfam, userdata, status) \
= self.gen2people[0][0]
if not person:
return
gen_remapped = self.generations_desc - 1 # remapped generation
if gen_remapped == 0: gen_remapped = (self.generations_desc + self.generations_asc - 1) # remapped generation
radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner = \
self.get_radiusinout_for_generation_pair(0)
radiusin = TRANSLATE_PX
radiusout = radiusout_pers
self.draw_person(cr, person, radiusin, radiusout, math.pi / 2, math.pi / 2 + 2 * math.pi,
gen_remapped, False, userdata, is_central_person=True)
# 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)
cr.fill()
cr.rotate(math.radians(self.rotate_value))
# Ascendance
for generation in range(self.generations_asc - 1, 0, -1):
for p in range(len(self.data[generation])):
(person, parents, child, userdata) = self.data[generation][p]
if person:
start, stop, state = self.angle[generation][p]
if state in [NORMAL, EXPANDED]:
radiusin, radiusout = self.get_radiusinout_for_generation_asc(generation)
dup = False
gen_remapped = generation + self.generations_desc - 1 # remapped generation
self.draw_person(cr, person, radiusin, radiusout, start, stop,
gen_remapped, dup, userdata, thick=(state == EXPANDED),
has_moregen_indicator=(generation == self.generations_asc - 1 and parents))
# Descendance
for gen in range(self.generations_desc):
radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner = \
self.get_radiusinout_for_generation_pair(gen)
gen_remapped = (self.generations_desc - gen - 1)
if gen_remapped == 0: gen_remapped = (self.generations_desc + self.generations_asc - 1) # remapped generation
if gen > 0:
for pdata in self.gen2people[gen]:
# person, duplicate or not, start angle, slice size,
# parent pos in fam, nrfam, userdata, status
pers, dup, start, slice, pospar, nrfam, userdata, status = pdata
if status != COLLAPSED:
self.draw_person(cr, pers, radiusin_pers, radiusout_pers,
start, start + slice, gen_remapped, dup, userdata,
thick=status != NORMAL)
#if gen < self.generations_desc - 1:
for famdata in self.gen2fam[gen]:
# family, duplicate or not, start angle, slice size,
# spouse pos in gen, nrchildren, userdata, status
fam, dup, start, slice, posfam, nrchild, userdata, partner, status = famdata
if status != COLLAPSED:
more_pers_flag = (gen == self.generations_desc - 1
and len(fam.get_child_ref_list()) > 0)
self.draw_person(cr, partner, radiusin_partner, radiusout_partner, start, start + slice,
gen_remapped, dup, userdata, thick=(status != NORMAL), has_moregen_indicator=more_pers_flag)
cr.restore()
if self.background in [BACKGROUND_GRAD_AGE, BACKGROUND_GRAD_PERIOD]:
self.draw_gradient_legend(cr, widget, halfdist)
def cell_address_under_cursor(self, curx, cury):
"""
Determine the cell address in the fan under the cursor
position x and y.
None if outside of diagram
"""
radius, rads, raw_rads = self.cursor_to_polar(curx, cury, get_raw_rads=True)
if radius < TRANSLATE_PX:
return None
radius_parents = self.get_radiusinout_for_generation_asc(0)[1]
if (radius < radius_parents) or \
(self.radian_in_bounds(self.rootangle_rad_desc[0], rads, self.rootangle_rad_desc[1])):
cell_address = self.cell_address_under_cursor_desc(rads, radius)
if cell_address is not None:
return (TYPE_DESCENDANCE,) + cell_address
elif self.radian_in_bounds(self.rootangle_rad_asc[0], rads, self.rootangle_rad_asc[1]):
cell_address = self.cell_address_under_cursor_asc(rads, radius)
if cell_address and cell_address[0]==0: return None # There is a gap before first parents
if cell_address is not None:
return (TYPE_ASCENDANCE,) + cell_address
return None
def cell_address_under_cursor_desc(self, rads, radius):
"""
Determine the cell address in the fan under the cursor
position x and y.
None if outside of diagram
"""
generation, selected, btype = None, None, TYPE_BOX_NORMAL
for gen in range(self.generations_desc):
radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner \
= self.get_radiusinout_for_generation_pair(gen)
if radiusin_pers <= radius <= radiusout_pers:
generation, btype = gen, TYPE_BOX_NORMAL
break
if radiusin_partner <= radius <= radiusout_partner:
generation, btype = gen, TYPE_BOX_FAMILY
break
# find what person is in this position:
if not (generation is None) and 0 <= generation:
selected = FanChartDescWidget.personpos_at_angle(self, generation, rads, btype)
if (generation is None or selected is None):
return None
return generation, selected, btype
def cell_address_under_cursor_asc(self, rads, radius):
"""
Determine the cell address in the fan under the cursor
position x and y.
None if outside of diagram
"""
generation, selected = None, None
for gen in range(self.generations_asc):
radiusin, radiusout = self.get_radiusinout_for_generation_asc(gen)
if radiusin <= radius <= radiusout:
generation = gen
break
# find what person is in this position:
if not (generation is None) and 0 <= generation:
selected = FanChartWidget.personpos_at_angle(self, generation, rads)
if (generation is None or selected is None):
return None
return generation, selected
def person_at(self, cell_address):
"""
returns the person at radius_first_gen
"""
direction = cell_address[0]
if direction == TYPE_ASCENDANCE:
return FanChartWidget.person_at(self, cell_address[1:])
elif direction == TYPE_DESCENDANCE:
return FanChartDescWidget.person_at(self, cell_address[1:])
return None
def family_at(self, cell_address):
"""
returns the family at cell_address
"""
direction = cell_address[0]
if direction == TYPE_ASCENDANCE:
return None
elif direction == TYPE_DESCENDANCE:
return FanChartDescWidget.family_at(self, cell_address[1:])
return None
def do_mouse_click(self):
# no drag occured, expand or collapse the section
self.toggle_cell_state(self._mouse_click_cell_address)
self._mouse_click = False
self.queue_draw()
def expand_parents(self, generation, selected, current):
if generation >= self.generations_asc: return
selected = 2 * selected
start, stop, state = self.angle[generation][selected]
if state in [NORMAL, EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected] = [current, current + slice, state]
self.expand_parents(generation + 1, selected, current)
current += slice
start, stop, state = self.angle[generation][selected + 1]
if state in [NORMAL, EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected + 1] = [current, current + slice,
state]
self.expand_parents(generation + 1, selected + 1, current)
def show_parents(self, generation, selected, angle, slice):
if generation >= self.generations_asc: return
selected *= 2
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle + slice
self.angle[generation][selected][2] = 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][2] = NORMAL
self.show_parents(generation + 1, selected + 1, angle + slice, slice / 2.0)
def hide_parents(self, generation, selected, angle):
if generation >= self.generations_asc: return
selected = 2 * selected
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle
self.angle[generation][selected][2] = 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][2] = COLLAPSED
self.hide_parents(generation + 1, selected + 1, angle)
def shrink_parents(self, generation, selected, current):
if generation >= self.generations_asc: return
selected = 2 * selected
start, stop, state = self.angle[generation][selected]
if state in [NORMAL, EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected] = [current, current + slice,
state]
self.shrink_parents(generation + 1, selected, current)
current += slice
start, stop, state = self.angle[generation][selected + 1]
if state in [NORMAL, EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected + 1] = [current, current + slice,
state]
self.shrink_parents(generation + 1, selected + 1, current)
def toggle_cell_state(self, cell_address):
direction = cell_address[0]
if direction == TYPE_ASCENDANCE:
FanChartWidget.toggle_cell_state(self, cell_address[1:])
elif direction == TYPE_DESCENDANCE:
FanChartDescWidget.toggle_cell_state(self, cell_address[1:])
self._compute_angles(*self.rootangle_rad_desc)
class FanChart2WayGrampsGUI(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.generations_asc, self.generations_desc, self.flipupsidedownname, self.twolinename, self.background,
self.background_gradient, self.fonttype, self.grad_start, self.grad_end,
self.generic_filter, self.alpha_filter,
self.angle_algo, self.dupcolor)
self.fan.reset()
self.fan.queue_draw()

View File

@ -68,14 +68,21 @@ from .fanchart import *
#-------------------------------------------------------------------------
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_GENPERSON_RATIO = 0.55 # ratio of generation radius for person (rest for partner)
PIXELS_PER_GEN_SMALL = 80
PIXELS_PER_GEN_LARGE = 160
N_GEN_SMALL = 4
PIXELS_PER_GENFAMILY = 25 # size of radius for family
PIXELS_PER_RECLAIM = 4 # size of the radius of pixels taken from family to reclaim space
PIXELS_PARTNER_GAP = 0 # Padding between someone and his partner
PIXELS_CHILDREN_GAP = 5 # Padding between generations
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
TYPE_BOX_NORMAL = 0
TYPE_BOX_FAMILY = 1
#-------------------------------------------------------------------------
#
@ -87,18 +94,18 @@ class FanChartDescWidget(FanChartBaseWidget):
"""
Interactive Fan Chart Widget.
"""
CENTER = 60 # we require a larger center
CENTER = 50 # we require a larger center as CENTER includes the 1st partner
def __init__(self, dbstate, uistate, 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',
self.set_values(None, 9, True, True, BACKGROUND_GRAD_GEN, 'Sans', '#0000FF',
'#FF0000', None, 0.5, FORM_CIRCLE, ANGLE_WEIGHT, '#888a85')
FanChartBaseWidget.__init__(self, dbstate, uistate, callback_popup)
def set_values(self, root_person_handle, maxgen, background,
def set_values(self, root_person_handle, maxgen, flipupsidedownname, twolinename, background,
fontdescr, grad_start, grad_end,
filter, alpha_filter, form, angle_algo, dupcolor):
"""
@ -106,6 +113,7 @@ class FanChartDescWidget(FanChartBaseWidget):
:param root_person_handle: person to show
:param maxgen: maximum generations to show
:param flipupsidedownname: flip name on the left of the fanchart for the display of person's name
:param background: config setting of which background procedure to use
:type background: int
:param fontdescr: string describing the font to use
@ -131,24 +139,28 @@ class FanChartDescWidget(FanChartBaseWidget):
self.anglealgo = angle_algo
self.dupcolor = hex_to_rgb(dupcolor)
self.childring = False
def gen_pixels(self):
"""
how many pixels a generation takes up in the fanchart
"""
return PIXELS_PER_GENPERSON + PIXELS_PER_GENFAMILY
self.flipupsidedownname = flipupsidedownname
self.twolinename = twolinename
def set_generations(self):
"""
Set the generations to max, and fill data structures with initial data.
"""
if self.form == FORM_CIRCLE:
self.rootangle_rad = [math.radians(0), math.radians(360)]
elif self.form == FORM_HALFCIRCLE:
self.rootangle_rad = [math.radians(90), math.radians(90 + 180)]
elif self.form == FORM_QUADRANT:
self.rootangle_rad = [math.radians(90), math.radians(90 + 90)]
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.innerring = []
self.gen2people[0] = [(None, False, 0, 2*pi, 0, 0, [], NORMAL)] #no center person
self.gen2fam[0] = [] #no families
self.angle = {}
self.angle[-2] = []
@ -156,14 +168,6 @@ class FanChartDescWidget(FanChartBaseWidget):
self.gen2fam[i] = []
self.gen2people[i] = []
self.gen2people[self.generations] = [] #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()
@ -173,15 +177,13 @@ class FanChartDescWidget(FanChartBaseWidget):
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.gen2people[0] = [[person, False, 0, 2*pi, 0, 0, [], NORMAL]]
self.handle2desc[self.rootpersonh] = 0
# fill in data for the parents
self.parentsroot = []
self.innerring = []
handleparents = []
family_handle_list = person.get_parent_family_handle_list()
if family_handle_list:
@ -189,173 +191,145 @@ class FanChartDescWidget(FanChartBaseWidget):
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)
for hparent in [family.get_father_handle(), family.get_mother_handle()]:
if hparent and hparent not in handleparents:
parent = self.dbstate.db.get_person_from_handle(hparent)
if parent:
self.innerring.append((parent, []))
handleparents.append(hparent)
#recursively fill in the datastructures:
nrdesc = self.__rec_fill_data(0, person, 0)
nrdesc = self._rec_fill_data(0, person, 0, self.generations)
self.handle2desc[person.handle] += nrdesc
self.__compute_angles()
self._compute_angles(*self.rootangle_rad)
def __rec_fill_data(self, gen, person, pos):
def _rec_fill_data(self, gen, person, pos, maxgen):
"""
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():
marriage_handle_list = person.get_family_handle_list()
self.gen2people[gen][pos][5] = len(marriage_handle_list)
for family_handle in marriage_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 = ''
spouse = None
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 may occur via father and via mother in the chart, only
# first to show and count.
fam_duplicate = family_handle in self.famhandle2desc
# 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])
# spouse pos in gen, nrchildren, userdata, parnter, status
self.gen2fam[gen].append([family, fam_duplicate, 0, 0, pos, 0, [], spouse, NORMAL])
posfam = len(self.gen2fam[gen]) - 1
if not famdup:
if not fam_duplicate and gen < maxgen-1:
nrchild = len(family.get_child_ref_list())
self.gen2fam[gen][-1][6] = nrchild
self.gen2fam[gen][posfam][5] = 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
child_dup = child_ref.ref in self.handle2desc
if not child_dup:
self.handle2desc[child_ref.ref] = 0 # mark this child as processed
# 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])
# parent pos in fam, nrfam, userdata, status
self.gen2people[gen+1].append([child, child_dup, 0, 0, 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)
if not child_dup:
nrdesc = self._rec_fill_data(gen+1, child, pospers, maxgen)
self.handle2desc[child_ref.ref] += nrdesc
totdescfam += nrdesc # add children of him as descendants
if not fam_duplicate:
self.famhandle2desc[family_handle] = totdescfam
totdesc += totdescfam
return totdesc
def __compute_angles(self):
def _compute_angles(self, start_rad, stop_rad):
"""
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
start, slice = start_rad, stop_rad - start_rad
nr_gen = len(self.gen2people)-1
# Fill in central person angles
gen = 0
data = self.gen2people[gen][0]
data[2] = start
data[3] = slice
for gen in range(1, nrgen):
nrpeople = len(self.gen2people[gen])
for gen in range(0, nr_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]]
dupfam = data[1]
for data_fam in self.gen2fam[gen]: # for each partner/fam of gen-1
#obtain start and stop from the people of this partner
persondata = self.gen2people[gen][data_fam[4]]
dupfam = data_fam[1]
if dupfam:
# we don't show the descendants here, but in the first
# occurrence of the family
nrdescfam = 0
nrdescpartner = self.handle2desc[partnerdata[0].handle]
nrfam = partnerdata[6]
# we don't show again the descendants here
nrdescfam = 0
else:
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
nrdescfam = self.famhandle2desc[data_fam[0].handle]
nrdescperson = self.handle2desc[persondata[0].handle]
nrfam = persondata[5]
personstart, personslice = persondata[2:4]
if prevpartnerdatahandle != persondata[0].handle:
#partner of a new person: reset the offset
offset = 0
prevpartnerdatahandle = partnerdata[0].handle
slice = partslice/(nrdescpartner+nrfam)*(nrdescfam+1)
if data[9] == COLLAPSED:
prevpartnerdatahandle = persondata[0].handle
slice = personslice/(nrdescperson+nrfam)*(nrdescfam+1)
if data_fam[8] == COLLAPSED:
slice = 0
elif data[9] == EXPANDED:
slice = partslice
elif data_fam[8] == EXPANDED:
slice = personslice
data[2] = partstart + offset
data[3] = slice
data_fam[2] = personstart + offset
data_fam[3] = slice
offset += slice
## if nrdescpartner == 0:
## if nrdescperson == 0:
## #no offspring, draw as large as fraction of
## #nr families
## nrfam = partnerdata[6]
## slice = partslice/nrfam
## data[2] = partstart + offset
## data[3] = slice
## nrfam = persondata[6]
## slice = personslice/nrfam
## data_fam[2] = personstart + offset
## data_fam[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
## nrfam = persondata[6]
## slice = personslice/(nrdescperson + nrfam - 1)*(nrdescfam+1)
## data_fam[2] = personstart + offset
## data_fam[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
## nrfam = persondata[6]
## slice = personslice/(nrdescperson + nrfam - 1)*(nrdescfam+1)
## data_fam[2] = personstart + offset
## data_fam[3] = slice
## offset += slice
prevfamdatahandle = None
offset = 0
for data in self.gen2people[gen]:
for persondata in self.gen2people[gen+1] if gen < nr_gen else []:
#obtain start and stop of family this is child of
parentfamdata = self.gen2fam[gen-1][data[5]]
parentfamdata = self.gen2fam[gen][persondata[4]]
nrdescfam = 0
if not parentfamdata[1]:
nrdescfam = self.famhandle2desc[parentfamdata[0].handle]
nrdesc = 0
if not data[1]:
nrdesc = self.handle2desc[data[0].handle]
if not persondata[1]:
nrdesc = self.handle2desc[persondata[0].handle]
famstart = parentfamdata[2]
famslice = parentfamdata[3]
nrchild = parentfamdata[6]
nrchild = parentfamdata[5]
#now we divide this slice to the weight of children,
#adding one for every child
if self.anglealgo == ANGLE_CHEQUI:
@ -368,32 +342,48 @@ class FanChartDescWidget(FanChartBaseWidget):
#reset the offset
offset = 0
prevfamdatahandle = parentfamdata[0].handle
if data[8] == COLLAPSED:
if persondata[7] == COLLAPSED:
slice = 0
elif data[8] == EXPANDED:
elif persondata[7] == EXPANDED:
slice = famslice
data[2] = famstart + offset
data[3] = slice
persondata[2] = famstart + offset
persondata[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
return gen + 1
return 1
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
radius = PIXELS_PER_GEN_SMALL * N_GEN_SMALL + PIXELS_PER_GEN_LARGE \
* ( self.nrgen() - N_GEN_SMALL ) + self.CENTER
return radius
def get_radiusinout_for_generation(self,generation):
radius_first_gen = self.CENTER - (1-PIXELS_PER_GENPERSON_RATIO) * PIXELS_PER_GEN_SMALL
if generation < N_GEN_SMALL:
radius_start = PIXELS_PER_GEN_SMALL * generation + radius_first_gen
return (radius_start,radius_start + PIXELS_PER_GEN_SMALL)
else:
radius_start = PIXELS_PER_GEN_SMALL * N_GEN_SMALL + PIXELS_PER_GEN_LARGE \
* ( generation - N_GEN_SMALL ) + radius_first_gen
return (radius_start,radius_start + PIXELS_PER_GEN_LARGE)
def get_radiusinout_for_generation_pair(self,generation):
radiusin, radiusout = self.get_radiusinout_for_generation(generation)
radius_spread = radiusout - radiusin - PIXELS_CHILDREN_GAP - PIXELS_PARTNER_GAP
radiusin_pers = radiusin + PIXELS_CHILDREN_GAP
radiusout_pers = radiusin_pers + PIXELS_PER_GENPERSON_RATIO * radius_spread
radiusin_partner = radiusout_pers + PIXELS_PARTNER_GAP
radiusout_partner = radiusout
return (radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner)
def people_generator(self):
"""
@ -401,16 +391,16 @@ class FanChartDescWidget(FanChartBaseWidget):
"""
for generation in range(self.generations):
for data in self.gen2people[generation]:
yield (data[0], data[7])
for generation in range(self.generations-1):
yield (data[0], data[6])
for generation in range(self.generations):
for data in self.gen2fam[generation]:
yield (data[8], data[7])
yield (data[7], data[6])
def innerpeople_generator(self):
"""
a generator over all people inside of the core person
"""
for parentdata in self.parentsroot:
for parentdata in self.innerring:
parent, userdata = parentdata
yield (parent, userdata)
@ -432,184 +422,112 @@ class FanChartDescWidget(FanChartBaseWidget):
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)
self.center_xy = self.center_xy_from_delta()
cr.translate(*self.center_xy)
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) \
(person, dup, start, slice, 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()
radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner = \
self.get_radiusinout_for_generation_pair(0)
if not self.innerring: radiusin_pers = TRANSLATE_PX
self.draw_person(cr, person, radiusin_pers, radiusout_pers, math.pi/2, math.pi/2 + 2*math.pi,
0, False, userdata, is_central_person =True)
#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
if self.innerring: # has at least one parent
cr.fill()
self.draw_parentring(cr)
self.draw_innerring_people(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 gen in range(self.generations):
radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner = \
self.get_radiusinout_for_generation_pair(gen)
if gen > 0:
for pdata in self.gen2people[gen]:
# person, duplicate or not, start angle, slice size,
# parent pos in fam, nrfam, userdata, status
pers, dup, start, slice, pospar, nrfam, userdata, status = \
pdata
if status != COLLAPSED:
self.draw_person(cr, pers, radiusin_pers, radiusout_pers,
start, start + slice, gen, dup, userdata,
thick=status != NORMAL)
#if gen < self.generations-1:
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,\
# spouse pos in gen, nrchildren, userdata, status
fam, dup, start, slice, 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)
more_pers_flag = (gen == self.generations - 1
and len(fam.get_child_ref_list()) > 0)
self.draw_person(cr, partner, radiusin_partner, radiusout_partner, start, start + slice,
gen, dup, userdata, thick = (status != NORMAL), has_moregen_indicator = more_pers_flag )
cr.restore()
if self.background in [BACKGROUND_GRAD_AGE, BACKGROUND_GRAD_PERIOD]:
self.draw_gradient(cr, widget, halfdist)
self.draw_gradient_legend(cr, widget, halfdist)
def draw_person(self, cr, name, start_rad, slice, radius, radiusend,
generation, dup, person, userdata, family=False, thick=False):
def cell_address_under_cursor(self, curx, cury):
"""
Display the piece of pie for a given person. start_rad and slice
are in radial.
Determine the cell address in the fan under the cursor
position x and y.
None if outside of diagram
"""
if slice == 0:
return
cr.save()
full = False
if abs(slice - 2*pi) < 1e-6:
full = True
stop_rad = start_rad + slice
if not person:
#an family with partner not set. Don't have a color for this,
# let's make it transparent
r, g, b, a = (255, 255, 255, 0)
elif not dup:
r, g, b, a = self.background_box(person, generation, userdata)
radius, rads, raw_rads = self.cursor_to_polar(curx, cury, get_raw_rads=True)
btype = TYPE_BOX_NORMAL
if radius < TRANSLATE_PX:
return None
elif (self.innerring and self.angle[-2] and
radius < CHILDRING_WIDTH + TRANSLATE_PX):
generation = -2 # indication of one of the children
elif radius < self.CENTER:
generation = 0
else:
#duplicate color
a = 1
r, g, b = self.dupcolor #(136, 138, 133)
# 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
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()
generation = None
for gen in range(self.generations):
radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner \
= self.get_radiusinout_for_generation_pair(gen)
if radiusin_pers <= radius <= radiusout_pers:
generation, btype = gen, TYPE_BOX_NORMAL
break
if radiusin_partner <= radius <= radiusout_partner:
generation, btype = gen, TYPE_BOX_FAMILY
break
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
# find what person is in this position:
selected = None
if not (generation is None) and 0 <= generation:
selected = self.personpos_at_angle(generation, rads, btype)
elif generation == -2:
for p in range(len(self.angle[generation])):
start, stop, state = self.angle[generation][p]
if self.radian_in_bounds(start, raw_rads, stop):
selected = p
break
if (generation is None or selected is None):
return None
return generation, selected, btype
def draw_parentring(self, cr):
def draw_innerring_people(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)
nrparent = len(self.innerring)
#Y axis is downward. positve angles are hence clockwise
startangle = math.pi
if nrparent <= 2:
@ -618,104 +536,100 @@ class FanChartDescWidget(FanChartBaseWidget):
angleinc = math.pi/2
else:
angleinc = 2 * math.pi / nrchild
for data in self.parentsroot:
for data in self.innerring:
self.draw_innerring(cr, data[0], data[1], startangle, angleinc)
startangle += angleinc
def personpos_at_angle(self, generation, angledeg, btype):
def personpos_at_angle(self, generation, rads, btype):
"""
returns the person in generation generation at angle.
"""
angle = angledeg / 360 * 2 * pi
selected = None
datas = 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
if generation==0:
return 0 # central person is always ok !
datas = self.gen2people[generation]
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
datas = self.gen2fam[generation]
else:
return None
for p, pdata in enumerate(datas):
# person, duplicate or not, start angle, slice size,
# parent pos in fam, nrfam, userdata, status
start, stop = pdata[2], pdata[2] + pdata[3]
if self.radian_in_bounds(start, rads, stop):
selected = p
break
return selected
def person_at(self, generation, pos, btype):
def person_at(self, cell_address):
"""
returns the person at generation, pos, btype
"""
if pos is None:
return None
generation, pos, btype = cell_address
if generation == -2:
person, userdata = self.parentsroot[pos]
person, userdata = self.innerring[pos]
elif btype == TYPE_BOX_NORMAL:
# person, duplicate or not, start angle, slice size,
# text, parent pos in fam, nrfam, userdata, status
# 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]
# spouse pos in gen, nrchildren, userdata, person, status
person = self.gen2fam[generation][pos][7]
return person
def family_at(self, generation, pos, btype):
def family_at(self, cell_address):
"""
returns the family at generation, pos, btype
"""
generation, pos, btype = cell_address
if pos is None or btype == TYPE_BOX_NORMAL or generation < 0:
return None
return self.gen2fam[generation][pos][0]
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.toggle_cell_state(self._mouse_click_cell_address)
self._compute_angles(*self.rootangle_rad)
self._mouse_click = False
self.queue_draw()
def change_slice(self, generation, selected, btype):
def toggle_cell_state(self, cell_address):
generation, selected, btype = cell_address
if generation < 1:
return
if btype == TYPE_BOX_NORMAL:
data = self.gen2people[generation][selected]
parpos = data[5]
status = data[8]
parpos = data[4]
status = data[7]
if status == NORMAL:
#should be expanded, rest collapsed
for entry in self.gen2people[generation]:
if entry[5] == parpos:
if entry[4] == parpos:
entry[7] = COLLAPSED
data[7] = EXPANDED
else:
#is expanded, set back to normal
for entry in self.gen2people[generation]:
if entry[4] == parpos:
entry[7] = NORMAL
if btype == TYPE_BOX_FAMILY:
data = self.gen2fam[generation][selected]
parpos = data[4]
status = data[8]
if status == NORMAL:
#should be expanded, rest collapsed
for entry in self.gen2fam[generation]:
if entry[4] == parpos:
entry[8] = COLLAPSED
data[8] = EXPANDED
else:
#is expanded, set back to normal
for entry in self.gen2people[generation]:
if entry[5] == parpos:
for entry in self.gen2fam[generation]:
if entry[4] == 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
@ -727,7 +641,7 @@ class FanChartDescGrampsGUI(FanChartGrampsGUI):
data.
"""
root_person_handle = self.get_active('Person')
self.fan.set_values(root_person_handle, self.maxgen, self.background,
self.fan.set_values(root_person_handle, self.maxgen, self.flipupsidedownname, self.twolinename, self.background,
self.fonttype, self.grad_start, self.grad_end,
self.generic_filter, self.alpha_filter, self.form,
self.angle_algo, self.dupcolor)

View File

@ -0,0 +1,100 @@
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# $Id$
## Based on the normal fanchart
#-------------------------------------------------------------------------
#
# Python modules
#
#-------------------------------------------------------------------------
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Pango
from gi.repository import Gtk
import math
from gi.repository import Gdk
try:
import cairo
except ImportError:
pass
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
from gramps.gen.plug import Gramplet
from gramps.gen.errors import WindowActiveError
from gramps.gui.editors import EditPerson
from gramps.gui.widgets.fanchart2way import (FanChart2WayWidget, FanChart2WayGrampsGUI,
ANGLE_WEIGHT)
from gramps.gui.widgets.fanchart import FORM_HALFCIRCLE, BACKGROUND_SCHEME1
class FanChart2WayGramplet(FanChart2WayGrampsGUI, Gramplet):
"""
The Gramplet code that realizes the FanChartWidget.
"""
def __init__(self, gui, nav_group=0):
Gramplet.__init__(self, gui, nav_group)
FanChart2WayGrampsGUI.__init__(self, self.on_childmenu_changed)
self.generations_asc = 5
self.generations_desc = 4
self.background = BACKGROUND_SCHEME1
self.fonttype = 'Sans'
self.grad_start = '#FF0000'
self.grad_end = '#0000FF'
self.dupcolor = '#888A85' #light grey
self.generic_filter = None
self.alpha_filter = 0.2
self.form = FORM_HALFCIRCLE
self.angle_algo = ANGLE_WEIGHT
self.flipupsidedownname = True
self.twolinename = True
self.childring = False
self.background_gradient = True
#self.filter = filter
self.set_fan(FanChart2WayWidget(self.dbstate, self.uistate, self.on_popup))
# Replace the standard textview with the fan chart widget:
self.gui.get_container_widget().remove(self.gui.textview)
self.gui.get_container_widget().add_with_viewport(self.fan)
# Make sure it is visible:
self.fan.show()
def init(self):
self.set_tooltip(_("Click to expand/contract person\nRight-click for options\nClick and drag in open area to rotate"))
def active_changed(self, handle):
"""
Method called when active person changes.
"""
# Reset everything but rotation angle (leave it as is)
self.update()
def on_childmenu_changed(self, obj, person_handle):
"""Callback for the pulldown menu selection, changing to the person
attached with menu item."""
self.set_active('Person', person_handle)
return True

View File

@ -50,6 +50,8 @@ class FanChartDescGramplet(FanChartDescGrampsGUI, Gramplet):
self.alpha_filter = 0.2
self.form = FORM_HALFCIRCLE
self.angle_algo = ANGLE_WEIGHT
self.flipupsidedownname = True
self.twolinename = True
self.set_fan(FanChartDescWidget(self.dbstate, self.uistate,
self.on_popup))
# Replace the standard textview with the fan chart widget:

View File

@ -47,6 +47,8 @@ class FanChartGramplet(FanChartGrampsGUI, Gramplet):
self.maxgen = 6
self.background = BACKGROUND_SCHEME1
self.childring = True
self.flipupsidedownname = True
self.twolinename = True
self.radialtext = True
self.fonttype = 'Sans'
self.grad_start = '#0000FF'

View File

@ -147,6 +147,23 @@ register(GRAMPLET,
navtypes=["Person"],
)
register(GRAMPLET,
id= "2-Way Fan Chart",
name=_("2-Way Fan Chart"),
description = _("Gramplet showing active person's direct ancestors and descendants as a fanchart"),
status = STABLE,
fname="fanchart2waygramplet.py",
height=300,
expand=True,
gramplet = 'FanChart2WayGramplet',
detached_height =300,
detached_width = 300,
gramplet_title=_("2-Way Fan"),
version="1.0.0",
gramps_target_version=MODULE_VERSION,
navtypes=["Person"],
)
register(GRAMPLET,
id="FAQ",
name=_("FAQ"),

View File

@ -0,0 +1,572 @@
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
# Copyright (C) 2009 Douglas S. Blank
# Copyright (C) 2014 Bastien Jacquet
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
## 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 gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
import gramps.gui.widgets.fanchart as fanchart
import gramps.gui.widgets.fanchart2way as fanchart2way
from gramps.gui.views.navigationview import NavigationView
from gramps.gui.views.bookmarks import PersonBookmarks
from gramps.gui.utils import SystemFonts
# the print settings to remember between print sessions
PRINT_SETTINGS = None
class FanChart2WayView(fanchart2way.FanChart2WayGrampsGUI, NavigationView):
"""
The Gramplet code that realizes the FanChartWidget.
"""
#settings in the config file
CONFIGSETTINGS = (
('interface.fanview-maxgen-asc', 4),
('interface.fanview-maxgen-desc', 4),
('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN),
('interface.fanview-background-gradient', True),
('interface.fanview-radialtext', True),
('interface.fanview-twolinename', True),
('interface.fanview-flipupsidedownname', True),
('interface.fanview-font', 'Sans'),
('interface.fanview-form', fanchart.FORM_CIRCLE),
('interface.color-start-grad', '#ef2929'),
('interface.color-end-grad', '#3d37e9'),
('interface.angle-algorithm', fanchart2way.ANGLE_WEIGHT),
('interface.duplicate-color', '#888a85')
)
def __init__(self, pdata, dbstate, uistate, nav_group=0):
self.dbstate = dbstate
self.uistate = uistate
NavigationView.__init__(self, _('2-Way Fan Chart'),
pdata, dbstate, uistate,
PersonBookmarks,
nav_group)
fanchart2way.FanChart2WayGrampsGUI.__init__(self, self.on_childmenu_changed)
#set needed values
self.generations_asc = self._config.get('interface.fanview-maxgen-asc')
self.generations_desc = self._config.get('interface.fanview-maxgen-desc')
self.background = self._config.get('interface.fanview-background')
self.background_gradient = self._config.get('interface.fanview-background-gradient')
self.radialtext = self._config.get('interface.fanview-radialtext')
self.twolinename = self._config.get('interface.fanview-twolinename')
self.flipupsidedownname = self._config.get('interface.fanview-flipupsidedownname')
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 = fanchart.FORM_CIRCLE
self.angle_algo = self._config.get('interface.angle-algorithm')
self.dupcolor = self._config.get('interface.duplicate-color')
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(fanchart2way.FanChart2WayWidget(self.dbstate, self.uistate,
self.on_popup))
self.scrolledwindow = Gtk.ScrolledWindow(hadjustment=None,
vadjustment=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..."),
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)
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
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 = 9
grid = Gtk.Grid()
grid.set_border_width(12)
grid.set_column_spacing(6)
grid.set_row_spacing(6)
configdialog.add_spinner(grid, _("Max ancestor generations"), 0,
'interface.fanview-maxgen-asc', (1, 11),
callback=self.cb_update_maxgen)
configdialog.add_spinner(grid, _("Max descendant generations"), 1,
'interface.fanview-maxgen-desc', (1, 11),
callback=self.cb_update_maxgen)
configdialog.add_combo(grid,
_('Text Font'),
2, '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(grid,
_('Background'),
3, 'interface.fanview-background',
backgrvals,
callback=self.cb_update_background, valueactive=False,
setactive=nrval
)
# show names one two line
configdialog.add_checkbox(grid,
_('Add global background colored gradient'),
4, 'interface.fanview-background-gradient')
#colors, stored as hex values
configdialog.add_color(grid, _('Start gradient/Main color'), 5,
'interface.color-start-grad', col=1)
configdialog.add_color(grid, _('End gradient/2nd color'), 6,
'interface.color-end-grad', col=1)
configdialog.add_color(grid, _('Color for duplicates'), 7,
'interface.duplicate-color', col=1)
# algo for the fan angle distribution
configdialog.add_combo(grid, _('Fan chart distribution'), 8,
'interface.angle-algorithm',
((fanchart2way.ANGLE_CHEQUI,
_('Homogeneous children distribution')),
(fanchart2way.ANGLE_WEIGHT,
_('Size proportional to number of descendants')),
),
callback=self.cb_update_anglealgo)
# show names one two line
configdialog.add_checkbox(grid,
_('Show names on two lines'),
9, 'interface.fanview-twolinename')
# Flip names
configdialog.add_checkbox(grid,
_('Flip name on the left of the fan'),
10, 'interface.fanview-flipupsidedownname')
return _('Layout'), grid
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)
self._config.connect('interface.duplicate-color',
self.cb_update_color)
self._config.connect('interface.fanview-flipupsidedownname',
self.cb_update_flipupsidedownname)
self._config.connect('interface.fanview-twolinename',
self.cb_update_twolinename)
self._config.connect('interface.fanview-background-gradient',
self.cb_update_background_gradient)
def cb_update_maxgen(self, spinbtn, constant):
self._config.set(constant, spinbtn.get_value_as_int())
self.generations_asc = int(self._config.get('interface.fanview-maxgen-asc'))
self.generations_desc = int(self._config.get('interface.fanview-maxgen-desc'))
self.update()
def cb_update_twolinename(self, client, cnxn_id, entry, data):
"""
Called when the configuration menu changes the twolinename setting.
"""
self.twolinename = (entry == 'True')
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_background_gradient(self, client, cnxn_id, entry, data):
"""
Called when the configuration menu changes the twolinename setting.
"""
self.background_gradient = (entry == 'True')
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.dupcolor = self._config.get('interface.duplicate-color')
self.update()
def cb_update_flipupsidedownname(self, client, cnxn_id, entry, data):
"""
Called when the configuration menu changes the flipupsidedownname setting.
"""
self.flipupsidedownname = (entry == 'True')
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

@ -58,6 +58,9 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
CONFIGSETTINGS = (
('interface.fanview-maxgen', 9),
('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN),
('interface.fanview-radialtext', True),
('interface.fanview-twolinename', True),
('interface.fanview-flipupsidedownname', True),
('interface.fanview-font', 'Sans'),
('interface.fanview-form', fanchart.FORM_CIRCLE),
('interface.color-start-grad', '#ef2929'),
@ -77,6 +80,9 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
#set needed values
self.maxgen = self._config.get('interface.fanview-maxgen')
self.background = self._config.get('interface.fanview-background')
self.radialtext = self._config.get('interface.fanview-radialtext')
self.twolinename = self._config.get('interface.fanview-twolinename')
self.flipupsidedownname = self._config.get('interface.fanview-flipupsidedownname')
self.fonttype = self._config.get('interface.fanview-font')
self.grad_start = self._config.get('interface.color-start-grad')
@ -265,7 +271,7 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
"""
Function that builds the widget in the configuration dialog
"""
nrentry = 8
nrentry = 9
grid = Gtk.Grid()
grid.set_border_width(12)
grid.set_column_spacing(6)
@ -326,6 +332,16 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
),
callback=self.cb_update_anglealgo)
# show names one two line
configdialog.add_checkbox(grid,
_('Show names on two lines'),
8, 'interface.fanview-twolinename')
# Flip names
configdialog.add_checkbox(grid,
_('Flip name on the left of the fan'),
9, 'interface.fanview-flipupsidedownname')
return _('Layout'), grid
def config_connect(self):
@ -340,12 +356,23 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
self.cb_update_color)
self._config.connect('interface.duplicate-color',
self.cb_update_color)
self._config.connect('interface.fanview-flipupsidedownname',
self.cb_update_flipupsidedownname)
self._config.connect('interface.fanview-twolinename',
self.cb_update_twolinename)
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_twolinename(self, client, cnxn_id, entry, data):
"""
Called when the configuration menu changes the twolinename setting.
"""
self.twolinename = (entry == 'True')
self.update()
def cb_update_background(self, obj, constant):
entry = obj.get_active()
Gtk.TreePath.new_from_string('%d' % entry)
@ -376,6 +403,13 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
self.dupcolor = self._config.get('interface.duplicate-color')
self.update()
def cb_update_flipupsidedownname(self, client, cnxn_id, entry, data):
"""
Called when the configuration menu changes the flipupsidedownname setting.
"""
self.flipupsidedownname = (entry == 'True')
self.update()
def cb_update_font(self, obj, constant):
entry = obj.get_active()
self._config.set(constant, self.allfonts[entry][1])

View File

@ -59,6 +59,8 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN),
('interface.fanview-childrenring', True),
('interface.fanview-radialtext', True),
('interface.fanview-twolinename', True),
('interface.fanview-flipupsidedownname', True),
('interface.fanview-font', 'Sans'),
('interface.fanview-form', fanchart.FORM_CIRCLE),
('interface.color-start-grad', '#ef2929'),
@ -78,6 +80,8 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
self.background = self._config.get('interface.fanview-background')
self.childring = self._config.get('interface.fanview-childrenring')
self.radialtext = self._config.get('interface.fanview-radialtext')
self.twolinename = self._config.get('interface.fanview-twolinename')
self.flipupsidedownname = self._config.get('interface.fanview-flipupsidedownname')
self.fonttype = self._config.get('interface.fanview-font')
self.grad_start = self._config.get('interface.color-start-grad')
@ -263,7 +267,7 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
"""
Function that builds the widget in the configuration dialog
"""
nrentry = 7
nrentry = 9
grid = Gtk.Grid()
grid.set_border_width(12)
grid.set_column_spacing(6)
@ -311,6 +315,16 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
(2, _('Quadrant'))),
callback=self.cb_update_form)
# show names one two line
configdialog.add_checkbox(grid,
_('Show names on two lines'),
6, 'interface.fanview-twolinename')
# Flip names
configdialog.add_checkbox(grid,
_('Flip name on the left of the fan'),
7, 'interface.fanview-flipupsidedownname')
# options users should not change:
configdialog.add_checkbox(grid,
_('Show children ring'),
@ -330,6 +344,10 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
"""
self._config.connect('interface.fanview-childrenring',
self.cb_update_childrenring)
self._config.connect('interface.fanview-twolinename',
self.cb_update_twolinename)
self._config.connect('interface.fanview-flipupsidedownname',
self.cb_update_flipupsidedownname)
self._config.connect('interface.fanview-radialtext',
self.cb_update_radialtext)
self._config.connect('interface.color-start-grad',
@ -385,6 +403,20 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
self.grad_end = self._config.get('interface.color-end-grad')
self.update()
def cb_update_twolinename(self, client, cnxn_id, entry, data):
"""
Called when the configuration menu changes the twolinename setting.
"""
self.twolinename = (entry == 'True')
self.update()
def cb_update_flipupsidedownname(self, client, cnxn_id, entry, data):
"""
Called when the configuration menu changes the flipupsidedownname setting.
"""
self.flipupsidedownname = (entry == 'True')
self.update()
def cb_update_font(self, obj, constant):
entry = obj.get_active()
self._config.set(constant, self.allfonts[entry][1])

View File

@ -166,6 +166,21 @@ viewclass = 'FanChartDescView',
stock_icon = 'gramps-fanchartdesc',
)
register(VIEW,
id = 'fanchart2wayview',
name = _("2-Way Fan"),
category = ("Ancestry", _("Charts")),
description = _("Showing ascendants and descendants through a fanchart"),
version = '1.0',
gramps_target_version = MODULE_VERSION,
status = STABLE,
fname = 'fanchart2wayview.py',
authors = ["B. Jacquet"],
authors_email = ["bastien.jacquet_dev@m4x.org"],
viewclass = 'FanChart2WayView',
stock_icon = 'gramps-fanchart2way',
)
register(VIEW,
id = 'personview',
name = _("Grouped People"),

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 52 KiB