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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + application + gramps + pedigree + + + + + B. Jacquet + + + + + + + rework of the work of B. Malengier which was based on original design of Don Allingham + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +