GTK3: working fanchart gramplet. moved reused part to gui.widgets

svn: r20252
This commit is contained in:
Benny Malengier 2012-08-22 15:50:34 +00:00
parent e42a90f3b1
commit 869777a651
6 changed files with 561 additions and 1011 deletions

View File

@ -477,6 +477,7 @@ src/gui/views/treemodels/treebasemodel.py
# gui/widgets - the GUI widgets package
src/gui/widgets/buttons.py
src/gui/widgets/expandcollapsearrow.py
src/gui/widgets/fanchart.py
src/gui/widgets/grampletpane.py
src/gui/widgets/labels.py
src/gui/widgets/monitoredwidgets.py

View File

@ -11,6 +11,7 @@ pkgpython_PYTHON = \
basicentry.py \
buttons.py \
expandcollapsearrow.py \
fanchart.py \
grampletpane.py \
labels.py \
linkbox.py \

View File

@ -26,6 +26,7 @@
from basicentry import *
from buttons import *
from expandcollapsearrow import *
from fanchart import *
from labels import *
from linkbox import *
from photo import *

555
src/gui/widgets/fanchart.py Normal file
View File

@ -0,0 +1,555 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2006 Donald N. Allingham
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
## Based on the paper:
## http://www.cs.utah.edu/~draperg/research/fanchart/draperg_FHT08.pdf
## and the applet:
## http://www.cs.utah.edu/~draperg/research/fanchart/demo/
## Found by redwood:
## http://www.gramps-project.org/bugs/view.php?id=2611
#-------------------------------------------------------------------------
#
# Python modules
#
#-------------------------------------------------------------------------
from gi.repository import Pango
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import PangoCairo
import math
#-------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------
from gen.display.name import displayer as name_displayer
import gen.lib
import gui.utils
#-------------------------------------------------------------------------
#
# Functions
#
#-------------------------------------------------------------------------
def gender_code(is_male):
"""
Given boolean is_male (means position in FanChart) return code.
"""
if is_male:
return gen.lib.Person.MALE
else:
return gen.lib.Person.FEMALE
#-------------------------------------------------------------------------
#
# FanChartWidget
#
#-------------------------------------------------------------------------
class FanChartWidget(Gtk.DrawingArea):
"""
Interactive Fan Chart Widget.
"""
BORDER_WIDTH = 10
GENCOLOR = ((229,191,252),
(191,191,252),
(191,222,252),
(183,219,197),
(206,246,209))
COLLAPSED = 0
NORMAL = 1
EXPANDED = 2
def __init__(self, generations, context_popup_callback=None):
"""
Fan Chart Widget. Handles visualization of data in self.data.
See main() of FanChartGramplet for example of model format.
"""
GObject.GObject.__init__(self)
self.translating = False
self.last_x, self.last_y = None, None
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)
self.connect("draw", self.on_draw)
self.context_popup_callback = context_popup_callback
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK |
Gdk.EventMask.POINTER_MOTION_MASK)
self.pixels_per_generation = 50 # size of radius for generation
## gotten from experiments with "sans serif 8":
self.degrees_per_radius = .80
## Other fonts will have different settings. Can you compute that
## from the font size? I have no idea.
self.generations = generations
self.rotate_value = 90 # degrees, initially, 1st gen male on right half
self.center_xy = [0, 0] # distance from center (x, y)
self.set_generations(self.generations)
self.center = 50 # pixel radius of center
self.layout = self.create_pango_layout('cairo')
self.layout.set_font_description(Pango.FontDescription("sans 8"))
self.set_size_request(120,120)
def reset_generations(self):
"""
Reset all of the data on where slices appear, and if they are expanded.
"""
self.set_generations(self.generations)
def set_generations(self, generations):
"""
Set the generations to max, and fill data structures with initial data.
"""
self.generations = generations
self.angle = {}
self.data = {}
for i in range(self.generations):
# name, person, parents?, children?
self.data[i] = [(None,) * 4] * 2 ** i
self.angle[i] = []
angle = 0
slice = 360.0 / (2 ** i)
gender = True
for count in range(len(self.data[i])):
# start, stop, male, state
self.angle[i].append([angle, angle + slice,gender,self.NORMAL])
angle += slice
gender = not gender
def do_size_request(self, requisition):
"""
Overridden method to handle size request events.
"""
width, height = self.layout.get_size()
requisition.width = (width // Pango.SCALE + self.BORDER_WIDTH*4)* 1.45
requisition.height = (3 * height // Pango.SCALE + self.BORDER_WIDTH*4) * 1.2
def do_get_preferred_width(self):
""" GTK3 uses width for height sizing model. This method will
override the virtual method
"""
req = Gtk.Requisition()
self.do_size_request(req)
return req.width, req.width
def do_get_preferred_height(self):
""" GTK3 uses width for height sizing model. This method will
override the virtual method
"""
req = Gtk.Requisition()
self.do_size_request(req)
return req.height, req.height
def on_draw(self, widget, cr):
"""
The main method to do the drawing.
"""
# first do size request of what we will need
nrgen = None
for generation in range(self.generations - 1, 0, -1):
for p in range(len(self.data[generation])):
(text, person, parents, child) = self.data[generation][p]
if person:
nrgen = generation
break
if nrgen is not None:
break
if nrgen is None:
nrgen = 1
halfdist = self.pixels_per_generation * nrgen + self.center
self.set_size_request(2 * halfdist, 2 * halfdist)
#obtain the allocation
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cr.translate(w/2. - self.center_xy[0], h/2. - self.center_xy[1])
cr.save()
cr.rotate(self.rotate_value * math.pi/180)
for generation in range(self.generations - 1, 0, -1):
for p in range(len(self.data[generation])):
(text, person, parents, child) = self.data[generation][p]
if person:
start, stop, male, state = self.angle[generation][p]
if state in [self.NORMAL, self.EXPANDED]:
self.draw_person(cr, gender_code(male),
text, start, stop,
generation, state, parents, child)
cr.set_source_rgb(1, 1, 1) # white
cr.move_to(0,0)
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.move_to(0,0)
cr.fill()
cr.set_source_rgb(0, 0, 0) # black
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.stroke()
# Draw center person:
(text, person, parents, child) = self.data[0][0]
cr.restore()
if person:
cr.save()
name = name_displayer.display(person)
self.draw_text(cr, name, self.center - 10, 95, 455)
cr.restore()
if child: # has at least one child
cr.set_source_rgb(0, 0, 0) # black
cr.move_to(0,0)
cr.arc(0, 0, 10, 0, 2 * math.pi)
cr.move_to(0,0)
cr.fill()
fontw, fonth = self.layout.get_pixel_size()
cr.move_to((w - fontw - 4), (h - fonth ))
self.layout.context_changed()
PangoCairo.show_layout(cr, self.layout)
def draw_person(self, cr, gender, name, start, stop, generation,
state, parents, child):
"""
Display the piece of pie for a given person. start and stop
are in degrees.
"""
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
start_rad = start * math.pi/180
stop_rad = stop * math.pi/180
r,g,b = self.GENCOLOR[generation % len(self.GENCOLOR)]
if gender == gen.lib.Person.MALE:
r *= .9
g *= .9
b *= .9
radius = generation * self.pixels_per_generation + self.center
# If max generation, and they have parents:
if generation == self.generations - 1 and parents:
# draw an indicator
cr.move_to(0, 0)
cr.set_source_rgb(255, 255, 255) # white
cr.arc(0, 0, radius + 10, start_rad, stop_rad)
cr.fill()
cr.move_to(0, 0)
cr.set_source_rgb(0, 0, 0) # black
cr.arc(0, 0, radius + 10, start_rad, stop_rad)
cr.line_to(0, 0)
cr.stroke()
cr.set_source_rgb(r/255., g/255., b/255.)
cr.move_to(0, 0)
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, stop_rad)
cr.line_to(0, 0)
cr.arc(0, 0, radius, start_rad, stop_rad)
cr.line_to(0, 0)
if state == self.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:
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 text_limit(self, text, degrees, radius):
"""
Trims the text to fit a given angle at a given radius. Probably
a better way to do this.
"""
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:
# offset for cairo-font system is 90:
pos = start + ((stop - start) - self.text_degrees(text,radius))/2.0 + 90
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cr.save()
# Create a PangoLayout, set the font and text
# Draw the layout N_WORDS times in a circle
for i in range(len(text)):
cr.save()
layout = self.create_pango_layout(text[i])
layout.set_font_description(Pango.FontDescription("sans 8"))
angle = 360.0 * i / (radius * self.degrees_per_radius) + pos
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
layout.context_changed()
width, height = layout.get_size()
cr.move_to(- (width / Pango.SCALE) / 2.0, - radius)
PangoCairo.show_layout(cr, layout)
cr.restore()
cr.restore()
def expand_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected+1, current)
def show_parents(self, generation, selected, angle, slice):
if generation >= self.generations: return
selected *= 2
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle + slice
self.angle[generation][selected][3] = self.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][3] = self.NORMAL
self.show_parents(generation+1, selected + 1, angle + slice, slice/2.0)
def hide_parents(self, generation, selected, angle):
if generation >= self.generations: return
selected = 2 * selected
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle
self.angle[generation][selected][3] = self.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][3] = self.COLLAPSED
self.hide_parents(generation + 1, selected+1, angle)
def shrink_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected] = [current, current + slice,
male,state]
self.shrink_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.shrink_parents(generation + 1, selected+1, current)
def change_slice(self, generation, selected):
gstart, gstop, gmale, gstate = self.angle[generation][selected]
if gstate == self.NORMAL: # let's expand
if gmale:
# go to right
stop = gstop + (gstop - gstart)
self.angle[generation][selected] = [gstart,stop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, gstart)
start,stop,male,state = self.angle[generation][selected+1]
self.angle[generation][selected+1] = [stop,stop,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected+1, stop)
else:
# go to left
start = gstart - (gstop - gstart)
self.angle[generation][selected] = [start,gstop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected-1, start)
elif gstate == self.EXPANDED: # let's shrink
if gmale:
# shrink from right
slice = (gstop - gstart)/2.0
stop = gstop - slice
self.angle[generation][selected] = [gstart,stop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, gstart)
self.angle[generation][selected+1][0] = stop # start
self.angle[generation][selected+1][1] = stop + slice # stop
self.angle[generation][selected+1][3] = self.NORMAL
self.show_parents(generation+1, selected+1, stop, slice/2.0)
else:
# shrink from left
slice = (gstop - gstart)/2.0
start = gstop - slice
self.angle[generation][selected] = [start,gstop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start+slice,male,
self.NORMAL]
self.show_parents(generation+1, selected-1, start, slice/2.0)
def on_mouse_up(self, widget, event):
# Done with mouse movement
if self.last_x is None or self.last_y is None:
return True
if self.translating:
self.translating = False
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
self.center_xy = w/2 - event.x, h/2 - event.y
self.last_x, self.last_y = None, None
self.queue_draw()
return True
def on_mouse_move(self, widget, event):
if self.last_x is None or self.last_y is None:
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
radius = math.sqrt((event.x - cx) ** 2 + (event.y - cy) ** 2)
selected = None
if radius < self.center:
generation = 0
selected = 0
else:
generation = int((radius - self.center) /
self.pixels_per_generation) + 1
rads = math.atan2( (event.y - cy), (event.x - cx) )
if rads < 0: # second half of unit circle
rads = math.pi + (math.pi + rads)
pos = ((rads/(math.pi * 2) - self.rotate_value/360.) * 360.0) % 360
if (0 < generation < self.generations):
for p in range(len(self.angle[generation])):
if self.data[generation][p][1]: # there is a person there
start, stop, male, state = self.angle[generation][p]
if state == self.COLLAPSED: continue
if start <= pos <= stop:
selected = p
break
tooltip = ""
if selected is not None:
text, person, parents, child = self.data[generation][selected]
if person:
tooltip = self.format_helper.format_person(person, 11)
self.set_tooltip_text(tooltip)
return False
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
if self.translating:
self.center_xy = w/2 - event.x, h/2 - event.y
self.queue_draw()
return True
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
# get the angles of the two points from the center:
start_angle = math.atan2(event.y - cy, event.x - cx)
end_angle = math.atan2(self.last_y - cy, self.last_x - cx)
if start_angle < 0: # second half of unit circle
start_angle = math.pi + (math.pi + start_angle)
if end_angle < 0: # second half of unit circle
end_angle = math.pi + (math.pi + end_angle)
# now look at change in angle:
diff_angle = (end_angle - start_angle) % (math.pi * 2.0)
self.rotate_value -= diff_angle * 180.0/ math.pi
self.queue_draw()
self.last_x, self.last_y = event.x, event.y
return True
def on_mouse_down(self, widget, event):
# compute angle, radius, find out who would be there (rotated)
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
self.translating = False # keep track of up/down/left/right movement
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
radius = math.sqrt((event.x - cx) ** 2 + (event.y - cy) ** 2)
if radius < self.center:
generation = 0
else:
generation = int((radius - self.center) /
self.pixels_per_generation) + 1
rads = math.atan2( (event.y - cy), (event.x - cx) )
if rads < 0: # second half of unit circle
rads = math.pi + (math.pi + rads)
pos = ((rads/(math.pi * 2) - self.rotate_value/360.) * 360.0) % 360
# 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):
for p in range(len(self.angle[generation])):
if self.data[generation][p][1]: # there is a person there
start, stop, male, state = self.angle[generation][p]
if state == self.COLLAPSED: continue
if start <= pos <= stop:
selected = p
break
# Handle the click:
if generation == 0:
# left mouse on center:
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
if selected is None: # clicked in open area, or center
if radius < self.center:
# right mouse
if gui.utils.is_right_click(event):
if self.data[0][0][1]:
self.context_popup_callback(widget, event,
self.data[0][0][1].handle)
return True
else:
return False
# else, what to do on left click?
else:
# save the mouse location for movements
self.last_x, self.last_y = event.x, event.y
return True
# Do things based on state, event.get_state(), or button, event.button
if event.button == 1: # left mouse
self.change_slice(generation, selected)
elif gui.utils.is_right_click(event):
text, person, parents, child = self.data[generation][selected]
if person and self.context_popup_callback:
self.context_popup_callback(widget, event, person.handle)
return True
self.queue_draw()
return True

