diff --git a/data/css/Web_Basic-Blue.css b/data/css/Web_Basic-Blue.css
index 44dabde96..3775627bc 100644
--- a/data/css/Web_Basic-Blue.css
+++ b/data/css/Web_Basic-Blue.css
@@ -288,10 +288,6 @@ table.infolist thead tr th {
table.infolist tr td {
border-bottom: dashed 1px #000;
vertical-align: middle;
- padding: 6px 0 6px 10px;
-}
-table.infolist tr td a {
- display: block;
}
table.infolist tr.BeginLetter td, table.infolist tr.BeginSurname td {
border-top: solid 1px #000;
@@ -351,15 +347,15 @@ div#Individuals {
margin: 0;
padding: 0;
}
-div#Individuals table.individuallist {
+div#Individuals table.IndividualList {
border-bottom: solid 1px #000;
}
-div#Individuals table.individuallist tbody tr td.ColumnSurname a:hover,
-div#Individuals table.individuallist tbody tr td.ColumnSurname a:active {
+div#Individuals table.IndividualList tbody tr td.ColumnSurname a:hover,
+div#Individuals table.IndividualList tbody tr td.ColumnSurname a:active {
cursor: default;
background: none;
}
-div#Individuals table.individuallist tbody tr td.ColumnName a {
+div#Individuals table.IndividualList tbody tr td.ColumnName a {
vertical-align: middle;
}
div#Individuals div table.infolist tr td p {
@@ -1129,9 +1125,10 @@ div.narrative {
}
.narrative p {
font: normal .9em/1.4em sans-serif;
- margin-top: .5em;
- margin-bottom: 0;
- padding: 0 20px 1em 20px;
+ margin: 0.1em 0 0.2em 0;
+}
+i + div.grampsstylednote p {
+ margin: 0.1em 0 0.2em 0;
}
/* Subsections : References
diff --git a/data/css/Web_Nebraska.css b/data/css/Web_Nebraska.css
index face9451d..9a5141282 100644
--- a/data/css/Web_Nebraska.css
+++ b/data/css/Web_Nebraska.css
@@ -276,7 +276,6 @@ table.infolist tr th a:hover {
table.infolist tr td {
font:normal 1.1em/1.4em serif;
vertical-align:middle;
- padding:.1em 10px;
}
table.infolist tr td a {
display:block;
@@ -396,30 +395,30 @@ table.surname thead tr th.ColumnParents, table.surname tbody tr td.ColumnParents
/* Individuals
----------------------------------------------------- */
#Individuals { }
-#Individuals table.individuallist {
+#Individuals table.IndividualList {
border-bottom:solid 1px #A97;
}
-#Individuals table.individuallist tbody tr td {
+#Individuals table.IndividualList tbody tr td {
border-bottom:dashed 1px #C1B398;
}
-#Individuals table.individuallist tbody tr td a:hover {
+#Individuals table.IndividualList tbody tr td a:hover {
text-decoration:none;
}
-table.individuallist tbody tr td.ColumnSurname a:hover, table.individuallist tbody tr td.ColumnSurname a:active {
+table.IndividualList tbody tr td.ColumnSurname a:hover, table.IndividualList tbody tr td.ColumnSurname a:active {
cursor:default;
color:black;
background:none;
}
-table.individuallist tbody tr td.ColumnName {
+table.IndividualList tbody tr td.ColumnName {
padding:0;
background-color:#FFF;
}
-table.individuallist tbody tr td.ColumnName a {
+table.IndividualList tbody tr td.ColumnName a {
display:block;
- padding:.6em 10px;
+ padding:.1em .1em;
vertical-align:middle;
}
-table.individuallist tbody tr td.ColumnName a:hover {
+table.IndividualList tbody tr td.ColumnName a:hover {
background-color:#C1B398;
}
#Individuals div table.infolist tr td p {
@@ -960,11 +959,12 @@ div#Addresses table.infolist tr td a, div#Addresses table.infolist tr td p a {
div.narrative {
padding-bottom:0;
}
+i + div.grampsstylednote p {
+ margin: 0.1em 0 0.2em 0;
+}
.narrative p {
+ margin: 0.1em 0 0.2em 0;
font:normal .9em/1.4em sans-serif;
- margin-top:.5em;
- margin-bottom:0;
- padding:0 20px 1em 20px;
}
/* Subsections : References
diff --git a/data/css/Web_Visually.css b/data/css/Web_Visually.css
index cea309708..f74a16832 100644
--- a/data/css/Web_Visually.css
+++ b/data/css/Web_Visually.css
@@ -454,30 +454,30 @@ div#Individuals {
margin: 0;
padding: 0;
}
-div#Individuals table.individuallist {
+div#Individuals table.IndividualList {
border-bottom: solid 1px #5D835F;
}
-div#Individuals table.individuallist tbody tr td {
+div#Individuals table.IndividualList tbody tr td {
border-bottom: dashed 1px #5D835F;
background-color: #D8F3D6;
}
-div#Individuals table.individuallist tbody tr td a {
+div#Individuals table.IndividualList tbody tr td a {
display: block;
padding: .6em 10px;
}
-div#Individuals table.individuallist tbody tr td.ColumnSurname a:hover,
-div#Individuals table.individuallist tbody tr td.ColumnSurname a:active {
+div#Individuals table.IndividualList tbody tr td.ColumnSurname a:hover,
+div#Individuals table.IndividualList tbody tr td.ColumnSurname a:active {
cursor:default;
color: #000;
background:none;
}
-div#Individuals table.individuallist tbody tr td.ColumnName {
+div#Individuals table.IndividualList tbody tr td.ColumnName {
background-color: #FFF;
}
-div#Individuals table.individuallist tbody tr td.ColumnName a {
+div#Individuals table.IndividualList tbody tr td.ColumnName a {
vertical-align:middle;
}
-div#Individuals table.individuallist tbody tr td.ColumnPartner {
+div#Individuals table.IndividualList tbody tr td.ColumnPartner {
background-color: #FFF;
}
div#Individuals div table.infolist tr td p {
diff --git a/data/css/narrative-maps.css b/data/css/narrative-maps.css
index 20bd9e767..ae926e390 100644
--- a/data/css/narrative-maps.css
+++ b/data/css/narrative-maps.css
@@ -33,7 +33,9 @@ body#FamilyMap {
border: solid 4px #000;
margin: 0px auto;
width: 800px;
- height: 800px;
+ height: 400px;
+ max-width: 90%;
+ max-height: 90%;
}
/* Place Maps
@@ -43,6 +45,8 @@ div#place_canvas {
border: solid 4px #000;
width: 500px;
height: 400px;
+ max-width: 90%;
+ max-height: 90%;
}
button#drop {
background-color: purple;
diff --git a/gramps/cli/grampscli.py b/gramps/cli/grampscli.py
index 49c656c7f..8514d394c 100644
--- a/gramps/cli/grampscli.py
+++ b/gramps/cli/grampscli.py
@@ -318,6 +318,7 @@ class CLIManager:
and self.dbstate.db.get_total() == 0):
self.dbstate.db.set_researcher(owner)
+ name_displayer.clear_custom_formats()
name_displayer.set_name_format(self.dbstate.db.name_formats)
fmt_default = config.get('preferences.name-format')
name_displayer.set_default_format(fmt_default)
diff --git a/gramps/cli/plug/__init__.py b/gramps/cli/plug/__init__.py
index cc46e25f2..299bf0add 100644
--- a/gramps/cli/plug/__init__.py
+++ b/gramps/cli/plug/__init__.py
@@ -47,7 +47,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, Option,
@@ -56,8 +57,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, DOCGEN_OPTIONS
from gramps.gen.dbstate import DbState
@@ -250,6 +251,15 @@ class CommandLineReport:
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)
@@ -320,6 +330,10 @@ class CommandLineReport:
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"
@@ -498,6 +512,15 @@ class CommandLineReport:
# 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:
@@ -665,7 +688,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,
diff --git a/gramps/gen/config.py b/gramps/gen/config.py
index a96e3a34e..47ed6322c 100644
--- a/gramps/gen/config.py
+++ b/gramps/gen/config.py
@@ -274,22 +274,25 @@ register('preferences.last-view', '')
register('preferences.last-views', [])
register('preferences.family-relation-type', 3) # UNKNOWN
register('preferences.age-display-precision', 1)
-register('preferences.color-gender-male-alive', '#b8cee6')
-register('preferences.color-gender-male-death', '#b8cee6')
-register('preferences.color-gender-female-alive', '#feccf0')
-register('preferences.color-gender-female-death', '#feccf0')
-register('preferences.color-gender-unknown-alive', '#f3dbb6')
-register('preferences.color-gender-unknown-death', '#f3dbb6')
-#register('preferences.color-gender-other-alive', '#fcaf3e')
-#register('preferences.color-gender-other-death', '#fcaf3e')
-register('preferences.bordercolor-gender-male-alive', '#1f4986')
-register('preferences.bordercolor-gender-male-death', '#000000')
-register('preferences.bordercolor-gender-female-alive', '#861f69')
-register('preferences.bordercolor-gender-female-death', '#000000')
-register('preferences.bordercolor-gender-unknown-alive', '#8e5801')
-register('preferences.bordercolor-gender-unknown-death', '#000000')
-#register('preferences.bordercolor-gender-other-alive', '#f57900')
-#register('preferences.bordercolor-gender-other-death', '#000000')
+
+register('colors.scheme', 0)
+register('colors.male-alive', ['#b8cee6', '#1f344a'])
+register('colors.male-dead', ['#b8cee6', '#2d3039'])
+register('colors.female-alive', ['#feccf0', '#62242D'])
+register('colors.female-dead', ['#feccf0', '#3a292b'])
+register('colors.unknown-alive', ['#f3dbb6', '#75507B'])
+register('colors.unknown-dead', ['#f3dbb6', '#35103b'])
+register('colors.family', ['#eeeeee', '#454545'])
+register('colors.family-divorced', ['#ffdede', '#5c3636'])
+register('colors.home-person', ['#bbe68a', '#304918'])
+register('colors.border-male-alive', ['#1f4986', '#171d26'])
+register('colors.border-male-dead', ['#000000', '#000000'])
+register('colors.border-female-alive', ['#861f69', '#261111'])
+register('colors.border-female-dead', ['#000000', '#000000'])
+register('colors.border-unknown-alive', ['#8e5801', '#8e5801'])
+register('colors.border-unknown-dead', ['#000000', '#000000'])
+register('colors.border-family', ['#cccccc', '#252525'])
+register('colors.border-family-divorced', ['#ff7373', '#720b0b'])
register('researcher.researcher-addr', '')
register('researcher.researcher-locality', '')
diff --git a/gramps/gen/datehandler/_date_de.py b/gramps/gen/datehandler/_date_de.py
index 3ba9993fa..4bb37cc8f 100644
--- a/gramps/gen/datehandler/_date_de.py
+++ b/gramps/gen/datehandler/_date_de.py
@@ -292,7 +292,7 @@ class DateDisplayDE(DateDisplay):
)
# this definition must agree with its "_display_gregorian" method
- def _display_gregorian(self, date_val):
+ def _display_gregorian(self, date_val, **kwargs):
"""
display gregorian calendar date in different format
"""
diff --git a/gramps/gen/datehandler/_date_el.py b/gramps/gen/datehandler/_date_el.py
index e1425132e..5668f9a69 100644
--- a/gramps/gen/datehandler/_date_el.py
+++ b/gramps/gen/datehandler/_date_el.py
@@ -155,7 +155,7 @@ class DateDisplayEL(DateDisplay):
)
# this definition must agree with its "_display_gregorian" method
- def _display_gregorian(self, date_val):
+ def _display_gregorian(self, date_val, **kwargs):
"""
display gregorian calendar date in different format
"""
diff --git a/gramps/gen/datehandler/_date_lt.py b/gramps/gen/datehandler/_date_lt.py
index c706ab5fc..072ee8747 100644
--- a/gramps/gen/datehandler/_date_lt.py
+++ b/gramps/gen/datehandler/_date_lt.py
@@ -187,7 +187,7 @@ class DateDisplayLT(DateDisplay):
"mmmm m. mėnesio diena d.", "Mėn diena, metai")
# this definition must agree with its "_display_gregorian" method
- def _display_gregorian(self, date_val):
+ def _display_gregorian(self, date_val, **kwargs):
"""
display gregorian calendar date in different format
"""
diff --git a/gramps/gen/datehandler/_date_nl.py b/gramps/gen/datehandler/_date_nl.py
index bcdf94bbd..5da036cfd 100644
--- a/gramps/gen/datehandler/_date_nl.py
+++ b/gramps/gen/datehandler/_date_nl.py
@@ -164,7 +164,7 @@ class DateDisplayNL(DateDisplay):
)
# this definition must agree with its "_display_gregorian" method
- def _display_gregorian(self, date_val):
+ def _display_gregorian(self, date_val, **kwargs):
"""
display gregorian calendar date in different format
"""
diff --git a/gramps/gen/datehandler/_date_pl.py b/gramps/gen/datehandler/_date_pl.py
index acfa6ec5d..3bec0a5e0 100644
--- a/gramps/gen/datehandler/_date_pl.py
+++ b/gramps/gen/datehandler/_date_pl.py
@@ -215,7 +215,7 @@ class DateDisplayPL(DateDisplay):
"XII"
)
- def _display_gregorian(self, date_val):
+ def _display_gregorian(self, date_val, **kwargs):
"""
display gregorian calendar date in different format
"""
diff --git a/gramps/gen/datehandler/_date_sr.py b/gramps/gen/datehandler/_date_sr.py
index 81e9aceb1..c77c16fb6 100644
--- a/gramps/gen/datehandler/_date_sr.py
+++ b/gramps/gen/datehandler/_date_sr.py
@@ -240,7 +240,7 @@ class DateDisplaySR_Base(DateDisplay):
"VII", "VIII", "IX", "X", "XI", "XII"
)
- def _display_gregorian(self, date_val):
+ def _display_gregorian(self, date_val, **kwargs):
"""
display gregorian calendar date in different format
"""
diff --git a/gramps/gen/display/name.py b/gramps/gen/display/name.py
index ffa4b94a1..6ab47682f 100644
--- a/gramps/gen/display/name.py
+++ b/gramps/gen/display/name.py
@@ -418,6 +418,11 @@ class NameDisplay:
result = raw_data[_FIRSTNAME]
return ' '.join(result.split())
+ def clear_custom_formats(self):
+ self.name_formats = {num: value
+ for num, value in self.name_formats.items()
+ if num >= 0}
+
def set_name_format(self, formats):
raw_func_dict = {
diff --git a/gramps/gen/filters/rules/person/_isdescendantfamilyof.py b/gramps/gen/filters/rules/person/_isdescendantfamilyof.py
index 605f7e044..49777d9c5 100644
--- a/gramps/gen/filters/rules/person/_isdescendantfamilyof.py
+++ b/gramps/gen/filters/rules/person/_isdescendantfamilyof.py
@@ -82,7 +82,8 @@ class IsDescendantFamilyOf(Rule):
while expand:
person = expand.pop(0)
- if person is None:
+ if person is None or person.handle in self.matches:
+ # if we have been here before, skip
continue
self.matches.add(person.handle)
for family_handle in person.get_family_handle_list():
diff --git a/gramps/gen/filters/rules/person/_isdescendantof.py b/gramps/gen/filters/rules/person/_isdescendantof.py
index 9b9056aaa..7e7ec7783 100644
--- a/gramps/gen/filters/rules/person/_isdescendantof.py
+++ b/gramps/gen/filters/rules/person/_isdescendantof.py
@@ -67,7 +67,8 @@ class IsDescendantOf(Rule):
return person.handle in self.map
def init_list(self, person, first):
- if not person:
+ if not person or person.handle in self.map:
+ # if we have been here before, skip
return
if not first:
self.map.add(person.handle)
diff --git a/gramps/gen/filters/rules/person/_islessthannthgenerationancestorof.py b/gramps/gen/filters/rules/person/_islessthannthgenerationancestorof.py
index b95e330d1..847d7294e 100644
--- a/gramps/gen/filters/rules/person/_islessthannthgenerationancestorof.py
+++ b/gramps/gen/filters/rules/person/_islessthannthgenerationancestorof.py
@@ -61,6 +61,9 @@ class IsLessThanNthGenerationAncestorOf(Rule):
queue = [(root_handle, 1)] # generation 1 is root
while queue:
handle, gen = queue.pop(0) # pop off front of queue
+ if handle in self.map:
+ # if we have been here before, skip
+ continue
self.map.add(handle)
gen += 1
if gen <= int(self.list[1]):
diff --git a/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofbookmarked.py b/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofbookmarked.py
index 187f6317d..5e0510cbf 100644
--- a/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofbookmarked.py
+++ b/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofbookmarked.py
@@ -71,7 +71,8 @@ class IsLessThanNthGenerationAncestorOfBookmarked(Rule):
def init_ancestor_list(self, handle, gen):
# if p.get_handle() in self.map:
# loop_error(self.orig,p)
- if not handle:
+ if not handle or handle in self.map:
+ # if been here already, skip
return
if gen:
self.map.add(handle)
diff --git a/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofdefaultperson.py b/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofdefaultperson.py
index 9f2ae9870..cbce57a74 100644
--- a/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofdefaultperson.py
+++ b/gramps/gen/filters/rules/person/_islessthannthgenerationancestorofdefaultperson.py
@@ -64,7 +64,8 @@ class IsLessThanNthGenerationAncestorOfDefaultPerson(Rule):
def init_ancestor_list(self, handle, gen):
# if p.get_handle() in self.map:
# loop_error(self.orig,p)
- if not handle:
+ if not handle or handle in self.map:
+ # if we have been here before, skip
return
if gen:
self.map.add(handle)
diff --git a/gramps/gen/filters/rules/person/_islessthannthgenerationdescendantof.py b/gramps/gen/filters/rules/person/_islessthannthgenerationdescendantof.py
index 74a70e3e5..3d5fb16da 100644
--- a/gramps/gen/filters/rules/person/_islessthannthgenerationdescendantof.py
+++ b/gramps/gen/filters/rules/person/_islessthannthgenerationdescendantof.py
@@ -65,7 +65,8 @@ class IsLessThanNthGenerationDescendantOf(Rule):
return person.handle in self.map
def init_list(self,person,gen):
- if not person:
+ if not person or person.handle in self.map:
+ # if we have been here before, skip
return
if gen:
self.map.add(person.handle)
diff --git a/gramps/gen/plug/__init__.py b/gramps/gen/plug/__init__.py
index 05a78efcc..a07d1b05a 100644
--- a/gramps/gen/plug/__init__.py
+++ b/gramps/gen/plug/__init__.py
@@ -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,
@@ -46,14 +46,14 @@ from ._options import (Options, OptionListCollection, OptionList,
OptionHandler, MenuOptions)
__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",
- "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",
- "CATEGORY_QR_PLACE", "CATEGORY_QR_REPOSITORY", "CATEGORY_QR_NOTE",
- "CATEGORY_QR_DATE", "PTYPE_STR", "CATEGORY_QR_MEDIA",
- "CATEGORY_QR_CITATION", "CATEGORY_QR_SOURCE_OR_CITATION",
- "START", "END", "make_environment"]
+ "PluginRegister", "BasePluginManager", "ImportPlugin",
+ "ExportPlugin", "DocGenPlugin", "REPORT", "TOOL", "CATEGORY_TEXT",
+ "CATEGORY_DRAW", "CATEGORY_CODE", "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", "CATEGORY_QR_PLACE",
+ "CATEGORY_QR_REPOSITORY", "CATEGORY_QR_NOTE", "CATEGORY_QR_DATE",
+ "PTYPE_STR", "CATEGORY_QR_MEDIA", "CATEGORY_QR_CITATION",
+ "CATEGORY_QR_SOURCE_OR_CITATION", "START", "END",
+ "make_environment"]
diff --git a/gramps/gen/plug/_pluginreg.py b/gramps/gen/plug/_pluginreg.py
index 6a786c0c0..e637e7ea8 100644
--- a/gramps/gen/plug/_pluginreg.py
+++ b/gramps/gen/plug/_pluginreg.py
@@ -97,8 +97,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
@@ -1043,6 +1045,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,
@@ -1242,8 +1245,7 @@ class PluginRegister:
"""
Return a list of :class:`PluginData` that are of type ptype
"""
- return [self.get_plugin(id) for id in
- set([x.id for x in self.__plugindata if x.ptype == ptype])]
+ return [x for x in self.__plugindata if x.ptype == ptype]
def report_plugins(self, gui=True):
"""
@@ -1352,6 +1354,4 @@ class PluginRegister:
"""
Return a list of :class:`PluginData` that have load_on_reg == True
"""
- return [self.get_plugin(id) for id in
- set([x.id for x in self.__plugindata
- if x.load_on_reg == True])]
+ return [x for x in self.__plugindata if x.load_on_reg == True]
diff --git a/gramps/gen/plug/docgen/__init__.py b/gramps/gen/plug/docgen/__init__.py
index d2dc8452c..a6fd37ca1 100644
--- a/gramps/gen/plug/docgen/__init__.py
+++ b/gramps/gen/plug/docgen/__init__.py
@@ -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
diff --git a/gramps/gen/plug/docgen/treedoc.py b/gramps/gen/plug/docgen/treedoc.py
new file mode 100644
index 000000000..41bf666cb
--- /dev/null
+++ b/gramps/gen/plug/docgen/treedoc.py
@@ -0,0 +1,633 @@
+#
+# 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':
+ scheme = config.get('colors.scheme')
+ male_bg = config.get('colors.male-dead')[scheme][1:]
+ female_bg = config.get('colors.female-dead')[scheme][1:]
+ neuter_bg = config.get('colors.unknown-dead')[scheme][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_media_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}]
diff --git a/gramps/gen/plug/report/_book.py b/gramps/gen/plug/report/_book.py
index aa867e5ec..9a709043c 100644
--- a/gramps/gen/plug/report/_book.py
+++ b/gramps/gen/plug/report/_book.py
@@ -495,14 +495,14 @@ class BookList:
"""
Saves the current BookList to the associated file.
"""
- with open(self.file, "w") as b_f:
+ with open(self.file, "w", encoding="utf-8") as b_f:
b_f.write("\n")
b_f.write('\n')
for name in sorted(self.bookmap): # enable a diff of archived copies
book = self.get_book(name)
- dbname = book.get_dbname()
+ dbname = escape(book.get_dbname())
b_f.write(' '
- '\n' % (name, dbname))
+ '\n' % (escape(name), dbname))
for item in book.get_item_list():
b_f.write(' - \n' % (
@@ -566,7 +566,7 @@ class BookList:
'\n' % book.get_format_name())
if book.get_output():
b_f.write(' '
- '\n' % book.get_output())
+ '\n' % escape(book.get_output()))
b_f.write('
\n')
b_f.write('\n')
@@ -578,8 +578,15 @@ class BookList:
try:
parser = make_parser()
parser.setContentHandler(BookParser(self, self.dbase))
- with open(self.file) as the_file:
- parser.parse(the_file)
+ # bug 10387; XML should be utf8, but was not previously saved
+ # that way. So try to read utf8, if fails, try with system
+ # encoding. Only an issue on non-utf8 systems.
+ try:
+ with open(self.file, encoding="utf-8") as the_file:
+ parser.parse(the_file)
+ except UnicodeDecodeError:
+ with open(self.file) as the_file:
+ parser.parse(the_file)
except (IOError, OSError, ValueError, SAXParseException, KeyError,
AttributeError):
LOG.debug("Failed to parse book list", exc_info=True)
diff --git a/gramps/gen/plug/report/_constants.py b/gramps/gen/plug/report/_constants.py
index 7a7016463..97ca81650 100644
--- a/gramps/gen/plug/report/_constants.py
+++ b/gramps/gen/plug/report/_constants.py
@@ -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"),
diff --git a/gramps/gen/utils/docgen/csvtab.py b/gramps/gen/utils/docgen/csvtab.py
index a9d6bfe51..01822ebf4 100644
--- a/gramps/gen/utils/docgen/csvtab.py
+++ b/gramps/gen/utils/docgen/csvtab.py
@@ -31,6 +31,7 @@ import csv
#
#-------------------------------------------------------------------------
from .tabbeddoc import *
+from ...constfunc import win
class CSVTab(TabbedDoc):
@@ -48,7 +49,8 @@ class CSVTab(TabbedDoc):
else:
self.filename = filename
- self.f = open(self.filename, "w")
+ self.f = open(self.filename, "w",
+ encoding='utf_8_sig' if win() else 'utf_8')
self.writer = csv.writer(self.f)
def close(self):
diff --git a/gramps/gui/configure.py b/gramps/gui/configure.py
index d8e7d3ab6..3e301ef28 100644
--- a/gramps/gui/configure.py
+++ b/gramps/gui/configure.py
@@ -249,13 +249,23 @@ class ConfigureDialog(ManagedWindow):
"""
self.__config.set(constant, obj.get_text())
- def update_color(self, obj, constant, color_hex_label):
+ def update_color(self, obj, pspec, constant, color_hex_label):
+ """
+ Called on changing some color.
+ Either on programmatically color change.
+ """
rgba = obj.get_rgba()
hexval = "#%02x%02x%02x" % (int(rgba.red * 255),
int(rgba.green * 255),
int(rgba.blue * 255))
color_hex_label.set_text(hexval)
- self.__config.set(constant, hexval)
+ colors = self.__config.get(constant)
+ if isinstance(colors, list):
+ scheme = self.__config.get('colors.scheme')
+ colors[scheme] = hexval
+ self.__config.set(constant, colors)
+ else:
+ self.__config.set(constant, hexval)
def update_checkbox(self, obj, constant, config=None):
if not config:
@@ -383,15 +393,24 @@ class ConfigureDialog(ManagedWindow):
grid.attach(entry, col_attach+1, index, 1, 1)
def add_color(self, grid, label, index, constant, config=None, col=0):
+ """
+ Add color chooser widget with label to the grid.
+ """
if not config:
config = self.__config
lwidget = BasicLabel(_("%s: ") % label) # needed for French, else ignore
- hexval = config.get(constant)
+ colors = config.get(constant)
+ if isinstance(colors, list):
+ scheme = config.get('colors.scheme')
+ hexval = colors[scheme]
+ else:
+ hexval = colors
color = Gdk.color_parse(hexval)
entry = Gtk.ColorButton(color=color)
color_hex_label = BasicLabel(hexval)
color_hex_label.set_hexpand(True)
- entry.connect('color-set', self.update_color, constant, color_hex_label)
+ entry.connect('notify::color', self.update_color, constant,
+ color_hex_label)
grid.attach(lwidget, col, index, 1, 1)
grid.attach(entry, col+1, index, 1, 1)
grid.attach(color_hex_label, col+2, index, 1, 1)
@@ -554,7 +573,7 @@ class GrampsPreferences(ConfigureDialog):
def add_color_panel(self, configdialog):
"""
- Add the tab to set defaults colors for graph boxes
+ Add the tab to set defaults colors for graph boxes.
"""
grid = Gtk.Grid()
grid.set_border_width(12)
@@ -562,40 +581,62 @@ class GrampsPreferences(ConfigureDialog):
grid.set_row_spacing(6)
self.add_text(grid, _('Set the colors used for boxes in the graphical views'),
0, line_wrap=False)
- self.add_color(grid, _('Gender Male Alive'), 1,
- 'preferences.color-gender-male-alive')
- self.add_color(grid, _('Border Male Alive'), 2,
- 'preferences.bordercolor-gender-male-alive')
- self.add_color(grid, _('Gender Male Death'), 3,
- 'preferences.color-gender-male-death')
- self.add_color(grid, _('Border Male Death'), 4,
- 'preferences.bordercolor-gender-male-death')
- self.add_color(grid, _('Gender Female Alive'), 1,
- 'preferences.color-gender-female-alive', col=4)
- self.add_color(grid, _('Border Female Alive'), 2,
- 'preferences.bordercolor-gender-female-alive', col=4)
- self.add_color(grid, _('Gender Female Death'), 3,
- 'preferences.color-gender-female-death', col=4)
- self.add_color(grid, _('Border Female Death'), 4,
- 'preferences.bordercolor-gender-female-death', col=4)
-## self.add_color(grid, _('Gender Other Alive'), 5,
-## 'preferences.color-gender-other-alive')
-## self.add_color(grid, _('Border Other Alive'), 6,
-## 'preferences.bordercolor-gender-other-alive')
-## self.add_color(grid, _('Gender Other Death'), 7,
-## 'preferences.color-gender-other-death')
-## self.add_color(grid, _('Border Other Death'), 8,
-## 'preferences.bordercolor-gender-other-death')
- self.add_color(grid, _('Gender Unknown Alive'), 5,
- 'preferences.color-gender-unknown-alive', col=4)
- self.add_color(grid, _('Border Unknown Alive'), 6,
- 'preferences.bordercolor-gender-unknown-alive', col=4)
- self.add_color(grid, _('Gender Unknown Death'), 7,
- 'preferences.color-gender-unknown-death', col=4)
- self.add_color(grid, _('Border Unknown Death'), 8,
- 'preferences.bordercolor-gender-unknown-death', col=4)
+
+ hbox = Gtk.Box(spacing=12)
+ self.color_scheme_box = Gtk.ComboBoxText()
+ formats = [_("Light colors"),
+ _("Dark colors"),]
+ list(map(self.color_scheme_box.append_text, formats))
+ scheme = config.get('colors.scheme')
+ self.color_scheme_box.set_active(scheme)
+ self.color_scheme_box.connect('changed', self.color_scheme_changed)
+ lwidget = BasicLabel(_("%s: ") % _('Color scheme'))
+ hbox.pack_start(lwidget, False, False, 0)
+ hbox.pack_start(self.color_scheme_box, False, False, 0)
+
+ restore_btn = Gtk.Button(_('Restore to defaults'))
+ restore_btn.connect('clicked', self.restore_colors)
+ hbox.pack_start(restore_btn, False, False, 0)
+ grid.attach(hbox, 1, 1, 6, 1)
+
+ color_list = [
+ (_('Male Alive'), 'male-alive', 2, 0),
+ (_('Male Dead'), 'male-dead', 4, 0),
+ (_('Female Alive'), 'female-alive', 2, 4),
+ (_('Female Dead'), 'female-dead', 4, 4),
+ (_('Unknown Alive'), 'unknown-alive', 6, 4),
+ (_('Unknown Dead'), 'unknown-dead', 8, 4),
+ (_('Family Node'), 'family', 7, 0),
+ (_('Family Divorced'), 'family-divorced', 9, 0),
+ (_('Home Person'), 'home-person', 6, 0),
+ (_('Border Male Alive'), 'border-male-alive', 3, 0),
+ (_('Border Male Dead'), 'border-male-dead', 5, 0),
+ (_('Border Female Alive'), 'border-female-alive', 3, 4),
+ (_('Border Female Dead'), 'border-female-dead', 5, 4),
+ (_('Border Unknown Alive'), 'border-unknown-alive', 7, 4),
+ (_('Border Unknown Dead'), 'border-unknown-dead', 9, 4),
+ (_('Border Family'), 'border-family', 8, 0),
+ (_('Border Family Divorced'), 'border-family-divorced', 10, 0),
+ ]
+
+ self.colors = {}
+ for color in color_list:
+ pref_name = 'colors.' + color[1]
+ self.colors[pref_name] = self.add_color(grid, color[0], color[2],
+ pref_name, col=color[3])
return _('Colors'), grid
+ def restore_colors(self, widget=None):
+ """
+ Restore colors of selected scheme to default.
+ """
+ scheme = config.get('colors.scheme')
+ for key, widget in self.colors.items():
+ color = Gdk.RGBA()
+ hexval = config.get_default(key)[scheme]
+ Gdk.RGBA.parse(color, hexval)
+ widget.set_rgba(color)
+
def add_advanced_panel(self, configdialog):
grid = Gtk.Grid()
grid.set_border_width(12)
@@ -1205,6 +1246,18 @@ class GrampsPreferences(ConfigureDialog):
self.old_format = the_list.get_value(the_iter, COL_FMT)
win = DisplayNameEditor(self.uistate, self.dbstate, self.track, self)
+ def color_scheme_changed(self, obj):
+ """
+ Called on swiching color scheme.
+ """
+ scheme = obj.get_active()
+ config.set('colors.scheme', scheme)
+ for key, widget in self.colors.items():
+ color = Gdk.RGBA()
+ hexval = config.get(key)[scheme]
+ Gdk.RGBA.parse(color, hexval)
+ widget.set_rgba(color)
+
def check_for_type_changed(self, obj):
active = obj.get_active()
if active == 0: # update
diff --git a/gramps/gui/dialog.py b/gramps/gui/dialog.py
index 0bb6d19ae..01f8f2cb9 100644
--- a/gramps/gui/dialog.py
+++ b/gramps/gui/dialog.py
@@ -35,6 +35,7 @@ _LOG = logging.getLogger(".dialog")
#-------------------------------------------------------------------------
from gi.repository import GObject
from gi.repository import Gtk
+from gi.repository import Gdk
from gi.repository import GdkPixbuf
#-------------------------------------------------------------------------
@@ -46,6 +47,7 @@ from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
from gramps.gen.const import ICON, URL_BUGHOME
from gramps.gen.config import config
+from gramps.gen.constfunc import is_quartz
from .glade import Glade
from .display import display_url
@@ -506,6 +508,12 @@ def main(args):
win = Gtk.Window()
win.set_title('Dialog test window')
win.set_position(Gtk.WindowPosition.CENTER)
+ #Set the mnemonic modifier on Macs to alt-ctrl so that it
+ #doesn't interfere with the extended keyboard, see
+ #https://gramps-project.org/bugs/view.php?id=6943
+ if is_quartz():
+ win.set_mnemonic_modifier(
+ Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK)
def cb(window, event):
Gtk.main_quit()
win.connect('delete-event', cb)
diff --git a/gramps/gui/editors/displaytabs/embeddedlist.py b/gramps/gui/editors/displaytabs/embeddedlist.py
index 8798fb1a3..7bbeaf0a3 100644
--- a/gramps/gui/editors/displaytabs/embeddedlist.py
+++ b/gramps/gui/editors/displaytabs/embeddedlist.py
@@ -42,6 +42,7 @@ from gi.repository import Pango
# Gramps classes
#
#-------------------------------------------------------------------------
+from ...widgets.cellrenderertextedit import CellRendererTextEdit
from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
from ...utils import is_right_click
@@ -482,7 +483,10 @@ class EmbeddedList(ButtonTab):
type_col = self._column_names[pair[1]][3]
if (type_col in [TEXT_COL, MARKUP_COL, TEXT_EDIT_COL]):
- renderer = Gtk.CellRendererText()
+ if type_col == TEXT_EDIT_COL:
+ renderer = CellRendererTextEdit()
+ else:
+ renderer = Gtk.CellRendererText()
renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
if type_col == TEXT_COL or type_col == TEXT_EDIT_COL:
column = Gtk.TreeViewColumn(name, renderer, text=pair[1])
@@ -519,9 +523,12 @@ class EmbeddedList(ButtonTab):
# insert the colum into the tree
column.set_resizable(True)
column.set_clickable(True)
- column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
- #column.set_min_width(self._column_names[pair[1]][2])
- column.set_fixed_width(self._column_names[pair[1]][2])
+ if self._column_names[pair[1]][2] != -1:
+ column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
+ #column.set_min_width(self._column_names[pair[1]][2])
+ column.set_fixed_width(self._column_names[pair[1]][2])
+ else:
+ column.set_expand(True)
column.set_sort_column_id(self._column_names[pair[1]][1])
self.columns.append(column)
diff --git a/gramps/gui/editors/displaytabs/surnametab.py b/gramps/gui/editors/displaytabs/surnametab.py
index 71f838723..c3e19a474 100644
--- a/gramps/gui/editors/displaytabs/surnametab.py
+++ b/gramps/gui/editors/displaytabs/surnametab.py
@@ -45,7 +45,7 @@ _ENTER = Gdk.keyval_from_name("Enter")
#
#-------------------------------------------------------------------------
from .surnamemodel import SurnameModel
-from .embeddedlist import EmbeddedList, TEXT_COL, MARKUP_COL, ICON_COL
+from .embeddedlist import EmbeddedList, TEXT_EDIT_COL
from ...ddtargets import DdTargets
from gramps.gen.lib import Surname, NameOriginType
from ...utils import get_primary_mask
@@ -71,9 +71,9 @@ class SurnameTab(EmbeddedList):
#index = column in model. Value =
# (name, sortcol in model, width, markup/text
_column_names = [
- (_('Prefix'), -1, 150, TEXT_COL, -1, None),
- (_('Surname'), -1, 250, TEXT_COL, -1, None),
- (_('Connector'), -1, 100, TEXT_COL, -1, None),
+ (_('Prefix'), 0, 150, TEXT_EDIT_COL, -1, None),
+ (_('Surname'), 1, -1, TEXT_EDIT_COL, -1, None),
+ (_('Connector'), 2, 100, TEXT_EDIT_COL, -1, None),
]
_column_combo = (_('Origin'), -1, 150, 3) # name, sort, width, modelcol
_column_toggle = (_('Name|Primary'), -1, 80, 4)
@@ -94,14 +94,6 @@ class SurnameTab(EmbeddedList):
#first the standard text columns with normal method
EmbeddedList.build_columns(self)
- # Need to add attributes to renderers
- # and connect renderers to the 'edited' signal
- for colno in range(len(self.columns)):
- for renderer in self.columns[colno].get_cells():
- renderer.set_property('editable', not self.dbstate.db.readonly)
- renderer.connect('editing_started', self.on_edit_start, colno)
- renderer.connect('edited', self.on_edit_inline, self.column_order()[colno][1])
-
# now we add the two special columns
# combobox for type
colno = len(self.columns)
@@ -133,7 +125,7 @@ class SurnameTab(EmbeddedList):
column.set_resizable(True)
column.set_sort_column_id(self._column_combo[1])
column.set_min_width(self._column_combo[2])
- column.set_expand(True)
+ column.set_expand(False)
self.columns.append(column)
self.tree.append_column(column)
# toggle box for primary
@@ -149,7 +141,7 @@ class SurnameTab(EmbeddedList):
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
column.set_alignment(0.5)
column.set_sort_column_id(self._column_toggle[1])
- column.set_min_width(self._column_toggle[2])
+ column.set_max_width(self._column_toggle[2])
self.columns.append(column)
self.tree.append_column(column)
@@ -161,6 +153,24 @@ class SurnameTab(EmbeddedList):
## svalue = self.cmborigmap[second]
## return glocale.strcoll(fvalue, svalue)
+ def setup_editable_col(self):
+ """
+ inherit this and set the variables needed for editable columns
+ Variable edit_col_funcs needs to be a dictionary from model col_nr to
+ function to call for
+ Example:
+ self.edit_col_funcs ={1: {'edit_start': self.on_edit_start,
+ 'edited': self.on_edited
+ }}
+ """
+ self.edit_col_funcs = {
+ 0: {'edit_start': self.on_edit_start,
+ 'edited': self.on_edit_inline},
+ 1: {'edit_start': self.on_edit_start,
+ 'edited': self.on_edit_inline},
+ 2: {'edit_start': self.on_edit_start,
+ 'edited': self.on_edit_inline}}
+
def get_data(self):
return self.obj.get_surname_list()
@@ -194,6 +204,16 @@ class SurnameTab(EmbeddedList):
if self.on_change:
self.on_change()
+ def post_rebuild(self, prebuildpath):
+ """
+ Called when data model has changed, in particular necessary when row
+ order is updated.
+ @param prebuildpath: path selected before rebuild, None if none
+ @type prebuildpath: tree path
+ """
+ if self.on_change:
+ self.on_change()
+
def column_order(self):
# order of columns for EmbeddedList. Only the text columns here
return ((1, 0), (1, 1), (1, 2))
@@ -239,11 +259,13 @@ class SurnameTab(EmbeddedList):
"""
self.on_edit_start(cellr, celle, path, colnr)
#set up autocomplete
+ entry = celle.get_child()
+ entry.set_width_chars(10)
completion = Gtk.EntryCompletion()
completion.set_model(self.cmborig)
completion.set_minimum_key_length(1)
completion.set_text_column(1)
- celle.get_child().set_completion(completion)
+ entry.set_completion(completion)
#
celle.connect('changed', self.on_origcmb_change, path, colnr)
diff --git a/gramps/gui/editors/editperson.py b/gramps/gui/editors/editperson.py
index 0f0477dc9..cd8276a52 100644
--- a/gramps/gui/editors/editperson.py
+++ b/gramps/gui/editors/editperson.py
@@ -197,9 +197,6 @@ class EditPerson(EditPrimary):
self.singsurnfr = SingSurn(self.top)
self.multsurnfr = self.top.get_object("hboxmultsurnames")
self.singlesurn_active = True
- self.surntab = SurnameTab(self.dbstate, self.uistate, self.track,
- self.obj.get_primary_name(),
- on_change=self._changed_name)
self.set_contexteventbox(self.top.get_object("eventboxtop"))
@@ -445,6 +442,9 @@ class EditPerson(EditPrimary):
self.preview_name = self.top.get_object("full_name")
self.preview_name.override_font(Pango.FontDescription('sans bold 12'))
+ self.surntab = SurnameTab(self.dbstate, self.uistate, self.track,
+ self.obj.get_primary_name(),
+ on_change=self._changed_name)
def get_start_date(self):
"""
@@ -936,7 +936,8 @@ class EditPerson(EditPrimary):
msurhbox = self.top.get_object("hboxmultsurnames")
msurhbox.remove(self.surntab)
self.surntab = SurnameTab(self.dbstate, self.uistate, self.track,
- self.obj.get_primary_name())
+ self.obj.get_primary_name(),
+ on_change=self._changed_name)
self.multsurnfr.set_size_request(-1,
int(config.get('interface.surname-box-height')))
msurhbox.pack_start(self.surntab, True, True, 0)
diff --git a/gramps/gui/glade.py b/gramps/gui/glade.py
index b6985dea7..1dcc0226e 100644
--- a/gramps/gui/glade.py
+++ b/gramps/gui/glade.py
@@ -46,6 +46,7 @@ from gi.repository import Gtk
#
#------------------------------------------------------------------------
from gramps.gen.const import GLADE_DIR, GRAMPS_LOCALE as glocale
+from gramps.gen.constfunc import is_quartz
#------------------------------------------------------------------------
#
@@ -142,11 +143,19 @@ class Glade(Gtk.Builder):
# toplevel is given
if toplevel:
loadlist = [toplevel] + also_load
- self.add_objects_from_file(path, loadlist)
+ with open(path, 'r', encoding='utf-8') as builder_file:
+ data = builder_file.read().replace('\n', '')
+ if is_quartz():
+ data = data.replace('GDK_CONTROL_MASK', 'GDK_META_MASK')
+ self.add_objects_from_string(data, loadlist)
self.__toplevel = self.get_object(toplevel)
# toplevel not given
else:
- self.add_from_file(path)
+ with open(path, 'r', encoding='utf-8') as builder_file:
+ data = builder_file.read().replace('\n', '')
+ if is_quartz():
+ data = data.replace('GDK_CONTROL_MASK', 'GDK_META_MASK')
+ self.add_from_string(data)
# first, use filename as possible toplevel widget name
self.__toplevel = self.get_object(filename.rpartition('.')[0])
diff --git a/gramps/gui/glade/editcitation.glade b/gramps/gui/glade/editcitation.glade
index a0844298a..4f8d0fa03 100644
--- a/gramps/gui/glade/editcitation.glade
+++ b/gramps/gui/glade/editcitation.glade
@@ -197,7 +197,7 @@