Source code for gramps.gui.widgets.fanchart

#
# 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) 2013 Vassilii Khachaturov
#
# 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

from __future__ import division

#-------------------------------------------------------------------------
#
# Python modules
#
#-------------------------------------------------------------------------
from gi.repository import Pango
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import PangoCairo
import cairo
import math
import colorsys
import sys
if sys.version_info[0] < 3:
    import cPickle as pickle
else:
    import pickle
from cgi import escape

#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from gramps.gen.db import DbTxn
from gramps.gen.display.name import displayer as name_displayer
from gramps.gen.errors import WindowActiveError
from gramps.gen.lib import ChildRef, Family, Name, Person, Surname
from gramps.gen.lib.date import Today
from ..editors import EditPerson, EditFamily
from .reorderfam import Reorder
from ..utils import color_graph_box, hex_to_rgb, is_right_click
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, preset_name)
from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
from gramps.gui.utilscairo import warpPath

#-------------------------------------------------------------------------
#
# Functions
#
#-------------------------------------------------------------------------
def gender_code(is_male):
    """
    Given boolean is_male (means position in FanChart) return code.
    """
    if is_male: 
        return Person.MALE
    else:
        return Person.FEMALE

#-------------------------------------------------------------------------
#
# Constants
#
#-------------------------------------------------------------------------

PIXELS_PER_GENERATION = 50 # size of radius for generation
BORDER_EDGE_WIDTH = 10     # empty white box size at edge to indicate parents
CHILDRING_WIDTH = 12       # width of the children ring inside the person
TRANSLATE_PX = 10          # size of the central circle, used to move the chart
PAD_PX = 4                 # padding with edges
PAD_TEXT = 2               # padding for text in boxes

BACKGROUND_SCHEME1 = 0
BACKGROUND_SCHEME2 = 1
BACKGROUND_GENDER = 2
BACKGROUND_WHITE = 3
BACKGROUND_GRAD_GEN = 4
BACKGROUND_GRAD_AGE = 5
BACKGROUND_SINGLE_COLOR = 6
BACKGROUND_GRAD_PERIOD = 7
GENCOLOR = {
    BACKGROUND_SCHEME1: ((255, 63,  0),
                         (255,175, 15),
                         (255,223, 87),
                         (255,255,111),
                         (159,255,159),
                         (111,215,255),
                         ( 79,151,255),
                         (231, 23,255),
                         (231, 23,121),
                         (210,170,124),
                         (189,153,112)),
    BACKGROUND_SCHEME2: ((229,191,252),
                         (191,191,252),
                         (191,222,252),
                         (183,219,197),
                         (206,246,209)),
    BACKGROUND_WHITE: ((255,255,255),
                       (255,255,255),),
    }

MAX_AGE = 100
GRADIENTSCALE = 5

FORM_CIRCLE = 0
FORM_HALFCIRCLE = 1
FORM_QUADRANT = 2

COLLAPSED = 0
NORMAL = 1
EXPANDED = 2

TYPE_BOX_NORMAL = 0
TYPE_BOX_FAMILY = 1

#-------------------------------------------------------------------------
#
# FanChartBaseWidget
#
#-------------------------------------------------------------------------

