Add support for new genealogy tree report category

Issue #10223.
This commit is contained in:
Nick Hall 2017-11-21 23:54:25 +00:00
parent ec44396680
commit 70a634da1e
11 changed files with 1065 additions and 235 deletions

View File

@ -46,7 +46,8 @@ log = logging.getLogger(".")
#-------------------------------------------------------------------------
from gramps.gen.plug import BasePluginManager
from gramps.gen.plug.docgen import (StyleSheet, StyleSheetList, PaperStyle,
PAPER_PORTRAIT, PAPER_LANDSCAPE, graphdoc)
PAPER_PORTRAIT, PAPER_LANDSCAPE, graphdoc,
treedoc)
from gramps.gen.plug.menu import (FamilyOption, PersonOption, NoteOption,
MediaOption, PersonListOption, NumberOption,
BooleanOption, DestinationOption, StringOption,
@ -54,8 +55,8 @@ from gramps.gen.plug.menu import (FamilyOption, PersonOption, NoteOption,
from gramps.gen.display.name import displayer as name_displayer
from gramps.gen.errors import ReportError, FilterError
from gramps.gen.plug.report import (CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_BOOK,
CATEGORY_GRAPHVIZ, CATEGORY_CODE,
ReportOptions, append_styles)
CATEGORY_GRAPHVIZ, CATEGORY_TREE,
CATEGORY_CODE, ReportOptions, append_styles)
from gramps.gen.plug.report._paper import paper_sizes
from gramps.gen.const import USER_HOME
from gramps.gen.dbstate import DbState
@ -233,6 +234,15 @@ class CommandLineReport(object):
if name not in self.option_class.options_dict:
self.option_class.options_dict[name] = \
menu.get_option_by_name(name).get_value()
if category == CATEGORY_TREE:
# Need to include Genealogy Tree options
self.__toptions = treedoc.TreeOptions()
menu = self.option_class.menu
self.__toptions.add_menu_options(menu)
for name in menu.get_all_option_names():
if name not in self.option_class.options_dict:
self.option_class.options_dict[
name] = menu.get_option_by_name(name).get_value()
self.option_class.load_previous_values()
_validate_options(self.option_class, database)
self.show = options_str_dict.pop('show', None)
@ -301,6 +311,10 @@ class CommandLineReport(object):
for graph_format in graphdoc.FORMATS:
self.options_help['off'][2].append(
graph_format["type"] + "\t" + graph_format["descr"] )
elif self.category == CATEGORY_TREE:
for tree_format in treedoc.FORMATS:
self.options_help['off'][2].append(
tree_format["type"] + "\t" + tree_format["descr"])
else:
self.options_help['off'][2] = "NA"
@ -478,6 +492,15 @@ class CommandLineReport(object):
# Pick the first one as the default.
self.format = graphdoc.FORMATS[0]["class"]
_chosen_format = graphdoc.FORMATS[0]["type"]
elif self.category == CATEGORY_TREE:
for tree_format in treedoc.FORMATS:
if tree_format['type'] == self.options_dict['off']:
if not self.format: # choose the first one, not the last
self.format = tree_format["class"]
if self.format is None:
# Pick the first one as the default.
self.format = tree_format.FORMATS[0]["class"]
_chosen_format = tree_format.FORMATS[0]["type"]
else:
self.format = None
if _chosen_format and _format_str:
@ -640,7 +663,7 @@ def cl_report(database, name, category, report_class, options_class,
clr.selected_style,
PaperStyle(clr.paper,clr.orien,clr.marginl,
clr.marginr,clr.margint,clr.marginb))
elif category == CATEGORY_GRAPHVIZ:
elif category in [CATEGORY_GRAPHVIZ, CATEGORY_TREE]:
clr.option_class.handler.doc = clr.format(
clr.option_class,
PaperStyle(clr.paper,clr.orien,clr.marginl,

View File

@ -27,7 +27,7 @@ The "plug" package for handling plugins in Gramps.
from ._plugin import Plugin
from ._pluginreg import (PluginData, PluginRegister, REPORT, TOOL,
CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_CODE,
CATEGORY_WEB, CATEGORY_BOOK, CATEGORY_GRAPHVIZ,
CATEGORY_WEB, CATEGORY_BOOK, CATEGORY_GRAPHVIZ, CATEGORY_TREE,
TOOL_DEBUG, TOOL_ANAL, TOOL_DBPROC, TOOL_DBFIX, TOOL_REVCTL,
TOOL_UTILS, CATEGORY_QR_MISC, CATEGORY_QR_PERSON,
CATEGORY_QR_FAMILY, CATEGORY_QR_EVENT, CATEGORY_QR_SOURCE,
@ -49,7 +49,7 @@ __all__ = [ "docbackend", "docgen", "menu", Plugin, PluginData,
PluginRegister, BasePluginManager,
ImportPlugin, ExportPlugin, DocGenPlugin,
REPORT, TOOL, CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_CODE,
CATEGORY_WEB, CATEGORY_BOOK, CATEGORY_GRAPHVIZ,
CATEGORY_WEB, CATEGORY_BOOK, CATEGORY_GRAPHVIZ, CATEGORY_TREE,
TOOL_DEBUG, TOOL_ANAL, TOOL_DBPROC, TOOL_DBFIX, TOOL_REVCTL,
TOOL_UTILS, CATEGORY_QR_MISC, CATEGORY_QR_PERSON,
CATEGORY_QR_FAMILY, CATEGORY_QR_EVENT, CATEGORY_QR_SOURCE,

View File

@ -94,8 +94,10 @@ CATEGORY_CODE = 2
CATEGORY_WEB = 3
CATEGORY_BOOK = 4
CATEGORY_GRAPHVIZ = 5
CATEGORY_TREE = 6
REPORT_CAT = [ CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_CODE,
CATEGORY_WEB, CATEGORY_BOOK, CATEGORY_GRAPHVIZ]
CATEGORY_WEB, CATEGORY_BOOK, CATEGORY_GRAPHVIZ,
CATEGORY_TREE]
#possible tool categories
TOOL_DEBUG = -1
TOOL_ANAL = 0
@ -1009,6 +1011,7 @@ def make_environment(**kwargs):
'CATEGORY_WEB': CATEGORY_WEB,
'CATEGORY_BOOK': CATEGORY_BOOK,
'CATEGORY_GRAPHVIZ': CATEGORY_GRAPHVIZ,
'CATEGORY_TREE': CATEGORY_TREE,
'TOOL_DEBUG': TOOL_DEBUG,
'TOOL_ANAL': TOOL_ANAL,
'TOOL_DBPROC': TOOL_DBPROC,

View File

@ -37,3 +37,4 @@ from .textdoc import TextDoc, IndexMark,INDEX_TYPE_ALP, INDEX_TYPE_TOC,\
URL_PATTERN, LOCAL_HYPERLINK, LOCAL_TARGET
from .drawdoc import DrawDoc
from .graphdoc import GVDoc
from .treedoc import TreeDoc

View File

@ -0,0 +1,632 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2017-2018 Nick Hall
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
""" LaTeX Genealogy Tree adapter for Trees """
#-------------------------------------------------------------------------
#
# Standard Python modules
#
#-------------------------------------------------------------------------
from abc import ABCMeta, abstractmethod
import os
import shutil
from io import StringIO
import tempfile
import logging
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
from ...utils.file import search_for
from ...lib import Person, EventType, EventRoleType, Date
from ...display.place import displayer as _pd
from ...utils.file import media_path_full
from . import BaseDoc, PAPER_PORTRAIT
from ..menu import NumberOption, TextOption, EnumeratedListOption
from ...constfunc import win
from ...config import config
from ...const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
#-------------------------------------------------------------------------
#
# set up logging
#
#-------------------------------------------------------------------------
LOG = logging.getLogger(".treedoc")
#-------------------------------------------------------------------------
#
# Private Constants
#
#-------------------------------------------------------------------------
_DETAIL = [{'name': _("Full"), 'value': "full"},
{'name': _("Medium"), 'value': "medium"},
{'name': _("Short"), 'value': "short"}]
_MARRIAGE = [{'name': _("Default"), 'value': ""},
{'name': _("Above"), 'value': "marriage above"},
{'name': _("Below"), 'value': "marriage below"},
{'name': _("Not shown"), 'value': "no marriage"}]
_COLOR = [{'name': _("None"), 'value': "none"},
{'name': _("Default"), 'value': "default"},
{'name': _("Preferences"), 'value': "preferences"}]
_TIMEFLOW = [{'name': _("Down (↓)"), 'value': ""},
{'name': _("Up (↑)"), 'value': "up"},
{'name': _("Right (→)"), 'value': "right"},
{'name': _("Left (←)"), 'value': "left"}]
_EDGES = [{'name': _("Perpendicular"), 'value': ""},
{'name': _("Rounded"), 'value': "rounded", },
{'name': _("Swing"), 'value': "swing", },
{'name': _("Mesh"), 'value': 'mesh'}]
_NOTELOC = [{'name': _("Top"), 'value': "t"},
{'name': _("Bottom"), 'value': "b"}]
_NOTESIZE = [{'name': _("Tiny"), 'value': "tiny"},
{'name': _("Script"), 'value': "scriptsize"},
{'name': _("Footnote"), 'value': "footnotesize"},
{'name': _("Small"), 'value': "small"},
{'name': _("Normal"), 'value': "normalsize"},
{'name': _("Large"), 'value': "large"},
{'name': _("Very large"), 'value': "Large"},
{'name': _("Extra large"), 'value': "LARGE"},
{'name': _("Huge"), 'value': "huge"},
{'name': _("Extra huge"), 'value': "Huge"}]
if win():
_LATEX_FOUND = search_for("lualatex.exe")
else:
_LATEX_FOUND = search_for("lualatex")
#------------------------------------------------------------------------------
#
# TreeOptions
#
#------------------------------------------------------------------------------
class TreeOptions:
"""
Defines all of the controls necessary
to configure the genealogy tree reports.
"""
def add_menu_options(self, menu):
"""
Add all graph related options to the menu.
:param menu: The menu the options should be added to.
:type menu: :class:`.Menu`
:return: nothing
"""
################################
category = _("Node Options")
################################
detail = EnumeratedListOption(_("Node detail"), "full")
for item in _DETAIL:
detail.add_item(item["value"], item["name"])
detail.set_help(_("Detail of information to be shown in a node."))
menu.add_option(category, "detail", detail)
marriage = EnumeratedListOption(_("Marriage"), "")
for item in _MARRIAGE:
marriage.add_item(item["value"], item["name"])
marriage.set_help(_("Position of marriage information."))
menu.add_option(category, "marriage", marriage)
nodesize = NumberOption(_("Node size"), 25, 5, 100, 5)
nodesize.set_help(_("One dimension of a node, in mm. If the timeflow "
"is up or down then this is the width, otherwise "
"it is the height."))
menu.add_option(category, "nodesize", nodesize)
levelsize = NumberOption(_("Level size"), 35, 5, 100, 5)
levelsize.set_help(_("One dimension of a node, in mm. If the timeflow "
"is up or down then this is the height, otherwise "
"it is the width."))
menu.add_option(category, "levelsize", levelsize)
nodecolor = EnumeratedListOption(_("Color"), "none")
for item in _COLOR:
nodecolor.add_item(item["value"], item["name"])
nodecolor.set_help(_("Node color."))
menu.add_option(category, "nodecolor", nodecolor)
################################
category = _("Tree Options")
################################
timeflow = EnumeratedListOption(_("Timeflow"), "")
for item in _TIMEFLOW:
timeflow.add_item(item["value"], item["name"])
timeflow.set_help(_("Direction that the graph will grow over time."))
menu.add_option(category, "timeflow", timeflow)
edges = EnumeratedListOption(_("Edge style"), "")
for item in _EDGES:
edges.add_item(item["value"], item["name"])
edges.set_help(_("Style of the edges between nodes."))
menu.add_option(category, "edges", edges)
leveldist = NumberOption(_("Level distance"), 5, 1, 20, 1)
leveldist.set_help(_("The minimum amount of free space, in mm, "
"between levels. For vertical graphs, this "
"corresponds to spacing between rows. For "
"horizontal graphs, this corresponds to spacing "
"between columns."))
menu.add_option(category, "leveldist", leveldist)
################################
category = _("Note")
################################
note = TextOption(_("Note to add to the tree"), [""])
note.set_help(_("This text will be added to the tree."))
menu.add_option(category, "note", note)
noteloc = EnumeratedListOption(_("Note location"), 't')
for item in _NOTELOC:
noteloc.add_item(item["value"], item["name"])
noteloc.set_help(_("Whether note will appear on top "
"or bottom of the page."))
menu.add_option(category, "noteloc", noteloc)
notesize = EnumeratedListOption(_("Note size"), 'normalsize')
for item in _NOTESIZE:
notesize.add_item(item["value"], item["name"])
notesize.set_help(_("The size of note text."))
menu.add_option(category, "notesize", notesize)
#------------------------------------------------------------------------------
#
# TreeDoc
#
#------------------------------------------------------------------------------
class TreeDoc(metaclass=ABCMeta):
"""
Abstract Interface for genealogy tree document generators. Output formats
for genealogy tree reports must implement this interface to be used by the
report system.
"""
@abstractmethod
def start_tree(self, option_list):
"""
Write the start of a tree.
"""
@abstractmethod
def end_tree(self):
"""
Write the end of a tree.
"""
@abstractmethod
def start_subgraph(self, level, subgraph_type, family, option_list=None):
"""
Write the start of a subgraph.
"""
@abstractmethod
def end_subgraph(self, level):
"""
Write the end of a subgraph.
"""
@abstractmethod
def write_node(self, db, level, node_type, person, marriage_flag,
option_list=None):
"""
Write the contents of a node.
"""
#------------------------------------------------------------------------------
#
# TreeDocBase
#
#------------------------------------------------------------------------------
class TreeDocBase(BaseDoc, TreeDoc):
"""
Base document generator for all Graphviz document generators. Classes that
inherit from this class will only need to implement the close function.
The close function will generate the actual file of the appropriate type.
"""
def __init__(self, options, paper_style):
BaseDoc.__init__(self, None, paper_style)
self._filename = None
self._tex = StringIO()
self._paper = paper_style
get_option = options.menu.get_option_by_name
self.detail = get_option('detail').get_value()
self.marriage = get_option('marriage').get_value()
self.nodesize = get_option('nodesize').get_value()
self.levelsize = get_option('levelsize').get_value()
self.nodecolor = get_option('nodecolor').get_value()
self.timeflow = get_option('timeflow').get_value()
self.edges = get_option('edges').get_value()
self.leveldist = get_option('leveldist').get_value()
self.note = get_option('note').get_value()
self.noteloc = get_option('noteloc').get_value()
self.notesize = get_option('notesize').get_value()
def write_start(self):
"""
Write the start of the document.
"""
paper_size = self._paper.get_size()
name = paper_size.get_name().lower()
if name == 'custom size':
width = str(paper_size.get_width())
height = str(paper_size.get_width())
paper = 'papersize={%scm,%scm}' % (width, height)
elif name in ('a', 'b', 'c', 'd', 'e'):
paper = 'ansi' + name + 'paper'
else:
paper = name + 'paper'
if self._paper.get_orientation() == PAPER_PORTRAIT:
orientation = 'portrait'
else:
orientation = 'landscape'
lmargin = self._paper.get_left_margin()
rmargin = self._paper.get_right_margin()
tmargin = self._paper.get_top_margin()
bmargin = self._paper.get_bottom_margin()
if lmargin == rmargin == tmargin == bmargin:
margin = 'margin=%scm'% lmargin
else:
if lmargin == rmargin:
margin = 'hmargin=%scm' % lmargin
else:
margin = 'hmargin={%scm,%scm}' % (lmargin, rmargin)
if tmargin == bmargin:
margin += ',vmargin=%scm' % tmargin
else:
margin += ',vmargin={%scm,%scm}' % (tmargin, bmargin)
self.write(0, '\\documentclass[%s]{article}\n' % orientation)
self.write(0, '\\IfFileExists{libertine.sty}{\n')
self.write(0, ' \\usepackage{libertine}\n')
self.write(0, '}{}\n')
self.write(0, '\\usepackage[%s,%s]{geometry}\n' % (paper, margin))
self.write(0, '\\usepackage[all]{genealogytree}\n')
self.write(0, '\\usepackage{color}\n')
self.write(0, '\\begin{document}\n')
if self.nodecolor == 'preferences':
male_bg = config.get('preferences.color-gender-male-death')[1:]
female_bg = config.get('preferences.color-gender-female-death')[1:]
neuter_bg = config.get('preferences.color-gender-unknown-death')[1:]
self.write(0, '\\definecolor{male-bg}{HTML}{%s}\n' % male_bg)
self.write(0, '\\definecolor{female-bg}{HTML}{%s}\n' % female_bg)
self.write(0, '\\definecolor{neuter-bg}{HTML}{%s}\n' % neuter_bg)
if ''.join(self.note) != '' and self.noteloc == 't':
for line in self.note:
self.write(0, '{\\%s %s}\\par\n' % (self.notesize, line))
self.write(0, '\\bigskip\n')
self.write(0, '\\begin{tikzpicture}\n')
def start_tree(self, option_list):
self.write(0, '\\genealogytree[\n')
self.write(0, 'processing=database,\n')
if self.marriage:
info = self.detail + ' ' + self.marriage
else:
info = self.detail
self.write(0, 'database format=%s,\n' % info)
if self.timeflow:
self.write(0, 'timeflow=%s,\n' % self.timeflow)
if self.edges:
self.write(0, 'edges=%s,\n' % self.edges)
if self.leveldist != 5:
self.write(0, 'level distance=%smm,\n' % self.leveldist)
if self.nodesize != 25:
self.write(0, 'node size=%smm,\n' % self.nodesize)
if self.levelsize != 35:
self.write(0, 'level size=%smm,\n' % self.levelsize)
if self.nodecolor == 'none':
self.write(0, 'tcbset={male/.style={},\n')
self.write(0, ' female/.style={},\n')
self.write(0, ' neuter/.style={}},\n')
if self.nodecolor == 'preferences':
self.write(0, 'tcbset={male/.style={colback=male-bg},\n')
self.write(0, ' female/.style={colback=female-bg},\n')
self.write(0, ' neuter/.style={colback=neuter-bg}},\n')
for option in option_list:
self.write(0, '%s,\n' % option)
self.write(0, ']{\n')
def end_tree(self):
self.write(0, '}\n')
def write_end(self):
"""
Write the end of the document.
"""
self.write(0, '\\end{tikzpicture}\n')
if ''.join(self.note) != '' and self.noteloc == 'b':
self.write(0, '\\bigskip\n')
for line in self.note:
self.write(0, '\\par{\\%s %s}\n' % (self.notesize, line))
self.write(0, '\\end{document}\n')
def start_subgraph(self, level, subgraph_type, family, option_list=None):
options = ['id=%s' % family.gramps_id]
if option_list:
options.extend(option_list)
if subgraph_type == 'sandclock':
self.write(level, 'sandclock{\n')
else:
self.write(level, '%s[%s]{\n' % (subgraph_type, ','.join(options)))
def end_subgraph(self, level):
self.write(level, '}\n')
def write_node(self, db, level, node_type, person, marriage_flag,
option_list=None):
options = ['id=%s' % person.gramps_id]
if option_list:
options.extend(option_list)
self.write(level, '%s[%s]{\n' % (node_type, ','.join(options)))
if person.gender == Person.MALE:
self.write(level+1, 'male,\n')
elif person.gender == Person.FEMALE:
self.write(level+1, 'female,\n')
elif person.gender == Person.UNKNOWN:
self.write(level+1, 'neuter,\n')
name = person.get_primary_name()
nick = name.get_nick_name()
surn = name.get_surname()
name_parts = [self.format_given_names(name),
'\\nick{{{}}}'.format(nick) if nick else '',
'\\surn{{{}}}'.format(surn) if surn else '']
self.write(level+1, 'name = {{{}}},\n'.format(
' '.join([e for e in name_parts if e])))
for eventref in person.get_event_ref_list():
if eventref.role == EventRoleType.PRIMARY:
event = db.get_event_from_handle(eventref.ref)
self.write_event(db, level+1, event)
if marriage_flag:
for handle in person.get_family_handle_list():
family = db.get_family_from_handle(handle)
for eventref in family.get_event_ref_list():
if eventref.role == EventRoleType.FAMILY:
event = db.get_event_from_handle(eventref.ref)
self.write_event(db, level+1, event)
for attr in person.get_attribute_list():
if str(attr.get_type()) == 'Occupation':
self.write(level+1, 'profession = {%s},\n' % attr.get_value())
if str(attr.get_type()) == 'Comment':
self.write(level+1, 'comment = {%s},\n' % attr.get_value())
for mediaref in person.get_media_list():
media = db.get_object_from_handle(mediaref.ref)
path = media_path_full(db, media.get_path())
if os.path.isfile(path):
self.write(level+1, 'image = {%s},\n' % path)
break # first image only
self.write(level, '}\n')
def write_event(self, db, level, event):
"""
Write an event.
"""
modifier = None
if event.type == EventType.BIRTH:
event_type = 'birth'
if 'died' in event.description.lower():
modifier = 'died'
if 'stillborn' in event.description.lower():
modifier = 'stillborn'
# modifier = 'out of wedlock'
elif event.type == EventType.BAPTISM:
event_type = 'baptism'
elif event.type == EventType.ENGAGEMENT:
event_type = 'engagement'
elif event.type == EventType.MARRIAGE:
event_type = 'marriage'
elif event.type == EventType.DIVORCE:
event_type = 'divorce'
elif event.type == EventType.DEATH:
event_type = 'death'
elif event.type == EventType.BURIAL:
event_type = 'burial'
if 'killed' in event.description.lower():
modifier = 'killed'
elif event.type == EventType.CREMATION:
event_type = 'burial'
modifier = 'cremated'
else:
return
date = event.get_date_object()
if date.get_calendar() == Date.CAL_GREGORIAN:
calendar = 'AD' # GR
elif date.get_calendar() == Date.CAL_JULIAN:
calendar = 'JU'
else:
calendar = ''
if date.get_modifier() == Date.MOD_ABOUT:
calendar = 'ca' + calendar
date_str = self.format_iso(date.get_ymd(), calendar)
if date.get_modifier() == Date.MOD_BEFORE:
date_str = '/' + date_str
elif date.get_modifier() == Date.MOD_AFTER:
date_str = date_str + '/'
elif date.is_compound():
stop_date = self.format_iso(date.get_stop_ymd(), calendar)
date_str = date_str + '/' + stop_date
place = _pd.display_event(db, event)
if modifier:
event_type += '+'
self.write(level, '%s = {%s}{%s}{%s},\n' %
(event_type, date_str, place, modifier))
elif place == '':
event_type += '-'
self.write(level, '%s = {%s},\n' % (event_type, date_str))
else:
self.write(level, '%s = {%s}{%s},\n' %
(event_type, date_str, place))
def format_given_names(self, name):
"""
Format given names.
"""
first = name.get_first_name()
call = name.get_call_name()
if call:
if call in first:
where = first.index(call)
return '{before}\\pref{{{call}}}{after}'.format(
before=first[:where],
call=call,
after=first[where+len(call):])
else:
# ignore erroneous call name
return first
else:
return first
def format_iso(self, date_tuple, calendar):
"""
Format an iso date.
"""
year, month, day = date_tuple
if year == 0:
iso_date = ''
elif month == 0:
iso_date = str(year)
elif day == 0:
iso_date = '%s-%s' % (year, month)
else:
iso_date = '%s-%s-%s' % (year, month, day)
if calendar and calendar != 'AD':
iso_date = '(%s)%s' % (calendar, iso_date)
return iso_date
def write(self, level, text):
"""
Write indented text.
"""
self._tex.write(' '*level + text)
def open(self, filename):
""" Implement TreeDocBase.open() """
self._filename = os.path.normpath(os.path.abspath(filename))
self.write_start()
def close(self):
"""
This isn't useful by itself. Other classes need to override this and
actually generate a file.
"""
self.write_end()
#------------------------------------------------------------------------------
#
# TreeTexDoc
#
#------------------------------------------------------------------------------
class TreeTexDoc(TreeDocBase):
"""
TreeTexDoc implementation that generates a .tex file.
"""
def close(self):
""" Implements TreeDocBase.close() """
TreeDocBase.close(self)
# Make sure the extension is correct
if self._filename[-4:] != ".tex":
self._filename += ".tex"
with open(self._filename, "w") as texfile:
texfile.write(self._tex.getvalue())
#------------------------------------------------------------------------------
#
# TreePdfDoc
#
#------------------------------------------------------------------------------
class TreePdfDoc(TreeDocBase):
"""
TreePdfDoc implementation that generates a .pdf file.
"""
def close(self):
""" Implements TreeDocBase.close() """
TreeDocBase.close(self)
# Make sure the extension is correct
if self._filename[-4:] != ".pdf":
self._filename += ".pdf"
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, 'temp.tex'), "w") as texfile:
texfile.write(self._tex.getvalue())
os.system('lualatex -output-directory %s temp.tex >/dev/null'
% tmpdir)
shutil.copy(os.path.join(tmpdir, 'temp.pdf'), self._filename)
#------------------------------------------------------------------------------
#
# Various Genealogy Tree formats.
#
#------------------------------------------------------------------------------
FORMATS = []
if _LATEX_FOUND:
FORMATS += [{'type' : "pdf",
'ext' : "pdf",
'descr': _("PDF"),
'mime' : "application/pdf",
'class': TreePdfDoc}]
FORMATS += [{'type' : "tex",
'ext' : "tex",
'descr': _("LaTeX File"),
'mime' : "application/x-latex",
'class': TreeTexDoc}]

View File

@ -39,7 +39,7 @@ import os
# Report categories
from .. import (CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_CODE, CATEGORY_WEB,
CATEGORY_BOOK, CATEGORY_GRAPHVIZ)
CATEGORY_BOOK, CATEGORY_GRAPHVIZ, CATEGORY_TREE)
standalone_categories = {
CATEGORY_TEXT : ("RepText", _("Text Reports")),
@ -48,6 +48,7 @@ standalone_categories = {
CATEGORY_WEB : ("RepWeb", _("Web Pages")),
CATEGORY_BOOK : ("RepBook", _("Books")),
CATEGORY_GRAPHVIZ : ("Graphs", _("Graphs")),
CATEGORY_TREE : ("Trees", _("Trees")),
}
book_categories = {
CATEGORY_TEXT : _("Text"),

View File

@ -0,0 +1,288 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2007-2008 Brian G. Matherly
# Copyright (C) 2007-2009 Stephane Charette
# Copyright (C) 2009 Gary Burton
# Contribution 2009 by Bob Ham <rah@bash.sh>
# Copyright (C) 2010 Jakim Friant
# Copyright (C) 2012-2013 Paul Franklin
# Copyright (C) 2017 Nick Hall
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
"""base class for generating dialogs for graph-based reports """
#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
from abc import ABCMeta, abstractmethod
import os
from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
#-------------------------------------------------------------------------------
#
# GTK+ modules
#
#-------------------------------------------------------------------------------
from gi.repository import Gtk
from gi.repository import GObject
#-------------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------------
from gramps.gen.config import config
from gramps.gen.plug.report import CATEGORY_GRAPHVIZ
from ._reportdialog import ReportDialog
from ._papermenu import PaperFrame
import gramps.gen.plug.docgen.graphdoc as graphdoc
from gramps.gen.plug.menu import Menu
#-------------------------------------------------------------------------------
#
# BaseFormatComboBox
#
#-------------------------------------------------------------------------------
class BaseFormatComboBox(Gtk.ComboBox):
"""
Combo box base class for graph-based report format choices.
"""
FORMATS=[]
def set(self, active=None):
self.store = Gtk.ListStore(GObject.TYPE_STRING)
self.set_model(self.store)
cell = Gtk.CellRendererText()
self.pack_start(cell, True)
self.add_attribute(cell, 'text', 0)
index = 0
active_index = 0
for item in self.FORMATS:
name = item["descr"]
self.store.append(row=[name])
if item['type'] == active:
active_index = index
index += 1
self.set_active(active_index)
def get_label(self):
return self.FORMATS[self.get_active()]["descr"]
def get_reference(self):
return self.FORMATS[self.get_active()]["class"]
def get_paper(self):
return 1
def get_styles(self):
return 0
def get_ext(self):
return '.%s' % self.FORMATS[self.get_active()]['ext']
def get_oformat_str(self): # the report's output-format type
return self.FORMATS[self.get_active()]["type"]
def is_file_output(self):
return True
def get_clname(self):
return self.FORMATS[self.get_active()]["type"]
#-----------------------------------------------------------------------
#
# GraphReportDialog
#
#-----------------------------------------------------------------------
class GraphReportDialog(ReportDialog, metaclass=ABCMeta):
"""A base class of ReportDialog customized for graph-based reports."""
def __init__(self, dbstate, uistate, opt, name, translated_name):
"""Initialize a dialog to request that the user select options
for a graphviz report. See the ReportDialog class for
more information."""
self.category = self.get_category()
self._goptions = self.get_options()
self.dbname = dbstate.db.get_dbname()
ReportDialog.__init__(self, dbstate, uistate, opt,
name, translated_name)
def init_options(self, option_class):
try:
if issubclass(option_class, object): # Old-style class
self.options = option_class(self.raw_name,
self.dbstate.get_database())
except TypeError:
self.options = option_class
menu = Menu()
self._goptions.add_menu_options(menu)
for category in menu.get_categories():
for name in menu.get_option_names(category):
option = menu.get_option(category, name)
self.options.add_menu_option(category, name, option)
self.options.load_previous_values()
def init_interface(self):
ReportDialog.init_interface(self)
self.doc_type_changed(self.format_menu)
self.notebook.set_current_page(1) # don't start on "Paper Options"
def setup_format_frame(self):
"""Set up the format frame of the dialog."""
self.make_doc_menu()
self.format_menu.set(self.options.handler.get_format_name())
self.format_menu.connect('changed', self.doc_type_changed)
label = Gtk.Label(label="%s:" % _("Output Format"))
label.set_halign(Gtk.Align.START)
self.grid.attach(label, 1, self.row, 1, 1)
self.format_menu.set_hexpand(True)
self.grid.attach(self.format_menu, 2, self.row, 2, 1)
self.row += 1
self.open_with_app = Gtk.CheckButton(_("Open with default viewer"))
self.open_with_app.set_active(
config.get('interface.open-with-default-viewer'))
self.grid.attach(self.open_with_app, 2, self.row, 2, 1)
self.row += 1
ext = self.format_menu.get_ext()
if ext is None:
ext = ""
else:
spath = self.get_default_directory()
if self.options.get_output():
base = os.path.basename(self.options.get_output())
else:
if self.dbname is None:
default_name = self.raw_name
else:
default_name = self.dbname + "_" + self.raw_name
base = "%s%s" % (default_name, ext) # "ext" already has a dot
spath = os.path.normpath(os.path.join(spath, base))
self.target_fileentry.set_filename(spath)
def setup_report_options_frame(self):
self.paper_label = Gtk.Label(label='<b>%s</b>' % _("Paper Options"))
self.paper_label.set_use_markup(True)
handler = self.options.handler
self.paper_frame = PaperFrame(
handler.get_paper_metric(),
handler.get_paper_name(),
handler.get_orientation(),
handler.get_margins(),
handler.get_custom_paper_size(),
)
self.notebook.insert_page(self.paper_frame, self.paper_label, 0)
self.paper_frame.show_all()
ReportDialog.setup_report_options_frame(self)
def doc_type_changed(self, obj):
"""
This routine is called when the user selects a new file
format for the report. It adjusts the various dialog sections
to reflect the appropriate values for the currently selected
file format. For example, a HTML document doesn't need any
paper size/orientation options, but it does need a template
file. Those changes are made here.
"""
self.open_with_app.set_sensitive(True)
fname = self.target_fileentry.get_full_path(0)
(spath, ext) = os.path.splitext(fname)
ext_val = obj.get_ext()
if ext_val:
fname = spath + ext_val
else:
fname = spath
self.target_fileentry.set_filename(fname)
def make_document(self):
"""Create a document of the type requested by the user.
"""
pstyle = self.paper_frame.get_paper_style()
self.doc = self.format(self.options, pstyle)
self.options.set_document(self.doc)
def on_ok_clicked(self, obj):
"""The user is satisfied with the dialog choices. Validate
the output file name before doing anything else. If there is
a file name, gather the options and create the report."""
# Is there a filename? This should also test file permissions, etc.
if not self.parse_target_frame():
self.window.run()
# Preparation
self.parse_format_frame()
self.parse_user_options()
self.options.handler.set_paper_metric(
self.paper_frame.get_paper_metric())
self.options.handler.set_paper_name(self.paper_frame.get_paper_name())
self.options.handler.set_orientation(self.paper_frame.get_orientation())
self.options.handler.set_margins(self.paper_frame.get_paper_margins())
self.options.handler.set_custom_paper_size(
self.paper_frame.get_custom_paper_size())
# Create the output document.
self.make_document()
# Save options
self.options.handler.save_options()
config.set('interface.open-with-default-viewer',
self.open_with_app.get_active())
def parse_format_frame(self):
"""Parse the format frame of the dialog. Save the user
selected output format for later use."""
self.format = self.format_menu.get_reference()
format_name = self.format_menu.get_clname()
self.options.handler.set_format_name(format_name)
def setup_style_frame(self):
"""Required by ReportDialog"""
pass
@abstractmethod
def make_doc_menu(self):
"""
Build a menu of document types that are appropriate for
this graph report.
"""
@abstractmethod
def get_category(self):
"""
Return the report category.
"""
@abstractmethod
def get_options(self):
"""
Return the graph options.
"""

View File

@ -1,12 +1,7 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2007-2008 Brian G. Matherly
# Copyright (C) 2007-2009 Stephane Charette
# Copyright (C) 2009 Gary Burton
# Contribution 2009 by Bob Ham <rah@bash.sh>
# Copyright (C) 2010 Jakim Friant
# Copyright (C) 2012-2013 Paul Franklin
# Copyright (C) 2017 Nick Hall
#
# 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
@ -23,174 +18,42 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
import os
from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
"""class for generating dialogs for graphviz-based reports """
#-------------------------------------------------------------------------------
#
# GTK+ modules
# Gramps modules
#
#-------------------------------------------------------------------------------
from gi.repository import Gtk
from gi.repository import GObject
#-------------------------------------------------------------------------------
#
# GRAMPS modules
#
#-------------------------------------------------------------------------------
from gramps.gen.config import config
from ._graphreportdialog import GraphReportDialog, BaseFormatComboBox
from gramps.gen.plug.report import CATEGORY_GRAPHVIZ
from ._reportdialog import ReportDialog
from ._papermenu import PaperFrame
import gramps.gen.plug.docgen.graphdoc as graphdoc
from gramps.gen.plug.menu import Menu
#-------------------------------------------------------------------------------
#
# GraphvizFormatComboBox
#
#-------------------------------------------------------------------------------
class GraphvizFormatComboBox(Gtk.ComboBox):
"""
Format combo box class for Graphviz report.
"""
def set(self, active=None):
self.store = Gtk.ListStore(GObject.TYPE_STRING)
self.set_model(self.store)
cell = Gtk.CellRendererText()
self.pack_start(cell, True)
self.add_attribute(cell, 'text', 0)
index = 0
active_index = 0
for item in graphdoc.FORMATS:
name = item["descr"]
self.store.append(row=[name])
if item['type'] == active:
active_index = index
index += 1
self.set_active(active_index)
def get_label(self):
return graphdoc.FORMATS[self.get_active()]["descr"]
def get_reference(self):
return graphdoc.FORMATS[self.get_active()]["class"]
def get_paper(self):
return 1
def get_styles(self):
return 0
def get_ext(self):
return '.%s' % graphdoc.FORMATS[self.get_active()]['ext']
def get_oformat_str(self): # the report's output-format type
return graphdoc.FORMATS[self.get_active()]["type"]
def is_file_output(self):
return True
def get_clname(self):
return graphdoc.FORMATS[self.get_active()]["type"]
#-----------------------------------------------------------------------
#
# GraphvizReportDialog
#
#-----------------------------------------------------------------------
class GraphvizReportDialog(ReportDialog):
"""A class of ReportDialog customized for graphviz based reports."""
def __init__(self, dbstate, uistate, opt, name, translated_name):
"""Initialize a dialog to request that the user select options
for a graphviz report. See the ReportDialog class for
more information."""
self.category = CATEGORY_GRAPHVIZ
self.__gvoptions = graphdoc.GVOptions()
self.dbname = dbstate.db.get_dbname()
ReportDialog.__init__(self, dbstate, uistate, opt,
name, translated_name)
def init_options(self, option_class):
try:
if issubclass(option_class, object): # Old-style class
self.options = option_class(self.raw_name,
self.dbstate.get_database())
except TypeError:
self.options = option_class
menu = Menu()
self.__gvoptions.add_menu_options(menu)
for category in menu.get_categories():
for name in menu.get_option_names(category):
option = menu.get_option(category, name)
self.options.add_menu_option(category, name, option)
self.options.load_previous_values()
class GraphvizReportDialog(GraphReportDialog):
def init_interface(self):
ReportDialog.init_interface(self)
self.doc_type_changed(self.format_menu)
self.notebook.set_current_page(1) # don't start on "Paper Options"
def setup_format_frame(self):
"""Set up the format frame of the dialog."""
def make_doc_menu(self):
"""
Build a menu of document types that are appropriate for
this graph report.
"""
self.format_menu = GraphvizFormatComboBox()
self.format_menu.set(self.options.handler.get_format_name())
self.format_menu.connect('changed', self.doc_type_changed)
label = Gtk.Label(label="%s:" % _("Output Format"))
label.set_halign(Gtk.Align.START)
self.grid.attach(label, 1, self.row, 1, 1)
self.format_menu.set_hexpand(True)
self.grid.attach(self.format_menu, 2, self.row, 2, 1)
self.row += 1
self.open_with_app = Gtk.CheckButton(_("Open with default viewer"))
self.open_with_app.set_active(
config.get('interface.open-with-default-viewer'))
self.grid.attach(self.open_with_app, 2, self.row, 2, 1)
self.row += 1
def get_category(self):
"""
Return the report category.
"""
return CATEGORY_GRAPHVIZ
ext = self.format_menu.get_ext()
if ext is None:
ext = ""
else:
spath = self.get_default_directory()
if self.options.get_output():
base = os.path.basename(self.options.get_output())
else:
if self.dbname is None:
default_name = self.raw_name
else:
default_name = self.dbname + "_" + self.raw_name
base = "%s%s" % (default_name, ext) # "ext" already has a dot
spath = os.path.normpath(os.path.join(spath, base))
self.target_fileentry.set_filename(spath)
def setup_report_options_frame(self):
self.paper_label = Gtk.Label(label='<b>%s</b>' % _("Paper Options"))
self.paper_label.set_use_markup(True)
handler = self.options.handler
self.paper_frame = PaperFrame(
handler.get_paper_metric(),
handler.get_paper_name(),
handler.get_orientation(),
handler.get_margins(),
handler.get_custom_paper_size(),
)
self.notebook.insert_page(self.paper_frame, self.paper_label, 0)
self.paper_frame.show_all()
ReportDialog.setup_report_options_frame(self)
def get_options(self):
"""
Return the graph options.
"""
return graphdoc.GVOptions()
def doc_type_changed(self, obj):
"""
@ -201,81 +64,31 @@ class GraphvizReportDialog(ReportDialog):
paper size/orientation options, but it does need a template
file. Those changes are made here.
"""
self.open_with_app.set_sensitive(True)
fname = self.target_fileentry.get_full_path(0)
(spath, ext) = os.path.splitext(fname)
GraphReportDialog.doc_type_changed(self, obj)
ext_val = obj.get_ext()
if ext_val:
fname = spath + ext_val
else:
fname = spath
self.target_fileentry.set_filename(fname)
output_format_str = obj.get_oformat_str()
output_format_str = obj.get_clname()
if output_format_str in ['gvpdf', 'gspdf', 'ps']:
# Always use 72 DPI for PostScript and PDF files.
self.__gvoptions.dpi.set_value(72)
self.__gvoptions.dpi.set_available(False)
self._goptions.dpi.set_value(72)
self._goptions.dpi.set_available(False)
else:
self.__gvoptions.dpi.set_available(True)
self._goptions.dpi.set_available(True)
if output_format_str in ['gspdf', 'dot']:
# Multiple pages only valid for dot and PDF via GhostsScript.
self.__gvoptions.h_pages.set_available(True)
self.__gvoptions.v_pages.set_available(True)
self._goptions.h_pages.set_available(True)
self._goptions.v_pages.set_available(True)
else:
self.__gvoptions.h_pages.set_value(1)
self.__gvoptions.v_pages.set_value(1)
self.__gvoptions.h_pages.set_available(False)
self.__gvoptions.v_pages.set_available(False)
self._goptions.h_pages.set_value(1)
self._goptions.v_pages.set_value(1)
self._goptions.h_pages.set_available(False)
self._goptions.v_pages.set_available(False)
def make_document(self):
"""Create a document of the type requested by the user.
"""
pstyle = self.paper_frame.get_paper_style()
self.doc = self.format(self.options, pstyle)
self.options.set_document(self.doc)
def on_ok_clicked(self, obj):
"""The user is satisfied with the dialog choices. Validate
the output file name before doing anything else. If there is
a file name, gather the options and create the report."""
# Is there a filename? This should also test file permissions, etc.
if not self.parse_target_frame():
self.window.run()
# Preparation
self.parse_format_frame()
self.parse_user_options()
self.options.handler.set_paper_metric(
self.paper_frame.get_paper_metric())
self.options.handler.set_paper_name(self.paper_frame.get_paper_name())
self.options.handler.set_orientation(self.paper_frame.get_orientation())
self.options.handler.set_margins(self.paper_frame.get_paper_margins())
self.options.handler.set_custom_paper_size(
self.paper_frame.get_custom_paper_size())
# Create the output document.
self.make_document()
# Save options
self.options.handler.save_options()
config.set('interface.open-with-default-viewer',
self.open_with_app.get_active())
def parse_format_frame(self):
"""Parse the format frame of the dialog. Save the user
selected output format for later use."""
self.format = self.format_menu.get_reference()
format_name = self.format_menu.get_clname()
self.options.handler.set_format_name(format_name)
def setup_style_frame(self):
"""Required by ReportDialog"""
pass
#-------------------------------------------------------------------------------
#
# GraphvizFormatComboBox
#
#-------------------------------------------------------------------------------
class GraphvizFormatComboBox(BaseFormatComboBox):
FORMATS = graphdoc.FORMATS

View File

@ -54,8 +54,9 @@ from .. import add_gui_options, make_gui_option
from ...user import User
from ...dialog import ErrorDialog, OptionDialog
from gramps.gen.plug.report import (CATEGORY_TEXT, CATEGORY_DRAW, CATEGORY_BOOK,
CATEGORY_CODE, CATEGORY_WEB, CATEGORY_GRAPHVIZ,
standalone_categories)
CATEGORY_CODE, CATEGORY_WEB,
CATEGORY_GRAPHVIZ, CATEGORY_TREE,
standalone_categories)
from gramps.gen.plug.docgen import StyleSheet, StyleSheetList
from ...managedwindow import ManagedWindow
from ._stylecombobox import StyleComboBox
@ -673,6 +674,9 @@ def report(dbstate, uistate, person, report_class, options_class,
elif category == CATEGORY_GRAPHVIZ:
from ._graphvizreportdialog import GraphvizReportDialog
dialog_class = GraphvizReportDialog
elif category == CATEGORY_TREE:
from ._treereportdialog import TreeReportDialog
dialog_class = TreeReportDialog
elif category == CATEGORY_WEB:
from ._webreportdialog import WebReportDialog
dialog_class = WebReportDialog

View File

@ -0,0 +1,64 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2017 Nick Hall
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
"""class for generating dialogs for graphviz-based reports """
#-------------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------------
from ._graphreportdialog import GraphReportDialog, BaseFormatComboBox
from gramps.gen.plug.report import CATEGORY_TREE
import gramps.gen.plug.docgen.treedoc as treedoc
#-----------------------------------------------------------------------
#
# TreeReportDialog
#
#-----------------------------------------------------------------------
class TreeReportDialog(GraphReportDialog):
def make_doc_menu(self):
"""
Build a menu of document types that are appropriate for
this graph report.
"""
self.format_menu = TreeFormatComboBox()
def get_category(self):
"""
Return the report category.
"""
return CATEGORY_TREE
def get_options(self):
"""
Return the graph options.
"""
return treedoc.TreeOptions()
#-------------------------------------------------------------------------------
#
# TreeFormatComboBox
#
#-------------------------------------------------------------------------------
class TreeFormatComboBox(BaseFormatComboBox):
FORMATS = treedoc.FORMATS

View File

@ -318,6 +318,7 @@ gramps/gen/plug/_pluginreg.py
gramps/gen/plug/docbackend/docbackend.py
gramps/gen/plug/docgen/graphdoc.py
gramps/gen/plug/docgen/paperstyle.py
gramps/gen/plug/docgen/treedoc.py
gramps/gen/plug/menu/_enumeratedlist.py
gramps/gen/plug/report/_book.py
gramps/gen/plug/report/_constants.py