4571: RTL support in fanchart
Merge my work on trunk to gramps40 now that 4.0.1 is out, as this fix is targeted for 4.0.2. svn: r22588
This commit is contained in:
parent
eb1c153280
commit
71abad6b6a
87
gramps/gui/utilscairo.py
Normal file
87
gramps/gui/utilscairo.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#
|
||||||
|
# Gramps - a GTK+/GNOME based genealogy program
|
||||||
|
#
|
||||||
|
# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
|
||||||
|
# Copyright (C) 2009 Douglas S. Blank
|
||||||
|
# Copyright (C) 2012 Benny Malengier
|
||||||
|
# Copyright (C) 2013 Vassilii Khachaturov
|
||||||
|
#
|
||||||
|
# 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$
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Python modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#from gi.repository import Pango
|
||||||
|
#from gi.repository import GObject
|
||||||
|
#from gi.repository import Gdk
|
||||||
|
#from gi.repository import Gtk
|
||||||
|
#from gi.repository import PangoCairo
|
||||||
|
import cairo
|
||||||
|
#import math
|
||||||
|
#import colorsys
|
||||||
|
#import sys
|
||||||
|
#if sys.version_info[0] < 3:
|
||||||
|
# import cPickle as pickle
|
||||||
|
#else:
|
||||||
|
# import pickle
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Functions
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
def warpPath(ctx, function):
|
||||||
|
"""Transform a path given a 2D transformation function.
|
||||||
|
|
||||||
|
ctx -- a cairo.Context, on which the path is set
|
||||||
|
function -- a 2D transform (x,y) |-> (x_new,y_new)
|
||||||
|
|
||||||
|
The transformed path replaces the original one on the context.
|
||||||
|
|
||||||
|
Taken from /usr/share/doc/python-cairo/examples/warpedtext.py
|
||||||
|
According to /usr/share/doc/python-cairo/copyright, licensed
|
||||||
|
under MOZILLA PUBLIC LICENSE 1.1, see that file for more detail.
|
||||||
|
"""
|
||||||
|
|
||||||
|
first = True
|
||||||
|
|
||||||
|
for type, points in ctx.copy_path():
|
||||||
|
if type == cairo.PATH_MOVE_TO:
|
||||||
|
if first:
|
||||||
|
ctx.new_path()
|
||||||
|
first = False
|
||||||
|
x, y = function(*points)
|
||||||
|
ctx.move_to(x, y)
|
||||||
|
|
||||||
|
elif type == cairo.PATH_LINE_TO:
|
||||||
|
x, y = function(*points)
|
||||||
|
ctx.line_to(x, y)
|
||||||
|
|
||||||
|
elif type == cairo.PATH_CURVE_TO:
|
||||||
|
x1, y1, x2, y2, x3, y3 = points
|
||||||
|
x1, y1 = function(x1, y1)
|
||||||
|
x2, y2 = function(x2, y2)
|
||||||
|
x3, y3 = function(x3, y3)
|
||||||
|
ctx.curve_to(x1, y1, x2, y2, x3, y3)
|
||||||
|
|
||||||
|
elif type == cairo.PATH_CLOSE_PATH:
|
||||||
|
ctx.close_path()
|
@ -4,6 +4,7 @@
|
|||||||
# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
|
# Copyright (C) 2001-2007 Donald N. Allingham, Martin Hawlisch
|
||||||
# Copyright (C) 2009 Douglas S. Blank
|
# Copyright (C) 2009 Douglas S. Blank
|
||||||
# Copyright (C) 2012 Benny Malengier
|
# Copyright (C) 2012 Benny Malengier
|
||||||
|
# Copyright (C) 2013 Vassilii Khachaturov
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -72,6 +73,7 @@ from gramps.gen.utils.db import (find_children, find_parents, find_witnessed_peo
|
|||||||
get_age, get_timeperiod, preset_name)
|
get_age, get_timeperiod, preset_name)
|
||||||
from gramps.gen.const import GRAMPS_LOCALE as glocale
|
from gramps.gen.const import GRAMPS_LOCALE as glocale
|
||||||
_ = glocale.translation.gettext
|
_ = glocale.translation.gettext
|
||||||
|
from gramps.gui.utilscairo import warpPath
|
||||||
|
|
||||||
#-------------------------------------------------------------------------
|
#-------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
@ -553,6 +555,22 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
cr.set_source_rgba(r/255., g/255., b/255., a)
|
cr.set_source_rgba(r/255., g/255., b/255., a)
|
||||||
cr.fill()
|
cr.fill()
|
||||||
|
|
||||||
|
def wrap_truncate_layout(self, layout, font, width_pixels):
|
||||||
|
"""Uses the layout to wrap and truncate its text to given width
|
||||||
|
|
||||||
|
Returns: (w,h) as returned by layout.get_pixel_size()
|
||||||
|
"""
|
||||||
|
|
||||||
|
layout.set_font_description(font)
|
||||||
|
layout.set_width(Pango.SCALE * width_pixels)
|
||||||
|
|
||||||
|
# NOTE: one may not truncate the text to just the 1st line's text,
|
||||||
|
# because the truncation can create invalid Unicode.
|
||||||
|
if layout.get_line_count() > 1:
|
||||||
|
layout.set_text(layout.get_text(), layout.get_line(0).length)
|
||||||
|
|
||||||
|
return layout.get_pixel_size()
|
||||||
|
|
||||||
def draw_text(self, cr, text, radius, start, stop,
|
def draw_text(self, cr, text, radius, start, stop,
|
||||||
height=PIXELS_PER_GENERATION, radial=False,
|
height=PIXELS_PER_GENERATION, radial=False,
|
||||||
fontcolor=(0, 0, 0), bold=False):
|
fontcolor=(0, 0, 0), bold=False):
|
||||||
@ -566,29 +584,31 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
font.set_size(fontsize * Pango.SCALE)
|
font.set_size(fontsize * Pango.SCALE)
|
||||||
if bold:
|
if bold:
|
||||||
font.set_weight(Pango.Weight.BOLD)
|
font.set_weight(Pango.Weight.BOLD)
|
||||||
cr.set_source_rgb(fontcolor[0], fontcolor[1], fontcolor[2])
|
cr.set_source_rgb(*fontcolor)
|
||||||
if radial and self.radialtext:
|
if radial and self.radialtext:
|
||||||
cr.save()
|
cr.save()
|
||||||
layout = self.create_pango_layout(text)
|
layout = self.create_pango_layout(text)
|
||||||
layout.set_font_description(font)
|
layout.set_font_description(font)
|
||||||
w, h = layout.get_size()
|
layout.set_wrap(Pango.WrapMode.CHAR)
|
||||||
w = w / Pango.SCALE + 5 # 5 pixel padding
|
|
||||||
h = h / Pango.SCALE + 4 # 4 pixel padding
|
# NOTE: for radial text, the sector radius height is the text width
|
||||||
|
w, h = self.wrap_truncate_layout(layout, font, height - 2*PAD_TEXT)
|
||||||
|
|
||||||
|
w = w + 5 # 5 pixel padding
|
||||||
|
h = h + 4 # 4 pixel padding
|
||||||
#first we check if height is ok
|
#first we check if height is ok
|
||||||
degneedheight = h / radius * (180 / math.pi)
|
degneedheight = math.degrees(h / radius)
|
||||||
degavailheight = stop-start
|
degavailheight = stop-start
|
||||||
degoffsetheight = 0
|
degoffsetheight = 0
|
||||||
if degneedheight > degavailheight:
|
if degneedheight > degavailheight:
|
||||||
#reduce height
|
#reduce height
|
||||||
fontsize = degavailheight / degneedheight * fontsize / 2
|
fontsize = degavailheight / degneedheight * fontsize / 2
|
||||||
font.set_size(fontsize * Pango.SCALE)
|
font.set_size(fontsize * Pango.SCALE)
|
||||||
layout = self.create_pango_layout(text)
|
w, h = self.wrap_truncate_layout(layout, font, height - 2*PAD_TEXT)
|
||||||
layout.set_font_description(font)
|
w = w + 5 # 5 pixel padding
|
||||||
w, h = layout.get_size()
|
h = h + 4 # 4 pixel padding
|
||||||
w = w / Pango.SCALE + 5 # 5 pixel padding
|
|
||||||
h = h / Pango.SCALE + 4 # 4 pixel padding
|
|
||||||
#first we check if height is ok
|
#first we check if height is ok
|
||||||
degneedheight = h / radius * (180 / math.pi)
|
degneedheight = math.degrees(h / radius)
|
||||||
degavailheight = stop-start
|
degavailheight = stop-start
|
||||||
if degneedheight > degavailheight:
|
if degneedheight > degavailheight:
|
||||||
#we could not fix it, no text
|
#we could not fix it, no text
|
||||||
@ -596,31 +616,13 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
if text:
|
if text:
|
||||||
#spread rest
|
#spread rest
|
||||||
degoffsetheight = (degavailheight - degneedheight) / 2
|
degoffsetheight = (degavailheight - degneedheight) / 2
|
||||||
txlen = len(text)
|
|
||||||
if w > height - PAD_TEXT:
|
|
||||||
txlen = int(w/height * txlen)
|
|
||||||
cont = True
|
|
||||||
while cont:
|
|
||||||
layout = self.create_pango_layout(text[:txlen])
|
|
||||||
layout.set_font_description(font)
|
|
||||||
w, h = layout.get_size()
|
|
||||||
w = w / Pango.SCALE + 2*PAD_TEXT # padding before/after
|
|
||||||
h = h / Pango.SCALE + 4 # padding in height text
|
|
||||||
if w > height:
|
|
||||||
if txlen <= 1:
|
|
||||||
cont = False
|
|
||||||
txlen = 0
|
|
||||||
else:
|
|
||||||
txlen -= 1
|
|
||||||
else:
|
|
||||||
cont = False
|
|
||||||
# offset for cairo-font system is 90
|
# offset for cairo-font system is 90
|
||||||
rotval = self.rotate_value % 360 - 90
|
rotval = self.rotate_value % 360 - 90
|
||||||
if (start + rotval) % 360 > 179:
|
if (start + rotval) % 360 > 179:
|
||||||
pos = start + degoffsetheight + 90 - 90
|
pos = start + degoffsetheight + 90 - 90
|
||||||
else:
|
else:
|
||||||
pos = stop - degoffsetheight + 180
|
pos = stop - degoffsetheight + 180
|
||||||
cr.rotate(pos * math.pi / 180)
|
cr.rotate(math.radians(pos))
|
||||||
layout.context_changed()
|
layout.context_changed()
|
||||||
if (start + rotval) % 360 > 179:
|
if (start + rotval) % 360 > 179:
|
||||||
cr.move_to(radius + PAD_TEXT, 0)
|
cr.move_to(radius + PAD_TEXT, 0)
|
||||||
@ -629,60 +631,90 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
PangoCairo.show_layout(cr, layout)
|
PangoCairo.show_layout(cr, layout)
|
||||||
cr.restore()
|
cr.restore()
|
||||||
else:
|
else:
|
||||||
# center text:
|
self.draw_arc_text(cr, text, radius, start, stop, font)
|
||||||
# 1. determine degrees of the text we can draw
|
|
||||||
degpadding = PAD_TEXT / radius * (180 / math.pi) # degrees for padding
|
|
||||||
degneed = degpadding
|
|
||||||
maxlen = len(text)
|
|
||||||
hoffset = 0
|
|
||||||
for i in range(len(text)):
|
|
||||||
layout = self.create_pango_layout(text[i])
|
|
||||||
layout.set_font_description(font)
|
|
||||||
w, h = layout.get_size()
|
|
||||||
w = w / Pango.SCALE + 2 # 2 pixel padding after letter
|
|
||||||
h = h / Pango.SCALE + 2 # 2 pixel padding
|
|
||||||
if h/2 > hoffset:
|
|
||||||
hoffset = h/2
|
|
||||||
degneed += w / radius * (180 / math.pi)
|
|
||||||
if degneed > stop - start - degpadding:
|
|
||||||
#outside of the box
|
|
||||||
maxlen = i
|
|
||||||
break
|
|
||||||
# 2. determine degrees over we can distribute before and after
|
|
||||||
if degneed > stop - start - degpadding:
|
|
||||||
degover = 0
|
|
||||||
else:
|
|
||||||
degover = stop - start - degneed - degpadding
|
|
||||||
# 3. now draw this text, letter per letter
|
|
||||||
text = text[:maxlen]
|
|
||||||
|
|
||||||
# offset for cairo-font system is 90, padding used is 5:
|
|
||||||
pos = start + 90 + degpadding + degover / 2
|
|
||||||
# Create a PangoLayout, set the font and text
|
|
||||||
# Draw the layout N_WORDS times in a circle
|
|
||||||
for i in range(len(text)):
|
|
||||||
layout = self.create_pango_layout(text[i])
|
|
||||||
layout.set_font_description(font)
|
|
||||||
w, h = layout.get_size()
|
|
||||||
w = w / Pango.SCALE + 2 # 4 pixel padding after word
|
|
||||||
h = h / Pango.SCALE + 2 # 4 pixel padding
|
|
||||||
degneed = w / radius * (180 / math.pi)
|
|
||||||
if pos+degneed > stop + 90:
|
|
||||||
#failsafe, outside of the box, redo
|
|
||||||
break
|
|
||||||
|
|
||||||
cr.save()
|
|
||||||
cr.rotate(pos * math.pi / 180)
|
|
||||||
pos = pos + degneed
|
|
||||||
# Inform Pango to re-layout the text with the new transformation
|
|
||||||
layout.context_changed()
|
|
||||||
#width, height = layout.get_size()
|
|
||||||
#r.move_to(- (width / Pango.SCALE) / 2.0, - radius)
|
|
||||||
cr.move_to(0, - radius - hoffset)
|
|
||||||
PangoCairo.show_layout(cr, layout)
|
|
||||||
cr.restore()
|
|
||||||
cr.restore()
|
cr.restore()
|
||||||
|
|
||||||
|
def draw_arc_text(self, cr, text, radius, start, stop, font):
|
||||||
|
"""
|
||||||
|
Display text at a particular radius, between start and stop
|
||||||
|
degrees, setting it up along the arc, center-justified.
|
||||||
|
|
||||||
|
Text not fitting a single line will be word-wrapped away.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. determine the spread of text we can draw, in radians
|
||||||
|
degpadding = math.degrees(PAD_TEXT / radius)
|
||||||
|
# offset for cairo-font system is 90, padding used is 5:
|
||||||
|
pos = start + 90 + degpadding/2
|
||||||
|
cr.save()
|
||||||
|
cr.rotate(math.radians(pos))
|
||||||
|
cr.new_path()
|
||||||
|
cr.move_to(0, -radius)
|
||||||
|
rad_spread = math.radians(stop - start - degpadding)
|
||||||
|
|
||||||
|
# 2. Use Pango.Layout to set up the text for us, and do
|
||||||
|
# the hard work in CTL text handling and line wrapping.
|
||||||
|
# Clip to the top line only so the text looks nice
|
||||||
|
# all around the circle at the same radius.
|
||||||
|
layout = self.create_pango_layout(text)
|
||||||
|
layout.set_wrap(Pango.WrapMode.WORD)
|
||||||
|
w, h = self.wrap_truncate_layout(layout, font, radius * rad_spread)
|
||||||
|
|
||||||
|
# 3. Use the layout to provide us the metrics of the text box
|
||||||
|
PangoCairo.layout_path(cr, layout)
|
||||||
|
#le = layout.get_line(0).get_pixel_extents()[0]
|
||||||
|
pe = cr.path_extents()
|
||||||
|
arc_used_ratio = w / (radius * rad_spread)
|
||||||
|
rad_mid = math.radians(pos) + rad_spread/2
|
||||||
|
|
||||||
|
# 4. The moment of truth: map the text box onto the sector, and render!
|
||||||
|
warpPath(cr, \
|
||||||
|
self.create_map_rect_to_sector(radius, pe, \
|
||||||
|
arc_used_ratio, rad_mid - rad_spread/2, rad_mid + rad_spread/2))
|
||||||
|
cr.fill()
|
||||||
|
cr.restore()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_map_rect_to_sector(radius, rect, arc_used_ratio, start_rad, stop_rad):
|
||||||
|
"""Create a 2D-transform, mapping a rectangle onto a circle sector.
|
||||||
|
|
||||||
|
radius -- average radius of the target sector
|
||||||
|
rect -- (x1, y1, x2, y2)
|
||||||
|
arc_used_ratio -- From 0.0 to 1.0. Rather than stretching onto the
|
||||||
|
whole sector, only the middle arc_used_ratio part will be mapped onto.
|
||||||
|
start_rad -- start radial angle of the sector, in radians
|
||||||
|
stop_rad -- stop radial angle of the sector, in radians
|
||||||
|
|
||||||
|
Returns a lambda (x,y)|->(xNew,yNew) to feed to warpPath.
|
||||||
|
"""
|
||||||
|
|
||||||
|
x0, y0, w, h = rect[0], rect[1], rect[2]-rect[0], rect[3]-rect[1]
|
||||||
|
|
||||||
|
radiusin = radius - h/2
|
||||||
|
radiusout = radius + h/2
|
||||||
|
drho = h
|
||||||
|
dphi = (stop_rad - start_rad)
|
||||||
|
|
||||||
|
# There has to be a clearer way to express this transform,
|
||||||
|
# by stacking a set of transforms on cr around using this function
|
||||||
|
# and doing a mapping between unit squares of rectangular and polar
|
||||||
|
# coordinates.
|
||||||
|
|
||||||
|
def phi(x):
|
||||||
|
return (x - x0) * dphi * arc_used_ratio / w \
|
||||||
|
+ (1 - arc_used_ratio) * dphi / 2 \
|
||||||
|
- math.pi/2
|
||||||
|
def rho(y):
|
||||||
|
return (y - y0) * (radiusin - radiusout)/h + radiusout
|
||||||
|
|
||||||
|
# In (user coordinates units - pixels):
|
||||||
|
# x from x0 to x0 + w
|
||||||
|
# y from y0 to y0 + h
|
||||||
|
# Out:
|
||||||
|
# (x, y) within the arc_used_ratio of a box like drawn by draw_radbox
|
||||||
|
return lambda x, y: \
|
||||||
|
(rho(y) * math.cos(phi(x)), rho(y) * math.sin(phi(x)))
|
||||||
|
|
||||||
def draw_gradient(self, cr, widget, halfdist):
|
def draw_gradient(self, cr, widget, halfdist):
|
||||||
gradwidth = 10
|
gradwidth = 10
|
||||||
gradheight = 10
|
gradheight = 10
|
||||||
@ -895,7 +927,7 @@ class FanChartBaseWidget(Gtk.DrawingArea):
|
|||||||
end_angle = math.pi + (math.pi + end_angle)
|
end_angle = math.pi + (math.pi + end_angle)
|
||||||
# now look at change in angle:
|
# now look at change in angle:
|
||||||
diff_angle = (end_angle - start_angle) % (math.pi * 2.0)
|
diff_angle = (end_angle - start_angle) % (math.pi * 2.0)
|
||||||
self.rotate_value -= diff_angle * 180.0/ math.pi
|
self.rotate_value -= math.degrees(diff_angle)
|
||||||
self.last_x, self.last_y = event.x, event.y
|
self.last_x, self.last_y = event.x, event.y
|
||||||
self.queue_draw()
|
self.queue_draw()
|
||||||
return True
|
return True
|
||||||
@ -1227,7 +1259,7 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
cr.translate(self.center_x, self.center_y)
|
cr.translate(self.center_x, self.center_y)
|
||||||
|
|
||||||
cr.save()
|
cr.save()
|
||||||
cr.rotate(self.rotate_value * math.pi/180)
|
cr.rotate(math.radians(self.rotate_value))
|
||||||
for generation in range(self.generations - 1, 0, -1):
|
for generation in range(self.generations - 1, 0, -1):
|
||||||
for p in range(len(self.data[generation])):
|
for p in range(len(self.data[generation])):
|
||||||
(text, person, parents, child, userdata) = self.data[generation][p]
|
(text, person, parents, child, userdata) = self.data[generation][p]
|
||||||
@ -1284,8 +1316,8 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
position in the chart
|
position in the chart
|
||||||
"""
|
"""
|
||||||
cr.save()
|
cr.save()
|
||||||
start_rad = start * math.pi/180
|
start_rad = math.radians(start)
|
||||||
stop_rad = stop * math.pi/180
|
stop_rad = math.radians(stop)
|
||||||
r, g, b, a = self.background_box(person, generation, userdata)
|
r, g, b, a = self.background_box(person, generation, userdata)
|
||||||
radius = generation * PIXELS_PER_GENERATION + self.CENTER
|
radius = generation * PIXELS_PER_GENERATION + self.CENTER
|
||||||
# If max generation, and they have parents:
|
# If max generation, and they have parents:
|
||||||
@ -1339,7 +1371,7 @@ class FanChartWidget(FanChartBaseWidget):
|
|||||||
radial = False
|
radial = False
|
||||||
radstart = radius - PIXELS_PER_GENERATION/2
|
radstart = radius - PIXELS_PER_GENERATION/2
|
||||||
if self.radialtext: ## and generation >= 6:
|
if self.radialtext: ## and generation >= 6:
|
||||||
spacepolartext = radstart * (stop-start)*math.pi/180
|
spacepolartext = radstart * math.radians(stop-start)
|
||||||
if spacepolartext < PIXELS_PER_GENERATION * 1.1:
|
if spacepolartext < PIXELS_PER_GENERATION * 1.1:
|
||||||
# more space to print it radial
|
# more space to print it radial
|
||||||
radial = True
|
radial = True
|
||||||
|
Loading…
Reference in New Issue
Block a user