diff --git a/po/POTFILES.in b/po/POTFILES.in index 6fc879582..be90837ee 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -489,6 +489,7 @@ src/gui/views/treemodels/treebasemodel.py src/gui/widgets/buttons.py src/gui/widgets/expandcollapsearrow.py src/gui/widgets/fanchart.py +src/gui/widgets/fanchartdesc.py src/gui/widgets/grampletpane.py src/gui/widgets/labels.py src/gui/widgets/monitoredwidgets.py @@ -693,6 +694,7 @@ src/plugins/view/citationlistview.py src/plugins/view/eventview.py src/plugins/view/familyview.py src/plugins/view/fanchartview.py +src/plugins/view/fanchartdescview.py src/plugins/view/geography.gpr.py src/plugins/view/geoclose.py src/plugins/view/geoevents.py diff --git a/src/gui/widgets/Makefile.am b/src/gui/widgets/Makefile.am index ca03d4b07..f5e93256a 100644 --- a/src/gui/widgets/Makefile.am +++ b/src/gui/widgets/Makefile.am @@ -12,6 +12,7 @@ pkgpython_PYTHON = \ buttons.py \ expandcollapsearrow.py \ fanchart.py \ + fanchartdesc.py \ grampletpane.py \ labels.py \ linkbox.py \ diff --git a/src/gui/widgets/fanchart.py b/src/gui/widgets/fanchart.py index 9499e99cc..3450882e9 100644 --- a/src/gui/widgets/fanchart.py +++ b/src/gui/widgets/fanchart.py @@ -86,7 +86,6 @@ def gender_code(is_male): PIXELS_PER_GENERATION = 50 # size of radius for generation BORDER_EDGE_WIDTH = 10 # empty white box size at edge to indicate parents -CENTER = 50 # pixel radius of center CHILDRING_WIDTH = 12 # width of the children ring inside the person TRANSLATE_PX = 10 # size of the central circle, used to move the chart PAD_PX = 4 # padding with edges @@ -142,9 +141,11 @@ TYPE_BOX_FAMILY = 1 class FanChartBaseWidget(Gtk.DrawingArea): """ a base widget for fancharts""" + CENTER = 50 # pixel radius of center, changes per fanchart def __init__(self, dbstate, callback_popup=None): GObject.GObject.__init__(self) + self.radialtext = True st_cont = self.get_style_context() col = st_cont.lookup_color('text_color') if col[0]: @@ -227,9 +228,9 @@ class FanChartBaseWidget(Gtk.DrawingArea): requisition.height = requisition.width elif self.form == FORM_HALFCIRCLE: requisition.width = 2 * self.halfdist() - requisition.height = requisition.width / 2 + CENTER + PAD_PX + requisition.height = requisition.width / 2 + self.CENTER + PAD_PX elif self.form == FORM_QUADRANT: - requisition.width = self.halfdist() + CENTER + PAD_PX + requisition.width = self.halfdist() + self.CENTER + PAD_PX requisition.height = requisition.width def do_get_preferred_width(self): @@ -293,7 +294,7 @@ class FanChartBaseWidget(Gtk.DrawingArea): userdata.append(period) def set_userdata_age(self, person, userdata): - agecol = (255, 255, 255) # white + agecol = (1, 1, 1) # white if person: age = get_age(self.dbstate.db, person) if age is not None: @@ -472,6 +473,31 @@ class FanChartBaseWidget(Gtk.DrawingArea): return True return False + def draw_radbox(self, cr, radiusin, radiusout, start_rad, stop_rad, color, + thick=False): + cr.move_to(radiusout * math.cos(start_rad), radiusout * math.sin(start_rad)) + cr.arc(0, 0, radiusout, start_rad, stop_rad) + cr.line_to(radiusin * math.cos(stop_rad), radiusin * math.sin(stop_rad)) + cr.arc_negative(0, 0, radiusin, stop_rad, start_rad) + cr.close_path() + ##path = cr.copy_path() # not working correct + cr.set_source_rgba(color[0], color[1], color[2], color[3]) + cr.fill() + #and again for the border + cr.move_to(radiusout * math.cos(start_rad), radiusout * math.sin(start_rad)) + cr.arc(0, 0, radiusout, start_rad, stop_rad) + cr.line_to(radiusin * math.cos(stop_rad), radiusin * math.sin(stop_rad)) + cr.arc_negative(0, 0, radiusin, stop_rad, start_rad) + cr.close_path() + ##cr.append_path(path) # not working correct + cr.set_source_rgb(0, 0, 0) # black + if thick: + cr.set_line_width(3) + else: + cr.set_line_width(1) + cr.stroke() + cr.set_line_width(1) + def draw_innerring(self, cr, person, userdata, start, inc): """ Procedure to draw a person in the inner ring position @@ -682,10 +708,10 @@ class FanChartBaseWidget(Gtk.DrawingArea): elif (self.angle[-2] and radius < TRANSLATE_PX + CHILDRING_WIDTH): generation = -2 # indication of one of the children - elif radius < CENTER: + elif radius < self.CENTER: generation = 0 else: - generation = int((radius - CENTER)/self.gen_pixels()) + 1 + generation = int((radius - self.CENTER)/self.gen_pixels()) + 1 btype = self.boxtype(radius) rads = math.atan2( (cury - cy), (curx - cx) ) @@ -730,6 +756,18 @@ class FanChartBaseWidget(Gtk.DrawingArea): """ raise NotImplementedError + def _have_children(self, person): + """ + Returns True if a person has children. + TODO: is there no util function for this + """ + if person: + for family_handle in person.get_family_handle_list(): + family = self.dbstate.db.get_family_from_handle(family_handle) + if family and len(family.get_child_ref_list()) > 0: + return True + return False + def on_mouse_down(self, widget, event): self.translating = False # keep track of up/down/left/right movement generation, selected, btype = self.person_under_cursor(event.x, event.y) @@ -785,9 +823,9 @@ class FanChartBaseWidget(Gtk.DrawingArea): if self.form == FORM_CIRCLE: self.center_xy = w/2 - event.x, h/2 - event.y elif self.form == FORM_HALFCIRCLE: - self.center_xy = w/2 - event.x, h - CENTER - PAD_PX - event.y + self.center_xy = w/2 - event.x, h - self.CENTER - PAD_PX - event.y elif self.form == FORM_QUADRANT: - self.center_xy = CENTER + PAD_PX - event.x, h - CENTER - PAD_PX - event.y + self.center_xy = self.CENTER + PAD_PX - event.x, h - self.CENTER - PAD_PX - event.y else: cx = w/2 - self.center_xy[0] cy = h/2 - self.center_xy[1] @@ -826,9 +864,9 @@ class FanChartBaseWidget(Gtk.DrawingArea): self.center_xy = w/2 - event.x, h/2 - event.y self.center_xy = w/2 - event.x, h/2 - event.y elif self.form == FORM_HALFCIRCLE: - self.center_xy = w/2 - event.x, h - CENTER - PAD_PX - event.y + self.center_xy = w/2 - event.x, h - self.CENTER - PAD_PX - event.y elif self.form == FORM_QUADRANT: - self.center_xy = CENTER + PAD_PX - event.x, h - CENTER - PAD_PX - event.y + self.center_xy = self.CENTER + PAD_PX - event.x, h - self.CENTER - PAD_PX - event.y self.last_x, self.last_y = None, None self.queue_draw() @@ -1012,18 +1050,6 @@ class FanChartWidget(FanChartBaseWidget): f = self._get_parent(person, True) return not m is f is None return False - - def _have_children(self, person): - """ - Returns True if a person has children. - TODO: is there no util function for this - """ - if person: - for family_handle in person.get_family_handle_list(): - family = self.dbstate.db.get_family_from_handle(family_handle) - if family and len(family.get_child_ref_list()) > 0: - return True - return False def _get_parent(self, person, father): """ @@ -1069,7 +1095,7 @@ class FanChartWidget(FanChartBaseWidget): Compute the half radius of the circle """ nrgen = self.nrgen() - return PIXELS_PER_GENERATION * nrgen + CENTER + BORDER_EDGE_WIDTH + return PIXELS_PER_GENERATION * nrgen + self.CENTER + BORDER_EDGE_WIDTH def people_generator(self): """ @@ -1101,9 +1127,9 @@ class FanChartWidget(FanChartBaseWidget): if self.form == FORM_CIRCLE: self.set_size_request(2 * halfdist, 2 * halfdist) elif self.form == FORM_HALFCIRCLE: - self.set_size_request(2 * halfdist, halfdist + CENTER + PAD_PX) + self.set_size_request(2 * halfdist, halfdist + self.CENTER + PAD_PX) elif self.form == FORM_QUADRANT: - self.set_size_request(halfdist + CENTER + PAD_PX, halfdist + CENTER + PAD_PX) + self.set_size_request(halfdist + self.CENTER + PAD_PX, halfdist + self.CENTER + PAD_PX) #obtain the allocation alloc = self.get_allocation() @@ -1117,10 +1143,10 @@ class FanChartWidget(FanChartBaseWidget): self.center_y = h/2 - self.center_xy[1] elif self.form == FORM_HALFCIRCLE: self.center_x = w/2. - self.center_xy[0] - self.center_y = h - CENTER - PAD_PX- self.center_xy[1] + self.center_y = h - self.CENTER - PAD_PX- self.center_xy[1] elif self.form == FORM_QUADRANT: - self.center_x = CENTER + PAD_PX - self.center_xy[0] - self.center_y = h - CENTER - PAD_PX - self.center_xy[1] + self.center_x = self.CENTER + PAD_PX - self.center_xy[0] + self.center_y = h - self.CENTER - PAD_PX - self.center_xy[1] cr.translate(self.center_x, self.center_y) cr.save() @@ -1137,17 +1163,17 @@ class FanChartWidget(FanChartBaseWidget): person, userdata) cr.set_source_rgb(1, 1, 1) # white cr.move_to(0,0) - cr.arc(0, 0, CENTER, 0, 2 * math.pi) + cr.arc(0, 0, self.CENTER, 0, 2 * math.pi) cr.fill() cr.set_source_rgb(0, 0, 0) # black - cr.arc(0, 0, CENTER, 0, 2 * math.pi) + cr.arc(0, 0, self.CENTER, 0, 2 * math.pi) cr.stroke() cr.restore() # Draw center person: (text, person, parents, child, userdata) = self.data[0][0] if person: r, g, b, a = self.background_box(person, 0, userdata) - cr.arc(0, 0, CENTER, 0, 2 * math.pi) + cr.arc(0, 0, self.CENTER, 0, 2 * math.pi) if self.childring and child: cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 2 * math.pi, 0) cr.close_path() @@ -1155,8 +1181,8 @@ class FanChartWidget(FanChartBaseWidget): cr.fill() cr.save() name = name_displayer.display(person) - self.draw_text(cr, name, CENTER - - (CENTER - (CHILDRING_WIDTH + TRANSLATE_PX))/2, 95, 455, + self.draw_text(cr, name, self.CENTER - + (self.CENTER - (CHILDRING_WIDTH + TRANSLATE_PX))/2, 95, 455, 10, False, self.fontcolor(r, g, b, a), self.fontbold(a)) cr.restore() @@ -1184,7 +1210,7 @@ class FanChartWidget(FanChartBaseWidget): start_rad = start * math.pi/180 stop_rad = stop * math.pi/180 r, g, b, a = self.background_box(person, generation, userdata) - radius = generation * PIXELS_PER_GENERATION + CENTER + radius = generation * PIXELS_PER_GENERATION + self.CENTER # If max generation, and they have parents: if generation == self.generations - 1 and parents: # draw an indicator diff --git a/src/gui/widgets/fanchartdesc.py b/src/gui/widgets/fanchartdesc.py new file mode 100644 index 000000000..fea0c1d1e --- /dev/null +++ b/src/gui/widgets/fanchartdesc.py @@ -0,0 +1,704 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch +# Copyright (C) 2009 Douglas S. Blank +# Copyright (C) 2012 Benny Malengier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +## Based on the paper: +## http://www.cs.utah.edu/~draperg/research/fanchart/draperg_FHT08.pdf +## and the applet: +## http://www.cs.utah.edu/~draperg/research/fanchart/demo/ + +## Found by redwood: +## http://www.gramps-project.org/bugs/view.php?id=2611 + +from __future__ import division + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +from gi.repository import Pango +from gi.repository import GObject +from gi.repository import Gdk +from gi.repository import Gtk +from gi.repository import PangoCairo +import cairo +import math +import colorsys +import cPickle as pickle +from cgi import escape + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from gen.display.name import displayer as name_displayer +from gen.errors import WindowActiveError +from gui.editors import EditPerson, EditFamily +import gen.lib +import gui.utils +from gui.ddtargets import DdTargets +from gen.utils.alive import probably_alive +from gen.utils.libformatting import FormattingHelper +from gen.utils.db import (find_children, find_parents, find_witnessed_people, + get_age, get_timeperiod) +from gen.plug.report.utils import find_spouse +from gui.widgets.fanchart import * + +#------------------------------------------------------------------------- +# +# Constants +# +#------------------------------------------------------------------------- +pi = math.pi + +PIXELS_PER_GENPERSON = 30 # size of radius for generation of children +PIXELS_PER_GENFAMILY = 20 # size of radius for family +PIXELS_PER_RECLAIM = 4 # size of the radius of pixels taken from family to reclaim space +PARENTRING_WIDTH = 12 # width of the parent ring inside the person + +ANGLE_CHEQUI = 0 #Algorithm with homogeneous children distribution +ANGLE_WEIGHT = 1 #Algorithm for angle computation based on nr of descendants + + +#------------------------------------------------------------------------- +# +# FanChartDescWidget +# +#------------------------------------------------------------------------- + +class FanChartDescWidget(FanChartBaseWidget): + """ + Interactive Fan Chart Widget. + """ + CENTER = 60 # we require a larger center + + def __init__(self, dbstate, callback_popup=None): + """ + Fan Chart Widget. Handles visualization of data in self.data. + See main() of FanChartGramplet for example of model format. + """ + self.set_values(None, 9, BACKGROUND_GRAD_GEN, 'Sans', '#0000FF', + '#FF0000', None, 0.5, FORM_CIRCLE, ANGLE_WEIGHT) + FanChartBaseWidget.__init__(self, dbstate, callback_popup) + + def set_values(self, root_person_handle, maxgen, background, + fontdescr, grad_start, grad_end, + filter, alpha_filter, form, angle_algo): + """ + Reset the values to be used: + root_person_handle = person to show + maxgen = maximum generations to show + background = config setting of which background procedure to use (int) + fontdescr = string describing the font to use + grad_start, grad_end: colors to use for background procedure + filter = the person filter to apply to the people in the chart + alpha = the alpha transparency value (0-1) to apply to filtered out data + form = the FORM_ constant for the fanchart + """ + self.rootpersonh = root_person_handle + self.generations = maxgen + self.background = background + self.fontdescr = fontdescr + self.grad_start = grad_start + self.grad_end = grad_end + self.filter = filter + self.alpha_filter = alpha_filter + self.form = form + self.anglealgo = angle_algo + + def gen_pixels(self): + """ + how many pixels a generation takes up in the fanchart + """ + return PIXELS_PER_GENPERSON + PIXELS_PER_GENFAMILY + + def set_generations(self): + """ + Set the generations to max, and fill data structures with initial data. + """ + self.handle2desc = {} + self.famhandle2desc = {} + self.handle2fam = {} + self.gen2people = {} + self.gen2fam = {} + self.parentsroot = [] + self.gen2people[0] = [(None, False, 0, 2*pi, '', 0, 0, [], NORMAL)] #no center person + self.gen2fam[0] = [] #no families + self.angle = {} + self.angle[-2] = [] + for i in range(1, self.generations-1): + self.gen2fam[i] = [] + self.gen2people[i] = [] + self.gen2people[self.generations-1] = [] #indication of more children + self.rotfactor = 1 + self.rotstartangle = 0 + if self.form == FORM_HALFCIRCLE: + self.rotfactor = 1/2 + self.rotangle = 90 + elif self.form == FORM_QUADRANT: + self.rotangle = 180 + self.rotfactor = 1/4 + + def _fill_data_structures(self): + self.set_generations() + person = self.dbstate.db.get_person_from_handle(self.rootpersonh) + if not person: + #nothing to do, just return + return + else: + name = name_displayer.display(person) + + # person, duplicate or not, start angle, slice size, + # text, parent pos in fam, nrfam, userdata, status + self.gen2people[0] = [[person, False, 0, 2*pi, name, 0, 0, [], NORMAL]] + self.handle2desc[self.rootpersonh] = 0 + # fill in data for the parents + self.parentsroot = [] + handleparents = [] + family_handle_list = person.get_parent_family_handle_list() + if family_handle_list: + for family_handle in family_handle_list: + family = self.dbstate.db.get_family_from_handle(family_handle) + if not family: + continue + hfather = family.get_father_handle() + if hfather and hfather not in handleparents: + father = self.dbstate.db.get_person_from_handle(hfather) + if father: + self.parentsroot.append((father, [])) + handleparents.append(hfather) + hmother = family.get_mother_handle() + if hmother and hmother not in handleparents: + mother = self.dbstate.db.get_person_from_handle(hmother) + if mother: + self.parentsroot.append((mother, [])) + handleparents.append(hmother) + + #recursively fill in the datastructures: + nrdesc = self.__rec_fill_data(0, person, 0) + self.handle2desc[person.handle] += nrdesc + self.__compute_angles() + + def __rec_fill_data(self, gen, person, pos): + """ + Recursively fill in the data + """ + totdesc = 0 + nrfam = len(person.get_family_handle_list()) + self.gen2people[gen][pos][6] = nrfam + for family_handle in person.get_family_handle_list(): + totdescfam = 0 + family = self.dbstate.db.get_family_from_handle(family_handle) + + spouse_handle = find_spouse(person, family) + if spouse_handle: + spouse = self.dbstate.db.get_person_from_handle(spouse_handle) + spname = name_displayer.display(spouse) + else: + spname = '' + if family_handle in self.famhandle2desc: + #family occurs via father and via mother in the chart, only + #first to show and count. + famdup = True + else: + famdup = False + # family, duplicate or not, start angle, slice size, + # text, spouse pos in gen, nrchildren, userdata, parnter, status + self.gen2fam[gen].append([family, famdup, 0, 0, spname, pos, 0, [], + spouse, NORMAL]) + posfam = len(self.gen2fam[gen]) - 1 + + if not famdup: + nrchild = len(family.get_child_ref_list()) + self.gen2fam[gen][-1][6] = nrchild + for child_ref in family.get_child_ref_list(): + child = self.dbstate.db.get_person_from_handle(child_ref.ref) + chname = name_displayer.display(child) + if child_ref.ref in self.handle2desc: + dup = True + else: + dup = False + self.handle2desc[child_ref.ref] = 0 + # person, duplicate or not, start angle, slice size, + # text, parent pos in fam, nrfam, userdata, status + self.gen2people[gen+1].append([child, dup, 0, 0, chname, + posfam, 0, [], NORMAL]) + totdescfam += 1 #add this person as descendant + pospers = len(self.gen2people[gen+1]) - 1 + if not dup and not(self.generations == gen+2): + nrdesc = self.__rec_fill_data(gen+1, child, pospers) + self.handle2desc[child_ref.ref] += nrdesc + totdescfam += nrdesc # add children of him as descendants + self.famhandle2desc[family_handle] = totdescfam + totdesc += totdescfam + return totdesc + + def __compute_angles(self): + """ + Compute the angles of the boxes + """ + #first we compute the size of the slice. + nrgen = self.nrgen() + #set angles root person + if self.form == FORM_CIRCLE: + slice = 2*pi + start = 0. + elif self.form == FORM_HALFCIRCLE: + slice = pi + start = pi/2 + elif self.form == FORM_QUADRANT: + slice = pi/2 + start = pi + gen = 0 + data = self.gen2people[gen][0] + data[2] = start + data[3] = slice + for gen in range(1, nrgen): + nrpeople = len(self.gen2people[gen]) + prevpartnerdatahandle = None + offset = 0 + for data in self.gen2fam[gen-1]: + #obtain start and stop of partner + partnerdata = self.gen2people[gen-1][data[5]] + nrdescfam = self.famhandle2desc[data[0].handle] + nrdescpartner = self.handle2desc[partnerdata[0].handle] + nrfam = partnerdata[6] + partstart = partnerdata[2] + partslice = partnerdata[3] + if prevpartnerdatahandle != partnerdata[0].handle: + #reset the offset + offset = 0 + prevpartnerdatahandle = partnerdata[0].handle + slice = partslice/(nrdescpartner+nrfam)*(nrdescfam+1) + if data[9] == COLLAPSED: + slice = 0 + elif data[9] == EXPANDED: + slice = partslice + + data[2] = partstart + offset + data[3] = slice + offset += slice + +## if nrdescpartner == 0: +## #no offspring, draw as large as fraction of +## #nr families +## nrfam = partnerdata[6] +## slice = partslice/nrfam +## data[2] = partstart + offset +## data[3] = slice +## offset += slice +## elif nrdescfam == 0: +## #no offspring this family, but there is another +## #family. We draw this as a weight of 1 +## nrfam = partnerdata[6] +## slice = partslice/(nrdescpartner + nrfam - 1)*(nrdescfam+1) +## data[2] = partstart + offset +## data[3] = slice +## offset += slice +## else: +## #this family has offspring. We give it space for it's +## #weight in offspring +## nrfam = partnerdata[6] +## slice = partslice/(nrdescpartner + nrfam - 1)*(nrdescfam+1) +## data[2] = partstart + offset +## data[3] = slice +## offset += slice + + prevfamdatahandle = None + offset = 0 + for data in self.gen2people[gen]: + #obtain start and stop of family this is child of + parentfamdata = self.gen2fam[gen-1][data[5]] + nrdescfam = self.famhandle2desc[parentfamdata[0].handle] + nrdesc = self.handle2desc[data[0].handle] + famstart = parentfamdata[2] + famslice = parentfamdata[3] + nrchild = parentfamdata[6] + #now we divide this slice to the weight of children, + #adding one for every child + if self.anglealgo == ANGLE_CHEQUI: + slice = famslice / nrchild + elif self.anglealgo == ANGLE_WEIGHT: + slice = famslice/(nrdescfam) * (nrdesc + 1) + else: + print self.anglealgo == ANGLE_WEIGHT,self.anglealgo, ANGLE_WEIGHT + raise NotImplementedError, 'Unknown angle algorithm %d' % self.anglealgo + if prevfamdatahandle != parentfamdata[0].handle: + #reset the offset + offset = 0 + prevfamdatahandle = parentfamdata[0].handle + if data[8] == COLLAPSED: + slice = 0 + elif data[8] == EXPANDED: + slice = famslice + data[2] = famstart + offset + data[3] = slice + offset += slice + + def nrgen(self): + #compute the number of generations present + nrgen = None + for gen in range(self.generations - 1, 0, -1): + if len(self.gen2people[gen]) > 0: + nrgen = gen + 1 + break + if nrgen is None: + nrgen = 1 + return nrgen + + def halfdist(self): + """ + Compute the half radius of the circle + """ + nrgen = self.nrgen() + ringpxs = (PIXELS_PER_GENPERSON + PIXELS_PER_GENFAMILY) * (nrgen - 1) + return ringpxs + self.CENTER + BORDER_EDGE_WIDTH + + def people_generator(self): + """ + a generator over all people outside of the core person + """ + for generation in range(self.generations): + for data in self.gen2people[generation]: + yield (data[0], data[7]) + for generation in range(self.generations-1): + for data in self.gen2fam[generation]: + yield (data[8], data[7]) + + def innerpeople_generator(self): + """ + a generator over all people inside of the core person + """ + for parentdata in self.parentsroot: + parent, userdata = parentdata + yield (parent, userdata) + + def on_draw(self, widget, cr, scale=1.): + """ + The main method to do the drawing. + If widget is given, we assume we draw in GTK3 and use the allocation. + To draw raw on the cairo context cr, set widget=None. + """ + # first do size request of what we will need + halfdist = self.halfdist() + if widget: + if self.form == FORM_CIRCLE: + self.set_size_request(2 * halfdist, 2 * halfdist) + elif self.form == FORM_HALFCIRCLE: + self.set_size_request(2 * halfdist, halfdist + self.CENTER + + PAD_PX) + elif self.form == FORM_QUADRANT: + self.set_size_request(halfdist + self.CENTER + PAD_PX, + halfdist + self.CENTER + PAD_PX) + + #obtain the allocation + alloc = self.get_allocation() + x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height + + cr.scale(scale, scale) + # when printing, we need not recalculate + if widget: + if self.form == FORM_CIRCLE: + self.center_x = w/2 - self.center_xy[0] + self.center_y = h/2 - self.center_xy[1] + elif self.form == FORM_HALFCIRCLE: + self.center_x = w/2. - self.center_xy[0] + self.center_y = h - self.CENTER - PAD_PX- self.center_xy[1] + elif self.form == FORM_QUADRANT: + self.center_x = self.CENTER + PAD_PX - self.center_xy[0] + self.center_y = h - self.CENTER - PAD_PX - self.center_xy[1] + cr.translate(self.center_x, self.center_y) + + cr.save() + #draw center + cr.set_source_rgb(1, 1, 1) # white + cr.move_to(0,0) + cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi) + cr.fill() + cr.set_source_rgb(0, 0, 0) # black + cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi) + cr.stroke() + cr.restore() + # Draw center person: + (person, dup, start, slice, text, parentfampos, nrfam, userdata, status) \ + = self.gen2people[0][0] + if person: + r, g, b, a = self.background_box(person, 0, userdata) + cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi) + if self.parentsroot: + cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, + 2 * math.pi, 0) + cr.close_path() + cr.set_source_rgba(r/255, g/255, b/255, a) + cr.fill() + cr.save() + name = name_displayer.display(person) + self.draw_text(cr, name, self.CENTER - PIXELS_PER_GENFAMILY + - (self.CENTER - PIXELS_PER_GENFAMILY + - (CHILDRING_WIDTH + TRANSLATE_PX))/2, + 95, 455, 10, False, + self.fontcolor(r, g, b, a), self.fontbold(a)) + cr.restore() + #draw center to move chart + cr.set_source_rgb(0, 0, 0) # black + cr.move_to(TRANSLATE_PX, 0) + cr.arc(0, 0, TRANSLATE_PX, 0, 2 * math.pi) + if self.parentsroot: # has at least one parent + cr.fill() + self.draw_parentring(cr) + else: + cr.stroke() + #now write all the families and children + cr.save() + cr.rotate(self.rotate_value * math.pi/180) + radstart = self.CENTER - PIXELS_PER_GENFAMILY - PIXELS_PER_GENPERSON + for gen in range(self.generations-1): + radstart += PIXELS_PER_GENPERSON + for famdata in self.gen2fam[gen]: + # family, duplicate or not, start angle, slice size, + # text, spouse pos in gen, nrchildren, userdata, status + fam, dup, start, slice, text, posfam, nrchild, userdata,\ + partner, status = famdata + if status != COLLAPSED: + self.draw_person(cr, text, start, slice, radstart, + radstart + PIXELS_PER_GENFAMILY, gen, dup, + partner, userdata, family=True, thick=status != NORMAL) + radstart += PIXELS_PER_GENFAMILY + for pdata in self.gen2people[gen+1]: + # person, duplicate or not, start angle, slice size, + # text, parent pos in fam, nrfam, userdata, status + pers, dup, start, slice, text, pospar, nrfam, userdata, status = \ + pdata + if status != COLLAPSED: + self.draw_person(cr, text, start, slice, radstart, + radstart + PIXELS_PER_GENPERSON, gen+1, dup, + pers, userdata, thick=status != NORMAL) + cr.restore() + + if self.background in [BACKGROUND_GRAD_AGE, BACKGROUND_GRAD_PERIOD]: + self.draw_gradient(cr, widget, halfdist) + + def draw_person(self, cr, name, start_rad, slice, radius, radiusend, + generation, dup, person, userdata, family=False, thick=False): + """ + Display the piece of pie for a given person. start_rad and slice + are in radial. + """ + if slice == 0: + return + cr.save() + full = False + if abs(slice - 2*pi) < 1e-6: + full = True + stop_rad = start_rad + slice + if not dup: + r, g, b, a = self.background_box(person, generation, userdata) + else: + #duplicate color + a = 1 + r, g, b = (0.2, 0.2, 0.2) + # If max generation, and they have children: + if (not family and generation == self.generations - 1 + and self._have_children(person)): + # draw an indicator + radmax = radiusend + BORDER_EDGE_WIDTH + cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad)) + cr.arc(0, 0, radmax, start_rad, stop_rad) + cr.line_to(radiusend*math.cos(stop_rad), radiusend*math.sin(stop_rad)) + cr.arc_negative(0, 0, radiusend, stop_rad, start_rad) + cr.close_path() + ##path = cr.copy_path() # not working correct + cr.set_source_rgb(1, 1, 1) # white + cr.fill() + #and again for the border + cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad)) + cr.arc(0, 0, radmax, start_rad, stop_rad) + cr.line_to(radiusend*math.cos(stop_rad), radiusend*math.sin(stop_rad)) + cr.arc_negative(0, 0, radiusend, stop_rad, start_rad) + cr.close_path() + ##cr.append_path(path) # not working correct + cr.set_source_rgb(0, 0, 0) # black + cr.stroke() + # now draw the person + self.draw_radbox(cr, radius, radiusend, start_rad, stop_rad, + (r/255, g/255, b/255, a), thick) + if self.last_x is None or self.last_y is None: + #we are not in a move, so draw text + radial = False + width = radiusend-radius + radstart = radius + width/2 + spacepolartext = radstart * (stop_rad-start_rad) + if spacepolartext < width * 1.1: + # more space to print it radial + radial = True + radstart = radius + 4 + self.draw_text(cr, name, radstart, start_rad/ math.pi*180, + stop_rad/ math.pi*180, width, radial, + self.fontcolor(r, g, b, a), self.fontbold(a)) + cr.restore() + + def boxtype(self, radius): + """ + default is only one type of box type + """ + if radius <= self.CENTER: + if radius >= self.CENTER - PIXELS_PER_GENFAMILY: + return TYPE_BOX_FAMILY + else: + return TYPE_BOX_NORMAL + else: + gen = int((radius - self.CENTER)/self.gen_pixels()) + 1 + radius = (radius - self.CENTER) % PIXELS_PER_GENERATION + if radius >= PIXELS_PER_GENPERSON: + if gen < self.generations - 1: + return TYPE_BOX_FAMILY + else: + # the last generation has no family boxes + None + else: + return TYPE_BOX_NORMAL + + def draw_parentring(self, cr): + cr.move_to(TRANSLATE_PX + CHILDRING_WIDTH, 0) + cr.set_source_rgb(0, 0, 0) # black + cr.set_line_width(1) + cr.arc(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 0, 2 * math.pi) + cr.stroke() + nrparent = len(self.parentsroot) + #Y axis is downward. positve angles are hence clockwise + startangle = math.pi + if nrparent <= 2: + angleinc = math.pi + elif nrparent <= 4: + angleinc = math.pi/2 + else: + angleinc = 2 * math.pi / nrchild + for data in self.parentsroot: + self.draw_innerring(cr, data[0], data[1], startangle, angleinc) + startangle += angleinc + + def personpos_at_angle(self, generation, angledeg, btype): + """ + returns the person in generation generation at angle. + """ + angle = angledeg / 360 * 2 * pi + selected = None + if btype == TYPE_BOX_NORMAL: + for p, pdata in enumerate(self.gen2people[generation]): + # person, duplicate or not, start angle, slice size, + # text, parent pos in fam, nrfam, userdata, status + start = pdata[2] + stop = start + pdata[3] + if start <= angle <= stop: + selected = p + break + elif btype == TYPE_BOX_FAMILY: + for p, pdata in enumerate(self.gen2fam[generation]): + # person, duplicate or not, start angle, slice size, + # text, parent pos in fam, nrfam, userdata, status + start = pdata[2] + stop = start + pdata[3] + if start <= angle <= stop: + selected = p + break + return selected + + def person_at(self, generation, pos, btype): + """ + returns the person at generation, pos, btype + """ + if pos is None: + return None + if generation == -2: + person, userdata = self.parentsroot[pos] + elif btype == TYPE_BOX_NORMAL: + # person, duplicate or not, start angle, slice size, + # text, parent pos in fam, nrfam, userdata, status + person = self.gen2people[generation][pos][0] + elif btype == TYPE_BOX_FAMILY: + # family, duplicate or not, start angle, slice size, + # text, spouse pos in gen, nrchildren, userdata, person, status + person = self.gen2fam[generation][pos][8] + return person + + def do_mouse_click(self): + # no drag occured, expand or collapse the section + self.change_slice(self._mouse_click_gen, self._mouse_click_sel, + self._mouse_click_btype) + self._mouse_click = False + self.queue_draw() + + def change_slice(self, generation, selected, btype): + if generation < 1: + return + if btype == TYPE_BOX_NORMAL: + data = self.gen2people[generation][selected] + parpos = data[5] + status = data[8] + if status == NORMAL: + #should be expanded, rest collapsed + for entry in self.gen2people[generation]: + if entry[5] == parpos: + entry[8] = COLLAPSED + data[8] = EXPANDED + else: + #is expanded, set back to normal + for entry in self.gen2people[generation]: + if entry[5] == parpos: + entry[8] = NORMAL + if btype == TYPE_BOX_FAMILY: + data = self.gen2fam[generation][selected] + parpos = data[5] + status = data[9] + if status == NORMAL: + #should be expanded, rest collapsed + for entry in self.gen2fam[generation]: + if entry[5] == parpos: + entry[9] = COLLAPSED + data[9] = EXPANDED + else: + #is expanded, set back to normal + for entry in self.gen2fam[generation]: + if entry[5] == parpos: + entry[9] = NORMAL + + self.__compute_angles() + +class FanChartDescGrampsGUI(FanChartGrampsGUI): + """ class for functions fanchart GUI elements will need in Gramps + """ + + def main(self): + """ + Fill the data structures with the active data. This initializes all + data. + """ + root_person_handle = self.get_active('Person') + self.fan.set_values(root_person_handle, self.maxgen, self.background, + self.fonttype, self.grad_start, self.grad_end, + self.generic_filter, self.alpha_filter, self.form, + self.angle_algo) + self.fan.reset() + self.fan.queue_draw() diff --git a/src/plugins/view/Makefile.am b/src/plugins/view/Makefile.am index 1d583469b..23b58a66e 100644 --- a/src/plugins/view/Makefile.am +++ b/src/plugins/view/Makefile.am @@ -12,13 +12,14 @@ pkgpython_PYTHON = \ eventview.py \ familyview.py \ fanchartview.py \ + fanchartdescview.py \ geoclose.py \ geoevents.py \ geoplaces.py \ geoperson.py \ geofamily.py \ - geofamclose.py \ - geomoves.py \ + geofamclose.py \ + geomoves.py \ geography.gpr.py \ htmlrenderer.gpr.py \ grampletview.py \ diff --git a/src/plugins/view/fanchartdescview.py b/src/plugins/view/fanchartdescview.py new file mode 100644 index 000000000..ea27e7447 --- /dev/null +++ b/src/plugins/view/fanchartdescview.py @@ -0,0 +1,521 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch +# Copyright (C) 2009 Douglas S. Blank +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# $Id$ + +## Based on the paper: +## http://www.cs.utah.edu/~draperg/research/fanchart/draperg_FHT08.pdf +## and the applet: +## http://www.cs.utah.edu/~draperg/research/fanchart/demo/ + +## Found by redwood: +## http://www.gramps-project.org/bugs/view.php?id=2611 + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +from gi.repository import Gdk +from gi.repository import Gtk +import cairo +from gen.ggettext import gettext as _ + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +import gen.lib +import gui.widgets.fanchart as fanchart +import gui.widgets.fanchartdesc as fanchartdesc +from gui.views.navigationview import NavigationView +from gui.views.bookmarks import PersonBookmarks +from gui.utils import SystemFonts + +# the print settings to remember between print sessions +PRINT_SETTINGS = None + +class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView): + """ + The Gramplet code that realizes the FanChartWidget. + """ + #settings in the config file + CONFIGSETTINGS = ( + ('interface.fanview-maxgen', 9), + ('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN), + ('interface.fanview-font', 'Sans'), + ('interface.fanview-form', fanchart.FORM_CIRCLE), + ('interface.color-start-grad', '#ef2929'), + ('interface.color-end-grad', '#3d37e9'), + ('interface.angle-algorithm', fanchartdesc.ANGLE_WEIGHT), + ) + def __init__(self, pdata, dbstate, uistate, nav_group=0): + self.dbstate = dbstate + self.uistate = uistate + + NavigationView.__init__(self, _('Descendant Fan Chart'), + pdata, dbstate, uistate, + dbstate.db.get_bookmarks(), + PersonBookmarks, + nav_group) + fanchartdesc.FanChartDescGrampsGUI.__init__(self, self.on_childmenu_changed) + #set needed values + self.maxgen = self._config.get('interface.fanview-maxgen') + self.background = self._config.get('interface.fanview-background') + self.fonttype = self._config.get('interface.fanview-font') + + self.grad_start = self._config.get('interface.color-start-grad') + self.grad_end = self._config.get('interface.color-end-grad') + self.form = self._config.get('interface.fanview-form') + self.angle_algo = self._config.get('interface.angle-algorithm') + self.generic_filter = None + self.alpha_filter = 0.2 + + dbstate.connect('active-changed', self.active_changed) + dbstate.connect('database-changed', self.change_db) + + self.additional_uis.append(self.additional_ui()) + self.allfonts = [x for x in enumerate(SystemFonts().get_system_fonts())] + + def navigation_type(self): + return 'Person' + + def build_widget(self): + self.set_fan(fanchartdesc.FanChartDescWidget(self.dbstate, self.on_popup)) + self.scrolledwindow = Gtk.ScrolledWindow(None, None) + self.scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC) + self.fan.show_all() + self.scrolledwindow.add_with_viewport(self.fan) + + return self.scrolledwindow + + def get_stock(self): + """ + The category stock icon + """ + return 'gramps-pedigree' + + def get_viewtype_stock(self): + """Type of view in category + """ + return 'gramps-fanchart' + + def additional_ui(self): + return ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + + def define_actions(self): + """ + Required define_actions function for PageView. Builds the action + group information required. + """ + NavigationView.define_actions(self) + + self._add_action('PrintView', Gtk.STOCK_PRINT, _("_Print/Save View..."), + accel="P", + tip=_("Print or save the Fan Chart View"), + callback=self.printview) + def build_tree(self): + """ + Generic method called by PageView to construct the view. + Here the tree builds when active person changes or db changes or on + callbacks like person_rebuild, so build will be double sometimes. + However, change in generic filter also triggers build_tree ! So we + need to reset. + """ + self.update() + + def active_changed(self, handle): + """ + Method called when active person changes. + """ + # Reset everything but rotation angle (leave it as is) + self.update() + + def _connect_db_signals(self): + """ + Connect database signals. + """ + self._add_db_signal('person-add', self.person_rebuild) + self._add_db_signal('person-update', self.person_rebuild) + self._add_db_signal('person-delete', self.person_rebuild) + self._add_db_signal('person-rebuild', self.person_rebuild_bm) + self._add_db_signal('family-update', self.person_rebuild) + self._add_db_signal('family-add', self.person_rebuild) + self._add_db_signal('family-delete', self.person_rebuild) + self._add_db_signal('family-rebuild', self.person_rebuild) + + def change_db(self, db): + self._change_db(db) + self.bookmarks.update_bookmarks(self.dbstate.db.get_bookmarks()) + if self.active: + self.bookmarks.redraw() + self.update() + + def update(self): + self.main() + + def goto_handle(self, handle): + self.change_active(handle) + self.main() + + def get_active(self, object): + """overrule get_active, to support call as in Gramplets + """ + return NavigationView.get_active(self) + + def person_rebuild(self, *args): + self.update() + + def person_rebuild_bm(self, *args): + """Large change to person database""" + self.person_rebuild() + if self.active: + self.bookmarks.redraw() + + def printview(self, obj): + """ + Print or save the view that is currently shown + """ + widthpx = 2 * self.fan.halfdist() + heightpx = widthpx + if self.form == fanchart.FORM_HALFCIRCLE: + heightpx = heightpx / 2 + fanchart.CENTER + fanchart.PAD_PX + elif self.form == fanchart.FORM_QUADRANT: + heightpx = heightpx / 2 + fanchart.CENTER + fanchart.PAD_PX + widthpx = heightpx + + prt = CairoPrintSave(widthpx, heightpx, self.fan.on_draw, self.uistate.window) + prt.run() + + def on_childmenu_changed(self, obj, person_handle): + """Callback for the pulldown menu selection, changing to the person + attached with menu item.""" + self.change_active(person_handle) + return True + + def can_configure(self): + """ + See :class:`~gui.views.pageview.PageView + :return: bool + """ + return True + + def _get_configure_page_funcs(self): + """ + Return a list of functions that create gtk elements to use in the + notebook pages of the Configure dialog + + :return: list of functions + """ + return [self.config_panel] + + def config_panel(self, configdialog): + """ + Function that builds the widget in the configuration dialog + """ + nrentry = 7 + table = Gtk.Table(6, 3) + table.set_border_width(12) + table.set_col_spacings(6) + table.set_row_spacings(6) + + configdialog.add_spinner(table, _("Max generations"), 0, + 'interface.fanview-maxgen', (1, 11), + callback=self.cb_update_maxgen) + configdialog.add_combo(table, + _('Text Font'), + 1, 'interface.fanview-font', + self.allfonts, callback=self.cb_update_font, valueactive=True) + backgrvals = ( + (fanchart.BACKGROUND_GENDER, _('Gender colors')), + (fanchart.BACKGROUND_GRAD_GEN, _('Generation based gradient')), + (fanchart.BACKGROUND_GRAD_AGE, _('Age (0-100) based gradient')), + (fanchart.BACKGROUND_SINGLE_COLOR, + _('Single main (filter) color')), + (fanchart.BACKGROUND_GRAD_PERIOD, _('Time period based gradient')), + (fanchart.BACKGROUND_WHITE, _('White')), + (fanchart.BACKGROUND_SCHEME1, _('Color scheme classic report')), + (fanchart.BACKGROUND_SCHEME2, _('Color scheme classic view')), + ) + curval = self._config.get('interface.fanview-background') + nrval = 0 + for nr, val in backgrvals: + if curval == nr: + break + nrval += 1 + configdialog.add_combo(table, + _('Background'), + 2, 'interface.fanview-background', + backgrvals, + callback=self.cb_update_background, valueactive=False, + setactive=nrval + ) + #colors, stored as hex values + configdialog.add_color(table, _('Start gradient/Main color'), 3, + 'interface.color-start-grad', col=1) + configdialog.add_color(table, _('End gradient/2nd color'), 4, + 'interface.color-end-grad', col=1) + # form of the fan + configdialog.add_combo(table, _('Fan chart type'), 5, + 'interface.fanview-form', + ((fanchart.FORM_CIRCLE, _('Full Circle')), + (fanchart.FORM_HALFCIRCLE, _('Half Circle')), + (fanchart.FORM_QUADRANT, _('Quadrant'))), + callback=self.cb_update_form) + # algo for the fan angle distribution + configdialog.add_combo(table, _('Fan chart distribution'), 6, + 'interface.angle-algorithm', + ((fanchartdesc.ANGLE_CHEQUI, + _('Homogeneous children distribution')), + (fanchartdesc.ANGLE_WEIGHT, + _('Size proportional to number of descendants')), + ), + callback=self.cb_update_anglealgo) + + return _('Layout'), table + + def config_connect(self): + """ + Overwriten from :class:`~gui.views.pageview.PageView method + This method will be called after the ini file is initialized, + use it to monitor changes in the ini file + """ + self._config.connect('interface.color-start-grad', + self.cb_update_color) + self._config.connect('interface.color-end-grad', + self.cb_update_color) + + def cb_update_maxgen(self, spinbtn, constant): + self.maxgen = spinbtn.get_value_as_int() + self._config.set(constant, self.maxgen) + self.update() + + def cb_update_background(self, obj, constant): + entry = obj.get_active() + Gtk.TreePath.new_from_string('%d' % entry) + val = int(obj.get_model().get_value( + obj.get_model().get_iter_from_string('%d' % entry), 0)) + self._config.set(constant, val) + self.background = val + self.update() + + def cb_update_form(self, obj, constant): + entry = obj.get_active() + self._config.set(constant, entry) + self.form = entry + self.update() + + def cb_update_anglealgo(self, obj, constant): + entry = obj.get_active() + self._config.set(constant, entry) + self.angle_algo = entry + self.update() + + def cb_update_color(self, client, cnxn_id, entry, data): + """ + Called when the configuration menu changes the childrenring setting. + """ + self.grad_start = self._config.get('interface.color-start-grad') + self.grad_end = self._config.get('interface.color-end-grad') + self.update() + + def cb_update_font(self, obj, constant): + entry = obj.get_active() + self._config.set(constant, self.allfonts[entry][1]) + self.fonttype = self.allfonts[entry][1] + self.update() + + def get_default_gramplets(self): + """ + Define the default gramplets for the sidebar and bottombar. + """ + return (("Person Filter",), + ()) + +#------------------------------------------------------------------------ +# +# CairoPrintSave class +# +#------------------------------------------------------------------------ +class CairoPrintSave(): + """Act as an abstract document that can render onto a cairo context. + + It can render the model onto cairo context pages, according to the received + page style. + + """ + + def __init__(self, widthpx, heightpx, drawfunc, parent): + """ + This class provides the things needed so as to dump a cairo drawing on + a context to output + """ + self.widthpx = widthpx + self.heightpx = heightpx + self.drawfunc = drawfunc + self.parent = parent + + def run(self): + """Create the physical output from the meta document. + + """ + global PRINT_SETTINGS + + # set up a print operation + operation = Gtk.PrintOperation() + operation.connect("draw_page", self.on_draw_page) + operation.connect("preview", self.on_preview) + operation.connect("paginate", self.on_paginate) + operation.set_n_pages(1) + #paper_size = Gtk.PaperSize.new(name="iso_a4") + ## WHY no Gtk.Unit.PIXEL ?? Is there a better way to convert + ## Pixels to MM ?? + paper_size = Gtk.PaperSize.new_custom("custom", + "Custom Size", + round(self.widthpx * 0.2646), + round(self.heightpx * 0.2646), + Gtk.Unit.MM) + page_setup = Gtk.PageSetup() + page_setup.set_paper_size(paper_size) + #page_setup.set_orientation(Gtk.PageOrientation.PORTRAIT) + operation.set_default_page_setup(page_setup) + #operation.set_use_full_page(True) + + if PRINT_SETTINGS is not None: + operation.set_print_settings(PRINT_SETTINGS) + + # run print dialog + while True: + self.preview = None + res = operation.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.parent) + if self.preview is None: # cancel or print + break + # set up printing again; can't reuse PrintOperation? + operation = Gtk.PrintOperation() + operation.set_default_page_setup(page_setup) + operation.connect("draw_page", self.on_draw_page) + operation.connect("preview", self.on_preview) + operation.connect("paginate", self.on_paginate) + # set print settings if it was stored previously + if PRINT_SETTINGS is not None: + operation.set_print_settings(PRINT_SETTINGS) + + # store print settings if printing was successful + if res == Gtk.PrintOperationResult.APPLY: + PRINT_SETTINGS = operation.get_print_settings() + + def on_draw_page(self, operation, context, page_nr): + """Draw a page on a Cairo context. + """ + cr = context.get_cairo_context() + pxwidth = round(context.get_width()) + pxheight = round(context.get_height()) + scale = min(pxwidth/self.widthpx, pxheight/self.heightpx) + if scale > 1: + scale = 1 + self.drawfunc(None, cr, scale=scale) + + def on_paginate(self, operation, context): + """Paginate the whole document in chunks. + We don't need this as there is only one page, however, + we provide a dummy holder here, because on_preview crashes if no + default application is set with gir 3.3.2 (typically evince not installed)! + It will provide the start of the preview dialog, which cannot be + started in on_preview + """ + finished = True + # update page number + operation.set_n_pages(1) + + # start preview if needed + if self.preview: + self.preview.run() + + return finished + + def on_preview(self, operation, preview, context, parent): + """Implement custom print preview functionality. + We provide a dummy holder here, because on_preview crashes if no + default application is set with gir 3.3.2 (typically evince not installed)! + """ + dlg = Gtk.MessageDialog(parent, + flags=Gtk.DialogFlags.MODAL, + type=Gtk.MessageType.WARNING, + buttons=Gtk.ButtonsType.CLOSE, + message_format=_('No preview available')) + self.preview = dlg + self.previewopr = operation + #dlg.format_secondary_markup(msg2) + dlg.set_title("Fan Chart Preview - Gramps") + dlg.connect('response', self.previewdestroy) + + # give a dummy cairo context to Gtk.PrintContext, + try: + width = int(round(context.get_width())) + except ValueError: + width = 0 + try: + height = int(round(context.get_height())) + except ValueError: + height = 0 + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + cr = cairo.Context(surface) + context.set_cairo_context(cr, 72.0, 72.0) + + return True + + def previewdestroy(self, dlg, res): + self.preview.destroy() + self.previewopr.end_preview() diff --git a/src/plugins/view/view.gpr.py b/src/plugins/view/view.gpr.py index 06784cc80..5dcbd6a0a 100644 --- a/src/plugins/view/view.gpr.py +++ b/src/plugins/view/view.gpr.py @@ -140,17 +140,32 @@ register(VIEW, id = 'fanchartview', name = _("Fan Chart View"), category = ("Ancestry", _("Ancestry")), -description = _("The view showing relations through a fanchart"), +description = _("A view showing parents through a fanchart"), version = '1.0', gramps_target_version = '4.0', status = STABLE, fname = 'fanchartview.py', -authors = [u"Douglas S. Blank"], -authors_email = ["doug.blank@gmail.com"], +authors = [u"Douglas S. Blank", u"B. Malengier"], +authors_email = ["doug.blank@gmail.com", "benny.malengier@gmail.com"], viewclass = 'FanChartView', stock_icon = 'gramps-fanchart', ) +register(VIEW, +id = 'fanchartdescview', +name = _("Descendants Fan Chart View"), +category = ("Ancestry", _("Ancestry")), +description = _("Showing descendants through a fanchart"), +version = '1.0', +gramps_target_version = '4.0', +status = STABLE, +fname = 'fanchartdescview.py', +authors = [u"B. Malengier"], +authors_email = ["benny.malengier@gmail.com"], +viewclass = 'FanChartDescView', +stock_icon = 'gramps-fanchart', + ) + register(VIEW, id = 'personview', name = _("Person Tree View"),