[docs]class FanChartBaseWidget(Gtk.DrawingArea): """ a base widget for fancharts""" CENTER = 50 # pixel radius of center, changes per fanchart def __init__(self, dbstate, uistate, callback_popup=None): GObject.GObject.__init__(self) self.radialtext = True st_cont = self.get_style_context() col = st_cont.lookup_color('text_color') if col[0]: self.textcolor = (col[1].red, col[1].green, col[1].blue) else: self.textcolor = (0, 0, 0) self.dbstate = dbstate self.uistate = uistate self.translating = False self.goto = None self.on_popup = callback_popup self.last_x, self.last_y = None, None self.fontdescr = "Sans" self.fontsize = 8 self.connect("button_release_event", self.on_mouse_up) self.connect("motion_notify_event", self.on_mouse_move) self.connect("button-press-event", self.on_mouse_down) #we want to grab key events also self.set_can_focus(True) self.connect("key-press-event", self.on_key_press) self.connect("draw", self.on_draw) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.KEY_PRESS_MASK) # Enable drag self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY) tglist = Gtk.TargetList.new([]) tglist.add(DdTargets.PERSON_LINK.atom_drag_type, DdTargets.PERSON_LINK.target_flags, DdTargets.PERSON_LINK.app_id) #allow drag to a text document, info on drag_get will be 0L ! tglist.add_text_targets(0) self.drag_source_set_target_list(tglist) self.connect("drag_data_get", self.on_drag_data_get) self.connect("drag_begin", self.on_drag_begin) self.connect("drag_end", self.on_drag_end) # Enable drop self.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.DROP, [], Gdk.DragAction.COPY) tglist = Gtk.TargetList.new([]) tglist.add(DdTargets.PERSON_LINK.atom_drag_type, DdTargets.PERSON_LINK.target_flags, DdTargets.PERSON_LINK.app_id) self.drag_dest_set_target_list(tglist) self.connect('drag_data_received', self.on_drag_data_received) self._mouse_click = False self.rotate_value = 90 # degrees, initially, 1st gen male on right half self.center_xy = [0, 0] # distance from center (x, y) self.center_x = 0 self.center_y = 0 self.mouse_x = 0 self.mouse_y = 0 #(re)compute everything self.reset() self.set_size_request(120, 120)
[docs] 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()
def _fill_data_structures(self): """ fill in the data structures that will be needed to draw the chart """ raise NotImplementedError
[docs] def do_size_request(self, requisition): """ Overridden method to handle size request events. """ if self.form == FORM_CIRCLE: requisition.width = 2 * self.halfdist() requisition.height = requisition.width elif self.form == FORM_HALFCIRCLE: requisition.width = 2 * self.halfdist() requisition.height = requisition.width / 2 + self.CENTER + PAD_PX elif self.form == FORM_QUADRANT: requisition.width = self.halfdist() + self.CENTER + PAD_PX requisition.height = requisition.width
[docs] def do_get_preferred_width(self): """ GTK3 uses width for height sizing model. This method will override the virtual method """ req = Gtk.Requisition() self.do_size_request(req) return req.width, req.width
[docs] def do_get_preferred_height(self): """ GTK3 uses width for height sizing model. This method will override the virtual method """ req = Gtk.Requisition() self.do_size_request(req) return req.height, req.height
[docs] def halfdist(self): """ Compute the half radius of the circle """ raise NotImplementedError
[docs] def gen_pixels(self): """ how many pixels a generation takes up in the fanchart """ raise NotImplementedError
[docs] def on_draw(self, widget, cr, scale=1.): """ callback to draw the fanchart """ raise NotImplementedError
[docs] def people_generator(self): """ a generator over all people outside of the core person """ raise NotImplementedError
[docs] def innerpeople_generator(self): """ a generator over all people inside of the core person """ raise NotImplementedError
[docs] def set_userdata_timeperiod(self, person, userdata): """ set the userdata as used by timeperiod """ period = None if person: period = get_timeperiod(self.dbstate.db, person) if period is not None: if period > self.maxperiod: self.maxperiod = period if period < self.minperiod: self.minperiod = period userdata.append(period)
[docs] def set_userdata_age(self, person, userdata): agecol = (1, 1, 1) # white if person: age = get_age(self.dbstate.db, person) if age is not None: age = age[0] if age < 0: age = 0 elif age > MAX_AGE: age = MAX_AGE #now determine fraction for gradient agefrac = age / MAX_AGE agecol = colorsys.hsv_to_rgb( (1-agefrac) * self.cstart_hsv[0] + agefrac * self.cend_hsv[0], (1-agefrac) * self.cstart_hsv[1] + agefrac * self.cend_hsv[1], (1-agefrac) * self.cstart_hsv[2] + agefrac * self.cend_hsv[2], ) userdata.append((agecol[0]*255, agecol[1]*255, agecol[2]*255))
[docs] def prepare_background_box(self): """ Method that is called every reset of the chart, to precomputed values needed for the background of the boxes """ maxgen = self.generations cstart = hex_to_rgb(self.grad_start) cend = hex_to_rgb(self.grad_end) self.cstart_hsv = colorsys.rgb_to_hsv(cstart[0]/255, cstart[1]/255, cstart[2]/255) self.cend_hsv = colorsys.rgb_to_hsv(cend[0]/255, cend[1]/255, cend[2]/255) if self.background in [BACKGROUND_GENDER, BACKGROUND_SINGLE_COLOR]: # nothing to precompute self.colors = None self.maincolor = cstart elif self.background == BACKGROUND_GRAD_GEN: #compute the colors, -1, 0, ..., maxgen divs = [x/(maxgen-1) for x in range(maxgen)] rgb_colors = [colorsys.hsv_to_rgb( (1-x) * self.cstart_hsv[0] + x * self.cend_hsv[0], (1-x) * self.cstart_hsv[1] + x * self.cend_hsv[1], (1-x) * self.cstart_hsv[2] + x * self.cend_hsv[2], ) for x in divs] self.colors = [(255*r, 255*g, 255*b) for r, g, b in rgb_colors] elif self.background == BACKGROUND_GRAD_PERIOD: # we fill in in the data structure what the period is, None if not found self.colors = None self.minperiod = 1e10 self.maxperiod = -1e10 gen_people = self.people_generator() for person, userdata in gen_people: self.set_userdata_timeperiod(person, userdata) # same for child gen_inner = self.innerpeople_generator() for child, userdata in gen_inner: self.set_userdata_timeperiod(child, userdata) #now create gradient data, 5 values from min to max rounded to nearest 50 if self.maxperiod < self.minperiod: self.maxperiod = self.minperiod = Today().get_year() rper = self.maxperiod // 50 if rper * 50 != self.maxperiod: self.maxperiod = rper * 50 + 50 self.minperiod = 50 * (self.minperiod // 50) periodrange = self.maxperiod - self.minperiod steps = 2 * GRADIENTSCALE - 1 divs = [x/(steps-1) for x in range(steps)] self.gradval = ['%d' % int(self.minperiod + x * periodrange) for x in divs] for i in range(len(self.gradval)): if i % 2 == 1: self.gradval[i] = '' self.gradcol = [colorsys.hsv_to_rgb( (1-div) * self.cstart_hsv[0] + div * self.cend_hsv[0], (1-div) * self.cstart_hsv[1] + div * self.cend_hsv[1], (1-div) * self.cstart_hsv[2] + div * self.cend_hsv[2], ) for div in divs] elif self.background == BACKGROUND_GRAD_AGE: # we fill in in the data structure what the color age is, white if no age self.colors = None gen_people = self.people_generator() for person, userdata in gen_people: self.set_userdata_age(person, userdata) # same for child gen_inner = self.innerpeople_generator() for child, userdata in gen_inner: self.set_userdata_age(child, userdata) #now create gradient data, 5 values from 0 to max steps = 2 * GRADIENTSCALE - 1 divs = [x/(steps-1) for x in range(steps)] self.gradval = ['%d' % int(x * MAX_AGE) for x in divs] self.gradval[-1] = '%d+' % MAX_AGE for i in range(len(self.gradval)): if i % 2 == 1: self.gradval[i] = '' self.gradcol = [colorsys.hsv_to_rgb( (1-div) * self.cstart_hsv[0] + div * self.cend_hsv[0], (1-div) * self.cstart_hsv[1] + div * self.cend_hsv[1], (1-div) * self.cstart_hsv[2] + div * self.cend_hsv[2], ) for div in divs] else: # known colors per generation, set or compute them self.colors = GENCOLOR[self.background]
[docs] def background_box(self, person, generation, userdata): """ determine red, green, blue value of background of the box of person, which has gender gender, and is in ring generation """ if generation == 0 and self.background in [BACKGROUND_GENDER, BACKGROUND_GRAD_GEN, BACKGROUND_SCHEME1, BACKGROUND_SCHEME2]: # white for center person: color = (255, 255, 255) elif self.background == BACKGROUND_GENDER: try: alive = probably_alive(person, self.dbstate.db) except RuntimeError: alive = False backgr, border = color_graph_box(alive, person.gender) color = hex_to_rgb(backgr) elif self.background == BACKGROUND_SINGLE_COLOR: color = self.maincolor elif self.background == BACKGROUND_GRAD_AGE: color = userdata[0] elif self.background == BACKGROUND_GRAD_PERIOD: period = userdata[0] if period is None: color = (255, 255, 255) # white else: if self.maxperiod != self.minperiod: periodfrac = ((period - self.minperiod) / (self.maxperiod - self.minperiod)) else: periodfrac = 0.5 periodcol = colorsys.hsv_to_rgb( (1-periodfrac) * self.cstart_hsv[0] + periodfrac * self.cend_hsv[0], (1-periodfrac) * self.cstart_hsv[1] + periodfrac * self.cend_hsv[1], (1-periodfrac) * self.cstart_hsv[2] + periodfrac * self.cend_hsv[2], ) color = (periodcol[0]*255, periodcol[1]*255, periodcol[2]*255) else: if self.background == BACKGROUND_GRAD_GEN and generation < 0: generation = 0 color = self.colors[generation % len(self.colors)] if person.gender == Person.MALE: color = [x*.9 for x in color] # now we set transparency data if self.filter and not self.filter.match(person.handle, self.dbstate.db): if self.background == BACKGROUND_SINGLE_COLOR: alpha = 0. # no color shown else: alpha = self.alpha_filter else: alpha = 1. return color[0], color[1], color[2], alpha
[docs] def fontcolor(self, r, g, b, a): """ return the font color based on the r, g, b of the background """ if a == 0: return self.textcolor try: return self.cache_fontcolor[(r, g, b)] except KeyError: hls = colorsys.rgb_to_hls(r/255, g/255, b/255) # we use the lightness value to determine white or black font if hls[1] > 0.4: self.cache_fontcolor[(r, g, b)] = (0, 0, 0) else: self.cache_fontcolor[(r, g, b)] = (1, 1, 1) return self.cache_fontcolor[(r, g, b)]
[docs] def fontbold(self, a): """ The font should be bold if no transparency and font is set. In that case, True is returned """ if a >= 1. and self.filter: return True return False
[docs] def draw_radbox(self, cr, radiusin, radiusout, start_rad, stop_rad, color, thick=False): cr.move_to(radiusout * math.cos(start_rad), radiusout * math.sin(start_rad)) cr.arc(0, 0, radiusout, start_rad, stop_rad) cr.line_to(radiusin * math.cos(stop_rad), radiusin * math.sin(stop_rad)) cr.arc_negative(0, 0, radiusin, stop_rad, start_rad) cr.close_path() ##path = cr.copy_path() # not working correct cr.set_source_rgba(color[0], color[1], color[2], color[3]) cr.fill() #and again for the border cr.move_to(radiusout * math.cos(start_rad), radiusout * math.sin(start_rad)) cr.arc(0, 0, radiusout, start_rad, stop_rad) cr.line_to(radiusin * math.cos(stop_rad), radiusin * math.sin(stop_rad)) cr.arc_negative(0, 0, radiusin, stop_rad, start_rad) cr.close_path() ##cr.append_path(path) # not working correct cr.set_source_rgb(0, 0, 0) # black if thick: cr.set_line_width(3) else: cr.set_line_width(1) cr.stroke() cr.set_line_width(1)
[docs] def draw_innerring(self, cr, person, userdata, start, inc): """ Procedure to draw a person in the inner ring position """ # in polar coordinates what is to draw rmin = TRANSLATE_PX rmax = TRANSLATE_PX + CHILDRING_WIDTH thetamin = start thetamax = start + inc # add child to angle storage self.angle[-2].append([thetamin, thetamax, None]) #draw child now cr.move_to(rmin*math.cos(thetamin), rmin*math.sin(thetamin)) cr.arc(0, 0, rmin, thetamin, thetamax) cr.line_to(rmax*math.cos(thetamax), rmax*math.sin(thetamax)) cr.arc_negative(0, 0, rmax, thetamax, thetamin) cr.close_path() ##path = cr.copy_path() # not working correct cr.set_source_rgb(0, 0, 0) # black cr.set_line_width(1) cr.stroke() #now again to fill if person: r, g, b, a = self.background_box(person, -1, userdata) else: r=255; g=255; b=255; a=1 cr.move_to(rmin*math.cos(thetamin), rmin*math.sin(thetamin)) cr.arc(0, 0, rmin, thetamin, thetamax) cr.line_to(rmax*math.cos(thetamax), rmax*math.sin(thetamax)) cr.arc_negative(0, 0, rmax, thetamax, thetamin) cr.close_path() ##cr.append_path(path) # not working correct cr.set_source_rgba(r/255., g/255., b/255., a) cr.fill()
[docs] def wrap_truncate_layout(self, layout, font, width_pixels): """Uses the layout to wrap and truncate its text to given width Returns: (w,h) as returned by layout.get_pixel_size() """ layout.set_font_description(font) layout.set_width(Pango.SCALE * width_pixels) # NOTE: one may not truncate the text to just the 1st line's text, # because the truncation can create invalid Unicode. if layout.get_line_count() > 1: layout.set_text(layout.get_text(), layout.get_line(0).length) return layout.get_pixel_size()
[docs] def draw_text(self, cr, text, radius, start, stop, height=PIXELS_PER_GENERATION, radial=False, fontcolor=(0, 0, 0), bold=False): """ Display text at a particular radius, between start and stop degrees. """ cr.save() font = Pango.FontDescription(self.fontdescr) fontsize = self.fontsize font.set_size(fontsize * Pango.SCALE) if bold: font.set_weight(Pango.Weight.BOLD) cr.set_source_rgb(*fontcolor) if radial and self.radialtext: cr.save() layout = self.create_pango_layout(text) layout.set_font_description(font) layout.set_wrap(Pango.WrapMode.CHAR) # NOTE: for radial text, the sector radius height is the text width w, h = self.wrap_truncate_layout(layout, font, height - 2*PAD_TEXT) w = w + 5 # 5 pixel padding h = h + 4 # 4 pixel padding #first we check if height is ok degneedheight = math.degrees(h / radius) degavailheight = stop-start degoffsetheight = 0 if degneedheight > degavailheight: #reduce height fontsize = degavailheight / degneedheight * fontsize / 2 font.set_size(fontsize * Pango.SCALE) w, h = self.wrap_truncate_layout(layout, font, height - 2*PAD_TEXT) w = w + 5 # 5 pixel padding h = h + 4 # 4 pixel padding #first we check if height is ok degneedheight = math.degrees(h / radius) degavailheight = stop-start if degneedheight > degavailheight: #we could not fix it, no text text = "" if text: #spread rest degoffsetheight = (degavailheight - degneedheight) / 2 # offset for cairo-font system is 90 rotval = self.rotate_value % 360 - 90 if (start + rotval) % 360 > 179: pos = start + degoffsetheight + 90 - 90 else: pos = stop - degoffsetheight + 180 cr.rotate(math.radians(pos)) layout.context_changed() if (start + rotval) % 360 > 179: cr.move_to(radius + PAD_TEXT, 0) else: cr.move_to(-radius - height + PAD_TEXT, 0) PangoCairo.show_layout(cr, layout) cr.restore() else: self.draw_arc_text(cr, text, radius, start, stop, font) cr.restore()
[docs] def draw_arc_text(self, cr, text, radius, start, stop, font): """ Display text at a particular radius, between start and stop degrees, setting it up along the arc, center-justified. Text not fitting a single line will be word-wrapped away. """ # 1. determine the spread of text we can draw, in radians degpadding = math.degrees(PAD_TEXT / radius) # offset for cairo-font system is 90, padding used is 5: pos = start + 90 + degpadding/2 cr.save() cr.rotate(math.radians(pos)) cr.new_path() cr.move_to(0, -radius) rad_spread = math.radians(stop - start - degpadding) # 2. Use Pango.Layout to set up the text for us, and do # the hard work in CTL text handling and line wrapping. # Clip to the top line only so the text looks nice # all around the circle at the same radius. layout = self.create_pango_layout(text) layout.set_wrap(Pango.WrapMode.WORD) w, h = self.wrap_truncate_layout(layout, font, radius * rad_spread) # 3. Use the layout to provide us the metrics of the text box PangoCairo.layout_path(cr, layout) #le = layout.get_line(0).get_pixel_extents()[0] pe = cr.path_extents() if pe == (0.0, 0.0, 0.0, 0.0): # 7710: When scrolling the path extents are zero on Ubuntu 14.04 return arc_used_ratio = w / (radius * rad_spread) rad_mid = math.radians(pos) + rad_spread/2 # 4. The moment of truth: map the text box onto the sector, and render! warpPath(cr, \ self.create_map_rect_to_sector(radius, pe, \ arc_used_ratio, rad_mid - rad_spread/2, rad_mid + rad_spread/2)) cr.fill() cr.restore()
@staticmethod
[docs] def create_map_rect_to_sector(radius, rect, arc_used_ratio, start_rad, stop_rad): """ Create a 2D-transform, mapping a rectangle onto a circle sector. :param radius: average radius of the target sector :param rect: (x1, y1, x2, y2) :param arc_used_ratio: From 0.0 to 1.0. Rather than stretching onto the whole sector, only the middle arc_used_ratio part will be mapped onto. :param start_rad: start radial angle of the sector, in radians :param stop_rad: stop radial angle of the sector, in radians :returns: a lambda (x,y)|->(xNew,yNew) to feed to warpPath. """ x0, y0, w, h = rect[0], rect[1], rect[2]-rect[0], rect[3]-rect[1] radiusin = radius - h/2 radiusout = radius + h/2 drho = h dphi = (stop_rad - start_rad) # There has to be a clearer way to express this transform, # by stacking a set of transforms on cr around using this function # and doing a mapping between unit squares of rectangular and polar # coordinates. def phi(x): return (x - x0) * dphi * arc_used_ratio / w \ + (1 - arc_used_ratio) * dphi / 2 \ - math.pi/2 def rho(y): return (y - y0) * (radiusin - radiusout)/h + radiusout # In (user coordinates units - pixels): # x from x0 to x0 + w # y from y0 to y0 + h # Out: # (x, y) within the arc_used_ratio of a box like drawn by draw_radbox return lambda x, y: \ (rho(y) * math.cos(phi(x)), rho(y) * math.sin(phi(x)))
[docs] def draw_gradient(self, cr, widget, halfdist): gradwidth = 10 gradheight = 10 starth = 15 startw = 5 alloc = self.get_allocation() x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height cr.save() cr.translate(-self.center_x, -self.center_y) font = Pango.FontDescription(self.fontdescr) fontsize = self.fontsize font.set_size(fontsize * Pango.SCALE) for color, text in zip(self.gradcol, self.gradval): cr.move_to(startw, starth) cr.rectangle(startw, starth, gradwidth, gradheight) cr.set_source_rgb(color[0], color[1], color[2]) cr.fill() layout = self.create_pango_layout(text) layout.set_font_description(font) cr.move_to(startw+gradwidth+4, starth) cr.set_source_rgb(0, 0, 0) #black PangoCairo.show_layout(cr, layout) starth = starth+gradheight cr.restore()
[docs] def person_under_cursor(self, curx, cury): """ Determine the generation and the position in the generation at position x and y, as well as the type of box. generation = -1 on center black dot generation >= self.generations outside of diagram """ # compute angle, radius, find out who would be there (rotated) # center coordinate cx = self.center_x cy = self.center_y radius = math.sqrt((curx - cx) ** 2 + (cury - cy) ** 2) if radius < TRANSLATE_PX: generation = -1 elif (self.childring and self.angle[-2] and radius < TRANSLATE_PX + CHILDRING_WIDTH): generation = -2 # indication of one of the children elif radius < self.CENTER: generation = 0 else: generation = int((radius - self.CENTER)/self.gen_pixels()) + 1 btype = self.boxtype(radius) rads = math.atan2( (cury - cy), (curx - cx) ) if rads < 0: # second half of unit circle rads = math.pi + (math.pi + rads) #angle before rotation: pos = ((rads/(math.pi * 2) - self.rotate_value/360.) * 360.0) % 360 #children are in cairo angle (clockwise) from pi to 3 pi #rads however is clock 0 to 2 pi if rads < math.pi: rads += 2 * math.pi # if generation is in expand zone: # FIXME: add a way of expanding # find what person is in this position: selected = None if (0 <= generation < self.generations): selected = self.personpos_at_angle(generation, pos, btype) elif generation == -2: for p in range(len(self.angle[generation])): start, stop, state = self.angle[generation][p] if start <= rads <= stop: selected = p break return generation, selected, btype
[docs] def boxtype(self, radius): """ default is only one type of box type """ return TYPE_BOX_NORMAL
[docs] def personpos_at_angle(self, generation, angledeg, btype): """ returns the person in generation generation at angle of type btype. """ raise NotImplementedError
[docs] def person_at(self, generation, pos, btype): """ returns the person at generation, pos, btype """ raise NotImplementedError
[docs] def family_at(self, generation, pos, btype): """ returns the family at generation, pos, btype """ raise NotImplementedError
def _have_children(self, person): """ Returns True if a person has children. TODO: is there no util function for this """ if person: for family_handle in person.get_family_handle_list(): family = self.dbstate.db.get_family_from_handle(family_handle) if family and len(family.get_child_ref_list()) > 0: return True return False
[docs] def on_key_press(self, widget, eventkey): """grab key press """ if self.mouse_x and self.mouse_y: generation, selected, btype = self.person_under_cursor(self.mouse_x, self.mouse_y) if selected is None: return False person = self.person_at(generation, selected, btype) family = self.family_at(generation, selected, btype) if person and (Gdk.keyval_name(eventkey.keyval) == 'e'): # we edit the person self.edit_person_cb(None, person.handle) return True elif family and (Gdk.keyval_name(eventkey.keyval) == 'f'): # we edit the family self.edit_fam_cb(None, family.handle) return True return False
[docs] def on_mouse_down(self, widget, event): self.translating = False # keep track of up/down/left/right movement generation, selected, btype = self.person_under_cursor(event.x, event.y) if event.button == 1: #we grab the focus to enable to see key_press events self.grab_focus() # left mouse on center dot, we translate on left click if generation == -1: if event.button == 1: # left mouse # save the mouse location for movements self.translating = True self.last_x, self.last_y = event.x, event.y return True #click in open area, prepare for a rotate if selected is None: # save the mouse location for movements self.last_x, self.last_y = event.x, event.y return True #left click on person, prepare for expand/collapse or drag if event.button == 1: self._mouse_click = True self._mouse_click_gen = generation self._mouse_click_sel = selected self._mouse_click_btype = btype return False #right click on person, context menu # Do things based on state, event.get_state(), or button, event.button if is_right_click(event): person = self.person_at(generation, selected, btype) family = self.family_at(generation, selected, btype) fhandle = None if family: fhandle = family.handle if person and self.on_popup: self.on_popup(widget, event, person.handle, fhandle) return True return False
[docs] def on_mouse_move(self, widget, event): self._mouse_click = False if self.last_x is None or self.last_y is None: # while mouse is moving, we must update the tooltip based on person generation, selected, btype = self.person_under_cursor(event.x, event.y) self.mouse_x = event.x self.mouse_y = event.y tooltip = "" person = self.person_at(generation, selected, btype) if person: tooltip = self.format_helper.format_person(person, 11) self.set_tooltip_text(tooltip) return False #translate or rotate should happen alloc = self.get_allocation() x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height if self.translating: if self.form == FORM_CIRCLE: self.center_xy = w/2 - event.x, h/2 - event.y elif self.form == FORM_HALFCIRCLE: self.center_xy = w/2 - event.x, h - self.CENTER - PAD_PX - event.y elif self.form == FORM_QUADRANT: self.center_xy = self.CENTER + PAD_PX - event.x, h - self.CENTER - PAD_PX - event.y else: cx = w/2 - self.center_xy[0] cy = h/2 - self.center_xy[1] # get the angles of the two points from the center: start_angle = math.atan2(event.y - cy, event.x - cx) end_angle = math.atan2(self.last_y - cy, self.last_x - cx) if start_angle < 0: # second half of unit circle start_angle = math.pi + (math.pi + start_angle) if end_angle < 0: # second half of unit circle end_angle = math.pi + (math.pi + end_angle) # now look at change in angle: diff_angle = (end_angle - start_angle) % (math.pi * 2.0) self.rotate_value -= math.degrees(diff_angle) self.last_x, self.last_y = event.x, event.y self.queue_draw() return True
[docs] def do_mouse_click(self): """ action to take on left mouse click """ pass
[docs] def on_mouse_up(self, widget, event): if self._mouse_click: self.do_mouse_click() return True if self.last_x is None or self.last_y is None: # No translate or rotate return True if self.translating: self.translating = False alloc = self.get_allocation() x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height if self.form == FORM_CIRCLE: self.center_xy = w/2 - event.x, h/2 - event.y self.center_xy = w/2 - event.x, h/2 - event.y elif self.form == FORM_HALFCIRCLE: self.center_xy = w/2 - event.x, h - self.CENTER - PAD_PX - event.y elif self.form == FORM_QUADRANT: self.center_xy = self.CENTER + PAD_PX - event.x, h - self.CENTER - PAD_PX - event.y self.last_x, self.last_y = None, None self.queue_draw() return True
[docs] def on_drag_begin(self, widget, data): """Set up some inital conditions for drag. Set up icon.""" self.in_drag = True self.drag_source_set_icon_stock('gramps-person')
[docs] def on_drag_end(self, widget, data): """Set up some inital conditions for drag. Set up icon.""" self.in_drag = False
[docs] def on_drag_data_get(self, widget, context, sel_data, info, time): """ Returned parameters after drag. Specified for 'person-link', for others return text info about person. """ tgs = [x.name() for x in context.list_targets()] person = self.person_at(self._mouse_click_gen, self._mouse_click_sel, self._mouse_click_btype) if info == DdTargets.PERSON_LINK.app_id: data = (DdTargets.PERSON_LINK.drag_type, id(self), person.get_handle(), 0) sel_data.set(sel_data.get_target(), 8, pickle.dumps(data)) elif ('TEXT' in tgs or 'text/plain' in tgs) and info == 0: sel_data.set_text(self.format_helper.format_person(person, 11), -1)
[docs] def on_drag_data_received(self, widget, context, x, y, sel_data, info, time): """ Handle the standard gtk interface for drag_data_received. If the selection data is defined, extract the value from sel_data.data """ gen, persatcurs, btype = self.person_under_cursor(x, y) if gen == -1 or gen == 0: if sel_data and sel_data.get_data(): (drag_type, idval, handle, val) = pickle.loads(sel_data.get_data()) self.goto(self, handle)
[docs] def edit_person_cb(self, obj, person_handle): person = self.dbstate.db.get_person_from_handle(person_handle) if person: try: EditPerson(self.dbstate, self.uistate, [], person) except WindowActiveError: pass return True return False
[docs] def edit_fam_cb(self, obj, family_handle): fam = self.dbstate.db.get_family_from_handle(family_handle) if fam: try: EditFamily(self.dbstate, self.uistate, [], fam) except WindowActiveError: pass return True return False #------------------------------------------------------------------------- # # FanChartWidget # #-------------------------------------------------------------------------
[docs]class FanChartWidget(FanChartBaseWidget): """ Interactive Fan Chart Widget. """ 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, True, True, 'Sans', '#0000FF', '#FF0000', None, 0.5, FORM_CIRCLE) FanChartBaseWidget.__init__(self, dbstate, uistate, callback_popup)
[docs] def set_values(self, root_person_handle, maxgen, background, childring, radialtext, fontdescr, grad_start, grad_end, filter, alpha_filter, form): """ Reset the values to be used: :param root_person_handle: person to show :param maxgen: maximum generations to show :param background: config setting of which background procedure to use :type background: int :param childring: to show the center ring with children or not :param radialtext: try to use radial text or not :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: the alpha transparency value (0-1) to apply to filtered out data :param form: the ``FORM_`` constant for the fanchart """ self.rootpersonh = root_person_handle self.generations = maxgen self.radialtext = radialtext self.childring = childring self.background = background self.fontdescr = fontdescr self.grad_start = grad_start self.grad_end = grad_end self.filter = filter self.alpha_filter = alpha_filter self.form = form
[docs] def set_generations(self): """ Set the generations to max, and fill data structures with initial data. """ self.angle = {} if self.childring: self.angle[-2] = [] self.data = {} self.childrenroot = [] for i in range(self.generations): # name, person, parents?, children? self.data[i] = [(None,) * 5] * 2 ** i self.angle[i] = [] factor = 1 angle = 0 if self.form == FORM_HALFCIRCLE: factor = 1/2 angle = 90 elif self.form == FORM_QUADRANT: angle = 180 factor = 1/4 slice = 360.0 / (2 ** i) * factor for count in range(len(self.data[i])): # start, stop, male, state self.angle[i].append([angle, angle + slice, NORMAL]) angle += slice
def _fill_data_structures(self): self.set_generations() person = self.dbstate.db.get_person_from_handle(self.rootpersonh) if not person: name = None else: name = name_displayer.display(person) parents = self._have_parents(person) child = self._have_children(person) # our data structure is the text, the person object, parents, child and # list for userdata which we might fill in later. self.data[0][0] = (name, person, parents, child, []) self.childrenroot = [] if child: childlist = find_children(self.dbstate.db, person) for child_handle in childlist: child = self.dbstate.db.get_person_from_handle(child_handle) if not child: continue else: self.childrenroot.append((child_handle, child.get_gender(), self._have_children(child), [])) for current in range(1, self.generations): parent = 0 # name, person, parents, children for (n, p, q, c, d) in self.data[current - 1]: # Get father's details: person = self._get_parent(p, True) if person: name = name_displayer.display(person) else: name = None if current == self.generations - 1: parents = self._have_parents(person) else: parents = None self.data[current][parent] = (name, person, parents, None, []) if person is None: # start,stop,male/right,state self.angle[current][parent][2] = COLLAPSED parent += 1 # Get mother's details: person = self._get_parent(p, False) if person: name = name_displayer.display(person) else: name = None if current == self.generations - 1: parents = self._have_parents(person) else: parents = None self.data[current][parent] = (name, person, parents, None, []) if person is None: # start,stop,male/right,state self.angle[current][parent][2] = COLLAPSED parent += 1 def _have_parents(self, person): """ Returns True if a person has parents. TODO: is there no util function for this """ if person: m = self._get_parent(person, False) f = self._get_parent(person, True) return not m is f is None return False def _get_parent(self, person, father): """ Get the father of the family if father == True, otherwise mother """ if person: parent_handle_list = person.get_parent_family_handle_list() if parent_handle_list: family_id = parent_handle_list[0] family = self.dbstate.db.get_family_from_handle(family_id) if family: if father: person_handle = Family.get_father_handle(family) else: person_handle = Family.get_mother_handle(family) if person_handle: return self.dbstate.db.get_person_from_handle(person_handle) return None
[docs] def gen_pixels(self): """ how many pixels a generation takes up in the fanchart """ return PIXELS_PER_GENERATION
[docs] def nrgen(self): #compute the number of generations present nrgen = None for generation in range(self.generations - 1, 0, -1): for p in range(len(self.data[generation])): (text, person, parents, child, userdata) = self.data[generation][p] if person: nrgen = generation break if nrgen is not None: break if nrgen is None: nrgen = 1 return nrgen
[docs] def halfdist(self): """ Compute the half radius of the circle """ nrgen = self.nrgen() return PIXELS_PER_GENERATION * nrgen + self.CENTER + BORDER_EDGE_WIDTH
[docs] def people_generator(self): """ a generator over all people outside of the core person """ for generation in range(self.generations): for p in range(len(self.data[generation])): (text, person, parents, child, userdata) = self.data[generation][p] yield (person, userdata)
[docs] def innerpeople_generator(self): """ a generator over all people inside of the core person """ for childdata in self.childrenroot: child_handle, child_gender, has_child, userdata = childdata child = self.dbstate.db.get_person_from_handle(child_handle) yield (child, userdata)
[docs] def on_draw(self, widget, cr, scale=1.): """ The main method to do the drawing. If widget is given, we assume we draw in GTK3 and use the allocation. To draw raw on the cairo context cr, set widget=None. """ # first do size request of what we will need halfdist = self.halfdist() if widget: if self.form == FORM_CIRCLE: self.set_size_request(2 * halfdist, 2 * halfdist) elif self.form == FORM_HALFCIRCLE: self.set_size_request(2 * halfdist, halfdist + self.CENTER + PAD_PX) elif self.form == FORM_QUADRANT: self.set_size_request(halfdist + self.CENTER + PAD_PX, halfdist + self.CENTER + PAD_PX) #obtain the allocation alloc = self.get_allocation() x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height cr.scale(scale, scale) # when printing, we need not recalculate if widget: if self.form == FORM_CIRCLE: self.center_x = w/2 - self.center_xy[0] self.center_y = h/2 - self.center_xy[1] elif self.form == FORM_HALFCIRCLE: self.center_x = w/2. - self.center_xy[0] self.center_y = h - self.CENTER - PAD_PX- self.center_xy[1] elif self.form == FORM_QUADRANT: self.center_x = self.CENTER + PAD_PX - self.center_xy[0] self.center_y = h - self.CENTER - PAD_PX - self.center_xy[1] cr.translate(self.center_x, self.center_y) cr.save() cr.rotate(math.radians(self.rotate_value)) for generation in range(self.generations - 1, 0, -1): for p in range(len(self.data[generation])): (text, person, parents, child, userdata) = self.data[generation][p] if person: start, stop, state = self.angle[generation][p] if state in [NORMAL, EXPANDED]: self.draw_person(cr, gender_code(p%2 == 0), text, start, stop, generation, state, parents, child, person, userdata) cr.set_source_rgb(1, 1, 1) # white cr.move_to(0,0) cr.arc(0, 0, self.CENTER, 0, 2 * math.pi) cr.fill() cr.set_source_rgb(0, 0, 0) # black cr.arc(0, 0, self.CENTER, 0, 2 * math.pi) cr.stroke() cr.restore() # Draw center person: (text, person, parents, child, userdata) = self.data[0][0] if person: r, g, b, a = self.background_box(person, 0, userdata) cr.arc(0, 0, self.CENTER, 0, 2 * math.pi) if self.childring and child: cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 2 * math.pi, 0) cr.close_path() 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 - (self.CENTER - (CHILDRING_WIDTH + TRANSLATE_PX))/2, 95, 455, 10, False, self.fontcolor(r, g, b, a), self.fontbold(a)) cr.restore() #draw center to move chart cr.set_source_rgb(0, 0, 0) # black cr.move_to(TRANSLATE_PX, 0) cr.arc(0, 0, TRANSLATE_PX, 0, 2 * math.pi) if child: # has at least one child cr.fill() else: cr.stroke() if child and self.childring: self.draw_childring(cr) if self.background in [BACKGROUND_GRAD_AGE, BACKGROUND_GRAD_PERIOD]: self.draw_gradient(cr, widget, halfdist)
[docs] def draw_person(self, cr, gender, name, start, stop, generation, state, parents, child, person, userdata): """ Display the piece of pie for a given person. start and stop are in degrees. Gender is indication of father position or mother position in the chart """ cr.save() start_rad = math.radians(start) stop_rad = math.radians(stop) r, g, b, a = self.background_box(person, generation, userdata) radius = generation * PIXELS_PER_GENERATION + self.CENTER # If max generation, and they have parents: if generation == self.generations - 1 and parents: # draw an indicator radmax = radius + BORDER_EDGE_WIDTH cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad)) cr.arc(0, 0, radius + BORDER_EDGE_WIDTH, start_rad, stop_rad) cr.line_to(radius*math.cos(stop_rad), radius*math.sin(stop_rad)) cr.arc_negative(0, 0, radius, stop_rad, start_rad) cr.close_path() ##path = cr.copy_path() # not working correct cr.set_source_rgb(255, 255, 255) # 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, radius + BORDER_EDGE_WIDTH, start_rad, stop_rad) cr.line_to(radius*math.cos(stop_rad), radius*math.sin(stop_rad)) cr.arc_negative(0, 0, radius, stop_rad, start_rad) cr.close_path() ##cr.append_path(path) # not working correct cr.set_source_rgb(0, 0, 0) # black cr.stroke() # now draw the person cr.move_to(radius * math.cos(start_rad), radius * math.sin(start_rad)) cr.arc(0, 0, radius, start_rad, stop_rad) radmin = radius - PIXELS_PER_GENERATION cr.line_to(radmin * math.cos(stop_rad), radmin * math.sin(stop_rad)) cr.arc_negative(0, 0, radmin, stop_rad, start_rad) cr.close_path() ##path = cr.copy_path() # not working correct cr.set_source_rgba(r/255., g/255., b/255., a) cr.fill() #and again for the border cr.move_to(radius * math.cos(start_rad), radius * math.sin(start_rad)) cr.arc(0, 0, radius, start_rad, stop_rad) radmin = radius - PIXELS_PER_GENERATION cr.line_to(radmin * math.cos(stop_rad), radmin * math.sin(stop_rad)) cr.arc_negative(0, 0, radmin, stop_rad, start_rad) cr.close_path() ##cr.append_path(path) # not working correct cr.set_source_rgb(0, 0, 0) # black if state == NORMAL: # normal cr.set_line_width(1) else: # EXPANDED cr.set_line_width(3) cr.stroke() cr.set_line_width(1) if self.last_x is None or self.last_y is None: #we are not in a move, so draw text radial = False radstart = radius - PIXELS_PER_GENERATION/2 if self.radialtext: ## and generation >= 6: spacepolartext = radstart * math.radians(stop-start) if spacepolartext < PIXELS_PER_GENERATION * 1.1: # more space to print it radial radial = True radstart = radius - PIXELS_PER_GENERATION self.draw_text(cr, name, radstart, start, stop, PIXELS_PER_GENERATION, radial, self.fontcolor(r, g, b, a), self.fontbold(a)) cr.restore()
[docs] def draw_childring(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() nrchild = len(self.childrenroot) #Y axis is downward. positve angles are hence clockwise startangle = math.pi if nrchild <= 4: angleinc = math.pi/2 else: angleinc = 2 * math.pi / nrchild for childdata in self.childrenroot: child_handle, child_gender, has_child, userdata = childdata child = self.dbstate.db.get_person_from_handle(child_handle) self.draw_innerring(cr, child, userdata, startangle, angleinc) startangle += angleinc
[docs] def expand_parents(self, generation, selected, current): if generation >= self.generations: 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)
[docs] def show_parents(self, generation, selected, angle, slice): if generation >= self.generations: return selected *= 2 self.angle[generation][selected][0] = angle self.angle[generation][selected][1] = angle + slice self.angle[generation][selected][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)
[docs] def hide_parents(self, generation, selected, angle): if generation >= self.generations: return selected = 2 * selected self.angle[generation][selected][0] = angle self.angle[generation][selected][1] = angle self.angle[generation][selected][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)
[docs] def shrink_parents(self, generation, selected, current): if generation >= self.generations: 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)
[docs] def change_slice(self, generation, selected): if generation < 1: return gstart, gstop, gstate = self.angle[generation][selected] if gstate == NORMAL: # let's expand if selected % 2 == 0: # go to right stop = gstop + (gstop - gstart) self.angle[generation][selected] = [gstart, stop, EXPANDED] self.expand_parents(generation + 1, selected, gstart) start, stop, state = self.angle[generation][selected+1] self.angle[generation][selected+1] = [stop, stop, COLLAPSED] self.hide_parents(generation+1, selected+1, stop) else: # go to left start = gstart - (gstop - gstart) self.angle[generation][selected] = [start, gstop, EXPANDED] self.expand_parents(generation + 1, selected, start) start, stop, state = self.angle[generation][selected-1] self.angle[generation][selected-1] = [start, start, COLLAPSED] self.hide_parents(generation+1, selected-1, start) elif gstate == EXPANDED: # let's shrink if selected % 2 == 0: # shrink from right slice = (gstop - gstart)/2.0 stop = gstop - slice self.angle[generation][selected] = [gstart, stop, NORMAL] self.shrink_parents(generation+1, selected, gstart) self.angle[generation][selected+1][0] = stop # start self.angle[generation][selected+1][1] = stop + slice # stop self.angle[generation][selected+1][2] = NORMAL self.show_parents(generation+1, selected+1, stop, slice/2.0) else: # shrink from left slice = (gstop - gstart)/2.0 start = gstop - slice self.angle[generation][selected] = [start, gstop, NORMAL] self.shrink_parents(generation+1, selected, start) start, stop, state = self.angle[generation][selected-1] self.angle[generation][selected-1] = [start, start+slice, NORMAL] self.show_parents(generation+1, selected-1, start, slice/2.0)
[docs] def personpos_at_angle(self, generation, angledeg, btype): """ returns the person in generation generation at angle. """ if generation == 0: return 0 selected = None for p in range(len(self.angle[generation])): if self.data[generation][p][1]: # there is a person there start, stop, state = self.angle[generation][p] if state == COLLAPSED: continue if start <= angledeg <= stop: selected = p break return selected
[docs] def person_at(self, generation, pos, btype): """ returns the person at generation, pos, btype """ if pos is None: return None if generation == -2: child_handle = self.childrenroot[pos][0] person = self.dbstate.db.get_person_from_handle(child_handle) else: person = self.data[generation][pos][1] return person
[docs] def family_at(self, generation, pos, btype): """ returns the family at generation, pos, btype Difficult here, we would need to go to child, and then obtain the first parent family, as that is the family that is shown. """ return None
[docs] 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 = False self.queue_draw()
[docs]class FanChartGrampsGUI(object): """ class for functions fanchart GUI elements will need in Gramps """ def __init__(self, on_childmenu_changed): """ Common part of GUI that shows Fan Chart, needs to know what to do if one moves via Fan Ch def set_fan(self, fan):art to a new person on_childmenu_changed: in popup, function called on moving to a new person """ self.fan = None self.on_childmenu_changed = on_childmenu_changed self.format_helper = FormattingHelper(self.dbstate)
[docs] def set_fan(self, fan): """ Set the fanchartwidget to work on """ self.fan = fan self.fan.format_helper = self.format_helper self.fan.goto = self.on_childmenu_changed
[docs] def main(self): """ Fill the data structures with the active data. This initializes all data. """ root_person_handle = self.get_active('Person') self.fan.set_values(root_person_handle, self.maxgen, self.background, self.childring, self.radialtext, self.fonttype, self.grad_start, self.grad_end, self.generic_filter, self.alpha_filter, self.form) self.fan.reset() self.fan.queue_draw()
[docs] def on_popup(self, obj, event, person_handle, family_handle=None): """ Builds the full menu (including Siblings, Spouses, Children, and Parents) with navigation. """ #store menu for GTK3 to avoid it being destroyed before showing self.menu = Gtk.Menu() menu = self.menu menu.set_title(_('People Menu')) person = self.dbstate.db.get_person_from_handle(person_handle) if not person: return 0 go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO,Gtk.IconSize.MENU) go_image.show() go_item = Gtk.ImageMenuItem(name_displayer.display(person)) go_item.set_image(go_image) go_item.connect("activate", self.on_childmenu_changed, person_handle) go_item.show() menu.append(go_item) edit_item = Gtk.ImageMenuItem.new_from_stock(stock_id=Gtk.STOCK_EDIT, accel_group=None) edit_item.connect("activate", self.edit_person_cb, person_handle) edit_item.show() menu.append(edit_item) if family_handle: family = self.dbstate.db.get_family_from_handle(family_handle) edit_fam_item = Gtk.ImageMenuItem.new_from_stock( stock_id=Gtk.STOCK_EDIT, accel_group=None) edit_fam_item.set_label(_("Edit family")) edit_fam_item.connect("activate", self.edit_fam_cb, family_handle) edit_fam_item.show() menu.append(edit_fam_item) #see if a reorder button is needed if family.get_father_handle() == person_handle: parth = family.get_mother_handle() else: parth = family.get_father_handle() lenfams = 0 if parth: partner = self.dbstate.db.get_person_from_handle(parth) lenfams = len(partner.get_family_handle_list()) if lenfams in [0, 1]: lenfams = len(partner.get_parent_family_handle_list()) reord_fam_item = Gtk.ImageMenuItem.new_from_stock( stock_id=Gtk.STOCK_SORT_ASCENDING, accel_group=None) reord_fam_item.set_label(_("Reorder families")) reord_fam_item.connect("activate", self.reord_fam_cb, parth) reord_fam_item.set_sensitive(lenfams > 1) reord_fam_item.show() menu.append(reord_fam_item) clipboard_item = Gtk.ImageMenuItem.new_from_stock(stock_id=Gtk.STOCK_COPY, accel_group=None) clipboard_item.connect("activate", self.copy_person_to_clipboard_cb, person_handle) clipboard_item.show() menu.append(clipboard_item) # collect all spouses, parents and children linked_persons = [] # Go over spouses and build their menu item = Gtk.MenuItem(label=_("Spouses")) fam_list = person.get_family_handle_list() no_spouses = 1 for fam_id in fam_list: family = self.dbstate.db.get_family_from_handle(fam_id) if family.get_father_handle() == person.get_handle(): sp_id = family.get_mother_handle() else: sp_id = family.get_father_handle() spouse = self.dbstate.db.get_person_from_handle(sp_id) if not spouse: continue if no_spouses: no_spouses = 0 item.set_submenu(Gtk.Menu()) sp_menu = item.get_submenu() go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU) go_image.show() sp_item = Gtk.ImageMenuItem(name_displayer.display(spouse)) sp_item.set_image(go_image) linked_persons.append(sp_id) sp_item.connect("activate", self.on_childmenu_changed, sp_id) sp_item.show() sp_menu.append(sp_item) if no_spouses: item.set_sensitive(0) item.show() menu.append(item) # Go over siblings and build their menu item = Gtk.MenuItem(label=_("Siblings")) pfam_list = person.get_parent_family_handle_list() no_siblings = 1 for f in pfam_list: fam = self.dbstate.db.get_family_from_handle(f) sib_list = fam.get_child_ref_list() for sib_ref in sib_list: sib_id = sib_ref.ref if sib_id == person.get_handle(): continue sib = self.dbstate.db.get_person_from_handle(sib_id) if not sib: continue if no_siblings: no_siblings = 0 item.set_submenu(Gtk.Menu()) sib_menu = item.get_submenu() if find_children(self.dbstate.db,sib): label = Gtk.Label(label='<b><i>%s</i></b>' % escape(name_displayer.display(sib))) else: label = Gtk.Label(label=escape(name_displayer.display(sib))) go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU) go_image.show() sib_item = Gtk.ImageMenuItem(None) sib_item.set_image(go_image) label.set_use_markup(True) label.show() label.set_alignment(0,0) sib_item.add(label) linked_persons.append(sib_id) sib_item.connect("activate", self.on_childmenu_changed, sib_id) sib_item.show() sib_menu.append(sib_item) if no_siblings: item.set_sensitive(0) item.show() menu.append(item) # Go over children and build their menu item = Gtk.MenuItem(label=_("Children")) no_children = 1 childlist = find_children(self.dbstate.db, person) for child_handle in childlist: child = self.dbstate.db.get_person_from_handle(child_handle) if not child: continue if no_children: no_children = 0 item.set_submenu(Gtk.Menu()) child_menu = item.get_submenu() if find_children(self.dbstate.db,child): label = Gtk.Label(label='<b><i>%s</i></b>' % escape(name_displayer.display(child))) else: label = Gtk.Label(label=escape(name_displayer.display(child))) go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU) go_image.show() child_item = Gtk.ImageMenuItem(None) child_item.set_image(go_image) label.set_use_markup(True) label.show() label.set_alignment(0,0) child_item.add(label) linked_persons.append(child_handle) child_item.connect("activate", self.on_childmenu_changed, child_handle) child_item.show() child_menu.append(child_item) if no_children: item.set_sensitive(0) item.show() menu.append(item) # Go over parents and build their menu item = Gtk.MenuItem(label=_("Parents")) item.set_submenu(Gtk.Menu()) par_menu = item.get_submenu() no_parents = 1 par_list = find_parents(self.dbstate.db,person) for par_id in par_list: par = self.dbstate.db.get_person_from_handle(par_id) if not par: continue if no_parents: no_parents = 0 if find_parents(self.dbstate.db,par): label = Gtk.Label(label='<b><i>%s</i></b>' % escape(name_displayer.display(par))) else: label = Gtk.Label(label=escape(name_displayer.display(par))) go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU) go_image.show() par_item = Gtk.ImageMenuItem(None) par_item.set_image(go_image) label.set_use_markup(True) label.show() label.set_alignment(0,0) par_item.add(label) linked_persons.append(par_id) par_item.connect("activate", self.on_childmenu_changed, par_id) par_item.show() par_menu.append(par_item) if no_parents: #show an add button add_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_ADD, None) add_item.connect("activate", self.on_add_parents, person_handle) add_item.show() par_menu.append(add_item) item.show() menu.append(item) # Go over parents and build their menu item = Gtk.MenuItem(label=_("Related")) no_related = 1 for p_id in find_witnessed_people(self.dbstate.db,person): #if p_id in linked_persons: # continue # skip already listed family members per = self.dbstate.db.get_person_from_handle(p_id) if not per: continue if no_related: no_related = 0 item.set_submenu(Gtk.Menu()) per_menu = item.get_submenu() label = Gtk.Label(label=escape(name_displayer.display(per))) go_image = Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU) go_image.show() per_item = Gtk.ImageMenuItem(None) per_item.set_image(go_image) label.set_use_markup(True) label.show() label.set_alignment(0, 0) per_item.add(label) per_item.connect("activate", self.on_childmenu_changed, p_id) per_item.show() per_menu.append(per_item) if no_related: item.set_sensitive(0) item.show() menu.append(item) #we now construct an add menu item = Gtk.MenuItem(label=_("Add")) item.set_submenu(Gtk.Menu()) add_menu = item.get_submenu() if family_handle: # allow to add a child to this family add_child_item = Gtk.ImageMenuItem.new_from_stock( stock_id=Gtk.STOCK_ADD, accel_group=None) add_child_item.set_label(_("Add child to family")) add_child_item.connect("activate", self.add_child_to_fam_cb, family_handle) add_child_item.show() add_menu.append(add_child_item) elif person_handle: #allow to add a partner to this person add_partner_item = Gtk.ImageMenuItem.new_from_stock( stock_id=Gtk.STOCK_ADD, accel_group=None) add_partner_item.set_label(_("Add partner to person")) add_partner_item.connect("activate", self.add_partner_to_pers_cb, person_handle) add_partner_item.show() add_menu.append(add_partner_item) add_pers_item = Gtk.ImageMenuItem.new_from_stock(stock_id=Gtk.STOCK_ADD, accel_group=None) add_pers_item.set_label(_("Add a person")) add_pers_item.connect("activate", self.add_person_cb) add_pers_item.show() add_menu.append(add_pers_item) item.show() menu.append(item) menu.popup(None, None, None, None, event.button, event.time) return 1
[docs] def edit_person_cb(self, obj, person_handle): person = self.dbstate.db.get_person_from_handle(person_handle) if person: try: EditPerson(self.dbstate, self.uistate, [], person) except WindowActiveError: pass return True return False
[docs] def edit_fam_cb(self, obj, family_handle): fam = self.dbstate.db.get_family_from_handle(family_handle) if fam: try: EditFamily(self.dbstate, self.uistate, [], fam) except WindowActiveError: pass return True return False
[docs] def reord_fam_cb(self, obj, person_handle): try: Reorder(self.dbstate, self.uistate, [], person_handle) except WindowActiveError: pass return True
[docs] def add_person_cb(self, obj): """ Add a person """ person = Person() #the editor requires a surname person.primary_name.add_surname(Surname()) person.primary_name.set_primary_surname(0) try: EditPerson(self.dbstate, self.uistate, [], person) except WindowActiveError: pass
[docs] def add_child_to_fam_cb(self, obj, family_handle): callback = lambda x: self.callback_add_child(x, family_handle) person = Person() name = Name() #the editor requires a surname name.add_surname(Surname()) name.set_primary_surname(0) family = self.dbstate.db.get_family_from_handle(family_handle) father = self.dbstate.db.get_person_from_handle( family.get_father_handle()) if father: preset_name(father, name) person.set_primary_name(name) try: EditPerson(self.dbstate, self.uistate, [], person, callback=callback) except WindowActiveError: pass
[docs] def callback_add_child(self, person, family_handle): ref = ChildRef() ref.ref = person.get_handle() family = self.dbstate.db.get_family_from_handle(family_handle) family.add_child_ref(ref) with DbTxn(_("Add Child to Family"), self.dbstate.db) as trans: #add parentref to child person.add_parent_family_handle(family_handle) #default relationship is used self.dbstate.db.commit_person(person, trans) #add child to family self.dbstate.db.commit_family(family, trans)
[docs] def add_partner_to_pers_cb(self, obj, person_handle): """ Add a family with the person preset """ family = Family() person = self.dbstate.db.get_person_from_handle(person_handle) if not person: return if person.gender == Person.MALE: family.set_father_handle(person.handle) else: family.set_mother_handle(person.handle) try: EditFamily(self.dbstate, self.uistate, [], family) except WindowActiveError: pass
[docs] def on_add_parents(self, obj, person_handle): family = Family() childref = ChildRef() childref.set_reference_handle(person_handle) family.add_child_ref(childref) try: EditFamily(self.dbstate, self.uistate, [], family) except WindowActiveError: return
[docs] def copy_person_to_clipboard_cb(self, obj,person_handle): """Renders the person data into some lines of text and puts that into the clipboard""" person = self.dbstate.db.get_person_from_handle(person_handle) if person: cb = Gtk.Clipboard.get_for_display(Gdk.Display.get_default(), Gdk.SELECTION_CLIPBOARD) cb.set_text( self.format_helper.format_person(person,11), -1) return True return False