GTK3: working fanchart gramplet. moved reused part to gui.widgets
svn: r20252
This commit is contained in:
parent
e42a90f3b1
commit
869777a651
@ -477,6 +477,7 @@ src/gui/views/treemodels/treebasemodel.py
|
|||||||
# gui/widgets - the GUI widgets package
|
# gui/widgets - the GUI widgets package
|
||||||
src/gui/widgets/buttons.py
|
src/gui/widgets/buttons.py
|
||||||
src/gui/widgets/expandcollapsearrow.py
|
src/gui/widgets/expandcollapsearrow.py
|
||||||
|
src/gui/widgets/fanchart.py
|
||||||
src/gui/widgets/grampletpane.py
|
src/gui/widgets/grampletpane.py
|
||||||
src/gui/widgets/labels.py
|
src/gui/widgets/labels.py
|
||||||
src/gui/widgets/monitoredwidgets.py
|
src/gui/widgets/monitoredwidgets.py
|
||||||
|
@ -11,6 +11,7 @@ pkgpython_PYTHON = \
|
|||||||
basicentry.py \
|
basicentry.py \
|
||||||
buttons.py \
|
buttons.py \
|
||||||
expandcollapsearrow.py \
|
expandcollapsearrow.py \
|
||||||
|
fanchart.py \
|
||||||
grampletpane.py \
|
grampletpane.py \
|
||||||
labels.py \
|
labels.py \
|
||||||
linkbox.py \
|
linkbox.py \
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
from basicentry import *
|
from basicentry import *
|
||||||
from buttons import *
|
from buttons import *
|
||||||
from expandcollapsearrow import *
|
from expandcollapsearrow import *
|
||||||
|
from fanchart import *
|
||||||
from labels import *
|
from labels import *
|
||||||
from linkbox import *
|
from linkbox import *
|
||||||
from photo import *
|
from photo import *
|
||||||
|
555
src/gui/widgets/fanchart.py
Normal file
555
src/gui/widgets/fanchart.py
Normal 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
|
@ -58,492 +58,7 @@ import gen.lib
|
|||||||
from gen.errors import WindowActiveError
|
from gen.errors import WindowActiveError
|
||||||
from gui.editors import EditPerson
|
from gui.editors import EditPerson
|
||||||
import gui.utils
|
import gui.utils
|
||||||
|
from gui.widgets.fanchart import FanChartWidget
|
||||||
#-------------------------------------------------------------------------
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
class FanChartGramplet(Gramplet):
|
class FanChartGramplet(Gramplet):
|
||||||
"""
|
"""
|
||||||
@ -555,6 +70,7 @@ class FanChartGramplet(Gramplet):
|
|||||||
self.format_helper = FormattingHelper(self.dbstate)
|
self.format_helper = FormattingHelper(self.dbstate)
|
||||||
self.gui.fan = FanChartWidget(self.generations,
|
self.gui.fan = FanChartWidget(self.generations,
|
||||||
context_popup_callback=self.on_popup)
|
context_popup_callback=self.on_popup)
|
||||||
|
self.gui.fan.format_helper = self.format_helper
|
||||||
# Replace the standard textview with the fan chart widget:
|
# Replace the standard textview with the fan chart widget:
|
||||||
self.gui.get_container_widget().remove(self.gui.textview)
|
self.gui.get_container_widget().remove(self.gui.textview)
|
||||||
self.gui.get_container_widget().add_with_viewport(self.gui.fan)
|
self.gui.get_container_widget().add_with_viewport(self.gui.fan)
|
||||||
|
@ -32,13 +32,8 @@
|
|||||||
# Python modules
|
# Python modules
|
||||||
#
|
#
|
||||||
#-------------------------------------------------------------------------
|
#-------------------------------------------------------------------------
|
||||||
from gi.repository import Pango
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Gdk
|
from gi.repository import Gdk
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
from gi.repository import PangoCairo
|
|
||||||
import cairo
|
|
||||||
import math
|
|
||||||
from cgi import escape
|
from cgi import escape
|
||||||
from gen.ggettext import gettext as _
|
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 gen.utils.db import (find_children, find_parents, find_witnessed_people)
|
||||||
from libformatting import FormattingHelper
|
from libformatting import FormattingHelper
|
||||||
import gen.lib
|
import gen.lib
|
||||||
|
from gui.widgets.fanchart import FanChartWidget
|
||||||
from gui.views.navigationview import NavigationView
|
from gui.views.navigationview import NavigationView
|
||||||
from gen.errors import WindowActiveError
|
from gen.errors import WindowActiveError
|
||||||
from gui.views.bookmarks import PersonBookmarks
|
from gui.views.bookmarks import PersonBookmarks
|
||||||
from gui.editors import EditPerson
|
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):
|
class FanChartView(NavigationView):
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user