diff --git a/src/plugins/gramplet/FanChartGramplet.py b/src/plugins/gramplet/FanChartGramplet.py index 767ddfa3e..8e5b60433 100644 --- a/src/plugins/gramplet/FanChartGramplet.py +++ b/src/plugins/gramplet/FanChartGramplet.py @@ -59,6 +59,11 @@ class FanChartWidget(gtk.Widget): 'size-allocate': 'override', 'size-request': 'override', } + GENCOLOR = ((229,191,252), + (191,191,252), + (191,222,252), + (183,219,197), + (206,246,209)) def __init__(self, generations): """ @@ -70,19 +75,30 @@ class FanChartWidget(gtk.Widget): ## gotten from experiments with "sans serif 8": self.degrees_per_radius = .80 self.generations = generations - self.rotate_value = 0 - self.data = {} - for i in range(self.generations): - self.data[i] = [None for j in range(2 ** i)] + self.rotate_value = 90 + self.set_generations(self.generations) self.center = 50 self.layout = self.create_pango_layout('cairo') self.layout.set_font_description(pango.FontDescription("sans serif 8")) + def reset_generations(self): + self.set_generations(self.generations) + def set_generations(self, generations): self.generations = generations + self.angle = {} self.data = {} for i in range(self.generations): self.data[i] = [None for j in range(2 ** i)] + self.angle[i] = [] + angle = 0 + slice = 360.0 / (2 ** i) + gender = True + for a in range(len(self.data[i])): + # start, stop, male, state + self.angle[i].append([angle, angle + slice, gender, 1]) + angle += slice + gender = not gender def do_realize(self): self.set_flags(self.flags() | gtk.REALIZED) @@ -132,16 +148,17 @@ class FanChartWidget(gtk.Widget): def _expose_cairo(self, event, cr): x, y, w, h = self.allocation cr.translate(w/2., h/2.) + cr.save() cr.rotate(self.rotate_value * math.pi/180) for generation in range(self.generations - 1, 0, -1): - slice = 360.0 / len(self.data[generation]) - current = 0 - for person in self.data[generation]: + for p in range(len(self.data[generation])): + person = self.data[generation][p] if person: + start, stop, male, state = self.angle[generation][p] name = name_displayer.display(person) gender = person.get_gender() - self.draw_person(cr, gender, name, current, generation) - current += slice + if state > 0: + self.draw_person(cr, gender, name, start, stop, generation, state) cr.set_source_rgb(1, 1, 1) # white cr.move_to(0,0) cr.arc(0, 0, self.center, 0, 2 * math.pi) @@ -151,48 +168,72 @@ class FanChartWidget(gtk.Widget): cr.arc(0, 0, self.center, 0, 2 * math.pi) cr.stroke() person = self.data[0][0] + # Draw center person: + cr.restore() if person: cr.save() name = name_displayer.display(person) - layout = self.create_pango_layout(name) - layout.set_font_description(pango.FontDescription("sans serif 8")) - cr.move_to(-self.center + 10, -4) - cr.show_layout(layout) + self.draw_text(cr, name, self.center - 10, 95, 455) + #layout = self.create_pango_layout(name) + #layout.set_font_description(pango.FontDescription("sans serif 8")) + #cr.move_to(-self.center + 10, -4) # start place for center text + #cr.show_layout(layout) cr.restore() fontw, fonth = self.layout.get_pixel_size() cr.move_to((w - fontw - 4), (h - fonth )) cr.update_layout(self.layout) cr.show_layout(self.layout) - def draw_person(self, cr, gender, name, start, generation): + def draw_person(self, cr, gender, name, start, stop, generation, state): + """ + Display the piece of pie for a given person. start and stop + are in degrees. + """ x, y, w, h = self.allocation start_rad = start * math.pi/180 - if gender == 0: - cr.set_source_rgb(1, 0.69, 0.69) # pink - elif gender == 1: - cr.set_source_rgb(0, 1, 1) # cyan + stop_rad = stop * math.pi/180 + r,g,b = self.GENCOLOR[generation % len(self.GENCOLOR)] + if gender == 1: + r -= r * .10 + g -= g * .10 + b -= b * .10 + cr.set_source_rgb(r/255., g/255., b/255.) radius = generation * self.pixels_per_generation + self.center cr.move_to(0, 0) - cr.arc(0, 0, radius, start_rad, start_rad + (2 * math.pi) / 2 ** generation) + cr.arc(0, 0, radius, start_rad, stop_rad) cr.move_to(0, 0) cr.fill() cr.set_source_rgb(0, 0, 0) # black - cr.arc(0, 0, radius, start_rad, start_rad) + cr.arc(0, 0, radius, start_rad, stop_rad) cr.line_to(0, 0) - cr.arc(0, 0, radius, start_rad, start_rad + (2 * math.pi) / 2 ** generation) + cr.arc(0, 0, radius, start_rad, stop_rad) cr.line_to(0, 0) - cr.set_line_width(1) + cr.set_line_width(state) cr.stroke() - degrees = 6 # self.text_degrees(name, radius)/2 - #FIXME: if degrees bigger than arc available, trim or wrap? - self.draw_text(cr, name, - radius - self.pixels_per_generation/2, - start + 90 + degrees) + self.draw_text(cr, name, radius - self.pixels_per_generation/2, + start, stop) def text_degrees(self, text, radius): + """ + Returns the number of degrees of text at a given radius. + """ return 360.0 * len(text)/(radius * self.degrees_per_radius) - def draw_text(self, cr, text, radius, pos): + def text_limit(self, text, degrees, radius): + while self.text_degrees(text, radius) > degrees: + text = text[:-1] + return text + + def draw_text(self, cr, text, radius, start, stop): + """ + Display text at a particular radius, between start and stop + degrees. + """ + # trim to fit: + text = self.text_limit(text, stop - start, radius - 15) + # center text: + pos = start + ((stop - start) - self.text_degrees(text, radius))/2.0 + 90 + # offset for cairo-font system is 90 x, y, w, h = self.allocation cr.save() # Create a PangoLayout, set the font and text @@ -202,7 +243,7 @@ class FanChartWidget(gtk.Widget): layout = self.create_pango_layout(text[i]) layout.set_font_description(pango.FontDescription("sans serif 8")) angle = 360.0 * i / (radius * self.degrees_per_radius) + pos - cr.set_source_rgb(0, 0, 1) # blue clickable + cr.set_source_rgb(0, 0, 0) # black cr.rotate(angle * (math.pi / 180)); # Inform Pango to re-layout the text with the new transformation cr.update_layout(layout) @@ -217,58 +258,124 @@ class FanChartGramplet(Gramplet): self.generations = 6 self.gui.fan = FanChartWidget(self.generations) self.gui.get_container_widget().remove(self.gui.textview) - vbox = gtk.VBox() - self.scale = gtk.HScale() - self.scale.set_draw_value(0) - self.scale.set_value_pos(gtk.POS_LEFT) - self.scale.set_range(0, 360) - self.scale.connect("value-changed", self.scale_changed) - vbox.pack_start(self.scale, False, False) - vbox.pack_start(self.gui.fan) - self.gui.get_container_widget().add_with_viewport(vbox) + self.gui.get_container_widget().add_with_viewport(self.gui.fan) #container is a gtk.ScrolledWindow container = self.gui.get_container_widget() container.connect("button-press-event", self.on_mouse_down) - vbox.show() self.gui.fan.show() - self.scale.show() - def scale_changed(self, widget): - self.gui.fan.rotate_value = widget.get_value() - self.gui.fan.queue_draw() + def expand_parents(self, generation, selected, to_right): + print "expanding", generation, selected + if generation >= self.generations: return + selected = 2 * selected + start,stop,male,state = self.gui.fan.angle[generation][selected] + slice = (stop - start) * 2.0 + current = start + if state > 0: + if to_right: + current = start+slice + self.gui.fan.angle[generation][selected] = start,current,male,state + else: + current = stop - slice + self.gui.fan.angle[generation][selected] = current,stop,male,state + self.expand_parents(generation + 1, selected, to_right) + start,stop,male,state = self.gui.fan.angle[generation][selected+1] + if state > 0: + self.gui.fan.angle[generation][selected+1] = current,current+slice,male,state + self.expand_parents(generation + 1, selected+1, to_right) + + def shrink_parents(self, generation, selected, from_right): + pass + + def change_slice(self, generation, selected): + gstart, gstop, gmale, gstate = self.gui.fan.angle[generation][selected] + if gstate == 1: # normal, let's expand + if gmale: + # go to right + stop = gstop + (gstop - gstart) + self.gui.fan.angle[generation][selected] = [gstart,stop,gmale,2] + start,stop,male,state = self.gui.fan.angle[generation][selected+1] + self.gui.fan.angle[generation][selected+1] = [stop,stop,male,0] + else: + # go to left + start = gstart - (gstop - gstart) + self.gui.fan.angle[generation][selected] = [start,gstop,gmale,2] + start,stop,male,state = self.gui.fan.angle[generation][selected-1] + self.gui.fan.angle[generation][selected-1] = [start,start,male,0] + # now we need to expand each of the slices outward: + #self.expand_parents(generation + 1, selected, gmale) + elif gstate == 2: # expanded, let's shrink + if gmale: + # shrink from right + slice = (gstop - gstart)/2.0 + stop = gstop - slice + self.gui.fan.angle[generation][selected] = [gstart,stop,gmale,1] + start,stop,male,state = self.gui.fan.angle[generation][selected+1] + self.gui.fan.angle[generation][selected+1] = [stop-slice,stop,male,1] + else: + # shrink from left + slice = (gstop - gstart)/2.0 + start = gstop - slice + self.gui.fan.angle[generation][selected] = [start,gstop,gmale,1] + start,stop,male,state = self.gui.fan.angle[generation][selected-1] + self.gui.fan.angle[generation][selected-1] = [start,start+slice,male,1] + # now we need to shrink each of the slices outward: + #self.shrink_parents(self, generation + 1, selected, gmale) def on_mouse_down(self, widget, e): # compute angle, radius, find out who would be there (rotated) x, y, w, h = self.gui.fan.allocation cx = w/2 cy = h/2 - radius = math.sqrt((e.x - cx) ** 2 + (e.y - cy - 20) ** 2) + radius = math.sqrt((e.x - cx) ** 2 + (e.y - cy) ** 2) if radius < self.gui.fan.center: generation = 0 else: generation = int((radius - self.gui.fan.center) / self.gui.fan.pixels_per_generation) + 1 - rads = math.atan2( (e.y - cy), (e.x - cx - 20) ) + rads = math.atan2( (e.y - cy), (e.x - cx) ) if rads < 0: rads = math.pi + (math.pi + rads) - pos = (rads/(math.pi * 2) - self.gui.fan.rotate_value/360.) - pos = int(math.floor(pos * 2 ** generation)) - pos = pos % (2 ** generation) - if (0 < generation < self.generations and - 0 <= pos < len(self.gui.fan.data[generation])): - person = self.gui.fan.data[generation][pos] - if person: - #name = name_displayer.display(person) - self.dbstate.change_active_person(person) + pos = ((rads/(math.pi * 2) - self.gui.fan.rotate_value/360.) * 360.0) % 360 + # find what person is in this position: + selected = None + if (0 < generation < self.generations): + for p in range(len(self.gui.fan.angle[generation])): + start, stop, male, state = self.gui.fan.angle[generation][p] + if state == 0: continue + if start <= pos <= stop: + selected = p + break + print "---------------" + print " angle:", pos + print " generation:", generation + print " selected:", selected + if selected == None: + if radius < self.gui.fan.center: + pass # TODO: select children + elif e.x > cx: # on right, rotate clockwise + self.gui.fan.rotate_value += 90.0 + else: # on left, rotate counterclockwise + self.gui.fan.rotate_value -= 90.0 + self.gui.fan.rotate_value %= 360 + self.gui.fan.queue_draw() + return True + self.change_slice(generation, selected) + person = self.gui.fan.data[generation][selected] + if person: + name = name_displayer.display(person) + print name, start, stop + #self.dbstate.change_active_person(person) + self.gui.fan.queue_draw() return True def db_changed(self): self.sa = SimpleAccess(self.dbstate.db) def active_changed(self, handle): - self.gui.fan.rotate_value = 0 - self.scale.set_value(0) - self.scale.set_value_pos(gtk.POS_LEFT) + # Reset everything + self.gui.fan.rotate_value = 90 + self.gui.fan.reset_generations() self.update() def main(self): @@ -277,8 +384,12 @@ class FanChartGramplet(Gramplet): parent = 0 for p in self.gui.fan.data[current - 1]: self.gui.fan.data[current][parent] = self.sa.father(p) + if self.gui.fan.data[current][parent] is None: + self.gui.fan.angle[current][parent][3] = 0 # start,stop,g,state parent += 1 self.gui.fan.data[current][parent] = self.sa.mother(p) + if self.gui.fan.data[current][parent] is None: + self.gui.fan.angle[current][parent][3] = 0 # start,stop,g,state parent += 1 self.gui.fan.queue_draw()