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:
parent
3d854ba944
commit
fec5d532d1
@ -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
625
gramps/gui/widgets/fanchart2way.py
Normal file
625
gramps/gui/widgets/fanchart2way.py
Normal 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()
|
@ -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 famdata in self.gen2fam[gen]:
|
||||
# family, duplicate or not, start angle, slice size,
|
||||
# text, spouse pos in gen, nrchildren, userdata, status
|
||||
fam, dup, start, slice, text, posfam, nrchild, userdata,\
|
||||
partner, status = famdata
|
||||
if status != COLLAPSED:
|
||||
self.draw_person(cr, text, start, slice, radstart,
|
||||
radstart + PIXELS_PER_GENFAMILY, gen, dup,
|
||||
partner, userdata, family=True, thick=status != NORMAL)
|
||||
radstart += PIXELS_PER_GENFAMILY
|
||||
for pdata in self.gen2people[gen+1]:
|
||||
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,
|
||||
# text, parent pos in fam, nrfam, userdata, status
|
||||
pers, dup, start, slice, text, pospar, nrfam, userdata, status = \
|
||||
# parent pos in fam, nrfam, userdata, status
|
||||
pers, dup, start, slice, 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)
|
||||
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,
|
||||
# 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 - 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)
|
||||
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()
|
||||
radius, rads, raw_rads = self.cursor_to_polar(curx, cury, get_raw_rads=True)
|
||||
|
||||
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
|
||||
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:
|
||||
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
|
||||
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 draw_parentring(self, cr):
|
||||
# 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_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]):
|
||||
datas = self.gen2fam[generation]
|
||||
else:
|
||||
return None
|
||||
for p, pdata in enumerate(datas):
|
||||
# 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:
|
||||
# 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)
|
||||
|
100
gramps/plugins/gramplet/fanchart2waygramplet.py
Normal file
100
gramps/plugins/gramplet/fanchart2waygramplet.py
Normal 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
|
@ -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:
|
||||
|
@ -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'
|
||||
|
@ -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"),
|
||||
|
572
gramps/plugins/view/fanchart2wayview.py
Normal file
572
gramps/plugins/view/fanchart2wayview.py
Normal 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()
|
@ -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])
|
||||
|
@ -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])
|
||||
|
@ -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"),
|
||||
|
BIN
images/hicolor/16x16/actions/gramps-fanchart2way.png
Normal file
BIN
images/hicolor/16x16/actions/gramps-fanchart2way.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 980 B |
BIN
images/hicolor/22x22/actions/gramps-fanchart2way.png
Normal file
BIN
images/hicolor/22x22/actions/gramps-fanchart2way.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
images/hicolor/48x48/actions/gramps-fanchart2way.png
Normal file
BIN
images/hicolor/48x48/actions/gramps-fanchart2way.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
1599
images/hicolor/scalable/actions/gramps-fanchart2way.svg
Normal file
1599
images/hicolor/scalable/actions/gramps-fanchart2way.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 52 KiB |
Loading…
Reference in New Issue
Block a user