View File

@ -58,492 +58,7 @@ import gen.lib
from gen.errors import WindowActiveError
from gui.editors import EditPerson
import gui.utils
#-------------------------------------------------------------------------
#
# Functions
#
#-------------------------------------------------------------------------
def gender_code(is_male):
"""
Given boolean is_male (means position in FanChart) return code.
"""
if is_male:
return gen.lib.Person.MALE
else:
return gen.lib.Person.FEMALE
#-------------------------------------------------------------------------
#
# FanChartWidget
#
#-------------------------------------------------------------------------
class FanChartWidget(Gtk.Widget):
"""
Interactive Fan Chart Widget.
"""
BORDER_WIDTH = 10
__gsignals__ = { 'realize': 'override',
'expose-event' : 'override',
'size-allocate': 'override',
'size-request': 'override',
}
GENCOLOR = ((229,191,252),
(191,191,252),
(191,222,252),
(183,219,197),
(206,246,209))
COLLAPSED = 0
NORMAL = 1
EXPANDED = 2
def __init__(self, generations, context_popup_callback=None):
"""
Fan Chart Widget. Handles visualization of data in self.data.
See main() of FanChartGramplet for example of model format.
"""
GObject.GObject.__init__(self)
self.translating = False
self.last_x, self.last_y = None, None
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)
self.context_popup_callback = context_popup_callback
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK |
Gdk.EventMask.POINTER_MOTION_MASK)
self.pixels_per_generation = 50 # size of radius for generation
## gotten from experiments with "sans serif 8":
self.degrees_per_radius = .80
## Other fonts will have different settings. Can you compute that
## from the font size? I have no idea.
self.generations = generations
self.rotate_value = 90 # degrees, initially, 1st gen male on right half
self.center_xy = [0, 0] # distance from center (x, y)
self.set_generations(self.generations)
self.center = 50 # pixel radius of center
self.layout = self.create_pango_layout('cairo')
self.layout.set_font_description(Pango.FontDescription("sans 8"))
def reset_generations(self):
"""
Reset all of the data on where slices appear, and if they are expanded.
"""
self.set_generations(self.generations)
def set_generations(self, generations):
"""
Set the generations to max, and fill data structures with initial data.
"""
self.generations = generations
self.angle = {}
self.data = {}
for i in range(self.generations):
# name, person, parents?, children?
self.data[i] = [(None,) * 4] * 2 ** i
self.angle[i] = []
angle = 0
slice = 360.0 / (2 ** i)
gender = True
for count in range(len(self.data[i])):
# start, stop, male, state
self.angle[i].append([angle, angle + slice,gender,self.NORMAL])
angle += slice
gender = not gender
def do_realize(self):
"""
Overriden method to handle the realize event.
"""
self.set_flags(self.flags() | Gtk.REALIZED)
self.window = Gdk.Window(self.get_parent_window(),
width=self.allocation.width,
height=self.allocation.height,
window_type=Gdk.WindowType.CHILD,
wclass=Gdk.WindowWindowClass.INPUT_OUTPUT,
event_mask=self.get_events() | Gdk.EventMask.EXPOSURE_MASK)
if not hasattr(self.window, "cairo_create"):
self.draw_gc = Gdk.GC(self.window,
line_width=5,
line_style=Gdk.SOLID,
join_style=Gdk.JOIN_ROUND)
self.window.set_user_data(self)
self.style.attach(self.window)
self.style.set_background(self.window, Gtk.StateType.NORMAL)
self.window.move_resize(*self.allocation)
def do_size_request(self, requisition):
"""
Overridden method to handle size request events.
"""
width, height = self.layout.get_size()
requisition.width = (width // Pango.SCALE + self.BORDER_WIDTH*4)* 1.45
requisition.height = (3 * height // Pango.SCALE + self.BORDER_WIDTH*4) * 1.2
def do_size_allocate(self, allocation):
"""
Overridden method to handle size allocation events.
"""
self.allocation = allocation
if self.get_realized():
self.window.move_resize(*allocation)
def _expose_gdk(self, event):
x, y, w, h = self.allocation
self.layout = self.create_pango_layout('no cairo')
fontw, fonth = self.layout.get_pixel_size()
self.style.paint_layout(self.window, self.state, False,
event.area, self, "label",
(w - fontw) / 2, (h - fonth) / 2,
self.layout)
def do_expose_event(self, event):
"""
Overridden method to handle expose events.
"""
try:
cr = self.window.cairo_create()
except AttributeError:
return self._expose_gdk(event)
return self._expose_cairo(event, cr)
def _expose_cairo(self, event, cr):
"""
The main method to do the drawing.
"""
x, y, w, h = self.allocation
cr.translate(w/2. - self.center_xy[0], h/2. - self.center_xy[1])
cr.save()
cr.rotate(self.rotate_value * math.pi/180)
for generation in range(self.generations - 1, 0, -1):
for p in range(len(self.data[generation])):
(text, person, parents, child) = self.data[generation][p]
if person:
start, stop, male, state = self.angle[generation][p]
if state in [self.NORMAL, self.EXPANDED]:
self.draw_person(cr, gender_code(male),
text, start, stop,
generation, state, parents, child)
cr.set_source_rgb(1, 1, 1) # white
cr.move_to(0,0)
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.move_to(0,0)
cr.fill()
cr.set_source_rgb(0, 0, 0) # black
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.stroke()
# Draw center person:
(text, person, parents, child) = self.data[0][0]
cr.restore()
if person:
cr.save()
name = name_displayer.display(person)
self.draw_text(cr, name, self.center - 10, 95, 455)
cr.restore()
if child: # has at least one child
cr.set_source_rgb(0, 0, 0) # black
cr.move_to(0,0)
cr.arc(0, 0, 10, 0, 2 * math.pi)
cr.move_to(0,0)
cr.fill()
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, stop, generation,
state, parents, child):
"""
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
stop_rad = stop * math.pi/180
r,g,b = self.GENCOLOR[generation % len(self.GENCOLOR)]
if gender == gen.lib.Person.MALE:
r *= .9
g *= .9
b *= .9
radius = generation * self.pixels_per_generation + self.center
# If max generation, and they have parents:
if generation == self.generations - 1 and parents:
# draw an indicator
cr.move_to(0, 0)
#cr.set_source_rgba(1, 0.2, 0.2, 0.6) # pink
cr.set_source_rgb(255, 255, 255) # white
cr.arc(0, 0, radius + 5, start_rad, stop_rad)
cr.fill()
cr.move_to(0, 0)
cr.set_source_rgb(0, 0, 0) # black
cr.arc(0, 0, radius + 5, start_rad, stop_rad)
cr.line_to(0, 0)
cr.stroke()
cr.set_source_rgb(r/255., g/255., b/255.)
cr.move_to(0, 0)
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, stop_rad)
cr.line_to(0, 0)
cr.arc(0, 0, radius, start_rad, stop_rad)
cr.line_to(0, 0)
if state == self.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:
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 text_limit(self, text, degrees, radius):
"""
Trims the text to fit a given angle at a given radius. Probably
a better way to do this.
"""
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:
# offset for cairo-font system is 90:
pos = start + ((stop - start) - self.text_degrees(text,radius))/2.0 + 90
x, y, w, h = self.allocation
cr.save()
# Create a PangoLayout, set the font and text
# Draw the layout N_WORDS times in a circle
for i in range(len(text)):
cr.save()
layout = self.create_pango_layout(text[i])
layout.set_font_description(Pango.FontDescription("sans 8"))
angle = 360.0 * i / (radius * self.degrees_per_radius) + pos
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)
width, height = layout.get_size()
cr.move_to(- (width / Pango.SCALE) / 2.0, - radius)
cr.show_layout(layout)
cr.restore()
cr.restore()
def expand_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected+1, current)
def show_parents(self, generation, selected, angle, slice):
if generation >= self.generations: return
selected *= 2
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle + slice
self.angle[generation][selected][3] = self.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][3] = self.NORMAL
self.show_parents(generation+1, selected + 1, angle + slice, slice/2.0)
def hide_parents(self, generation, selected, angle):
if generation >= self.generations: return
selected = 2 * selected
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle
self.angle[generation][selected][3] = self.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][3] = self.COLLAPSED
self.hide_parents(generation + 1, selected+1, angle)
def shrink_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected] = [current, current + slice,
male,state]
self.shrink_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.shrink_parents(generation + 1, selected+1, current)
def change_slice(self, generation, selected):
gstart, gstop, gmale, gstate = self.angle[generation][selected]
if gstate == self.NORMAL: # let's expand
if gmale:
# go to right
stop = gstop + (gstop - gstart)
self.angle[generation][selected] = [gstart,stop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, gstart)
start,stop,male,state = self.angle[generation][selected+1]
self.angle[generation][selected+1] = [stop,stop,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected+1, stop)
else:
# go to left
start = gstart - (gstop - gstart)
self.angle[generation][selected] = [start,gstop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected-1, start)
elif gstate == self.EXPANDED: # let's shrink
if gmale:
# shrink from right
slice = (gstop - gstart)/2.0
stop = gstop - slice
self.angle[generation][selected] = [gstart,stop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, gstart)
self.angle[generation][selected+1][0] = stop # start
self.angle[generation][selected+1][1] = stop + slice # stop
self.angle[generation][selected+1][3] = self.NORMAL
self.show_parents(generation+1, selected+1, stop, slice/2.0)
else:
# shrink from left
slice = (gstop - gstart)/2.0
start = gstop - slice
self.angle[generation][selected] = [start,gstop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start+slice,male,
self.NORMAL]
self.show_parents(generation+1, selected-1, start, slice/2.0)
def on_mouse_up(self, widget, event):
# Done with mouse movement
if self.last_x is None or self.last_y is None: return True
if self.translating:
self.translating = False
x, y, w, h = self.allocation
self.center_xy = w/2 - event.x, h/2 - event.y
self.last_x, self.last_y = None, None
self.queue_draw()
return True
def on_mouse_move(self, widget, event):
if self.last_x is None or self.last_y is None: return False
x, y, w, h = self.allocation
if self.translating:
self.center_xy = w/2 - event.x, h/2 - event.y
self.queue_draw()
return True
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
# get the angles of the two points from the center:
start_angle = math.atan2(event.y - cy, event.x - cx)
end_angle = math.atan2(self.last_y - cy, self.last_x - cx)
if start_angle < 0: # second half of unit circle
start_angle = math.pi + (math.pi + start_angle)
if end_angle < 0: # second half of unit circle
end_angle = math.pi + (math.pi + end_angle)
# now look at change in angle:
diff_angle = (end_angle - start_angle) % (math.pi * 2.0)
self.rotate_value -= diff_angle * 180.0/ math.pi
self.queue_draw()
self.last_x, self.last_y = event.x, event.y
return True
def on_mouse_down(self, widget, event):
# compute angle, radius, find out who would be there (rotated)
x, y, w, h = self.allocation
self.translating = False # keep track of up/down/left/right movement
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
radius = math.sqrt((event.x - cx) ** 2 + (event.y - cy) ** 2)
if radius < self.center:
generation = 0
else:
generation = int((radius - self.center) /
self.pixels_per_generation) + 1
rads = math.atan2( (event.y - cy), (event.x - cx) )
if rads < 0: # second half of unit circle
rads = math.pi + (math.pi + rads)
pos = ((rads/(math.pi * 2) - self.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.angle[generation])):
if self.data[generation][p][1]: # there is a person there
start, stop, male, state = self.angle[generation][p]
if state == self.COLLAPSED: continue
if start <= pos <= stop:
selected = p
break
# Handle the click:
if generation == 0:
# left mouse on center:
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
if selected is None: # clicked in open area, or center
if radius < self.center:
# right mouse
if (gui.utils.is_right_click(event)
and self.context_popup_callback):
if self.data[0][0][1]:
self.context_popup_callback(widget, event,
self.data[0][0][1].handle)
return True
else:
return False
# else, what to do on left click?
else:
# save the mouse location for movements
self.last_x, self.last_y = event.x, event.y
return True
# Do things based on state, event.get_state(), or button, event.button
if event.button == 1: # left mouse
self.change_slice(generation, selected)
elif gui.utils.is_right_click(event): # right mouse
text, person, parents, child = self.data[generation][selected]
if person and self.context_popup_callback:
self.context_popup_callback(widget, event, person.handle)
return True
self.queue_draw()
return True
from gui.widgets.fanchart import FanChartWidget
class FanChartGramplet(Gramplet):
"""
@ -555,6 +70,7 @@ class FanChartGramplet(Gramplet):
self.format_helper = FormattingHelper(self.dbstate)
self.gui.fan = FanChartWidget(self.generations,
context_popup_callback=self.on_popup)
self.gui.fan.format_helper = self.format_helper
# 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.gui.fan)

View File

@ -32,13 +32,8 @@
# 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
from cgi import escape
from gen.ggettext import gettext as _
@ -51,530 +46,11 @@ from gen.display.name import displayer as name_displayer
from gen.utils.db import (find_children, find_parents, find_witnessed_people)
from libformatting import FormattingHelper
import gen.lib
from gui.widgets.fanchart import FanChartWidget
from gui.views.navigationview import NavigationView
from gen.errors import WindowActiveError
from gui.views.bookmarks import PersonBookmarks
from gui.editors import EditPerson
import gui.utils
#-------------------------------------------------------------------------
#
# Functions
#
#-------------------------------------------------------------------------
def gender_code(is_male):
"""
Given boolean is_male (means position in FanChart) return code.
"""
if is_male:
return gen.lib.Person.MALE
else:
return gen.lib.Person.FEMALE
class AttachList(object):
def __init__(self):
self.list = []
self.max_x = 0
self.max_y = 0
def attach(self, widget, x0, x1, y0, y1, xoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL,
yoptions=Gtk.AttachOptions.EXPAND|Gtk.AttachOptions.FILL):
assert(widget)
assert(x1>x0)
self.list.append((widget, x0, x1, y0, y1, xoptions, yoptions))
self.max_x = max(self.max_x, x1)
self.max_y = max(self.max_y, y1)
#-------------------------------------------------------------------------
#
# FanChartWidget
#
#-------------------------------------------------------------------------
class FanChartWidget(Gtk.DrawingArea):
"""
Interactive Fan Chart Widget.
"""
BORDER_WIDTH = 10
GENCOLOR = ((229,191,252),
(191,191,252),
(191,222,252),
(183,219,197),
(206,246,209))
COLLAPSED = 0
NORMAL = 1
EXPANDED = 2
def __init__(self, generations, context_popup_callback=None):
"""
Fan Chart Widget. Handles visualization of data in self.data.
See main() of FanChartGramplet for example of model format.
"""
GObject.GObject.__init__(self)
self.translating = False
self.last_x, self.last_y = None, None
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)
self.connect("draw", self.on_draw)
self.context_popup_callback = context_popup_callback
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK |
Gdk.EventMask.POINTER_MOTION_MASK)
self.pixels_per_generation = 50 # size of radius for generation
## gotten from experiments with "sans serif 8":
self.degrees_per_radius = .80
## Other fonts will have different settings. Can you compute that
## from the font size? I have no idea.
self.generations = generations
self.rotate_value = 90 # degrees, initially, 1st gen male on right half
self.center_xy = [0, 0] # distance from center (x, y)
self.set_generations(self.generations)
self.center = 50 # pixel radius of center
self.layout = self.create_pango_layout('cairo')
self.layout.set_font_description(Pango.FontDescription("sans 8"))
self.set_size_request(120,120)
def reset_generations(self):
"""
Reset all of the data on where slices appear, and if they are expanded.
"""
self.set_generations(self.generations)
def set_generations(self, generations):
"""
Set the generations to max, and fill data structures with initial data.
"""
self.generations = generations
self.angle = {}
self.data = {}
for i in range(self.generations):
# name, person, parents?, children?
self.data[i] = [(None,) * 4] * 2 ** i
self.angle[i] = []
angle = 0
slice = 360.0 / (2 ** i)
gender = True
for count in range(len(self.data[i])):
# start, stop, male, state
self.angle[i].append([angle, angle + slice,gender,self.NORMAL])
angle += slice
gender = not gender
def do_size_request(self, requisition):
"""
Overridden method to handle size request events.
"""
width, height = self.layout.get_size()
requisition.width = (width // Pango.SCALE + self.BORDER_WIDTH*4)* 1.45
requisition.height = (3 * height // Pango.SCALE + self.BORDER_WIDTH*4) * 1.2
def do_get_preferred_width(self):
""" GTK3 uses width for height sizing model. This method will
override the virtual method
"""
req = Gtk.Requisition()
self.do_size_request(req)
return req.width, req.width
def do_get_preferred_height(self):
""" GTK3 uses width for height sizing model. This method will
override the virtual method
"""
req = Gtk.Requisition()
self.do_size_request(req)
return req.height, req.height
def on_draw(self, widget, cr):
"""
The main method to do the drawing.
"""
# first do size request of what we will need
nrgen = None
for generation in range(self.generations - 1, 0, -1):
for p in range(len(self.data[generation])):
(text, person, parents, child) = self.data[generation][p]
if person:
nrgen = generation
break
if nrgen is not None:
break
if nrgen is None:
nrgen = 1
halfdist = self.pixels_per_generation * nrgen + self.center
self.set_size_request(2 * halfdist, 2 * halfdist)
#obtain the allocation
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cr.translate(w/2. - self.center_xy[0], h/2. - self.center_xy[1])
cr.save()
cr.rotate(self.rotate_value * math.pi/180)
for generation in range(self.generations - 1, 0, -1):
for p in range(len(self.data[generation])):
(text, person, parents, child) = self.data[generation][p]
if person:
start, stop, male, state = self.angle[generation][p]
if state in [self.NORMAL, self.EXPANDED]:
self.draw_person(cr, gender_code(male),
text, start, stop,
generation, state, parents, child)
cr.set_source_rgb(1, 1, 1) # white
cr.move_to(0,0)
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.move_to(0,0)
cr.fill()
cr.set_source_rgb(0, 0, 0) # black
cr.arc(0, 0, self.center, 0, 2 * math.pi)
cr.stroke()
# Draw center person:
(text, person, parents, child) = self.data[0][0]
cr.restore()
if person:
cr.save()
name = name_displayer.display(person)
self.draw_text(cr, name, self.center - 10, 95, 455)
cr.restore()
if child: # has at least one child
cr.set_source_rgb(0, 0, 0) # black
cr.move_to(0,0)
cr.arc(0, 0, 10, 0, 2 * math.pi)
cr.move_to(0,0)
cr.fill()
fontw, fonth = self.layout.get_pixel_size()
cr.move_to((w - fontw - 4), (h - fonth ))
self.layout.context_changed()
PangoCairo.show_layout(cr, self.layout)
def draw_person(self, cr, gender, name, start, stop, generation,
state, parents, child):
"""
Display the piece of pie for a given person. start and stop
are in degrees.
"""
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
start_rad = start * math.pi/180
stop_rad = stop * math.pi/180
r,g,b = self.GENCOLOR[generation % len(self.GENCOLOR)]
if gender == gen.lib.Person.MALE:
r *= .9
g *= .9
b *= .9
radius = generation * self.pixels_per_generation + self.center
# If max generation, and they have parents:
if generation == self.generations - 1 and parents:
# draw an indicator
cr.move_to(0, 0)
cr.set_source_rgb(255, 255, 255) # white
cr.arc(0, 0, radius + 10, start_rad, stop_rad)
cr.fill()
cr.move_to(0, 0)
cr.set_source_rgb(0, 0, 0) # black
cr.arc(0, 0, radius + 10, start_rad, stop_rad)
cr.line_to(0, 0)
cr.stroke()
cr.set_source_rgb(r/255., g/255., b/255.)
cr.move_to(0, 0)
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, stop_rad)
cr.line_to(0, 0)
cr.arc(0, 0, radius, start_rad, stop_rad)
cr.line_to(0, 0)
if state == self.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:
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 text_limit(self, text, degrees, radius):
"""
Trims the text to fit a given angle at a given radius. Probably
a better way to do this.
"""
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:
# offset for cairo-font system is 90:
pos = start + ((stop - start) - self.text_degrees(text,radius))/2.0 + 90
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cr.save()
# Create a PangoLayout, set the font and text
# Draw the layout N_WORDS times in a circle
for i in range(len(text)):
cr.save()
layout = self.create_pango_layout(text[i])
layout.set_font_description(Pango.FontDescription("sans 8"))
angle = 360.0 * i / (radius * self.degrees_per_radius) + pos
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
layout.context_changed()
width, height = layout.get_size()
cr.move_to(- (width / Pango.SCALE) / 2.0, - radius)
PangoCairo.show_layout(cr, layout)
cr.restore()
cr.restore()
def expand_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) * 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.expand_parents(generation + 1, selected+1, current)
def show_parents(self, generation, selected, angle, slice):
if generation >= self.generations: return
selected *= 2
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle + slice
self.angle[generation][selected][3] = self.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][3] = self.NORMAL
self.show_parents(generation+1, selected + 1, angle + slice, slice/2.0)
def hide_parents(self, generation, selected, angle):
if generation >= self.generations: return
selected = 2 * selected
self.angle[generation][selected][0] = angle
self.angle[generation][selected][1] = angle
self.angle[generation][selected][3] = self.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][3] = self.COLLAPSED
self.hide_parents(generation + 1, selected+1, angle)
def shrink_parents(self, generation, selected, current):
if generation >= self.generations: return
selected = 2 * selected
start,stop,male,state = self.angle[generation][selected]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected] = [current, current + slice,
male,state]
self.shrink_parents(generation + 1, selected, current)
current += slice
start,stop,male,state = self.angle[generation][selected+1]
if state in [self.NORMAL, self.EXPANDED]:
slice = (stop - start) / 2.0
self.angle[generation][selected+1] = [current,current+slice,
male,state]
self.shrink_parents(generation + 1, selected+1, current)
def change_slice(self, generation, selected):
gstart, gstop, gmale, gstate = self.angle[generation][selected]
if gstate == self.NORMAL: # let's expand
if gmale:
# go to right
stop = gstop + (gstop - gstart)
self.angle[generation][selected] = [gstart,stop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, gstart)
start,stop,male,state = self.angle[generation][selected+1]
self.angle[generation][selected+1] = [stop,stop,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected+1, stop)
else:
# go to left
start = gstart - (gstop - gstart)
self.angle[generation][selected] = [start,gstop,gmale,
self.EXPANDED]
self.expand_parents(generation + 1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start,male,
self.COLLAPSED]
self.hide_parents(generation+1, selected-1, start)
elif gstate == self.EXPANDED: # let's shrink
if gmale:
# shrink from right
slice = (gstop - gstart)/2.0
stop = gstop - slice
self.angle[generation][selected] = [gstart,stop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, gstart)
self.angle[generation][selected+1][0] = stop # start
self.angle[generation][selected+1][1] = stop + slice # stop
self.angle[generation][selected+1][3] = self.NORMAL
self.show_parents(generation+1, selected+1, stop, slice/2.0)
else:
# shrink from left
slice = (gstop - gstart)/2.0
start = gstop - slice
self.angle[generation][selected] = [start,gstop,gmale,
self.NORMAL]
self.shrink_parents(generation+1, selected, start)
start,stop,male,state = self.angle[generation][selected-1]
self.angle[generation][selected-1] = [start,start+slice,male,
self.NORMAL]
self.show_parents(generation+1, selected-1, start, slice/2.0)
def on_mouse_up(self, widget, event):
# Done with mouse movement
if self.last_x is None or self.last_y is None:
return True
if self.translating:
self.translating = False
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
self.center_xy = w/2 - event.x, h/2 - event.y
self.last_x, self.last_y = None, None
self.queue_draw()
return True
def on_mouse_move(self, widget, event):
if self.last_x is None or self.last_y is None:
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
radius = math.sqrt((event.x - cx) ** 2 + (event.y - cy) ** 2)
selected = None
if radius < self.center:
generation = 0
selected = 0
else:
generation = int((radius - self.center) /
self.pixels_per_generation) + 1
rads = math.atan2( (event.y - cy), (event.x - cx) )
if rads < 0: # second half of unit circle
rads = math.pi + (math.pi + rads)
pos = ((rads/(math.pi * 2) - self.rotate_value/360.) * 360.0) % 360
if (0 < generation < self.generations):
for p in range(len(self.angle[generation])):
if self.data[generation][p][1]: # there is a person there
start, stop, male, state = self.angle[generation][p]
if state == self.COLLAPSED: continue
if start <= pos <= stop:
selected = p
break
tooltip = ""
if selected is not None:
text, person, parents, child = self.data[generation][selected]
if person:
tooltip = self.format_helper.format_person(person, 11)
self.set_tooltip_text(tooltip)
return False
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
if self.translating:
self.center_xy = w/2 - event.x, h/2 - event.y
self.queue_draw()
return True
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
# get the angles of the two points from the center:
start_angle = math.atan2(event.y - cy, event.x - cx)
end_angle = math.atan2(self.last_y - cy, self.last_x - cx)
if start_angle < 0: # second half of unit circle
start_angle = math.pi + (math.pi + start_angle)
if end_angle < 0: # second half of unit circle
end_angle = math.pi + (math.pi + end_angle)
# now look at change in angle:
diff_angle = (end_angle - start_angle) % (math.pi * 2.0)
self.rotate_value -= diff_angle * 180.0/ math.pi
self.queue_draw()
self.last_x, self.last_y = event.x, event.y
return True
def on_mouse_down(self, widget, event):
# compute angle, radius, find out who would be there (rotated)
alloc = self.get_allocation()
x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
self.translating = False # keep track of up/down/left/right movement
cx = w/2 - self.center_xy[0]
cy = h/2 - self.center_xy[1]
radius = math.sqrt((event.x - cx) ** 2 + (event.y - cy) ** 2)
if radius < self.center:
generation = 0
else:
generation = int((radius - self.center) /
self.pixels_per_generation) + 1
rads = math.atan2( (event.y - cy), (event.x - cx) )
if rads < 0: # second half of unit circle
rads = math.pi + (math.pi + rads)
pos = ((rads/(math.pi * 2) - self.rotate_value/360.) * 360.0) % 360
# 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):
for p in range(len(self.angle[generation])):
if self.data[generation][p][1]: # there is a person there
start, stop, male, state = self.angle[generation][p]
if state == self.COLLAPSED: continue
if start <= pos <= stop:
selected = p
break
# Handle the click:
if generation == 0:
# left mouse on center:
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
if selected is None: # clicked in open area, or center
if radius < self.center:
# right mouse
if gui.utils.is_right_click(event):
if self.data[0][0][1]:
self.context_popup_callback(widget, event,
self.data[0][0][1].handle)
return True
else:
return False
# else, what to do on left click?
else:
# save the mouse location for movements
self.last_x, self.last_y = event.x, event.y
return True
# Do things based on state, event.get_state(), or button, event.button
if event.button == 1: # left mouse
self.change_slice(generation, selected)
elif gui.utils.is_right_click(event):
text, person, parents, child = self.data[generation][selected]
if person and self.context_popup_callback:
self.context_popup_callback(widget, event, person.handle)
return True
self.queue_draw()
return True
class FanChartView(NavigationView):
"""