diff --git a/gramps/gen/display/name.py b/gramps/gen/display/name.py
index 1c6dcf787..ffa4b94a1 100644
--- a/gramps/gen/display/name.py
+++ b/gramps/gen/display/name.py
@@ -892,6 +892,24 @@ class NameDisplay:
name = person.get_primary_name()
return self.display_name(name)
+ def display_format(self, person, num):
+ """
+ Return a text string representing the L{gen.lib.Person} instance's
+ L{Name} using num format.
+
+ @param person: L{gen.lib.Person} instance that contains the
+ L{Name} that is to be displayed. The primary name is used for
+ the display.
+ @type person: L{gen.lib.Person}
+ @param num: num of the format to be used, as return by
+ name_displayer.add_name_format('name','format')
+ @type num: int
+ @returns: Returns the L{gen.lib.Person} instance's name
+ @rtype: str
+ """
+ name = person.get_primary_name()
+ return self.name_formats[num][_F_FN](name)
+
def display_formal(self, person):
"""
Return a text string representing the :class:`~.person.Person`
diff --git a/gramps/gui/widgets/fanchart.py b/gramps/gui/widgets/fanchart.py
index 6efb17523..67abe69d1 100644
--- a/gramps/gui/widgets/fanchart.py
+++ b/gramps/gui/widgets/fanchart.py
@@ -93,20 +93,6 @@ from gramps.gen.const import (
_ = 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
-
#-------------------------------------------------------------------------
#
# FanChartBaseWidget
@@ -115,7 +101,7 @@ def gender_code(is_male):
class FanChartBaseWidget(Gtk.DrawingArea):
""" a base widget for fancharts"""
- CENTER = 50 # pixel radius of center, changes per fanchart
+ CENTER = 60 # pixel radius of center, changes per fanchart
def __init__(self, dbstate, uistate, callback_popup=None):
Gtk.DrawingArea.__init__(self)
@@ -134,6 +120,8 @@ class FanChartBaseWidget(Gtk.DrawingArea):
self.last_x, self.last_y = None, None
self.fontdescr = "Sans"
self.fontsize = 8
+ self.twolineformat_nums=(name_displayer.add_name_format('fanchart_name_line1', '%l'),
+ name_displayer.add_name_format('fanchart_name_line2', '%f %s'))
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)
@@ -175,15 +163,18 @@ class FanChartBaseWidget(Gtk.DrawingArea):
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.center_delta_xy = [0, 0] # translation of the center of the fan wrt canonical center
+ self.center_xy = [0, 0] # coord of the center of the fan
self.mouse_x = 0
self.mouse_y = 0
#(re)compute everything
self.reset()
self.set_size_request(120, 120)
+ def __del__(self):
+ for num in self.twolineformat_nums:
+ name_displayer.del_name_format(num)
+
def reset(self):
"""
Reset the fan chart. This should trigger computation of all data
@@ -195,7 +186,7 @@ class FanChartBaseWidget(Gtk.DrawingArea):
self._fill_data_structures()
# prepare the colors for the boxes
- self.prepare_background_box()
+ self.prepare_background_box(self.generations)
def _fill_data_structures(self):
"""
@@ -245,6 +236,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
"""
raise NotImplementedError
+ def get_radiusinout_for_generation(self,generation):
+ raise NotImplementedError
+
def on_draw(self, widget, cr, scale=1.):
"""
callback to draw the fanchart
@@ -296,12 +290,11 @@ class FanChartBaseWidget(Gtk.DrawingArea):
)
userdata.append((agecol[0]*255, agecol[1]*255, agecol[2]*255))
- def prepare_background_box(self):
+ def prepare_background_box(self, maxgen):
"""
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,
@@ -462,6 +455,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
def draw_radbox(self, cr, radiusin, radiusout, start_rad, stop_rad, color,
thick=False):
+ """
+ Procedure to draw a person box in the outter ring position
+ """
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))
@@ -473,9 +469,13 @@ class FanChartBaseWidget(Gtk.DrawingArea):
#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))
+ if (start_rad - stop_rad) % (2 * math.pi) > 1e-5:
+ radial_motion_type = cr.line_to
+ else:
+ radial_motion_type = cr.move_to
+ radial_motion_type(radiusin * math.cos(stop_rad), radiusin * math.sin(stop_rad))
cr.arc_negative(0, 0, radiusin, stop_rad, start_rad)
- cr.close_path()
+ radial_motion_type(radiusout * math.cos(start_rad), radiusout * math.sin(start_rad))
##cr.append_path(path) # not working correct
cr.set_source_rgb(0, 0, 0) # black
if thick:
@@ -520,12 +520,98 @@ class FanChartBaseWidget(Gtk.DrawingArea):
cr.set_source_rgba(r/255., g/255., b/255., a)
cr.fill()
- def wrap_truncate_layout(self, layout, font, width_pixels):
- """Uses the layout to wrap and truncate its text to given width
+ def draw_person(self, cr, person, radiusin, radiusout, start_rad, stop_rad,
+ generation, dup, userdata, thick=False, has_moregen_indicator = False,
+ is_central_person=False):
+ """
+ Display the piece of pie for a given person. start_rad and stop_rad
+ are in radians.
+ """
+ cr.save()
+ # If we need an indicator of more generations:
+ if has_moregen_indicator:
+ # draw an indicator
+ color=(1.0, 1.0, 1.0, 1.0) # white
+ self.draw_radbox(cr, radiusout, radiusout + BORDER_EDGE_WIDTH, start_rad, stop_rad, color, thick=False)
+ # get the color of the background
+ if not person:
+ # if called on None, let's make a transparent box
+ r, g, b, a = (255, 255, 255, 0)
+ elif dup:
+ r, g, b = self.dupcolor #duplicate color
+ a = 1.0
+ else:
+ r, g, b, a = self.background_box(person, generation, userdata)
+ color=(r/255., g/255., b/255., a)
+
+ # now draw the person
+ if not is_central_person:
+ self.draw_radbox(cr, radiusin, radiusout, start_rad, stop_rad, color, thick)
+ else:
+ #special box for centrer pers
+ cr.arc(0, 0, radiusout, 0, 2 * math.pi)
+ if self.childring and len(self.childrenroot)>0:
+ cr.arc_negative(0, 0, radiusin, 2 * math.pi, 0)
+ cr.close_path()
+ cr.set_source_rgba(*color)
+ cr.fill()
+
+ if self.last_x is None or self.last_y is None:
+ #we are not in a move, so draw text
+ radial = False
+ if self.radialtext: ## and generation >= 6:
+ space_arc_text = (radiusin+radiusout)/2 * (stop_rad-start_rad)
+ # is there more space to print it radial ?
+ radial= (space_arc_text < (radiusout-radiusin) * 1.1)
+ self.draw_person_text(cr, person, radiusin, radiusout, start_rad, stop_rad,
+ radial, self.fontcolor(r, g, b, a), self.fontbold(a), can_flip=not is_central_person)
+ cr.restore()
+
+ def draw_person_text(self, cr, person, radiusin, radiusout, start, stop,
+ radial=False, fontcolor=(0, 0, 0), bold=False, can_flip = True):
+ if not person: return
+ draw_radial = radial and self.radialtext
+ if not self.twolinename:
+ name=name_displayer.display(person)
+ self.draw_text(cr, name, radiusin, radiusout, start, stop, draw_radial,
+ fontcolor, bold)
+ else:
+ text=name_displayer.display(person)
+ text_line1=name_displayer.display_format(person,self.twolineformat_nums[0])
+ text_line2=name_displayer.display_format(person,self.twolineformat_nums[1])
+ if draw_radial:
+ split_frac_line1=0.5
+ flipped = can_flip and ((math.degrees((start+stop)/2.0) + self.rotate_value - 90) % 360 < 179 and self.flipupsidedownname)
+ if flipped:
+ middle=(start*split_frac_line1+stop*(1.0-split_frac_line1))
+ (a11,a12,a21,a22)=(middle,stop,start,middle)
+ else:
+ middle=(start*(1.0-split_frac_line1)+stop*split_frac_line1)
+ (a11,a12,a21,a22)=(start,middle,middle,stop)
+ written_textwidth=self.draw_text(cr, text_line1, radiusin, radiusout, a11, a12, draw_radial, fontcolor, bold=1, flipped=flipped)
+ if written_textwidth == 0 and text_line1 != "":
+ #Not enought space for 2 line, fallback to 1 line
+ written_textwidth=self.draw_text(cr, text_line1, radiusin, radiusout, start, stop, draw_radial, fontcolor, bold=1, flipped=flipped)
+ self.draw_text(cr, text_line2, radiusin+written_textwidth+PAD_TEXT, radiusout, start, stop, draw_radial, fontcolor, bold, flipped)
+ else:
+ self.draw_text(cr, text_line2, radiusin, radiusout, a21, a22, draw_radial, fontcolor, bold, flipped)
+ else:
+ middle=(radiusin*.5+radiusout*.5)
+ flipped = can_flip and ((math.degrees((start+stop)/2.0) + self.rotate_value) % 360 < 179 and self.flipupsidedownname)
+ if flipped:
+ self.draw_text(cr, text_line2, middle, radiusout, start, stop, draw_radial, fontcolor, bold=0, flipped=flipped)
+ self.draw_text(cr, text_line1, radiusin, middle, start, stop, draw_radial, fontcolor, bold=1, flipped=flipped)
+ else:
+ self.draw_text(cr, text_line1, middle, radiusout, start, stop, draw_radial, fontcolor, bold=1, flipped=flipped)
+ self.draw_text(cr, text_line2, radiusin, middle, start, stop, draw_radial, fontcolor, bold=0, flipped=flipped)
+
+ def wrap_truncate_layout(self, layout, font, width_pixels, height_pixels, tryrescale=True):
+ """
+ Uses the layout to wrap and truncate its text to given width
Returns: (w,h) as returned by layout.get_pixel_size()
"""
-
+ all_text_backup = layout.get_text()
layout.set_font_description(font)
layout.set_width(Pango.SCALE * width_pixels)
@@ -534,166 +620,138 @@ class FanChartBaseWidget(Gtk.DrawingArea):
if layout.get_line_count() > 1:
layout.set_text(layout.get_text(), layout.get_line(0).length)
+ #2. we check if height is ok
+ w, h = layout.get_pixel_size()
+ if h > height_pixels:
+ if tryrescale:
+ #try to reduce the height
+ fontsize = max(height_pixels / h * font.get_size() /1.1, font.get_size()/2.0)
+ font.set_size(fontsize)
+ layout.set_text(all_text_backup, len(all_text_backup.encode('utf-8'))) # reducing the height allows for more characters
+ layout.set_font_description(font)
+ if layout.get_line_count() > 1:
+ layout.set_text(layout.get_text(), layout.get_line(0).length)
+ w, h = layout.get_pixel_size()
+ # we check again if height is ok
+ if h > height_pixels:
+ #we could not fix it, no text
+ layout.set_text("",0)
+ layout.context_changed()
return layout.get_pixel_size()
- def draw_text(self, cr, text, radius, start, stop,
- height=PIXELS_PER_GENERATION, radial=False,
- fontcolor=(0, 0, 0), bold=False):
+ def draw_text(self, cr, text, radiusin, radiusout, start_rad, stop_rad,
+ radial=False,
+ fontcolor=(0, 0, 0), bold=False, flipped = False):
"""
- Display text at a particular radius, between start and stop
- degrees.
+ Display text at a particular radius, between start_rad and stop_rad
+ radians.
"""
- 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.save()
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()
+ self.draw_radial_text(cr, text, radiusin, radiusout, start_rad, stop_rad, font, flipped)
else:
- self.draw_arc_text(cr, text, radius, start, stop, font)
+ self.draw_arc_text(cr, text, radiusin, radiusout, start_rad, stop_rad, font, flipped)
cr.restore()
- def draw_arc_text(self, cr, text, radius, start, stop, font):
+ def draw_radial_text(self, cr, text, radiusin, radiusout, start_rad, stop_rad, font, flipped):
+ layout = self.create_pango_layout(text)
+ layout.set_font_description(font)
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR)
+
+ # compute available text space
+ # NOTE: for radial text, the sector radius height is the text width
+ avail_height = (stop_rad - start_rad) * radiusin - 2.0 * PAD_TEXT
+ avail_width = radiusout - radiusin - 2.0 * PAD_TEXT
+
+ w, h = self.wrap_truncate_layout(layout, font, avail_width, avail_height, tryrescale=True)
+
+ # 2. now draw this text
+ # offset for cairo-font system is 90
+ if flipped:
+ angle = (start_rad + stop_rad)/2 + (h / radiusin / 2) + math.pi
+ start_pos = -radiusout + PAD_TEXT
+ else:
+ angle = (start_rad + stop_rad)/2 - (h / radiusin / 2)
+ start_pos = radiusin + PAD_TEXT
+ cr.rotate(angle)
+ layout.context_changed()
+ cr.move_to(start_pos, 0)
+ PangoCairo.show_layout(cr, layout)
+
+ def draw_arc_text(self, cr, text, radiusin, radiusout, start_rad, stop_rad, font, bottom_is_outside):
"""
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.
+ Text not fitting a single line will be char-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)
+ layout.set_font_description(font)
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR)
+
+ # get height of text:
+ textheight=layout.get_size()[1]/Pango.SCALE
+ radius_text=(radiusin+radiusout)/2.0
+
+ # 1. compute available text space
+ avail_height = radiusout - radiusin - 2.0 * PAD_TEXT
+ avail_width = (stop_rad - start_rad) * radius_text - 2.0 * PAD_TEXT
+
+ w, h = self.wrap_truncate_layout(layout, font, avail_width, avail_height, tryrescale=True)
+
+ # 2. Compute text position start angle
+ mid_rad = (stop_rad + start_rad)/2
+ pos_rad = mid_rad - (w/2/radius_text)
+ end_rad = pos_rad + (w/radius_text)
# 3. Use the layout to provide us the metrics of the text box
+ cr.new_path()
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))
+ self.create_map_rect_to_sector(radius_text, pos_rad, end_rad, textheight, bottom_is_outside ))
cr.fill()
- cr.restore()
@staticmethod
- def create_map_rect_to_sector(radius, rect, arc_used_ratio, start_rad, stop_rad):
+ def create_map_rect_to_sector(radius, start_rad, stop_rad, textheight, bottom_is_outside = False):
"""
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
+ :param textheight height of the text
+ :param bottom_is_outside flag defining if we write with the bottom toward outside
: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
+ if bottom_is_outside:
+ def phi(x):
+ return -x/radius + stop_rad
+ def rho(y):
+ return radius + (y - textheight/2.0)
+ else:
+ def phi(x):
+ return x/radius + start_rad
+ def rho(y):
+ return radius - ( y - textheight/2.0)
return lambda x, y: \
(rho(y) * math.cos(phi(x)), rho(y) * math.sin(phi(x)))
- def draw_gradient(self, cr, widget, halfdist):
+ def draw_gradient_legend(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)
+ cr.translate(-self.center_xy[0], -self.center_xy[1])
font = Pango.FontDescription(self.fontdescr)
fontsize = self.fontsize
@@ -711,75 +769,55 @@ class FanChartBaseWidget(Gtk.DrawingArea):
starth = starth+gradheight
cr.restore()
- def person_under_cursor(self, curx, cury):
+ def cursor_on_tranlation_dot(self, curx, cury):
+ """
+ Determine if the cursor at position x and y is
+ on the translation dot
+ """
+ fanxy = curx - self.center_xy[0], cury - self.center_xy[1]
+ radius = math.sqrt((fanxy[0]) ** 2 + (fanxy[1]) ** 2)
+ return radius < TRANSLATE_PX
+
+ def cursor_to_polar(self, curx, cury, get_raw_rads = False):
+ # compute angle, radius in unrotated fan
+ fanxy = curx - self.center_xy[0], cury - self.center_xy[1]
+ radius = math.sqrt((fanxy[0]) ** 2 + (fanxy[1]) ** 2)
+ #angle before rotation:
+ #children are in cairo angle (clockwise) from pi to 3 pi
+ #rads however is clock 0 to 2 pi
+ raw_rads = math.atan2( fanxy[1], fanxy[0]) % (2 * math.pi)
+ rads = (raw_rads - math.radians(self.rotate_value) ) % (2 * math.pi)
+ if get_raw_rads:
+ return radius, rads, raw_rads
+ else:
+ return radius, rads
+
+ def radian_in_bounds(self, start_rad, rads, stop_rad):
+ assert(start_rad <= stop_rad)
+ # we compare (rads - start_rad) % (2.0 * math.pi) and (stop_rad - start_rad)
+ slice = stop_rad - start_rad
+ dist_rads_to_start_rads = (rads - start_rad) % (2.0 * math.pi)
+ #print start_rad, rads, stop_rad, ". (rads-start), slice :", dist_rads_to_start_rads, slice
+ return dist_rads_to_start_rads < slice
+
+ def cell_address_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)
+ raise NotImplementedError
- # 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
-
- def boxtype(self, radius):
+ def person_at(self, cell_address):
"""
- default is only one type of box type
- """
- return TYPE_BOX_NORMAL
-
- def personpos_at_angle(self, generation, angledeg, btype):
- """
- returns the person in generation generation at angle of type btype.
+ returns the person at cell_address
"""
raise NotImplementedError
- def person_at(self, generation, pos, btype):
+ def family_at(self, cell_address):
"""
- returns the person at generation, pos, btype
- """
- raise NotImplementedError
-
- def family_at(self, generation, pos, btype):
- """
- returns the family at generation, pos, btype
+ returns the family at cell_address
"""
raise NotImplementedError
@@ -799,12 +837,10 @@ class FanChartBaseWidget(Gtk.DrawingArea):
"""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:
+ cell_address = self.cell_address_under_cursor(self.mouse_x, self.mouse_y)
+ if cell_address is None:
return False
- person = self.person_at(generation, selected, btype)
- family = self.family_at(generation, selected, btype)
+ person, family = self.person_at(cell_address), self.family_at(cell_address)
if person and (Gdk.keyval_name(eventkey.keyval) == 'e'):
# we edit the person
self.edit_person_cb(None, person.handle)
@@ -818,22 +854,22 @@ class FanChartBaseWidget(Gtk.DrawingArea):
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 self.cursor_on_tranlation_dot(event.x, event.y):
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
+ cell_address = self.cell_address_under_cursor(event.x, event.y)
#click in open area, prepare for a rotate
- if selected is None:
+ if cell_address is None:
# save the mouse location for movements
self.last_x, self.last_y = event.x, event.y
return True
@@ -841,16 +877,13 @@ class FanChartBaseWidget(Gtk.DrawingArea):
#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
+ self._mouse_click_cell_address = cell_address
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)
+ person, family = self.person_at(cell_address), self.family_at(cell_address)
fhandle = None
if family:
fhandle = family.handle
@@ -864,36 +897,24 @@ class FanChartBaseWidget(Gtk.DrawingArea):
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
+ cell_address = self.cell_address_under_cursor(event.x, event.y)
+ self.mouse_x, self.mouse_y = event.x, event.y
tooltip = ""
- person = self.person_at(generation, selected, btype)
- if person:
+ if cell_address:
+ person = self.person_at(cell_address)
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
+ canonical_center = self.center_xy_from_delta([0,0])
+ self.center_delta_xy = canonical_center[0]-event.x,canonical_center[1]-event.y
+ self.center_xy = self.center_xy_from_delta()
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)
+ start_angle = math.atan2(event.y - self.center_xy[1], event.x - self.center_xy[0])
+ end_angle = math.atan2(self.last_y - self.center_xy[1], self.last_x - self.center_xy[0])
# now look at change in angle:
diff_angle = (end_angle - start_angle) % (math.pi * 2.0)
self.rotate_value -= math.degrees(diff_angle)
@@ -901,6 +922,18 @@ class FanChartBaseWidget(Gtk.DrawingArea):
self.queue_draw()
return True
+ def center_xy_from_delta(self, delta=None):
+ alloc = self.get_allocation()
+ x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
+ if delta is None: delta = self.center_delta_xy
+ if self.form == FORM_CIRCLE:
+ canvas_xy = w/2 - delta[0], h/2 - delta[1]
+ elif self.form == FORM_HALFCIRCLE:
+ canvas_xy = w/2 - delta[0], h - self.CENTER - PAD_PX - delta[1]
+ elif self.form == FORM_QUADRANT:
+ canvas_xy = self.CENTER + PAD_PX - delta[0], h - self.CENTER - PAD_PX - delta[1]
+ return canvas_xy
+
def do_mouse_click(self):
"""
action to take on left mouse click
@@ -916,15 +949,6 @@ class FanChartBaseWidget(Gtk.DrawingArea):
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()
@@ -945,14 +969,14 @@ class FanChartBaseWidget(Gtk.DrawingArea):
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)
+ person = self.person_at(self._mouse_click_cell_address)
+ if person:
+ 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)
def on_drag_data_received(self, widget, context, x, y, sel_data, info, time):
"""
@@ -960,8 +984,9 @@ class FanChartBaseWidget(Gtk.DrawingArea):
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:
+ radius, rads = self.cursor_to_polar(x, y)
+
+ if radius < self.CENTER:
if sel_data and sel_data.get_data():
(drag_type, idval, handle, val) = pickle.loads(sel_data.get_data())
self.goto(self, handle)
@@ -1002,13 +1027,13 @@ class FanChartWidget(FanChartBaseWidget):
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',
+ self.set_values(None, 9, BACKGROUND_GRAD_GEN, True, True, True, True, 'Sans', '#0000FF',
'#FF0000', None, 0.5, FORM_CIRCLE)
FanChartBaseWidget.__init__(self, dbstate, uistate, callback_popup)
def set_values(self, root_person_handle, maxgen, background, childring,
- radialtext, fontdescr, grad_start, grad_end,
- filter, alpha_filter, form):
+ flipupsidedownname, twolinename, radialtext, fontdescr,
+ grad_start, grad_end, filter, alpha_filter, form):
"""
Reset the values to be used:
@@ -1017,6 +1042,8 @@ class FanChartWidget(FanChartBaseWidget):
: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 twolinename: uses two lines for the display of person's name
+ :param flipupsidedownname: flip name on the left of the fanchart for the display of person's name
: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
@@ -1030,6 +1057,8 @@ class FanChartWidget(FanChartBaseWidget):
self.generations = maxgen
self.radialtext = radialtext
self.childring = childring
+ self.twolinename = twolinename
+ self.flipupsidedownname = flipupsidedownname
self.background = background
self.fontdescr = fontdescr
self.grad_start = grad_start
@@ -1047,21 +1076,19 @@ class FanChartWidget(FanChartBaseWidget):
self.angle[-2] = []
self.data = {}
self.childrenroot = []
+ self.rootangle_rad = [math.radians(0), math.radians(360)]
+ if self.form == FORM_HALFCIRCLE:
+ self.rootangle_rad = [math.radians(90), math.radians(270)]
+ elif self.form == FORM_QUADRANT:
+ self.rootangle_rad = [math.radians(180), math.radians(270)]
for i in range(self.generations):
- # name, person, parents?, children?
- self.data[i] = [(None,) * 5] * 2 ** i
+ # person, parents?, children?
+ self.data[i] = [(None,) * 4] * 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
+ angle = self.rootangle_rad[0]
+ slice = 1/ (2 ** i) * (self.rootangle_rad[1] - self.rootangle_rad[0])
for count in range(len(self.data[i])):
- # start, stop, male, state
+ # start, stop, state
self.angle[i].append([angle, angle + slice, NORMAL])
angle += slice
@@ -1070,16 +1097,11 @@ class FanChartWidget(FanChartBaseWidget):
if not self.rootpersonh:
return
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)
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.data[0][0] = (person, parents, child, [])
self.childrenroot = []
if child:
childlist = find_children(self.dbstate.db, person)
@@ -1088,42 +1110,23 @@ class FanChartWidget(FanChartBaseWidget):
if not child:
continue
else:
- self.childrenroot.append((child_handle, child.get_gender(),
+ self.childrenroot.append((child, True,
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
+ # person, parents, children
+ for (p, q, c, d) in self.data[current - 1]:
+ # Get father's and mother's details:
+ for person in [self._get_parent(p, True), self._get_parent(p, False)]:
+ if current == self.generations - 1:
+ parents = self._have_parents(person)
+ else:
+ parents = None
+ self.data[current][parent] = (person, parents, None, [])
+ if person is None:
+ # start,stop,male/right,state
+ self.angle[current][parent][2] = COLLAPSED
+ parent += 1
def _have_parents(self, person):
"""
@@ -1162,25 +1165,25 @@ class FanChartWidget(FanChartBaseWidget):
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]
+ (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
+ return generation
+ return 1
def halfdist(self):
"""
Compute the half radius of the circle
"""
- nrgen = self.nrgen()
- return PIXELS_PER_GENERATION * nrgen + self.CENTER + BORDER_EDGE_WIDTH
+ return PIXELS_PER_GENERATION * self.nrgen() + self.CENTER + BORDER_EDGE_WIDTH
+
+ def get_radiusinout_for_generation(self,generation):
+ outerradius=generation * PIXELS_PER_GENERATION + self.CENTER
+ innerradius=(generation-1) * PIXELS_PER_GENERATION + self.CENTER
+ if generation==0:
+ innerradius= CHILDRING_WIDTH + TRANSLATE_PX
+ return (innerradius,outerradius)
def people_generator(self):
"""
@@ -1188,7 +1191,7 @@ class FanChartWidget(FanChartBaseWidget):
"""
for generation in range(self.generations):
for p in range(len(self.data[generation])):
- (text, person, parents, child, userdata) = self.data[generation][p]
+ (person, parents, child, userdata) = self.data[generation][p]
yield (person, userdata)
def innerpeople_generator(self):
@@ -1196,9 +1199,8 @@ class FanChartWidget(FanChartBaseWidget):
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)
+ (person, parents, child, userdata) = childdata
+ yield (person, userdata)
def on_draw(self, widget, cr, scale=1.):
"""
@@ -1216,62 +1218,33 @@ class FanChartWidget(FanChartBaseWidget):
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)
+ self.center_xy = self.center_xy_from_delta()
+ cr.translate(*self.center_xy)
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]
+ (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()
+ radiusin,radiusout = self.get_radiusinout_for_generation(generation)
+ dup = False
+ self.draw_person(cr, person, radiusin, radiusout, start, stop,
+ generation, dup, userdata, thick=(state == EXPANDED),
+ has_moregen_indicator = (generation == self.generations - 1 and parents) )
cr.restore()
# Draw center person:
- (text, person, parents, child, userdata) = self.data[0][0]
+ (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
+ radiusin, radiusout = self.get_radiusinout_for_generation(0)
+ if not child: radiusin = TRANSLATE_PX
+ self.draw_person(cr, person, radiusin, radiusout, math.pi/2, math.pi/2 + 2*math.pi,
+ 0, False, userdata, thick = False, has_moregen_indicator = False, is_central_person = True)
+ #draw center disk 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)
@@ -1282,80 +1255,7 @@ class FanChartWidget(FanChartBaseWidget):
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)
-
- 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()
+ self.draw_gradient_legend(cr, widget, halfdist)
def draw_childring(self, cr):
cr.move_to(TRANSLATE_PX + CHILDRING_WIDTH, 0)
@@ -1371,9 +1271,8 @@ class FanChartWidget(FanChartBaseWidget):
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)
+ (person, parents, child, userdata) = childdata
+ self.draw_innerring(cr, person, userdata, startangle, angleinc)
startangle += angleinc
def expand_parents(self, generation, selected, current):
@@ -1433,7 +1332,8 @@ class FanChartWidget(FanChartBaseWidget):
state]
self.shrink_parents(generation + 1, selected+1, current)
- def change_slice(self, generation, selected):
+ def toggle_cell_state(self, cell_address):
+ generation, selected = cell_address
if generation < 1:
return
gstart, gstop, gstate = self.angle[generation][selected]
@@ -1476,7 +1376,45 @@ class FanChartWidget(FanChartBaseWidget):
NORMAL]
self.show_parents(generation+1, selected-1, start, slice/2.0)
- def personpos_at_angle(self, generation, angledeg, btype):
+ def cell_address_under_cursor(self, curx, cury):
+ """
+ Determine the cell address in the fan under the cursor
+ position x and y.
+ None if outside of diagram
+ """
+ radius, rads, raw_rads = self.cursor_to_polar(curx, cury, get_raw_rads=True)
+
+ # find out the generation
+ if radius < TRANSLATE_PX:
+ return None
+ 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 = None
+ for gen in range(self.generations):
+ radiusin,radiusout = self.get_radiusinout_for_generation(gen)
+ if radiusin <= radius <= radiusout:
+ generation = gen
+ break
+
+ # find what person at this angle:
+ selected = None
+ if not (generation is None) and 0 <= generation:
+ selected = self.personpos_at_angle(generation, rads)
+ elif generation == -2:
+ for p in range(len(self.angle[generation])):
+ start, stop, state = self.angle[generation][p]
+ if self.radian_in_bounds(start, raw_rads, stop):
+ selected = p
+ break
+ if (generation is None or selected is None):
+ return None
+ return generation, selected
+
+ def personpos_at_angle(self, generation, rads):
"""
returns the person in generation generation at angle.
"""
@@ -1484,30 +1422,28 @@ class FanChartWidget(FanChartBaseWidget):
return 0
selected = None
for p in range(len(self.angle[generation])):
- if self.data[generation][p][1]: # there is a person there
+ if self.data[generation][p][0]: # there is a person there
start, stop, state = self.angle[generation][p]
if state == COLLAPSED: continue
- if start <= angledeg <= stop:
+ if self.radian_in_bounds(start, rads, stop):
selected = p
break
return selected
- def person_at(self, generation, pos, btype):
+ def person_at(self, cell_address):
"""
- returns the person at generation, pos, btype
+ returns the person at cell_address
"""
- if pos is None:
- return None
+ generation, pos = cell_address
if generation == -2:
- child_handle = self.childrenroot[pos][0]
- person = self.dbstate.db.get_person_from_handle(child_handle)
+ person = self.childrenroot[pos][0]
else:
- person = self.data[generation][pos][1]
+ person = self.data[generation][pos][0]
return person
- def family_at(self, generation, pos, btype):
+ def family_at(self, cell_address):
"""
- returns the family at generation, pos, btype
+ returns the family at cell_address
Difficult here, we would need to go to child, and then obtain the first
parent family, as that is the family that is shown.
"""
@@ -1515,7 +1451,7 @@ class FanChartWidget(FanChartBaseWidget):
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.toggle_cell_state(self._mouse_click_cell_address)
self._mouse_click = False
self.queue_draw()
@@ -1547,7 +1483,7 @@ class FanChartGrampsGUI:
"""
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.childring, self.flipupsidedownname, self.twolinename, self.radialtext, self.fonttype,
self.grad_start, self.grad_end,
self.generic_filter, self.alpha_filter, self.form)
self.fan.reset()
@@ -1576,6 +1512,7 @@ class FanChartGrampsGUI:
edit_item.connect("activate", self.edit_person_cb, person_handle)
edit_item.show()
menu.append(edit_item)
+ # action related to the clicked family (when there is one)
if family_handle:
family = self.dbstate.db.get_family_from_handle(family_handle)
edit_fam_item = Gtk.MenuItem()
@@ -1647,7 +1584,8 @@ class FanChartGrampsGUI:
# Go over siblings and build their menu
item = Gtk.MenuItem(label=_("Siblings"))
pfam_list = person.get_parent_family_handle_list()
- no_siblings = 1
+ siblings = []
+ step_siblings = []
for f in pfam_list:
fam = self.dbstate.db.get_family_from_handle(f)
sib_list = fam.get_child_ref_list()
@@ -1655,32 +1593,49 @@ class FanChartGrampsGUI:
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
+ siblings.append(sib_id)
+ # Collect a list of per-step-family step-siblings
+ for parent_h in [fam.get_father_handle(), fam.get_mother_handle()]:
+ parent = self.dbstate.db.get_person_from_handle(parent_h)
+ other_families = [self.dbstate.db.get_family_from_handle(fam_id)
+ for fam_id in parent.get_family_handle_list()
+ if fam_id not in pfam_list]
+ for step_fam in other_families:
+ fam_stepsiblings = [sib_ref.ref
+ for sib_ref in step_fam.get_child_ref_list()
+ if not (sib_ref.ref == person.get_handle())]
+ if fam_stepsiblings:
+ step_siblings.append(fam_stepsiblings)
- if no_siblings:
- no_siblings = 0
- item.set_submenu(Gtk.Menu())
- sib_menu = item.get_submenu()
- sib_menu.set_reserve_toggle_size(False)
-
- if find_children(self.dbstate.db,sib):
- label = Gtk.Label(label='%s' % escape(name_displayer.display(sib)))
- else:
- label = Gtk.Label(label=escape(name_displayer.display(sib)))
-
- sib_item = Gtk.MenuItem()
- label.set_use_markup(True)
- label.show()
- label.set_halign(Gtk.Align.START)
- 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:
+ # Add siblings sub-menu with a bar between each siblings group
+ if siblings or step_siblings:
+ item.set_submenu(Gtk.Menu())
+ sib_menu = item.get_submenu()
+ sib_menu.set_reserve_toggle_size(False)
+ sibs = [siblings]+step_siblings
+ for sib_group in sibs:
+ for sib_id in sib_group:
+ sib = self.dbstate.db.get_person_from_handle(sib_id)
+ if not sib:
+ continue
+ if find_children(self.dbstate.db,sib):
+ label = Gtk.Label(label='%s' % escape(name_displayer.display(sib)))
+ else:
+ label = Gtk.Label(label=escape(name_displayer.display(sib)))
+ sib_item = Gtk.MenuItem()
+ 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 sibs.index(sib_group) < len(sibs)-1:
+ sep = Gtk.SeparatorMenuItem.new()
+ sep.show()
+ sib_menu.append(sep)
+ else:
item.set_sensitive(0)
item.show()
menu.append(item)
diff --git a/gramps/gui/widgets/fanchart2way.py b/gramps/gui/widgets/fanchart2way.py
new file mode 100644
index 000000000..954d4a206
--- /dev/null
+++ b/gramps/gui/widgets/fanchart2way.py
@@ -0,0 +1,625 @@
+#
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
+# Copyright (C) 2009 Douglas S. Blank
+# Copyright (C) 2012 Benny Malengier
+# Copyright (C) 2014 Bastien Jacquet
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+## Based on the paper:
+## http://www.cs.utah.edu/~draperg/research/fanchart/draperg_FHT08.pdf
+## and the applet:
+## http://www.cs.utah.edu/~draperg/research/fanchart/demo/
+
+## Found by redwood:
+## http://www.gramps-project.org/bugs/view.php?id=2611
+
+#-------------------------------------------------------------------------
+#
+# Python modules
+#
+#-------------------------------------------------------------------------
+from gi.repository import Pango
+from gi.repository import GObject
+from gi.repository import Gdk
+from gi.repository import Gtk
+from gi.repository import PangoCairo
+import cairo
+import math
+import colorsys
+import sys
+import pickle
+from cgi import escape
+
+#-------------------------------------------------------------------------
+#
+# GRAMPS modules
+#
+#-------------------------------------------------------------------------
+from gramps.gen.display.name import displayer as name_displayer
+from gramps.gen.errors import WindowActiveError
+from ..editors import EditPerson, EditFamily
+from ..utils import hex_to_rgb
+from ..ddtargets import DdTargets
+from gramps.gen.utils.alive import probably_alive
+from gramps.gen.utils.libformatting import FormattingHelper
+from gramps.gen.utils.db import (find_children, find_parents, find_witnessed_people,
+ get_age, get_timeperiod)
+from gramps.gen.plug.report.utils import find_spouse
+from .fanchart import *
+from .fanchartdesc import *
+
+#-------------------------------------------------------------------------
+#
+# Constants
+#
+#-------------------------------------------------------------------------
+
+PIXELS_PER_GENPERSON_RATIO = 0.55 # ratio of generation radius for person (rest for partner)
+PIXELS_PER_GEN_SMALL = 80
+PIXELS_PER_GEN_LARGE = 160
+N_GEN_SMALL = 4
+PIXELS_PER_GENFAMILY = 25 # size of radius for family
+PIXELS_PER_RECLAIM = 4 # size of the radius of pixels taken from family to reclaim space
+PIXELS_PARTNER_GAP = 0 # Padding between someone and his partner
+PIXELS_CHILDREN_GAP = 5 # Padding between generations
+PARENTRING_WIDTH = 12 # width of the parent ring inside the person
+
+ANGLE_CHEQUI = 0 # Algorithm with homogeneous children distribution
+ANGLE_WEIGHT = 1 # Algorithm for angle computation based on nr of descendants
+
+TYPE_ASCENDANCE = 0
+TYPE_DESCENDANCE = 1
+
+#-------------------------------------------------------------------------
+#
+# FanChart2WayWidget
+#
+#-------------------------------------------------------------------------
+
+class FanChart2WayWidget(FanChartWidget, FanChartDescWidget):
+ """
+ Interactive Fan Chart Widget.
+ """
+ CENTER = 50 # we require a larger center
+
+ def __init__(self, dbstate, uistate, callback_popup=None):
+ """
+ Fan Chart Widget. Handles visualization of data in self.data.
+ See main() of FanChartGramplet for example of model format.
+ """
+ self.set_values(None, 6, 5, True, True, BACKGROUND_GRAD_GEN, True, 'Sans', '#0000FF',
+ '#FF0000', None, 0.5, ANGLE_WEIGHT, '#888a85')
+ FanChartBaseWidget.__init__(self, dbstate, uistate, callback_popup)
+
+ def reset(self):
+ """
+ Reset the fan chart. This should trigger computation of all data
+ structures needed
+ """
+ self.cache_fontcolor = {}
+
+ # fill the data structure
+ self._fill_data_structures()
+
+ # prepare the colors for the boxes
+ self.prepare_background_box(self.generations_asc + self.generations_desc - 1)
+
+ def set_values(self, root_person_handle, maxgen_asc, maxgen_desc, flipupsidedownname, twolinename, background,
+ background_gradient, fontdescr, grad_start, grad_end,
+ filter, alpha_filter, angle_algo, dupcolor):
+ """
+ Reset the values to be used:
+
+ :param root_person_handle: person to show
+ :param maxgen_asc: maximum of ascendant generations to show
+ :param maxgen_desc: maximum of descendant generations to show
+ :param flipupsidedownname: flip name on the left of the fanchart for the display of person's name
+ :param background: config setting of which background procedure to use
+ :type background: int
+ :param background_gradient: option to add an overall gradient for distinguishing Asc/Desc
+ :param fontdescr: string describing the font to use
+ :param grad_start: colors to use for background procedure
+ :param grad_end: colors to use for background procedure
+ :param filter: the person filter to apply to the people in the chart
+ :param alpha_filter: the alpha transparency value (0-1) to apply to
+ filtered out data
+ :param angle_algo: alorithm to use to calculate the sizes of the boxes
+ :param dupcolor: color to use for people or families that occur a second
+ or more time
+ """
+ self.rootpersonh = root_person_handle
+ self.generations_asc = maxgen_asc
+ self.generations_desc = maxgen_desc
+ self.background = background
+ self.background_gradient = background_gradient
+ self.fontdescr = fontdescr
+ self.grad_start = grad_start
+ self.grad_end = grad_end
+ self.filter = filter
+ self.form = FORM_CIRCLE
+ self.alpha_filter = alpha_filter
+ self.anglealgo = angle_algo
+ self.dupcolor = hex_to_rgb(dupcolor)
+ self.childring = False
+ self.flipupsidedownname = flipupsidedownname
+ self.twolinename = twolinename
+
+ def set_generations(self):
+ """
+ Set the generations to max, and fill data structures with initial data.
+ """
+ self.rootangle_rad_desc = [math.radians(275), math.radians(275 + 170)]
+ self.rootangle_rad_asc = [math.radians(90), math.radians(270)]
+
+ self.handle2desc = {}
+ self.famhandle2desc = {}
+ self.handle2fam = {}
+ self.gen2people = {}
+ self.gen2fam = {}
+ self.gen2people[0] = [(None, False, 0, 2 * math.pi, 0, 0, [], NORMAL)] # no center person
+ self.gen2fam[0] = [] # no families
+ for i in range(1, self.generations_desc):
+ self.gen2fam[i] = []
+ self.gen2people[i] = []
+ self.gen2people[self.generations_desc] = [] # indication of more children
+
+ # Ascendance part
+ self.angle = {}
+ self.data = {}
+ for i in range(self.generations_asc):
+ # name, person, parents?, children?
+ self.data[i] = [(None,) * 4] * 2 ** i
+ self.angle[i] = []
+ angle = self.rootangle_rad_asc[0]
+ slice = 1 / (2 ** i) * (self.rootangle_rad_asc[1] - self.rootangle_rad_asc[0])
+ for count in range(len(self.data[i])):
+ # start, stop, state
+ self.angle[i].append([angle, angle + slice, NORMAL])
+ angle += slice
+
+ def _fill_data_structures(self):
+ self.set_generations()
+ if not self.rootpersonh:
+ return
+ person = self.dbstate.db.get_person_from_handle(self.rootpersonh)
+ if not person:
+ # nothing to do, just return
+ return
+
+ # Descendance part
+ # person, duplicate or not, start angle, slice size,
+ # text, parent pos in fam, nrfam, userdata, status
+ self.gen2people[0] = [[person, False, 0, 2 * math.pi, 0, 0, [], NORMAL]]
+ self.handle2desc[self.rootpersonh] = 0
+ # recursively fill in the datastructures:
+ nrdesc = self._rec_fill_data(0, person, 0, self.generations_desc)
+ self.handle2desc[person.handle] += nrdesc
+ self._compute_angles(*self.rootangle_rad_desc)
+
+ # Ascendance part
+ parents = self._have_parents(person)
+ child = self._have_children(person)
+ # Ascendance data structure is the person object, parents, child and
+ # list for userdata which we might fill in later.
+ self.data[0][0] = (person, parents, child, [])
+ for current in range(1, self.generations_asc):
+ parent = 0
+ # name, person, parents, children
+ for (p, q, c, d) in self.data[current - 1]:
+ # Get father's and mother's details:
+ for person in [self._get_parent(p, True), self._get_parent(p, False)]:
+ if current == self.generations_asc - 1:
+ parents = self._have_parents(person)
+ else:
+ parents = None
+ self.data[current][parent] = (person, parents, None, [])
+ if person is None:
+ # start,stop,male/right,state
+ self.angle[current][parent][2] = COLLAPSED
+ parent += 1
+
+ def nrgen_desc(self):
+ # compute the number of generations present
+ for gen in range(self.generations_desc - 1, 0, -1):
+ if len(self.gen2people[gen]) > 0:
+ return gen + 1
+ return 1
+
+ def nrgen_asc(self):
+ # compute the number of generations present
+ for generation in range(self.generations_asc - 1, 0, -1):
+ for p in range(len(self.data[generation])):
+ (person, parents, child, userdata) = self.data[generation][p]
+ if person:
+ return generation
+ return 1
+
+ def maxradius_asc(self, generation):
+ """
+ Compute the current half radius of the ascendant circle
+ """
+ radiusin, radius_asc = self.get_radiusinout_for_generation_asc(generation)
+ return radius_asc + BORDER_EDGE_WIDTH
+
+ def maxradius_desc(self,generation):
+ """
+ Compute the current radius of the descendant circle
+ """
+ radiusin_pers, radiusout_pers, radiusin_partner, radius_desc = self.get_radiusinout_for_generation_pair(generation-1)
+ return radius_desc + BORDER_EDGE_WIDTH
+
+ def halfdist(self):
+ """
+ Compute the current max half radius of the circle
+ """
+ return max(self.maxradius_desc(self.nrgen_desc()), self.maxradius_asc(self.nrgen_asc()))
+
+ def get_radiusinout_for_generation_desc(self, generation):
+ """
+ Get the in and out radius for descendant generation (starting with center pers = 0)
+ """
+ radius_first_gen = self.CENTER - (1 - PIXELS_PER_GENPERSON_RATIO) * PIXELS_PER_GEN_SMALL
+ if generation < N_GEN_SMALL:
+ radius_start = PIXELS_PER_GEN_SMALL * generation + radius_first_gen
+ return (radius_start, radius_start + PIXELS_PER_GEN_SMALL)
+ else:
+ radius_start = PIXELS_PER_GEN_SMALL * N_GEN_SMALL + PIXELS_PER_GEN_LARGE \
+ * (generation - N_GEN_SMALL) + radius_first_gen
+ return (radius_start, radius_start + PIXELS_PER_GEN_LARGE)
+
+ def get_radiusinout_for_generation_asc(self, generation):
+ """
+ Get the in and out radius for ascendant generation (starting with center pers = 0)
+ """
+ radiusin, radius_first_gen = self.get_radiusinout_for_generation_desc(0)
+ outerradius = generation * PIXELS_PER_GENERATION + radius_first_gen
+ innerradius = (generation - 1) * PIXELS_PER_GENERATION + radius_first_gen
+ if generation == 0:
+ innerradius = CHILDRING_WIDTH + TRANSLATE_PX
+ return (innerradius, outerradius)
+
+ def get_radiusinout_for_generation_pair(self, generation):
+ """
+ Get the in and out radius for descendant generation pair (starting with center pers = 0)
+ :return: (radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner)
+ """
+ radiusin, radiusout = self.get_radiusinout_for_generation_desc(generation)
+ radius_spread = radiusout - radiusin - PIXELS_CHILDREN_GAP - PIXELS_PARTNER_GAP
+
+ radiusin_pers = radiusin + PIXELS_CHILDREN_GAP
+ radiusout_pers = radiusin_pers + PIXELS_PER_GENPERSON_RATIO * radius_spread
+ radiusin_partner = radiusout_pers + PIXELS_PARTNER_GAP
+ radiusout_partner = radiusout
+ return (radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner)
+
+ def people_generator(self):
+ """
+ a generator over all people outside of the core person
+ """
+ for generation in range(self.generations_desc):
+ for data in self.gen2people[generation]:
+ yield (data[0], data[6])
+ for generation in range(self.generations_desc):
+ for data in self.gen2fam[generation]:
+ yield (data[7], data[6])
+ for generation in range(self.generations_asc):
+ for p in range(len(self.data[generation])):
+ (person, parents, child, userdata) = self.data[generation][p]
+ yield (person, userdata)
+
+ def innerpeople_generator(self):
+ """
+ a generator over all people inside of the core person
+ """
+ if False:
+ yield
+
+ def draw_background(self, cr):
+ cr.save()
+
+ cr.rotate(math.radians(self.rotate_value))
+ delta = (self.rootangle_rad_asc[0] - self.rootangle_rad_desc[1]) / 2.0 % math.pi
+
+ cr.move_to(0, 0)
+ radius_gradient_asc = 1.5 * self.maxradius_asc(self.generations_asc)
+ gradient_asc = cairo.RadialGradient(0, 0, self.CENTER, 0, 0, radius_gradient_asc)
+ color = hex_to_rgb(self.grad_end)
+ gradient_asc.add_color_stop_rgba(0.0, color[0]/255, color[1]/255, color[2]/255, 0.5)
+ gradient_asc.add_color_stop_rgba(1.0, 1, 1, 1, 0.0)
+ start_rad, stop_rad = self.rootangle_rad_asc[0] - delta, self.rootangle_rad_asc[1] + delta
+ cr.set_source(gradient_asc)
+ cr.arc(0, 0, radius_gradient_asc, start_rad, stop_rad)
+ cr.fill()
+
+ cr.move_to(0, 0)
+ radius_gradient_desc = 1.5 * self.maxradius_desc(self.generations_desc)
+ gradient_desc = cairo.RadialGradient(0, 0, self.CENTER, 0, 0, radius_gradient_desc)
+ color = hex_to_rgb(self.grad_start)
+ gradient_desc.add_color_stop_rgba(0.0, color[0]/255, color[1]/255, color[2]/255, 0.5)
+ gradient_desc.add_color_stop_rgba(1.0, 1, 1, 1, 0.0)
+ start_rad, stop_rad = self.rootangle_rad_desc[0] - delta, self.rootangle_rad_desc[1] + delta
+ cr.set_source(gradient_desc)
+ cr.arc(0, 0, radius_gradient_desc, start_rad, stop_rad)
+ cr.fill()
+ cr.restore()
+
+
+ def on_draw(self, widget, cr, scale=1.):
+ """
+ The main method to do the drawing.
+ If widget is given, we assume we draw in GTK3 and use the allocation.
+ To draw raw on the cairo context cr, set widget=None.
+ """
+ # first do size request of what we will need
+ halfdist = self.halfdist()
+ if widget:
+ self.set_size_request(2 * halfdist, 2 * halfdist)
+
+ cr.scale(scale, scale)
+ if widget:
+ self.center_xy = self.center_xy_from_delta()
+ cr.translate(*self.center_xy)
+
+ cr.save()
+ # Draw background
+ if self.background_gradient:
+ self.draw_background(cr)
+ # Draw center person:
+ (person, dup, start, slice, parentfampos, nrfam, userdata, status) \
+ = self.gen2people[0][0]
+ if not person:
+ return
+ gen_remapped = self.generations_desc - 1 # remapped generation
+ if gen_remapped == 0: gen_remapped = (self.generations_desc + self.generations_asc - 1) # remapped generation
+ radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner = \
+ self.get_radiusinout_for_generation_pair(0)
+ radiusin = TRANSLATE_PX
+ radiusout = radiusout_pers
+ self.draw_person(cr, person, radiusin, radiusout, math.pi / 2, math.pi / 2 + 2 * math.pi,
+ gen_remapped, False, userdata, is_central_person=True)
+ # draw center to move chart
+ cr.set_source_rgb(0, 0, 0) # black
+ cr.move_to(TRANSLATE_PX, 0)
+ cr.arc(0, 0, TRANSLATE_PX, 0, 2 * math.pi)
+ cr.fill()
+
+ cr.rotate(math.radians(self.rotate_value))
+ # Ascendance
+ for generation in range(self.generations_asc - 1, 0, -1):
+ for p in range(len(self.data[generation])):
+ (person, parents, child, userdata) = self.data[generation][p]
+ if person:
+ start, stop, state = self.angle[generation][p]
+ if state in [NORMAL, EXPANDED]:
+ radiusin, radiusout = self.get_radiusinout_for_generation_asc(generation)
+ dup = False
+ gen_remapped = generation + self.generations_desc - 1 # remapped generation
+ self.draw_person(cr, person, radiusin, radiusout, start, stop,
+ gen_remapped, dup, userdata, thick=(state == EXPANDED),
+ has_moregen_indicator=(generation == self.generations_asc - 1 and parents))
+
+ # Descendance
+ for gen in range(self.generations_desc):
+ radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner = \
+ self.get_radiusinout_for_generation_pair(gen)
+ gen_remapped = (self.generations_desc - gen - 1)
+ if gen_remapped == 0: gen_remapped = (self.generations_desc + self.generations_asc - 1) # remapped generation
+ if gen > 0:
+ for pdata in self.gen2people[gen]:
+ # person, duplicate or not, start angle, slice size,
+ # parent pos in fam, nrfam, userdata, status
+ pers, dup, start, slice, pospar, nrfam, userdata, status = pdata
+ if status != COLLAPSED:
+ self.draw_person(cr, pers, radiusin_pers, radiusout_pers,
+ start, start + slice, gen_remapped, dup, userdata,
+ thick=status != NORMAL)
+ #if gen < self.generations_desc - 1:
+ for famdata in self.gen2fam[gen]:
+ # family, duplicate or not, start angle, slice size,
+ # spouse pos in gen, nrchildren, userdata, status
+ fam, dup, start, slice, posfam, nrchild, userdata, partner, status = famdata
+ if status != COLLAPSED:
+ more_pers_flag = (gen == self.generations_desc - 1
+ and len(fam.get_child_ref_list()) > 0)
+ self.draw_person(cr, partner, radiusin_partner, radiusout_partner, start, start + slice,
+ gen_remapped, dup, userdata, thick=(status != NORMAL), has_moregen_indicator=more_pers_flag)
+ cr.restore()
+
+ if self.background in [BACKGROUND_GRAD_AGE, BACKGROUND_GRAD_PERIOD]:
+ self.draw_gradient_legend(cr, widget, halfdist)
+
+ def cell_address_under_cursor(self, curx, cury):
+ """
+ Determine the cell address in the fan under the cursor
+ position x and y.
+ None if outside of diagram
+ """
+ radius, rads, raw_rads = self.cursor_to_polar(curx, cury, get_raw_rads=True)
+
+ if radius < TRANSLATE_PX:
+ return None
+ radius_parents = self.get_radiusinout_for_generation_asc(0)[1]
+ if (radius < radius_parents) or \
+ (self.radian_in_bounds(self.rootangle_rad_desc[0], rads, self.rootangle_rad_desc[1])):
+ cell_address = self.cell_address_under_cursor_desc(rads, radius)
+ if cell_address is not None:
+ return (TYPE_DESCENDANCE,) + cell_address
+ elif self.radian_in_bounds(self.rootangle_rad_asc[0], rads, self.rootangle_rad_asc[1]):
+ cell_address = self.cell_address_under_cursor_asc(rads, radius)
+ if cell_address and cell_address[0]==0: return None # There is a gap before first parents
+ if cell_address is not None:
+ return (TYPE_ASCENDANCE,) + cell_address
+
+ return None
+
+ def cell_address_under_cursor_desc(self, rads, radius):
+ """
+ Determine the cell address in the fan under the cursor
+ position x and y.
+ None if outside of diagram
+ """
+ generation, selected, btype = None, None, TYPE_BOX_NORMAL
+ for gen in range(self.generations_desc):
+ radiusin_pers, radiusout_pers, radiusin_partner, radiusout_partner \
+ = self.get_radiusinout_for_generation_pair(gen)
+ if radiusin_pers <= radius <= radiusout_pers:
+ generation, btype = gen, TYPE_BOX_NORMAL
+ break
+ if radiusin_partner <= radius <= radiusout_partner:
+ generation, btype = gen, TYPE_BOX_FAMILY
+ break
+ # find what person is in this position:
+ if not (generation is None) and 0 <= generation:
+ selected = FanChartDescWidget.personpos_at_angle(self, generation, rads, btype)
+
+ if (generation is None or selected is None):
+ return None
+
+ return generation, selected, btype
+
+ def cell_address_under_cursor_asc(self, rads, radius):
+ """
+ Determine the cell address in the fan under the cursor
+ position x and y.
+ None if outside of diagram
+ """
+
+ generation, selected = None, None
+ for gen in range(self.generations_asc):
+ radiusin, radiusout = self.get_radiusinout_for_generation_asc(gen)
+ if radiusin <= radius <= radiusout:
+ generation = gen
+ break
+
+ # find what person is in this position:
+ if not (generation is None) and 0 <= generation:
+ selected = FanChartWidget.personpos_at_angle(self, generation, rads)
+ if (generation is None or selected is None):
+ return None
+ return generation, selected
+
+ def person_at(self, cell_address):
+ """
+ returns the person at radius_first_gen
+ """
+ direction = cell_address[0]
+ if direction == TYPE_ASCENDANCE:
+ return FanChartWidget.person_at(self, cell_address[1:])
+ elif direction == TYPE_DESCENDANCE:
+ return FanChartDescWidget.person_at(self, cell_address[1:])
+ return None
+
+ def family_at(self, cell_address):
+ """
+ returns the family at cell_address
+ """
+ direction = cell_address[0]
+ if direction == TYPE_ASCENDANCE:
+ return None
+ elif direction == TYPE_DESCENDANCE:
+ return FanChartDescWidget.family_at(self, cell_address[1:])
+ return None
+
+ def do_mouse_click(self):
+ # no drag occured, expand or collapse the section
+ self.toggle_cell_state(self._mouse_click_cell_address)
+ self._mouse_click = False
+ self.queue_draw()
+
+ def expand_parents(self, generation, selected, current):
+ if generation >= self.generations_asc: return
+ selected = 2 * selected
+ start, stop, state = self.angle[generation][selected]
+ if state in [NORMAL, EXPANDED]:
+ slice = (stop - start) * 2.0
+ self.angle[generation][selected] = [current, current + slice, state]
+ self.expand_parents(generation + 1, selected, current)
+ current += slice
+ start, stop, state = self.angle[generation][selected + 1]
+ if state in [NORMAL, EXPANDED]:
+ slice = (stop - start) * 2.0
+ self.angle[generation][selected + 1] = [current, current + slice,
+ state]
+ self.expand_parents(generation + 1, selected + 1, current)
+
+ def show_parents(self, generation, selected, angle, slice):
+ if generation >= self.generations_asc: return
+ selected *= 2
+ self.angle[generation][selected][0] = angle
+ self.angle[generation][selected][1] = angle + slice
+ self.angle[generation][selected][2] = NORMAL
+ self.show_parents(generation + 1, selected, angle, slice / 2.0)
+ self.angle[generation][selected + 1][0] = angle + slice
+ self.angle[generation][selected + 1][1] = angle + slice + slice
+ self.angle[generation][selected + 1][2] = NORMAL
+ self.show_parents(generation + 1, selected + 1, angle + slice, slice / 2.0)
+
+ def hide_parents(self, generation, selected, angle):
+ if generation >= self.generations_asc: return
+ selected = 2 * selected
+ self.angle[generation][selected][0] = angle
+ self.angle[generation][selected][1] = angle
+ self.angle[generation][selected][2] = COLLAPSED
+ self.hide_parents(generation + 1, selected, angle)
+ self.angle[generation][selected + 1][0] = angle
+ self.angle[generation][selected + 1][1] = angle
+ self.angle[generation][selected + 1][2] = COLLAPSED
+ self.hide_parents(generation + 1, selected + 1, angle)
+
+ def shrink_parents(self, generation, selected, current):
+ if generation >= self.generations_asc: return
+ selected = 2 * selected
+ start, stop, state = self.angle[generation][selected]
+ if state in [NORMAL, EXPANDED]:
+ slice = (stop - start) / 2.0
+ self.angle[generation][selected] = [current, current + slice,
+ state]
+ self.shrink_parents(generation + 1, selected, current)
+ current += slice
+ start, stop, state = self.angle[generation][selected + 1]
+ if state in [NORMAL, EXPANDED]:
+ slice = (stop - start) / 2.0
+ self.angle[generation][selected + 1] = [current, current + slice,
+ state]
+ self.shrink_parents(generation + 1, selected + 1, current)
+
+ def toggle_cell_state(self, cell_address):
+ direction = cell_address[0]
+ if direction == TYPE_ASCENDANCE:
+ FanChartWidget.toggle_cell_state(self, cell_address[1:])
+ elif direction == TYPE_DESCENDANCE:
+ FanChartDescWidget.toggle_cell_state(self, cell_address[1:])
+ self._compute_angles(*self.rootangle_rad_desc)
+
+class FanChart2WayGrampsGUI(FanChartGrampsGUI):
+ """ class for functions fanchart GUI elements will need in Gramps
+ """
+
+ def main(self):
+ """
+ Fill the data structures with the active data. This initializes all
+ data.
+ """
+ root_person_handle = self.get_active('Person')
+ self.fan.set_values(root_person_handle, self.generations_asc, self.generations_desc, self.flipupsidedownname, self.twolinename, self.background,
+ self.background_gradient, self.fonttype, self.grad_start, self.grad_end,
+ self.generic_filter, self.alpha_filter,
+ self.angle_algo, self.dupcolor)
+ self.fan.reset()
+ self.fan.queue_draw()
diff --git a/gramps/gui/widgets/fanchartdesc.py b/gramps/gui/widgets/fanchartdesc.py
index bf8d14cba..0c250f3ed 100644
--- a/gramps/gui/widgets/fanchartdesc.py
+++ b/gramps/gui/widgets/fanchartdesc.py
@@ -68,14 +68,21 @@ from .fanchart import *
#-------------------------------------------------------------------------
pi = math.pi
-PIXELS_PER_GENPERSON = 30 # size of radius for generation of children
-PIXELS_PER_GENFAMILY = 20 # size of radius for family
+PIXELS_PER_GENPERSON_RATIO = 0.55 # ratio of generation radius for person (rest for partner)
+PIXELS_PER_GEN_SMALL = 80
+PIXELS_PER_GEN_LARGE = 160
+N_GEN_SMALL = 4
+PIXELS_PER_GENFAMILY = 25 # size of radius for family
PIXELS_PER_RECLAIM = 4 # size of the radius of pixels taken from family to reclaim space
+PIXELS_PARTNER_GAP = 0 # Padding between someone and his partner
+PIXELS_CHILDREN_GAP = 5 # Padding between generations
PARENTRING_WIDTH = 12 # width of the parent ring inside the person
ANGLE_CHEQUI = 0 #Algorithm with homogeneous children distribution
ANGLE_WEIGHT = 1 #Algorithm for angle computation based on nr of descendants
+TYPE_BOX_NORMAL = 0
+TYPE_BOX_FAMILY = 1
#-------------------------------------------------------------------------
#
@@ -87,18 +94,18 @@ class FanChartDescWidget(FanChartBaseWidget):
"""
Interactive Fan Chart Widget.
"""
- CENTER = 60 # we require a larger center
+ CENTER = 50 # we require a larger center as CENTER includes the 1st partner
def __init__(self, dbstate, uistate, callback_popup=None):
"""
Fan Chart Widget. Handles visualization of data in self.data.
See main() of FanChartGramplet for example of model format.
"""
- self.set_values(None, 9, BACKGROUND_GRAD_GEN, 'Sans', '#0000FF',
+ self.set_values(None, 9, True, True, BACKGROUND_GRAD_GEN, 'Sans', '#0000FF',
'#FF0000', None, 0.5, FORM_CIRCLE, ANGLE_WEIGHT, '#888a85')
FanChartBaseWidget.__init__(self, dbstate, uistate, callback_popup)
- def set_values(self, root_person_handle, maxgen, background,
+ def set_values(self, root_person_handle, maxgen, flipupsidedownname, twolinename, background,
fontdescr, grad_start, grad_end,
filter, alpha_filter, form, angle_algo, dupcolor):
"""
@@ -106,6 +113,7 @@ class FanChartDescWidget(FanChartBaseWidget):
:param root_person_handle: person to show
:param maxgen: maximum generations to show
+ :param flipupsidedownname: flip name on the left of the fanchart for the display of person's name
:param background: config setting of which background procedure to use
:type background: int
:param fontdescr: string describing the font to use
@@ -131,24 +139,28 @@ class FanChartDescWidget(FanChartBaseWidget):
self.anglealgo = angle_algo
self.dupcolor = hex_to_rgb(dupcolor)
self.childring = False
-
- def gen_pixels(self):
- """
- how many pixels a generation takes up in the fanchart
- """
- return PIXELS_PER_GENPERSON + PIXELS_PER_GENFAMILY
+ self.flipupsidedownname = flipupsidedownname
+ self.twolinename = twolinename
def set_generations(self):
"""
Set the generations to max, and fill data structures with initial data.
"""
+
+ if self.form == FORM_CIRCLE:
+ self.rootangle_rad = [math.radians(0), math.radians(360)]
+ elif self.form == FORM_HALFCIRCLE:
+ self.rootangle_rad = [math.radians(90), math.radians(90 + 180)]
+ elif self.form == FORM_QUADRANT:
+ self.rootangle_rad = [math.radians(90), math.radians(90 + 90)]
+
self.handle2desc = {}
self.famhandle2desc = {}
self.handle2fam = {}
self.gen2people = {}
self.gen2fam = {}
- self.parentsroot = []
- self.gen2people[0] = [(None, False, 0, 2*pi, '', 0, 0, [], NORMAL)] #no center person
+ self.innerring = []
+ self.gen2people[0] = [(None, False, 0, 2*pi, 0, 0, [], NORMAL)] #no center person
self.gen2fam[0] = [] #no families
self.angle = {}
self.angle[-2] = []
@@ -156,14 +168,6 @@ class FanChartDescWidget(FanChartBaseWidget):
self.gen2fam[i] = []
self.gen2people[i] = []
self.gen2people[self.generations] = [] #indication of more children
- self.rotfactor = 1
- self.rotstartangle = 0
- if self.form == FORM_HALFCIRCLE:
- self.rotfactor = 1/2
- self.rotangle = 90
- elif self.form == FORM_QUADRANT:
- self.rotangle = 180
- self.rotfactor = 1/4
def _fill_data_structures(self):
self.set_generations()
@@ -173,15 +177,13 @@ class FanChartDescWidget(FanChartBaseWidget):
if not person:
#nothing to do, just return
return
- else:
- name = name_displayer.display(person)
# person, duplicate or not, start angle, slice size,
# text, parent pos in fam, nrfam, userdata, status
- self.gen2people[0] = [[person, False, 0, 2*pi, name, 0, 0, [], NORMAL]]
+ self.gen2people[0] = [[person, False, 0, 2*pi, 0, 0, [], NORMAL]]
self.handle2desc[self.rootpersonh] = 0
# fill in data for the parents
- self.parentsroot = []
+ self.innerring = []
handleparents = []
family_handle_list = person.get_parent_family_handle_list()
if family_handle_list:
@@ -189,173 +191,145 @@ class FanChartDescWidget(FanChartBaseWidget):
family = self.dbstate.db.get_family_from_handle(family_handle)
if not family:
continue
- hfather = family.get_father_handle()
- if hfather and hfather not in handleparents:
- father = self.dbstate.db.get_person_from_handle(hfather)
- if father:
- self.parentsroot.append((father, []))
- handleparents.append(hfather)
- hmother = family.get_mother_handle()
- if hmother and hmother not in handleparents:
- mother = self.dbstate.db.get_person_from_handle(hmother)
- if mother:
- self.parentsroot.append((mother, []))
- handleparents.append(hmother)
+ for hparent in [family.get_father_handle(), family.get_mother_handle()]:
+ if hparent and hparent not in handleparents:
+ parent = self.dbstate.db.get_person_from_handle(hparent)
+ if parent:
+ self.innerring.append((parent, []))
+ handleparents.append(hparent)
#recursively fill in the datastructures:
- nrdesc = self.__rec_fill_data(0, person, 0)
+ nrdesc = self._rec_fill_data(0, person, 0, self.generations)
self.handle2desc[person.handle] += nrdesc
- self.__compute_angles()
+ self._compute_angles(*self.rootangle_rad)
- def __rec_fill_data(self, gen, person, pos):
+ def _rec_fill_data(self, gen, person, pos, maxgen):
"""
Recursively fill in the data
"""
totdesc = 0
- nrfam = len(person.get_family_handle_list())
- self.gen2people[gen][pos][6] = nrfam
- for family_handle in person.get_family_handle_list():
+ marriage_handle_list = person.get_family_handle_list()
+ self.gen2people[gen][pos][5] = len(marriage_handle_list)
+ for family_handle in marriage_handle_list:
totdescfam = 0
family = self.dbstate.db.get_family_from_handle(family_handle)
spouse_handle = find_spouse(person, family)
if spouse_handle:
spouse = self.dbstate.db.get_person_from_handle(spouse_handle)
- spname = name_displayer.display(spouse)
else:
- spname = ''
spouse = None
- if family_handle in self.famhandle2desc:
- #family occurs via father and via mother in the chart, only
- #first to show and count.
- famdup = True
- else:
- famdup = False
+ # family may occur via father and via mother in the chart, only
+ # first to show and count.
+ fam_duplicate = family_handle in self.famhandle2desc
# family, duplicate or not, start angle, slice size,
- # text, spouse pos in gen, nrchildren, userdata, parnter, status
- self.gen2fam[gen].append([family, famdup, 0, 0, spname, pos, 0, [],
- spouse, NORMAL])
+ # spouse pos in gen, nrchildren, userdata, parnter, status
+ self.gen2fam[gen].append([family, fam_duplicate, 0, 0, pos, 0, [], spouse, NORMAL])
posfam = len(self.gen2fam[gen]) - 1
- if not famdup:
+ if not fam_duplicate and gen < maxgen-1:
nrchild = len(family.get_child_ref_list())
- self.gen2fam[gen][-1][6] = nrchild
+ self.gen2fam[gen][posfam][5] = nrchild
for child_ref in family.get_child_ref_list():
child = self.dbstate.db.get_person_from_handle(child_ref.ref)
- chname = name_displayer.display(child)
- if child_ref.ref in self.handle2desc:
- dup = True
- else:
- dup = False
- self.handle2desc[child_ref.ref] = 0
+ child_dup = child_ref.ref in self.handle2desc
+ if not child_dup:
+ self.handle2desc[child_ref.ref] = 0 # mark this child as processed
# person, duplicate or not, start angle, slice size,
- # text, parent pos in fam, nrfam, userdata, status
- self.gen2people[gen+1].append([child, dup, 0, 0, chname,
- posfam, 0, [], NORMAL])
+ # parent pos in fam, nrfam, userdata, status
+ self.gen2people[gen+1].append([child, child_dup, 0, 0, posfam, 0, [], NORMAL])
totdescfam += 1 #add this person as descendant
pospers = len(self.gen2people[gen+1]) - 1
- if not dup and not(self.generations == gen+2):
- nrdesc = self.__rec_fill_data(gen+1, child, pospers)
+ if not child_dup:
+ nrdesc = self._rec_fill_data(gen+1, child, pospers, maxgen)
self.handle2desc[child_ref.ref] += nrdesc
totdescfam += nrdesc # add children of him as descendants
+ if not fam_duplicate:
self.famhandle2desc[family_handle] = totdescfam
totdesc += totdescfam
return totdesc
- def __compute_angles(self):
+ def _compute_angles(self, start_rad, stop_rad):
"""
Compute the angles of the boxes
"""
#first we compute the size of the slice.
- nrgen = self.nrgen()
#set angles root person
- if self.form == FORM_CIRCLE:
- slice = 2*pi
- start = 0.
- elif self.form == FORM_HALFCIRCLE:
- slice = pi
- start = pi/2
- elif self.form == FORM_QUADRANT:
- slice = pi/2
- start = pi
+ start, slice = start_rad, stop_rad - start_rad
+ nr_gen = len(self.gen2people)-1
+ # Fill in central person angles
gen = 0
data = self.gen2people[gen][0]
data[2] = start
data[3] = slice
- for gen in range(1, nrgen):
- nrpeople = len(self.gen2people[gen])
+ for gen in range(0, nr_gen):
prevpartnerdatahandle = None
offset = 0
- for data in self.gen2fam[gen-1]:
- #obtain start and stop of partner
- partnerdata = self.gen2people[gen-1][data[5]]
- dupfam = data[1]
+ for data_fam in self.gen2fam[gen]: # for each partner/fam of gen-1
+ #obtain start and stop from the people of this partner
+ persondata = self.gen2people[gen][data_fam[4]]
+ dupfam = data_fam[1]
if dupfam:
- # we don't show the descendants here, but in the first
- # occurrence of the family
- nrdescfam = 0
- nrdescpartner = self.handle2desc[partnerdata[0].handle]
- nrfam = partnerdata[6]
+ # we don't show again the descendants here
nrdescfam = 0
else:
- nrdescfam = self.famhandle2desc[data[0].handle]
- nrdescpartner = self.handle2desc[partnerdata[0].handle]
- nrfam = partnerdata[6]
- partstart = partnerdata[2]
- partslice = partnerdata[3]
- if prevpartnerdatahandle != partnerdata[0].handle:
- #reset the offset
+ nrdescfam = self.famhandle2desc[data_fam[0].handle]
+ nrdescperson = self.handle2desc[persondata[0].handle]
+ nrfam = persondata[5]
+ personstart, personslice = persondata[2:4]
+ if prevpartnerdatahandle != persondata[0].handle:
+ #partner of a new person: reset the offset
offset = 0
- prevpartnerdatahandle = partnerdata[0].handle
- slice = partslice/(nrdescpartner+nrfam)*(nrdescfam+1)
- if data[9] == COLLAPSED:
+ prevpartnerdatahandle = persondata[0].handle
+ slice = personslice/(nrdescperson+nrfam)*(nrdescfam+1)
+ if data_fam[8] == COLLAPSED:
slice = 0
- elif data[9] == EXPANDED:
- slice = partslice
+ elif data_fam[8] == EXPANDED:
+ slice = personslice
- data[2] = partstart + offset
- data[3] = slice
+ data_fam[2] = personstart + offset
+ data_fam[3] = slice
offset += slice
-## if nrdescpartner == 0:
+## if nrdescperson == 0:
## #no offspring, draw as large as fraction of
## #nr families
-## nrfam = partnerdata[6]
-## slice = partslice/nrfam
-## data[2] = partstart + offset
-## data[3] = slice
+## nrfam = persondata[6]
+## slice = personslice/nrfam
+## data_fam[2] = personstart + offset
+## data_fam[3] = slice
## offset += slice
## elif nrdescfam == 0:
## #no offspring this family, but there is another
## #family. We draw this as a weight of 1
-## nrfam = partnerdata[6]
-## slice = partslice/(nrdescpartner + nrfam - 1)*(nrdescfam+1)
-## data[2] = partstart + offset
-## data[3] = slice
+## nrfam = persondata[6]
+## slice = personslice/(nrdescperson + nrfam - 1)*(nrdescfam+1)
+## data_fam[2] = personstart + offset
+## data_fam[3] = slice
## offset += slice
## else:
## #this family has offspring. We give it space for it's
## #weight in offspring
-## nrfam = partnerdata[6]
-## slice = partslice/(nrdescpartner + nrfam - 1)*(nrdescfam+1)
-## data[2] = partstart + offset
-## data[3] = slice
+## nrfam = persondata[6]
+## slice = personslice/(nrdescperson + nrfam - 1)*(nrdescfam+1)
+## data_fam[2] = personstart + offset
+## data_fam[3] = slice
## offset += slice
prevfamdatahandle = None
offset = 0
- for data in self.gen2people[gen]:
+ for persondata in self.gen2people[gen+1] if gen < nr_gen else []:
#obtain start and stop of family this is child of
- parentfamdata = self.gen2fam[gen-1][data[5]]
+ parentfamdata = self.gen2fam[gen][persondata[4]]
nrdescfam = 0
if not parentfamdata[1]:
nrdescfam = self.famhandle2desc[parentfamdata[0].handle]
nrdesc = 0
- if not data[1]:
- nrdesc = self.handle2desc[data[0].handle]
+ if not persondata[1]:
+ nrdesc = self.handle2desc[persondata[0].handle]
famstart = parentfamdata[2]
famslice = parentfamdata[3]
- nrchild = parentfamdata[6]
+ nrchild = parentfamdata[5]
#now we divide this slice to the weight of children,
#adding one for every child
if self.anglealgo == ANGLE_CHEQUI:
@@ -368,32 +342,48 @@ class FanChartDescWidget(FanChartBaseWidget):
#reset the offset
offset = 0
prevfamdatahandle = parentfamdata[0].handle
- if data[8] == COLLAPSED:
+ if persondata[7] == COLLAPSED:
slice = 0
- elif data[8] == EXPANDED:
+ elif persondata[7] == EXPANDED:
slice = famslice
- data[2] = famstart + offset
- data[3] = slice
+ persondata[2] = famstart + offset
+ persondata[3] = slice
offset += slice
def nrgen(self):
#compute the number of generations present
- nrgen = None
for gen in range(self.generations - 1, 0, -1):
if len(self.gen2people[gen]) > 0:
- nrgen = gen + 1
- break
- if nrgen is None:
- nrgen = 1
- return nrgen
+ return gen + 1
+ return 1
def halfdist(self):
"""
Compute the half radius of the circle
"""
- nrgen = self.nrgen()
- ringpxs = (PIXELS_PER_GENPERSON + PIXELS_PER_GENFAMILY) * (nrgen - 1)
- return ringpxs + self.CENTER + BORDER_EDGE_WIDTH
+ radius = PIXELS_PER_GEN_SMALL * N_GEN_SMALL + PIXELS_PER_GEN_LARGE \
+ * ( self.nrgen() - N_GEN_SMALL ) + self.CENTER
+ return radius
+
+ def get_radiusinout_for_generation(self,generation):
+ radius_first_gen = self.CENTER - (1-PIXELS_PER_GENPERSON_RATIO) * PIXELS_PER_GEN_SMALL
+ if generation < N_GEN_SMALL:
+ radius_start = PIXELS_PER_GEN_SMALL * generation + radius_first_gen
+ return (radius_start,radius_start + PIXELS_PER_GEN_SMALL)
+ else:
+ radius_start = PIXELS_PER_GEN_SMALL * N_GEN_SMALL + PIXELS_PER_GEN_LARGE \
+ * ( generation - N_GEN_SMALL ) + radius_first_gen
+ return (radius_start,radius_start + PIXELS_PER_GEN_LARGE)
+
+ def get_radiusinout_for_generation_pair(self,generation):
+ radiusin, radiusout = self.get_radiusinout_for_generation(generation)
+ radius_spread = radiusout - radiusin - PIXELS_CHILDREN_GAP - PIXELS_PARTNER_GAP
+
+ radiusin_pers = radiusin + PIXELS_CHILDREN_GAP
+ radiusout_pers = radiusin_pers + PIXELS_PER_GENPERSON_RATIO * radius_spread
+ radiusin_partner = radiusout_pers + PIXELS_PARTNER_GAP
+ radiusout_partner = radiusout
+ return (radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner)
def people_generator(self):
"""
@@ -401,16 +391,16 @@ class FanChartDescWidget(FanChartBaseWidget):
"""
for generation in range(self.generations):
for data in self.gen2people[generation]:
- yield (data[0], data[7])
- for generation in range(self.generations-1):
+ yield (data[0], data[6])
+ for generation in range(self.generations):
for data in self.gen2fam[generation]:
- yield (data[8], data[7])
+ yield (data[7], data[6])
def innerpeople_generator(self):
"""
a generator over all people inside of the core person
"""
- for parentdata in self.parentsroot:
+ for parentdata in self.innerring:
parent, userdata = parentdata
yield (parent, userdata)
@@ -432,184 +422,112 @@ class FanChartDescWidget(FanChartBaseWidget):
self.set_size_request(halfdist + self.CENTER + PAD_PX,
halfdist + self.CENTER + PAD_PX)
- #obtain the allocation
- alloc = self.get_allocation()
- x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
-
cr.scale(scale, scale)
# when printing, we need not recalculate
if widget:
- if self.form == FORM_CIRCLE:
- self.center_x = w/2 - self.center_xy[0]
- self.center_y = h/2 - self.center_xy[1]
- elif self.form == FORM_HALFCIRCLE:
- self.center_x = w/2. - self.center_xy[0]
- self.center_y = h - self.CENTER - PAD_PX- self.center_xy[1]
- elif self.form == FORM_QUADRANT:
- self.center_x = self.CENTER + PAD_PX - self.center_xy[0]
- self.center_y = h - self.CENTER - PAD_PX - self.center_xy[1]
- cr.translate(self.center_x, self.center_y)
+ self.center_xy = self.center_xy_from_delta()
+ cr.translate(*self.center_xy)
cr.save()
- #draw center
- cr.set_source_rgb(1, 1, 1) # white
- cr.move_to(0,0)
- cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi)
- cr.fill()
- cr.set_source_rgb(0, 0, 0) # black
- cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi)
- cr.stroke()
- cr.restore()
# Draw center person:
- (person, dup, start, slice, text, parentfampos, nrfam, userdata, status) \
+ (person, dup, start, slice, parentfampos, nrfam, userdata, status) \
= self.gen2people[0][0]
if person:
r, g, b, a = self.background_box(person, 0, userdata)
- cr.arc(0, 0, self.CENTER-PIXELS_PER_GENFAMILY, 0, 2 * math.pi)
- if self.parentsroot:
- cr.arc_negative(0, 0, TRANSLATE_PX + CHILDRING_WIDTH,
- 2 * math.pi, 0)
- cr.close_path()
- cr.set_source_rgba(r/255, g/255, b/255, a)
- cr.fill()
- cr.save()
- name = name_displayer.display(person)
- self.draw_text(cr, name, self.CENTER - PIXELS_PER_GENFAMILY
- - (self.CENTER - PIXELS_PER_GENFAMILY
- - (CHILDRING_WIDTH + TRANSLATE_PX))/2,
- 95, 455, 10, False,
- self.fontcolor(r, g, b, a), self.fontbold(a))
- cr.restore()
+ radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner = \
+ self.get_radiusinout_for_generation_pair(0)
+ if not self.innerring: radiusin_pers = TRANSLATE_PX
+ self.draw_person(cr, person, radiusin_pers, radiusout_pers, math.pi/2, math.pi/2 + 2*math.pi,
+ 0, False, userdata, is_central_person =True)
#draw center to move chart
cr.set_source_rgb(0, 0, 0) # black
cr.move_to(TRANSLATE_PX, 0)
cr.arc(0, 0, TRANSLATE_PX, 0, 2 * math.pi)
- if self.parentsroot: # has at least one parent
+ if self.innerring: # has at least one parent
cr.fill()
- self.draw_parentring(cr)
+ self.draw_innerring_people(cr)
else:
cr.stroke()
#now write all the families and children
- cr.save()
cr.rotate(self.rotate_value * math.pi/180)
- radstart = self.CENTER - PIXELS_PER_GENFAMILY - PIXELS_PER_GENPERSON
- for gen in range(self.generations-1):
- radstart += PIXELS_PER_GENPERSON
+ for gen in range(self.generations):
+ radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner = \
+ self.get_radiusinout_for_generation_pair(gen)
+ if gen > 0:
+ for pdata in self.gen2people[gen]:
+ # person, duplicate or not, start angle, slice size,
+ # parent pos in fam, nrfam, userdata, status
+ pers, dup, start, slice, pospar, nrfam, userdata, status = \
+ pdata
+ if status != COLLAPSED:
+ self.draw_person(cr, pers, radiusin_pers, radiusout_pers,
+ start, start + slice, gen, dup, userdata,
+ thick=status != NORMAL)
+ #if gen < self.generations-1:
for famdata in self.gen2fam[gen]:
# family, duplicate or not, start angle, slice size,
- # text, spouse pos in gen, nrchildren, userdata, status
- fam, dup, start, slice, text, posfam, nrchild, userdata,\
+ # spouse pos in gen, nrchildren, userdata, status
+ fam, dup, start, slice, posfam, nrchild, userdata,\
partner, status = famdata
if status != COLLAPSED:
- self.draw_person(cr, text, start, slice, radstart,
- radstart + PIXELS_PER_GENFAMILY, gen, dup,
- partner, userdata, family=True, thick=status != NORMAL)
- radstart += PIXELS_PER_GENFAMILY
- for pdata in self.gen2people[gen+1]:
- # person, duplicate or not, start angle, slice size,
- # text, parent pos in fam, nrfam, userdata, status
- pers, dup, start, slice, text, pospar, nrfam, userdata, status = \
- pdata
- if status != COLLAPSED:
- self.draw_person(cr, text, start, slice, radstart,
- radstart + PIXELS_PER_GENPERSON, gen+1, dup,
- pers, userdata, thick=status != NORMAL)
+ more_pers_flag = (gen == self.generations - 1
+ and len(fam.get_child_ref_list()) > 0)
+ self.draw_person(cr, partner, radiusin_partner, radiusout_partner, start, start + slice,
+ gen, dup, userdata, thick = (status != NORMAL), has_moregen_indicator = more_pers_flag )
cr.restore()
if self.background in [BACKGROUND_GRAD_AGE, BACKGROUND_GRAD_PERIOD]:
- self.draw_gradient(cr, widget, halfdist)
+ self.draw_gradient_legend(cr, widget, halfdist)
- def draw_person(self, cr, name, start_rad, slice, radius, radiusend,
- generation, dup, person, userdata, family=False, thick=False):
+ def cell_address_under_cursor(self, curx, cury):
"""
- Display the piece of pie for a given person. start_rad and slice
- are in radial.
+ Determine the cell address in the fan under the cursor
+ position x and y.
+ None if outside of diagram
"""
- if slice == 0:
- return
- cr.save()
- full = False
- if abs(slice - 2*pi) < 1e-6:
- full = True
- stop_rad = start_rad + slice
- if not person:
- #an family with partner not set. Don't have a color for this,
- # let's make it transparent
- r, g, b, a = (255, 255, 255, 0)
- elif not dup:
- r, g, b, a = self.background_box(person, generation, userdata)
+ radius, rads, raw_rads = self.cursor_to_polar(curx, cury, get_raw_rads=True)
+
+ btype = TYPE_BOX_NORMAL
+ if radius < TRANSLATE_PX:
+ return None
+ elif (self.innerring and self.angle[-2] and
+ radius < CHILDRING_WIDTH + TRANSLATE_PX):
+ generation = -2 # indication of one of the children
+ elif radius < self.CENTER:
+ generation = 0
else:
- #duplicate color
- a = 1
- r, g, b = self.dupcolor #(136, 138, 133)
- # If max generation, and they have children:
- if (not family and generation == self.generations - 1
- and self._have_children(person)):
- # draw an indicator
- radmax = radiusend + BORDER_EDGE_WIDTH
- cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad))
- cr.arc(0, 0, radmax, start_rad, stop_rad)
- cr.line_to(radiusend*math.cos(stop_rad), radiusend*math.sin(stop_rad))
- cr.arc_negative(0, 0, radiusend, stop_rad, start_rad)
- cr.close_path()
- ##path = cr.copy_path() # not working correct
- cr.set_source_rgb(1, 1, 1) # white
- cr.fill()
- #and again for the border
- cr.move_to(radmax*math.cos(start_rad), radmax*math.sin(start_rad))
- cr.arc(0, 0, radmax, start_rad, stop_rad)
- cr.line_to(radiusend*math.cos(stop_rad), radiusend*math.sin(stop_rad))
- cr.arc_negative(0, 0, radiusend, stop_rad, start_rad)
- cr.close_path()
- ##cr.append_path(path) # not working correct
- cr.set_source_rgb(0, 0, 0) # black
- cr.stroke()
- # now draw the person
- self.draw_radbox(cr, radius, radiusend, start_rad, stop_rad,
- (r/255, g/255, b/255, a), thick)
- if self.last_x is None or self.last_y is None:
- #we are not in a move, so draw text
- radial = False
- width = radiusend-radius
- radstart = radius + width/2
- spacepolartext = radstart * (stop_rad-start_rad)
- if spacepolartext < width * 1.1:
- # more space to print it radial
- radial = True
- radstart = radius
- self.draw_text(cr, name, radstart, start_rad/ math.pi*180,
- stop_rad/ math.pi*180, width, radial,
- self.fontcolor(r, g, b, a), self.fontbold(a))
- cr.restore()
+ generation = None
+ for gen in range(self.generations):
+ radiusin_pers,radiusout_pers,radiusin_partner,radiusout_partner \
+ = self.get_radiusinout_for_generation_pair(gen)
+ if radiusin_pers <= radius <= radiusout_pers:
+ generation, btype = gen, TYPE_BOX_NORMAL
+ break
+ if radiusin_partner <= radius <= radiusout_partner:
+ generation, btype = gen, TYPE_BOX_FAMILY
+ break
- def boxtype(self, radius):
- """
- default is only one type of box type
- """
- if radius <= self.CENTER:
- if radius >= self.CENTER - PIXELS_PER_GENFAMILY:
- return TYPE_BOX_FAMILY
- else:
- return TYPE_BOX_NORMAL
- else:
- gen = int((radius - self.CENTER)/self.gen_pixels()) + 1
- radius = (radius - self.CENTER) % PIXELS_PER_GENERATION
- if radius >= PIXELS_PER_GENPERSON:
- if gen < self.generations - 1:
- return TYPE_BOX_FAMILY
- else:
- # the last generation has no family boxes
- None
- else:
- return TYPE_BOX_NORMAL
+ # find what person is in this position:
+ selected = None
+ if not (generation is None) and 0 <= generation:
+ selected = self.personpos_at_angle(generation, rads, btype)
+ elif generation == -2:
+ for p in range(len(self.angle[generation])):
+ start, stop, state = self.angle[generation][p]
+ if self.radian_in_bounds(start, raw_rads, stop):
+ selected = p
+ break
+ if (generation is None or selected is None):
+ return None
+ return generation, selected, btype
- def draw_parentring(self, cr):
+ def draw_innerring_people(self, cr):
cr.move_to(TRANSLATE_PX + CHILDRING_WIDTH, 0)
cr.set_source_rgb(0, 0, 0) # black
cr.set_line_width(1)
cr.arc(0, 0, TRANSLATE_PX + CHILDRING_WIDTH, 0, 2 * math.pi)
cr.stroke()
- nrparent = len(self.parentsroot)
+ nrparent = len(self.innerring)
#Y axis is downward. positve angles are hence clockwise
startangle = math.pi
if nrparent <= 2:
@@ -618,104 +536,100 @@ class FanChartDescWidget(FanChartBaseWidget):
angleinc = math.pi/2
else:
angleinc = 2 * math.pi / nrchild
- for data in self.parentsroot:
+ for data in self.innerring:
self.draw_innerring(cr, data[0], data[1], startangle, angleinc)
startangle += angleinc
- def personpos_at_angle(self, generation, angledeg, btype):
+ def personpos_at_angle(self, generation, rads, btype):
"""
returns the person in generation generation at angle.
"""
- angle = angledeg / 360 * 2 * pi
selected = None
+ datas = None
if btype == TYPE_BOX_NORMAL:
- for p, pdata in enumerate(self.gen2people[generation]):
- # person, duplicate or not, start angle, slice size,
- # text, parent pos in fam, nrfam, userdata, status
- start = pdata[2]
- stop = start + pdata[3]
- if start <= angle <= stop:
- selected = p
- break
+ if generation==0:
+ return 0 # central person is always ok !
+ datas = self.gen2people[generation]
elif btype == TYPE_BOX_FAMILY:
- for p, pdata in enumerate(self.gen2fam[generation]):
- # person, duplicate or not, start angle, slice size,
- # text, parent pos in fam, nrfam, userdata, status
- start = pdata[2]
- stop = start + pdata[3]
- if start <= angle <= stop:
- selected = p
- break
+ datas = self.gen2fam[generation]
+ else:
+ return None
+ for p, pdata in enumerate(datas):
+ # person, duplicate or not, start angle, slice size,
+ # parent pos in fam, nrfam, userdata, status
+ start, stop = pdata[2], pdata[2] + pdata[3]
+ if self.radian_in_bounds(start, rads, stop):
+ selected = p
+ break
return selected
- def person_at(self, generation, pos, btype):
+ def person_at(self, cell_address):
"""
returns the person at generation, pos, btype
"""
- if pos is None:
- return None
+ generation, pos, btype = cell_address
if generation == -2:
- person, userdata = self.parentsroot[pos]
+ person, userdata = self.innerring[pos]
elif btype == TYPE_BOX_NORMAL:
# person, duplicate or not, start angle, slice size,
- # text, parent pos in fam, nrfam, userdata, status
+ # parent pos in fam, nrfam, userdata, status
person = self.gen2people[generation][pos][0]
elif btype == TYPE_BOX_FAMILY:
# family, duplicate or not, start angle, slice size,
- # text, spouse pos in gen, nrchildren, userdata, person, status
- person = self.gen2fam[generation][pos][8]
+ # spouse pos in gen, nrchildren, userdata, person, status
+ person = self.gen2fam[generation][pos][7]
return person
- def family_at(self, generation, pos, btype):
+ def family_at(self, cell_address):
"""
returns the family at generation, pos, btype
"""
+ generation, pos, btype = cell_address
if pos is None or btype == TYPE_BOX_NORMAL or generation < 0:
return None
return self.gen2fam[generation][pos][0]
def do_mouse_click(self):
# no drag occured, expand or collapse the section
- self.change_slice(self._mouse_click_gen, self._mouse_click_sel,
- self._mouse_click_btype)
+ self.toggle_cell_state(self._mouse_click_cell_address)
+ self._compute_angles(*self.rootangle_rad)
self._mouse_click = False
self.queue_draw()
- def change_slice(self, generation, selected, btype):
+ def toggle_cell_state(self, cell_address):
+ generation, selected, btype = cell_address
if generation < 1:
return
if btype == TYPE_BOX_NORMAL:
data = self.gen2people[generation][selected]
- parpos = data[5]
- status = data[8]
+ parpos = data[4]
+ status = data[7]
if status == NORMAL:
#should be expanded, rest collapsed
for entry in self.gen2people[generation]:
- if entry[5] == parpos:
+ if entry[4] == parpos:
+ entry[7] = COLLAPSED
+ data[7] = EXPANDED
+ else:
+ #is expanded, set back to normal
+ for entry in self.gen2people[generation]:
+ if entry[4] == parpos:
+ entry[7] = NORMAL
+ if btype == TYPE_BOX_FAMILY:
+ data = self.gen2fam[generation][selected]
+ parpos = data[4]
+ status = data[8]
+ if status == NORMAL:
+ #should be expanded, rest collapsed
+ for entry in self.gen2fam[generation]:
+ if entry[4] == parpos:
entry[8] = COLLAPSED
data[8] = EXPANDED
else:
#is expanded, set back to normal
- for entry in self.gen2people[generation]:
- if entry[5] == parpos:
+ for entry in self.gen2fam[generation]:
+ if entry[4] == parpos:
entry[8] = NORMAL
- if btype == TYPE_BOX_FAMILY:
- data = self.gen2fam[generation][selected]
- parpos = data[5]
- status = data[9]
- if status == NORMAL:
- #should be expanded, rest collapsed
- for entry in self.gen2fam[generation]:
- if entry[5] == parpos:
- entry[9] = COLLAPSED
- data[9] = EXPANDED
- else:
- #is expanded, set back to normal
- for entry in self.gen2fam[generation]:
- if entry[5] == parpos:
- entry[9] = NORMAL
-
- self.__compute_angles()
class FanChartDescGrampsGUI(FanChartGrampsGUI):
""" class for functions fanchart GUI elements will need in Gramps
@@ -727,7 +641,7 @@ class FanChartDescGrampsGUI(FanChartGrampsGUI):
data.
"""
root_person_handle = self.get_active('Person')
- self.fan.set_values(root_person_handle, self.maxgen, self.background,
+ self.fan.set_values(root_person_handle, self.maxgen, self.flipupsidedownname, self.twolinename, self.background,
self.fonttype, self.grad_start, self.grad_end,
self.generic_filter, self.alpha_filter, self.form,
self.angle_algo, self.dupcolor)
diff --git a/gramps/plugins/gramplet/fanchart2waygramplet.py b/gramps/plugins/gramplet/fanchart2waygramplet.py
new file mode 100644
index 000000000..06680de2b
--- /dev/null
+++ b/gramps/plugins/gramplet/fanchart2waygramplet.py
@@ -0,0 +1,100 @@
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
+# Copyright (C) 2009 Douglas S. Blank
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# $Id$
+
+## Based on the normal fanchart
+
+#-------------------------------------------------------------------------
+#
+# Python modules
+#
+#-------------------------------------------------------------------------
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Pango
+from gi.repository import Gtk
+import math
+from gi.repository import Gdk
+try:
+ import cairo
+except ImportError:
+ pass
+
+#-------------------------------------------------------------------------
+#
+# GRAMPS modules
+#
+#-------------------------------------------------------------------------
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+_ = glocale.translation.gettext
+from gramps.gen.plug import Gramplet
+from gramps.gen.errors import WindowActiveError
+from gramps.gui.editors import EditPerson
+from gramps.gui.widgets.fanchart2way import (FanChart2WayWidget, FanChart2WayGrampsGUI,
+ ANGLE_WEIGHT)
+from gramps.gui.widgets.fanchart import FORM_HALFCIRCLE, BACKGROUND_SCHEME1
+
+class FanChart2WayGramplet(FanChart2WayGrampsGUI, Gramplet):
+ """
+ The Gramplet code that realizes the FanChartWidget.
+ """
+
+ def __init__(self, gui, nav_group=0):
+ Gramplet.__init__(self, gui, nav_group)
+ FanChart2WayGrampsGUI.__init__(self, self.on_childmenu_changed)
+ self.generations_asc = 5
+ self.generations_desc = 4
+ self.background = BACKGROUND_SCHEME1
+ self.fonttype = 'Sans'
+ self.grad_start = '#FF0000'
+ self.grad_end = '#0000FF'
+ self.dupcolor = '#888A85' #light grey
+ self.generic_filter = None
+ self.alpha_filter = 0.2
+ self.form = FORM_HALFCIRCLE
+ self.angle_algo = ANGLE_WEIGHT
+ self.flipupsidedownname = True
+ self.twolinename = True
+ self.childring = False
+ self.background_gradient = True
+ #self.filter = filter
+
+ self.set_fan(FanChart2WayWidget(self.dbstate, self.uistate, self.on_popup))
+ # Replace the standard textview with the fan chart widget:
+ self.gui.get_container_widget().remove(self.gui.textview)
+ self.gui.get_container_widget().add_with_viewport(self.fan)
+ # Make sure it is visible:
+ self.fan.show()
+
+ def init(self):
+ self.set_tooltip(_("Click to expand/contract person\nRight-click for options\nClick and drag in open area to rotate"))
+
+ def active_changed(self, handle):
+ """
+ Method called when active person changes.
+ """
+ # Reset everything but rotation angle (leave it as is)
+ self.update()
+
+ def on_childmenu_changed(self, obj, person_handle):
+ """Callback for the pulldown menu selection, changing to the person
+ attached with menu item."""
+ self.set_active('Person', person_handle)
+ return True
diff --git a/gramps/plugins/gramplet/fanchartdescgramplet.py b/gramps/plugins/gramplet/fanchartdescgramplet.py
index 89afc099f..ecb791a08 100644
--- a/gramps/plugins/gramplet/fanchartdescgramplet.py
+++ b/gramps/plugins/gramplet/fanchartdescgramplet.py
@@ -50,6 +50,8 @@ class FanChartDescGramplet(FanChartDescGrampsGUI, Gramplet):
self.alpha_filter = 0.2
self.form = FORM_HALFCIRCLE
self.angle_algo = ANGLE_WEIGHT
+ self.flipupsidedownname = True
+ self.twolinename = True
self.set_fan(FanChartDescWidget(self.dbstate, self.uistate,
self.on_popup))
# Replace the standard textview with the fan chart widget:
diff --git a/gramps/plugins/gramplet/fanchartgramplet.py b/gramps/plugins/gramplet/fanchartgramplet.py
index 6395b62ad..27a7b75c2 100644
--- a/gramps/plugins/gramplet/fanchartgramplet.py
+++ b/gramps/plugins/gramplet/fanchartgramplet.py
@@ -47,6 +47,8 @@ class FanChartGramplet(FanChartGrampsGUI, Gramplet):
self.maxgen = 6
self.background = BACKGROUND_SCHEME1
self.childring = True
+ self.flipupsidedownname = True
+ self.twolinename = True
self.radialtext = True
self.fonttype = 'Sans'
self.grad_start = '#0000FF'
diff --git a/gramps/plugins/gramplet/gramplet.gpr.py b/gramps/plugins/gramplet/gramplet.gpr.py
index 2a274a3ad..386b7c9cf 100644
--- a/gramps/plugins/gramplet/gramplet.gpr.py
+++ b/gramps/plugins/gramplet/gramplet.gpr.py
@@ -147,6 +147,23 @@ register(GRAMPLET,
navtypes=["Person"],
)
+register(GRAMPLET,
+ id= "2-Way Fan Chart",
+ name=_("2-Way Fan Chart"),
+ description = _("Gramplet showing active person's direct ancestors and descendants as a fanchart"),
+ status = STABLE,
+ fname="fanchart2waygramplet.py",
+ height=300,
+ expand=True,
+ gramplet = 'FanChart2WayGramplet',
+ detached_height =300,
+ detached_width = 300,
+ gramplet_title=_("2-Way Fan"),
+ version="1.0.0",
+ gramps_target_version=MODULE_VERSION,
+ navtypes=["Person"],
+ )
+
register(GRAMPLET,
id="FAQ",
name=_("FAQ"),
diff --git a/gramps/plugins/view/fanchart2wayview.py b/gramps/plugins/view/fanchart2wayview.py
new file mode 100644
index 000000000..6684a489d
--- /dev/null
+++ b/gramps/plugins/view/fanchart2wayview.py
@@ -0,0 +1,572 @@
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
+# Copyright (C) 2009 Douglas S. Blank
+# Copyright (C) 2014 Bastien Jacquet
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+## Based on the paper:
+## http://www.cs.utah.edu/~draperg/research/fanchart/draperg_FHT08.pdf
+## and the applet:
+## http://www.cs.utah.edu/~draperg/research/fanchart/demo/
+
+## Found by redwood:
+## http://www.gramps-project.org/bugs/view.php?id=2611
+
+#-------------------------------------------------------------------------
+#
+# Python modules
+#
+#-------------------------------------------------------------------------
+from gi.repository import Gdk
+from gi.repository import Gtk
+import cairo
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+_ = glocale.translation.gettext
+
+#-------------------------------------------------------------------------
+#
+# Gramps modules
+#
+#-------------------------------------------------------------------------
+import gramps.gui.widgets.fanchart as fanchart
+import gramps.gui.widgets.fanchart2way as fanchart2way
+from gramps.gui.views.navigationview import NavigationView
+from gramps.gui.views.bookmarks import PersonBookmarks
+from gramps.gui.utils import SystemFonts
+
+# the print settings to remember between print sessions
+PRINT_SETTINGS = None
+
+class FanChart2WayView(fanchart2way.FanChart2WayGrampsGUI, NavigationView):
+ """
+ The Gramplet code that realizes the FanChartWidget.
+ """
+ #settings in the config file
+ CONFIGSETTINGS = (
+ ('interface.fanview-maxgen-asc', 4),
+ ('interface.fanview-maxgen-desc', 4),
+ ('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN),
+ ('interface.fanview-background-gradient', True),
+ ('interface.fanview-radialtext', True),
+ ('interface.fanview-twolinename', True),
+ ('interface.fanview-flipupsidedownname', True),
+ ('interface.fanview-font', 'Sans'),
+ ('interface.fanview-form', fanchart.FORM_CIRCLE),
+ ('interface.color-start-grad', '#ef2929'),
+ ('interface.color-end-grad', '#3d37e9'),
+ ('interface.angle-algorithm', fanchart2way.ANGLE_WEIGHT),
+ ('interface.duplicate-color', '#888a85')
+ )
+ def __init__(self, pdata, dbstate, uistate, nav_group=0):
+ self.dbstate = dbstate
+ self.uistate = uistate
+
+ NavigationView.__init__(self, _('2-Way Fan Chart'),
+ pdata, dbstate, uistate,
+ PersonBookmarks,
+ nav_group)
+ fanchart2way.FanChart2WayGrampsGUI.__init__(self, self.on_childmenu_changed)
+ #set needed values
+ self.generations_asc = self._config.get('interface.fanview-maxgen-asc')
+ self.generations_desc = self._config.get('interface.fanview-maxgen-desc')
+ self.background = self._config.get('interface.fanview-background')
+ self.background_gradient = self._config.get('interface.fanview-background-gradient')
+ self.radialtext = self._config.get('interface.fanview-radialtext')
+ self.twolinename = self._config.get('interface.fanview-twolinename')
+ self.flipupsidedownname = self._config.get('interface.fanview-flipupsidedownname')
+ self.fonttype = self._config.get('interface.fanview-font')
+
+ self.grad_start = self._config.get('interface.color-start-grad')
+ self.grad_end = self._config.get('interface.color-end-grad')
+ self.form = fanchart.FORM_CIRCLE
+ self.angle_algo = self._config.get('interface.angle-algorithm')
+ self.dupcolor = self._config.get('interface.duplicate-color')
+ self.generic_filter = None
+ self.alpha_filter = 0.2
+
+ dbstate.connect('active-changed', self.active_changed)
+ dbstate.connect('database-changed', self.change_db)
+
+ self.additional_uis.append(self.additional_ui())
+ self.allfonts = [x for x in enumerate(SystemFonts().get_system_fonts())]
+
+ def navigation_type(self):
+ return 'Person'
+
+ def build_widget(self):
+ self.set_fan(fanchart2way.FanChart2WayWidget(self.dbstate, self.uistate,
+ self.on_popup))
+ self.scrolledwindow = Gtk.ScrolledWindow(hadjustment=None,
+ vadjustment=None)
+ self.scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC,
+ Gtk.PolicyType.AUTOMATIC)
+ self.fan.show_all()
+ self.scrolledwindow.add_with_viewport(self.fan)
+
+ return self.scrolledwindow
+
+ def get_stock(self):
+ """
+ The category stock icon
+ """
+ return 'gramps-pedigree'
+
+ def get_viewtype_stock(self):
+ """Type of view in category
+ """
+ return 'gramps-fanchart'
+
+ def additional_ui(self):
+ return '''
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '''
+
+ def define_actions(self):
+ """
+ Required define_actions function for PageView. Builds the action
+ group information required.
+ """
+ NavigationView.define_actions(self)
+
+ self._add_action('PrintView', Gtk.STOCK_PRINT, _("_Print..."),
+ accel="P",
+ tip=_("Print or save the Fan Chart View"),
+ callback=self.printview)
+ def build_tree(self):
+ """
+ Generic method called by PageView to construct the view.
+ Here the tree builds when active person changes or db changes or on
+ callbacks like person_rebuild, so build will be double sometimes.
+ However, change in generic filter also triggers build_tree ! So we
+ need to reset.
+ """
+ self.update()
+
+ def active_changed(self, handle):
+ """
+ Method called when active person changes.
+ """
+ # Reset everything but rotation angle (leave it as is)
+ self.update()
+
+ def _connect_db_signals(self):
+ """
+ Connect database signals.
+ """
+ self._add_db_signal('person-add', self.person_rebuild)
+ self._add_db_signal('person-update', self.person_rebuild)
+ self._add_db_signal('person-delete', self.person_rebuild)
+ self._add_db_signal('person-rebuild', self.person_rebuild_bm)
+ self._add_db_signal('family-update', self.person_rebuild)
+ self._add_db_signal('family-add', self.person_rebuild)
+ self._add_db_signal('family-delete', self.person_rebuild)
+ self._add_db_signal('family-rebuild', self.person_rebuild)
+
+ def change_db(self, db):
+ self._change_db(db)
+ if self.active:
+ self.bookmarks.redraw()
+ self.update()
+
+ def update(self):
+ self.main()
+
+ def goto_handle(self, handle):
+ self.change_active(handle)
+ self.main()
+
+ def get_active(self, object):
+ """overrule get_active, to support call as in Gramplets
+ """
+ return NavigationView.get_active(self)
+
+ def person_rebuild(self, *args):
+ self.update()
+
+ def person_rebuild_bm(self, *args):
+ """Large change to person database"""
+ self.person_rebuild()
+ if self.active:
+ self.bookmarks.redraw()
+
+ def printview(self, obj):
+ """
+ Print or save the view that is currently shown
+ """
+ widthpx = 2 * self.fan.halfdist()
+ heightpx = widthpx
+
+ prt = CairoPrintSave(widthpx, heightpx, self.fan.on_draw, self.uistate.window)
+ prt.run()
+
+ def on_childmenu_changed(self, obj, person_handle):
+ """Callback for the pulldown menu selection, changing to the person
+ attached with menu item."""
+ self.change_active(person_handle)
+ return True
+
+ def can_configure(self):
+ """
+ See :class:`~gui.views.pageview.PageView
+ :return: bool
+ """
+ return True
+
+ def _get_configure_page_funcs(self):
+ """
+ Return a list of functions that create gtk elements to use in the
+ notebook pages of the Configure dialog
+
+ :return: list of functions
+ """
+ return [self.config_panel]
+
+ def config_panel(self, configdialog):
+ """
+ Function that builds the widget in the configuration dialog
+ """
+ nrentry = 9
+ grid = Gtk.Grid()
+ grid.set_border_width(12)
+ grid.set_column_spacing(6)
+ grid.set_row_spacing(6)
+
+ configdialog.add_spinner(grid, _("Max ancestor generations"), 0,
+ 'interface.fanview-maxgen-asc', (1, 11),
+ callback=self.cb_update_maxgen)
+ configdialog.add_spinner(grid, _("Max descendant generations"), 1,
+ 'interface.fanview-maxgen-desc', (1, 11),
+ callback=self.cb_update_maxgen)
+ configdialog.add_combo(grid,
+ _('Text Font'),
+ 2, 'interface.fanview-font',
+ self.allfonts, callback=self.cb_update_font, valueactive=True)
+ backgrvals = (
+ (fanchart.BACKGROUND_GENDER, _('Gender colors')),
+ (fanchart.BACKGROUND_GRAD_GEN, _('Generation based gradient')),
+ (fanchart.BACKGROUND_GRAD_AGE, _('Age (0-100) based gradient')),
+ (fanchart.BACKGROUND_SINGLE_COLOR,
+ _('Single main (filter) color')),
+ (fanchart.BACKGROUND_GRAD_PERIOD, _('Time period based gradient')),
+ (fanchart.BACKGROUND_WHITE, _('White')),
+ (fanchart.BACKGROUND_SCHEME1, _('Color scheme classic report')),
+ (fanchart.BACKGROUND_SCHEME2, _('Color scheme classic view')),
+ )
+ curval = self._config.get('interface.fanview-background')
+ nrval = 0
+ for nr, val in backgrvals:
+ if curval == nr:
+ break
+ nrval += 1
+ configdialog.add_combo(grid,
+ _('Background'),
+ 3, 'interface.fanview-background',
+ backgrvals,
+ callback=self.cb_update_background, valueactive=False,
+ setactive=nrval
+ )
+
+ # show names one two line
+ configdialog.add_checkbox(grid,
+ _('Add global background colored gradient'),
+ 4, 'interface.fanview-background-gradient')
+
+ #colors, stored as hex values
+ configdialog.add_color(grid, _('Start gradient/Main color'), 5,
+ 'interface.color-start-grad', col=1)
+ configdialog.add_color(grid, _('End gradient/2nd color'), 6,
+ 'interface.color-end-grad', col=1)
+ configdialog.add_color(grid, _('Color for duplicates'), 7,
+ 'interface.duplicate-color', col=1)
+ # algo for the fan angle distribution
+ configdialog.add_combo(grid, _('Fan chart distribution'), 8,
+ 'interface.angle-algorithm',
+ ((fanchart2way.ANGLE_CHEQUI,
+ _('Homogeneous children distribution')),
+ (fanchart2way.ANGLE_WEIGHT,
+ _('Size proportional to number of descendants')),
+ ),
+ callback=self.cb_update_anglealgo)
+
+ # show names one two line
+ configdialog.add_checkbox(grid,
+ _('Show names on two lines'),
+ 9, 'interface.fanview-twolinename')
+
+ # Flip names
+ configdialog.add_checkbox(grid,
+ _('Flip name on the left of the fan'),
+ 10, 'interface.fanview-flipupsidedownname')
+
+ return _('Layout'), grid
+
+ def config_connect(self):
+ """
+ Overwriten from :class:`~gui.views.pageview.PageView method
+ This method will be called after the ini file is initialized,
+ use it to monitor changes in the ini file
+ """
+ self._config.connect('interface.color-start-grad',
+ self.cb_update_color)
+ self._config.connect('interface.color-end-grad',
+ self.cb_update_color)
+ self._config.connect('interface.duplicate-color',
+ self.cb_update_color)
+ self._config.connect('interface.fanview-flipupsidedownname',
+ self.cb_update_flipupsidedownname)
+ self._config.connect('interface.fanview-twolinename',
+ self.cb_update_twolinename)
+ self._config.connect('interface.fanview-background-gradient',
+ self.cb_update_background_gradient)
+
+ def cb_update_maxgen(self, spinbtn, constant):
+ self._config.set(constant, spinbtn.get_value_as_int())
+ self.generations_asc = int(self._config.get('interface.fanview-maxgen-asc'))
+ self.generations_desc = int(self._config.get('interface.fanview-maxgen-desc'))
+ self.update()
+
+ def cb_update_twolinename(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the twolinename setting.
+ """
+ self.twolinename = (entry == 'True')
+ self.update()
+
+ def cb_update_background(self, obj, constant):
+ entry = obj.get_active()
+ Gtk.TreePath.new_from_string('%d' % entry)
+ val = int(obj.get_model().get_value(
+ obj.get_model().get_iter_from_string('%d' % entry), 0))
+ self._config.set(constant, val)
+ self.background = val
+ self.update()
+
+ def cb_update_background_gradient(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the twolinename setting.
+ """
+ self.background_gradient = (entry == 'True')
+ self.update()
+
+ def cb_update_form(self, obj, constant):
+ entry = obj.get_active()
+ self._config.set(constant, entry)
+ self.form = entry
+ self.update()
+
+ def cb_update_anglealgo(self, obj, constant):
+ entry = obj.get_active()
+ self._config.set(constant, entry)
+ self.angle_algo = entry
+ self.update()
+
+ def cb_update_color(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the childrenring setting.
+ """
+ self.grad_start = self._config.get('interface.color-start-grad')
+ self.grad_end = self._config.get('interface.color-end-grad')
+ self.dupcolor = self._config.get('interface.duplicate-color')
+ self.update()
+
+ def cb_update_flipupsidedownname(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the flipupsidedownname setting.
+ """
+ self.flipupsidedownname = (entry == 'True')
+ self.update()
+
+ def cb_update_font(self, obj, constant):
+ entry = obj.get_active()
+ self._config.set(constant, self.allfonts[entry][1])
+ self.fonttype = self.allfonts[entry][1]
+ self.update()
+
+ def get_default_gramplets(self):
+ """
+ Define the default gramplets for the sidebar and bottombar.
+ """
+ return (("Person Filter",),
+ ())
+
+#------------------------------------------------------------------------
+#
+# CairoPrintSave class
+#
+#------------------------------------------------------------------------
+class CairoPrintSave():
+ """Act as an abstract document that can render onto a cairo context.
+
+ It can render the model onto cairo context pages, according to the received
+ page style.
+
+ """
+
+ def __init__(self, widthpx, heightpx, drawfunc, parent):
+ """
+ This class provides the things needed so as to dump a cairo drawing on
+ a context to output
+ """
+ self.widthpx = widthpx
+ self.heightpx = heightpx
+ self.drawfunc = drawfunc
+ self.parent = parent
+
+ def run(self):
+ """Create the physical output from the meta document.
+
+ """
+ global PRINT_SETTINGS
+
+ # set up a print operation
+ operation = Gtk.PrintOperation()
+ operation.connect("draw_page", self.on_draw_page)
+ operation.connect("preview", self.on_preview)
+ operation.connect("paginate", self.on_paginate)
+ operation.set_n_pages(1)
+ #paper_size = Gtk.PaperSize.new(name="iso_a4")
+ ## WHY no Gtk.Unit.PIXEL ?? Is there a better way to convert
+ ## Pixels to MM ??
+ paper_size = Gtk.PaperSize.new_custom("custom",
+ "Custom Size",
+ round(self.widthpx * 0.2646),
+ round(self.heightpx * 0.2646),
+ Gtk.Unit.MM)
+ page_setup = Gtk.PageSetup()
+ page_setup.set_paper_size(paper_size)
+ #page_setup.set_orientation(Gtk.PageOrientation.PORTRAIT)
+ operation.set_default_page_setup(page_setup)
+ #operation.set_use_full_page(True)
+
+ if PRINT_SETTINGS is not None:
+ operation.set_print_settings(PRINT_SETTINGS)
+
+ # run print dialog
+ while True:
+ self.preview = None
+ res = operation.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.parent)
+ if self.preview is None: # cancel or print
+ break
+ # set up printing again; can't reuse PrintOperation?
+ operation = Gtk.PrintOperation()
+ operation.set_default_page_setup(page_setup)
+ operation.connect("draw_page", self.on_draw_page)
+ operation.connect("preview", self.on_preview)
+ operation.connect("paginate", self.on_paginate)
+ # set print settings if it was stored previously
+ if PRINT_SETTINGS is not None:
+ operation.set_print_settings(PRINT_SETTINGS)
+
+ # store print settings if printing was successful
+ if res == Gtk.PrintOperationResult.APPLY:
+ PRINT_SETTINGS = operation.get_print_settings()
+
+ def on_draw_page(self, operation, context, page_nr):
+ """Draw a page on a Cairo context.
+ """
+ cr = context.get_cairo_context()
+ pxwidth = round(context.get_width())
+ pxheight = round(context.get_height())
+ scale = min(pxwidth/self.widthpx, pxheight/self.heightpx)
+ if scale > 1:
+ scale = 1
+ self.drawfunc(None, cr, scale=scale)
+
+ def on_paginate(self, operation, context):
+ """Paginate the whole document in chunks.
+ We don't need this as there is only one page, however,
+ we provide a dummy holder here, because on_preview crashes if no
+ default application is set with gir 3.3.2 (typically evince not installed)!
+ It will provide the start of the preview dialog, which cannot be
+ started in on_preview
+ """
+ finished = True
+ # update page number
+ operation.set_n_pages(1)
+
+ # start preview if needed
+ if self.preview:
+ self.preview.run()
+
+ return finished
+
+ def on_preview(self, operation, preview, context, parent):
+ """Implement custom print preview functionality.
+ We provide a dummy holder here, because on_preview crashes if no
+ default application is set with gir 3.3.2 (typically evince not installed)!
+ """
+ dlg = Gtk.MessageDialog(parent,
+ flags=Gtk.DialogFlags.MODAL,
+ type=Gtk.MessageType.WARNING,
+ buttons=Gtk.ButtonsType.CLOSE,
+ message_format=_('No preview available'))
+ self.preview = dlg
+ self.previewopr = operation
+ #dlg.format_secondary_markup(msg2)
+ dlg.set_title("Fan Chart Preview - Gramps")
+ dlg.connect('response', self.previewdestroy)
+
+ # give a dummy cairo context to Gtk.PrintContext,
+ try:
+ width = int(round(context.get_width()))
+ except ValueError:
+ width = 0
+ try:
+ height = int(round(context.get_height()))
+ except ValueError:
+ height = 0
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ cr = cairo.Context(surface)
+ context.set_cairo_context(cr, 72.0, 72.0)
+
+ return True
+
+ def previewdestroy(self, dlg, res):
+ self.preview.destroy()
+ self.previewopr.end_preview()
diff --git a/gramps/plugins/view/fanchartdescview.py b/gramps/plugins/view/fanchartdescview.py
index b92976db0..57adde49f 100644
--- a/gramps/plugins/view/fanchartdescview.py
+++ b/gramps/plugins/view/fanchartdescview.py
@@ -58,6 +58,9 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
CONFIGSETTINGS = (
('interface.fanview-maxgen', 9),
('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN),
+ ('interface.fanview-radialtext', True),
+ ('interface.fanview-twolinename', True),
+ ('interface.fanview-flipupsidedownname', True),
('interface.fanview-font', 'Sans'),
('interface.fanview-form', fanchart.FORM_CIRCLE),
('interface.color-start-grad', '#ef2929'),
@@ -77,6 +80,9 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
#set needed values
self.maxgen = self._config.get('interface.fanview-maxgen')
self.background = self._config.get('interface.fanview-background')
+ self.radialtext = self._config.get('interface.fanview-radialtext')
+ self.twolinename = self._config.get('interface.fanview-twolinename')
+ self.flipupsidedownname = self._config.get('interface.fanview-flipupsidedownname')
self.fonttype = self._config.get('interface.fanview-font')
self.grad_start = self._config.get('interface.color-start-grad')
@@ -265,7 +271,7 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
"""
Function that builds the widget in the configuration dialog
"""
- nrentry = 8
+ nrentry = 9
grid = Gtk.Grid()
grid.set_border_width(12)
grid.set_column_spacing(6)
@@ -326,6 +332,16 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
),
callback=self.cb_update_anglealgo)
+ # show names one two line
+ configdialog.add_checkbox(grid,
+ _('Show names on two lines'),
+ 8, 'interface.fanview-twolinename')
+
+ # Flip names
+ configdialog.add_checkbox(grid,
+ _('Flip name on the left of the fan'),
+ 9, 'interface.fanview-flipupsidedownname')
+
return _('Layout'), grid
def config_connect(self):
@@ -340,12 +356,23 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
self.cb_update_color)
self._config.connect('interface.duplicate-color',
self.cb_update_color)
+ self._config.connect('interface.fanview-flipupsidedownname',
+ self.cb_update_flipupsidedownname)
+ self._config.connect('interface.fanview-twolinename',
+ self.cb_update_twolinename)
def cb_update_maxgen(self, spinbtn, constant):
self.maxgen = spinbtn.get_value_as_int()
self._config.set(constant, self.maxgen)
self.update()
+ def cb_update_twolinename(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the twolinename setting.
+ """
+ self.twolinename = (entry == 'True')
+ self.update()
+
def cb_update_background(self, obj, constant):
entry = obj.get_active()
Gtk.TreePath.new_from_string('%d' % entry)
@@ -376,6 +403,13 @@ class FanChartDescView(fanchartdesc.FanChartDescGrampsGUI, NavigationView):
self.dupcolor = self._config.get('interface.duplicate-color')
self.update()
+ def cb_update_flipupsidedownname(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the flipupsidedownname setting.
+ """
+ self.flipupsidedownname = (entry == 'True')
+ self.update()
+
def cb_update_font(self, obj, constant):
entry = obj.get_active()
self._config.set(constant, self.allfonts[entry][1])
diff --git a/gramps/plugins/view/fanchartview.py b/gramps/plugins/view/fanchartview.py
index 08418b492..aad0b2174 100644
--- a/gramps/plugins/view/fanchartview.py
+++ b/gramps/plugins/view/fanchartview.py
@@ -59,6 +59,8 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
('interface.fanview-background', fanchart.BACKGROUND_GRAD_GEN),
('interface.fanview-childrenring', True),
('interface.fanview-radialtext', True),
+ ('interface.fanview-twolinename', True),
+ ('interface.fanview-flipupsidedownname', True),
('interface.fanview-font', 'Sans'),
('interface.fanview-form', fanchart.FORM_CIRCLE),
('interface.color-start-grad', '#ef2929'),
@@ -78,6 +80,8 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
self.background = self._config.get('interface.fanview-background')
self.childring = self._config.get('interface.fanview-childrenring')
self.radialtext = self._config.get('interface.fanview-radialtext')
+ self.twolinename = self._config.get('interface.fanview-twolinename')
+ self.flipupsidedownname = self._config.get('interface.fanview-flipupsidedownname')
self.fonttype = self._config.get('interface.fanview-font')
self.grad_start = self._config.get('interface.color-start-grad')
@@ -263,7 +267,7 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
"""
Function that builds the widget in the configuration dialog
"""
- nrentry = 7
+ nrentry = 9
grid = Gtk.Grid()
grid.set_border_width(12)
grid.set_column_spacing(6)
@@ -311,6 +315,16 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
(2, _('Quadrant'))),
callback=self.cb_update_form)
+ # show names one two line
+ configdialog.add_checkbox(grid,
+ _('Show names on two lines'),
+ 6, 'interface.fanview-twolinename')
+
+ # Flip names
+ configdialog.add_checkbox(grid,
+ _('Flip name on the left of the fan'),
+ 7, 'interface.fanview-flipupsidedownname')
+
# options users should not change:
configdialog.add_checkbox(grid,
_('Show children ring'),
@@ -330,6 +344,10 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
"""
self._config.connect('interface.fanview-childrenring',
self.cb_update_childrenring)
+ self._config.connect('interface.fanview-twolinename',
+ self.cb_update_twolinename)
+ self._config.connect('interface.fanview-flipupsidedownname',
+ self.cb_update_flipupsidedownname)
self._config.connect('interface.fanview-radialtext',
self.cb_update_radialtext)
self._config.connect('interface.color-start-grad',
@@ -385,6 +403,20 @@ class FanChartView(fanchart.FanChartGrampsGUI, NavigationView):
self.grad_end = self._config.get('interface.color-end-grad')
self.update()
+ def cb_update_twolinename(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the twolinename setting.
+ """
+ self.twolinename = (entry == 'True')
+ self.update()
+
+ def cb_update_flipupsidedownname(self, client, cnxn_id, entry, data):
+ """
+ Called when the configuration menu changes the flipupsidedownname setting.
+ """
+ self.flipupsidedownname = (entry == 'True')
+ self.update()
+
def cb_update_font(self, obj, constant):
entry = obj.get_active()
self._config.set(constant, self.allfonts[entry][1])
diff --git a/gramps/plugins/view/view.gpr.py b/gramps/plugins/view/view.gpr.py
index 639742e14..cda4edfc0 100644
--- a/gramps/plugins/view/view.gpr.py
+++ b/gramps/plugins/view/view.gpr.py
@@ -166,6 +166,21 @@ viewclass = 'FanChartDescView',
stock_icon = 'gramps-fanchartdesc',
)
+register(VIEW,
+id = 'fanchart2wayview',
+name = _("2-Way Fan"),
+category = ("Ancestry", _("Charts")),
+description = _("Showing ascendants and descendants through a fanchart"),
+version = '1.0',
+gramps_target_version = MODULE_VERSION,
+status = STABLE,
+fname = 'fanchart2wayview.py',
+authors = ["B. Jacquet"],
+authors_email = ["bastien.jacquet_dev@m4x.org"],
+viewclass = 'FanChart2WayView',
+stock_icon = 'gramps-fanchart2way',
+ )
+
register(VIEW,
id = 'personview',
name = _("Grouped People"),
diff --git a/images/hicolor/16x16/actions/gramps-fanchart2way.png b/images/hicolor/16x16/actions/gramps-fanchart2way.png
new file mode 100644
index 000000000..6da92274a
Binary files /dev/null and b/images/hicolor/16x16/actions/gramps-fanchart2way.png differ
diff --git a/images/hicolor/22x22/actions/gramps-fanchart2way.png b/images/hicolor/22x22/actions/gramps-fanchart2way.png
new file mode 100644
index 000000000..28342b589
Binary files /dev/null and b/images/hicolor/22x22/actions/gramps-fanchart2way.png differ
diff --git a/images/hicolor/48x48/actions/gramps-fanchart2way.png b/images/hicolor/48x48/actions/gramps-fanchart2way.png
new file mode 100644
index 000000000..376a7f232
Binary files /dev/null and b/images/hicolor/48x48/actions/gramps-fanchart2way.png differ
diff --git a/images/hicolor/scalable/actions/gramps-fanchart2way.svg b/images/hicolor/scalable/actions/gramps-fanchart2way.svg
new file mode 100644
index 000000000..6534c83cb
--- /dev/null
+++ b/images/hicolor/scalable/actions/gramps-fanchart2way.svg
@@ -0,0 +1,1599 @@
+
+
+
+