4571: RTL support in fan chart

gramps/gui/utilscairo.py:
new file, currently holds just the warpPath() function,
taken from /usr/share/doc/python-cairo/examples/warpedtext.py,
with explanatory docs added.

gramps/gui/widgets/fanchart.py, class FanChartBaseWidget:
draw_text() method:
previous logic using cairo toy text API didn't support CTL text.
It has been removed, and replaced with a call to a new
method, draw_arc_text().

Flagged a subtle Unicode issue in the remaining old code,
for radial-oriented text, with a FIXME, I'll probably fix it
later as a separate issue.

create_map_rect_to_sector() static method:
create a transform to use with gui.utilscairo.warpPath(),
currently used in draw_arc_text().

Following Benny's code review I have annotated the algorithm
and made it hopefully clear, but I guess it could be reworked
into a better form if re-expressed with stacked transforms /
complex numbers / matrices for easier later maintenance.
Meanwhile I have used the same approach as the older code
in the file, good enough for a patch under this feature request.

The only issue remaining from the code review is whether
the create_map_rect_to_sector() function should be moved
to gui.utilscairo; see the bug thread.

svn: r22548
This commit is contained in:
Vassilii Khachaturov 2013-06-21 14:13:40 +00:00
parent 4ba97726a2
commit face94275c
2 changed files with 182 additions and 52 deletions

87
gramps/gui/utilscairo.py Normal file
View 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()

View File

@ -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
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
@ -601,6 +603,10 @@ class FanChartBaseWidget(Gtk.DrawingArea):
txlen = int(w/height * txlen) txlen = int(w/height * txlen)
cont = True cont = True
while cont: while cont:
# FIXME this can make an invalid Unicode truncation
# if it comes in between CTL text. Should use the approach
# similar to draw_arc_text() and use layout wrapping logic
# instead, and then reset to proper length.
layout = self.create_pango_layout(text[:txlen]) layout = self.create_pango_layout(text[:txlen])
layout.set_font_description(font) layout.set_font_description(font)
w, h = layout.get_size() w, h = layout.get_size()
@ -629,60 +635,97 @@ 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 = PAD_TEXT / radius * (180 / math.pi) # degrees for padding
# 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.
layout = self.create_pango_layout(text)
layout.set_font_description(font)
layout.set_width(Pango.SCALE * radius * rad_spread)
layout.set_wrap(Pango.WrapMode.WORD)
# 3. clip to the top line only so the text looks nice
# all around the circle at the same radius.
# NOTE: one may not truncate the text var here,
# because the truncation can create invalid Unicode.
if layout.get_line_count() > 1:
layout.set_text(text, layout.get_line(0).length)
# 4. Use the layout to provide us the metrics of the text box
PangoCairo.layout_path(cr, layout)
w, h = layout.get_pixel_size()
#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
# 5. 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