diff --git a/GraphView/MANIFEST b/GraphView/MANIFEST
index c71668456..e10eb5d78 100644
--- a/GraphView/MANIFEST
+++ b/GraphView/MANIFEST
@@ -1,2 +1,3 @@
GraphView/gramps-graph.svg
GraphView/avatars/*
+GraphView/themes/*
diff --git a/GraphView/graphview.py b/GraphView/graphview.py
index deacd0d71..ad830ec07 100644
--- a/GraphView/graphview.py
+++ b/GraphView/graphview.py
@@ -52,7 +52,6 @@
# Gramps Modules
#
#-------------------------------------------------------------------------
-from gramps.gen import datehandler
from gramps.gen.config import config
from gramps.gen.constfunc import win
from gramps.gen.db import DbTxn
@@ -63,8 +62,7 @@
ChildRefType, EventType, EventRoleType)
from gramps.gen.utils.alive import probably_alive
from gramps.gen.utils.callback import Callback
-from gramps.gen.utils.db import (get_birth_or_fallback, get_death_or_fallback,
- find_children, find_parents, preset_name,
+from gramps.gen.utils.db import (find_children, find_parents, preset_name,
find_witnessed_people)
from gramps.gen.utils.file import search_for, media_path_full, find_file
from gramps.gen.utils.libformatting import FormattingHelper
@@ -129,6 +127,7 @@
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
from search_widget import SearchWidget, Popover, ListBoxRow, get_person_tooltip
from avatars import Avatars
+from theme import Themes
#-------------------------------------------------------------------------
@@ -167,7 +166,9 @@ class GraphView(NavigationView):
('interface.graphview-nodesep', 2),
('interface.graphview-person-theme', 0),
('interface.graphview-font', ['', 14]),
- ('interface.graphview-show-all-connected', False))
+ ('interface.graphview-show-all-connected', False),
+ ('interface.graphview-attrs-direction', 0),
+ )
def __init__(self, pdata, dbstate, uistate, nav_group=0):
NavigationView.__init__(self, _('Graph View'), pdata, dbstate, uistate,
@@ -400,6 +401,12 @@ def can_configure(self):
"""
return True
+ def cb_populate_only(self, *args):
+ """
+ Call when need populate only.
+ """
+ self.graph_widget.populate(self.get_active())
+
def cb_update_show_images(self, _client, _cnxn_id, entry, _data):
"""
Called when the configuration menu changes the images setting.
@@ -460,12 +467,6 @@ def cb_update_show_places(self, _client, _cnxn_id, entry, _data):
self.show_places = entry == 'True'
self.graph_widget.populate(self.get_active())
- def cb_update_place_fmt(self, _client, _cnxn_id, _entry, _data):
- """
- Called when the configuration menu changes the place setting.
- """
- self.graph_widget.populate(self.get_active())
-
def cb_update_show_tag_color(self, _client, _cnxn_id, entry, _data):
"""
Called when the configuration menu changes the show tags setting.
@@ -473,12 +474,6 @@ def cb_update_show_tag_color(self, _client, _cnxn_id, entry, _data):
self.show_tag_color = entry == 'True'
self.graph_widget.populate(self.get_active())
- def cb_update_show_lines(self, _client, _cnxn_id, _entry, _data):
- """
- Called when the configuration menu changes the line setting.
- """
- self.graph_widget.populate(self.get_active())
-
def cb_update_highlight_home_person(self, _client, _cnxn_id, entry, _data):
"""
Called when the configuration menu changes the highlight home
@@ -562,24 +557,6 @@ def cb_update_search_marked_first(self, _client, _cnxn_id, entry, _data):
value = entry == 'True'
self.graph_widget.search_widget.set_options(marked_first=value)
- def cb_update_spacing(self, _client, _cnxd_id, _entry, _data):
- """
- Called when the ranksep or nodesep setting changed.
- """
- self.graph_widget.populate(self.get_active())
-
- def cb_update_person_theme(self, _client, _cnxd_id, _entry, _data):
- """
- Called when person theme setting changed.
- """
- self.graph_widget.populate(self.get_active())
-
- def cb_show_all_connected(self, _client, _cnxd_id, _entry, _data):
- """
- Called when show all connected setting changed.
- """
- self.graph_widget.populate(self.get_active())
-
def config_change_font(self, font_button):
"""
Called when font is change.
@@ -595,6 +572,18 @@ def config_change_font(self, font_button):
self.graph_widget.retest_font = True
self.graph_widget.populate(self.get_active())
+ def update_theme_combo(self, obj, constant):
+ """
+ :param obj: the ComboBox object
+ :param constant: the config setting to which the value must be saved
+ """
+ item_iter = obj.get_active_iter()
+ if item_iter is None:
+ return
+ model = obj.get_model()
+ # save theme index instead of combo row index
+ self._config.set(constant, model.get_value(item_iter, 0))
+
def config_connect(self):
"""
Overwriten from :class:`~gui.views.pageview.PageView method
@@ -612,11 +601,11 @@ def config_connect(self):
self._config.connect('interface.graphview-show-places',
self.cb_update_show_places)
self._config.connect('interface.graphview-place-format',
- self.cb_update_place_fmt)
+ self.cb_populate_only)
self._config.connect('interface.graphview-show-tags',
self.cb_update_show_tag_color)
self._config.connect('interface.graphview-show-lines',
- self.cb_update_show_lines)
+ self.cb_populate_only)
self._config.connect('interface.graphview-highlight-home-person',
self.cb_update_highlight_home_person)
self._config.connect('interface.graphview-home-path-color',
@@ -638,13 +627,15 @@ def config_connect(self):
self._config.connect('interface.graphview-search-marked-first',
self.cb_update_search_marked_first)
self._config.connect('interface.graphview-ranksep',
- self.cb_update_spacing)
+ self.cb_populate_only)
self._config.connect('interface.graphview-nodesep',
- self.cb_update_spacing)
+ self.cb_populate_only)
self._config.connect('interface.graphview-person-theme',
- self.cb_update_person_theme)
+ self.cb_populate_only)
self._config.connect('interface.graphview-show-all-connected',
- self.cb_show_all_connected)
+ self.cb_populate_only)
+ self._config.connect('interface.graphview-attrs-direction',
+ self.cb_populate_only)
def _get_configure_page_funcs(self):
"""
@@ -685,8 +676,14 @@ def layout_config_panel(self, configdialog):
row += 1
configdialog.add_checkbox(
grid, _('Show places'), row, 'interface.graphview-show-places')
+ # Attributes
row += 1
+ attrs_items = [(0, _('Hide')), (1, _('Vertical')), (2, _('Horizontal'))]
+ configdialog.add_combo(grid, _('Show attributes (experimental)'), row,
+ 'interface.graphview-attrs-direction',
+ attrs_items)
# Place format:
+ row += 1
p_fmts = [(0, _("Default"))]
for (indx, fmt) in enumerate(place_displayer.get_formats()):
p_fmts.append((indx + 1, fmt.name))
@@ -712,15 +709,19 @@ def theme_config_panel(self, configdialog):
grid.set_column_spacing(6)
grid.set_row_spacing(6)
- p_themes = DotSvgGenerator(self.dbstate, self).get_person_themes()
- themes_list = []
- for t in p_themes:
- themes_list.append((t[0], t[1]))
-
row = 0
+ themes_list = self.graph_widget.dot_svg.themes.person_themes
+ # get active combobox row number (position)
+ theme_index = self.graph_widget.dot_svg.themes.person_theme_index
+ setactive = 0
+ for n, item in enumerate(themes_list):
+ if theme_index == item[0]:
+ setactive = n
+ break
configdialog.add_combo(grid, _('Person theme'), row,
'interface.graphview-person-theme',
- themes_list)
+ themes_list, setactive=setactive,
+ callback=self.update_theme_combo)
row += 1
configdialog.add_color(grid,
_('Path color to home person'),
@@ -1098,6 +1099,8 @@ def __init__(self, view, dbstate, uistate):
self.retest_font = True # flag indicates need to resize font
self.bold_size = self.norm_size = 0 # font sizes to send to dot
+ self.dot_svg = DotSvgGenerator(self)
+
def add_popover(self, widget, container):
"""
Add popover for button.
@@ -1426,11 +1429,7 @@ def populate(self, active_person):
self.hide_bkmark_popover()
# generate DOT and SVG data
- dot = DotSvgGenerator(self.dbstate, self.view,
- bold_size=self.bold_size,
- norm_size=self.norm_size)
- self.dot_data, self.svg_data = dot.build_graph(active_person)
- del dot
+ self.dot_data, self.svg_data = self.dot_svg.build_graph(active_person)
parser = GraphvizSvgParser(self, self.view)
parser.parse(self.svg_data)
@@ -1704,28 +1703,27 @@ def fit_text(self):
return self.bold_size, self.norm_size
text = "The quick Brown Fox jumped over the Lazy Dogs 1948-01-01."
- dot_test = DotSvgGenerator(self.dbstate, self.view)
- dot_test.init_dot()
+ self.dot_svg.init_dot()
# These are at the desired font sizes.
- dot_test.add_node('test_bold', '%s' % text, shape='box')
- dot_test.add_node('test_norm', text, shape='box')
+ self.dot_svg.add_node('test_bold', '%s' % text, shape='box')
+ self.dot_svg.add_node('test_norm', text, shape='box')
# now add nodes at increasing font sizes
for scale in range(35, 140, 2):
- f_size = dot_test.fontsize * scale / 100.0
- dot_test.add_node(
+ f_size = self.dot_svg.fontsize * scale / 100.0
+ self.dot_svg.add_node(
'test_bold' + str(scale),
'%(text)s' %
{'text': text, 'bsize': f_size}, shape='box')
- dot_test.add_node(
+ self.dot_svg.add_node(
'test_norm' + str(scale),
text, shape='box', fontsize=("%3.1f" % f_size))
# close the graphviz dot code with a brace
- dot_test.write('}\n')
+ self.dot_svg.write('}\n')
# get DOT and generate SVG data by Graphviz
- dot_data = dot_test.dot.getvalue().encode('utf8')
- svg_data = dot_test.make_svg(dot_data)
+ dot_data = self.dot_svg.dot.getvalue().encode('utf8')
+ svg_data = self.dot_svg.make_svg(dot_data)
svg_data = svg_data.decode('utf8')
# now lest find the box sizes, and font sizes for the generated svg.
@@ -2087,9 +2085,11 @@ def stop_text(self, tag):
font_desc = Pango.FontDescription.from_string(text_font)
- # set bold text using PangoMarkup
+ # set bold and underline text using PangoMarkup
if self.text_attrs.get('font-weight') == 'bold':
tag = '%s' % tag
+ if self.text_attrs.get('text-decoration') == 'underline':
+ tag = '%s' % tag
# text color
fill_color = self.text_attrs.get('fill')
@@ -2181,16 +2181,16 @@ class DotSvgGenerator(object):
"""
Generator of graphing instructions in dot format and svg data by Graphviz.
"""
- def __init__(self, dbstate, view, bold_size=0, norm_size=0):
+ def __init__(self, graph_widget):
"""
Initialise the DotSvgGenerator class.
"""
- self.bold_size = bold_size
- self.norm_size = norm_size
- self.dbstate = dbstate
- self.uistate = view.uistate
- self.database = dbstate.db
- self.view = view
+ self.bold_size = graph_widget.bold_size
+ self.norm_size = graph_widget.norm_size
+ self.dbstate = graph_widget.dbstate
+ self.uistate = graph_widget.uistate
+ self.database = graph_widget.dbstate.db
+ self.view = graph_widget.view
self.dot = None # will be StringIO()
@@ -2206,13 +2206,13 @@ def __init__(self, dbstate, view, bold_size=0, norm_size=0):
self.current_list = list()
self.home_person = None
- # Gtk style context for scrollwindow
- self.context = self.view.graph_widget.sw_style_context
-
# font if we use genealogical symbols
self.sym_font = None
+ self.symbols = Symbols()
+ self.font_changed()
self.avatars = Avatars(self.view._config)
+ self.themes = Themes(self, graph_widget)
def __del__(self):
"""
@@ -2230,29 +2230,18 @@ def init_dot(self):
self.dot.close()
self.dot = StringIO()
+ # Gtk style context for scrollwindow
+ self.context = self.view.graph_widget.sw_style_context
+
self.current_list.clear()
self.person_handles_dict.clear()
- self.show_images = self.view._config.get(
- 'interface.graphview-show-images')
- self.show_avatars = self.view._config.get(
- 'interface.graphview-show-avatars')
- self.show_full_dates = self.view._config.get(
- 'interface.graphview-show-full-dates')
- self.show_places = self.view._config.get(
- 'interface.graphview-show-places')
- self.place_format = self.view._config.get(
- 'interface.graphview-place-format') - 1
- self.show_tag_color = self.view._config.get(
- 'interface.graphview-show-tags')
spline = self.view._config.get('interface.graphview-show-lines')
self.spline = SPLINE.get(int(spline))
self.descendant_generations = self.view._config.get(
'interface.graphview-descendant-generations')
self.ancestor_generations = self.view._config.get(
'interface.graphview-ancestor-generations')
- self.person_theme_index = self.view._config.get(
- 'interface.graphview-person-theme')
self.show_all_connected = self.view._config.get(
'interface.graphview-show-all-connected')
ranksep = self.view._config.get('interface.graphview-ranksep')
@@ -2295,6 +2284,8 @@ def init_dot(self):
font = self.view._config.get('interface.graphview-font')
fontfamily = self.resolve_font_name(font[0])
self.fontsize = font[1]
+ self.bold_size = self.view.graph_widget.bold_size
+ self.norm_size = self.view.graph_widget.norm_size
if not self.bold_size:
self.bold_size = self.norm_size = font[1]
@@ -2339,9 +2330,10 @@ def init_dot(self):
% (self.norm_size, font_color))
self.write('\n')
self.uistate.connect('font-changed', self.font_changed)
- self.symbols = Symbols()
self.font_changed()
+ self.themes.update_options()
+
def resolve_font_name(self, font_name):
"""
Helps to resolve font by graphviz.
@@ -2414,13 +2406,13 @@ def make_svg(self, dot_data):
Make SVG data by Graphviz.
"""
if win():
- svg_data = Popen(['dot', '-Tsvg'],
+ svg_data = Popen(['dot', '-Tsvg', '-q2'],
creationflags=DETACHED_PROCESS,
stdin=PIPE,
stdout=PIPE,
stderr=PIPE).communicate(input=dot_data)[0]
else:
- svg_data = Popen(['dot', '-Tsvg'],
+ svg_data = Popen(['dot', '-Tsvg', '-q2'],
stdin=PIPE,
stdout=PIPE).communicate(input=dot_data)[0]
return svg_data
@@ -2736,7 +2728,11 @@ def add_persons_and_families(self):
for person_handle in self.person_handles:
person = self.database.get_person_from_handle(person_handle)
# Output the person's node
- label = self.get_person_label(person)
+ label = self.themes.person_theme.get_html(person)
+ # add tooltip for node
+ if self.themes.person_theme.tags:
+ self.add_tags_tooltip(person.handle,
+ self.themes.person_theme.tags)
(shape, style, color, fill) = self.get_gender_style(person)
self.add_node(person_handle, label, shape, color, style, fill, url)
@@ -2775,7 +2771,11 @@ def __add_family_node(self, fam_handle):
fam = self.database.get_family_from_handle(fam_handle)
fill, color = color_graph_family(fam, self.dbstate)
style = "filled"
- label = self.get_family_label(fam)
+ label = self.themes.family_theme.get_html(fam)
+ # add tooltip for node
+ if self.themes.family_theme.tags:
+ self.add_tags_tooltip(fam.handle,
+ self.themes.family_theme.tags)
self.add_node(fam_handle, label, "ellipse", color, style, fill)
@@ -2818,318 +2818,6 @@ def get_gender_style(self, person):
fill, color = color_graph_box(alive, gender)
return(shape, style, color, fill)
- def get_tags_and_table(self, obj):
- """
- Return html tags table for obj (person or family).
- """
- tag_table = ''
- tags = []
-
- for tag_handle in obj.get_tag_list():
- tags.append(self.dbstate.db.get_tag_from_handle(tag_handle))
-
- # prepare html table of tags
- if tags:
- tag_table = ('
')
- for tag in tags:
- rgba = Gdk.RGBA()
- rgba.parse(tag.get_color())
- value = '#%02x%02x%02x' % (int(rgba.red * 255),
- int(rgba.green * 255),
- int(rgba.blue * 255))
- tag_table += ' | ' % value
- tag_table += '
'
-
- return tags, tag_table
-
- def get_person_themes(self, index=-1):
- """
- Person themes.
- If index == -1 return list of themes.
- If index out of range return default theme.
- """
- person_themes = [
- (0, _('Default'),
- ''
- '%(img)s |
'
- '%(name)s'
- ' |
'
- '%(birth_str)s |
'
- '%(death_str)s |
'
- '%(tags)s |
'
- '
'
- ),
- (1, _('Image on right side'),
- ''
- ''
- '%(name)s'
- ' | '
- '
'
- ''
- '%(birth_wraped)s'
- ' | '
- '%(img)s | '
- '
'
- ''
- '%(death_wraped)s'
- ' | '
- '
'
- ''
- ' %(tags)s | '
- '
'
- '
'
- ),
- (2, _('Image on left side'),
- ''
- ''
- '%(name)s'
- ' | '
- '
'
- ''
- '%(img)s | '
- '%(birth_wraped)s'
- ' | '
- '
'
- ''
- '%(death_wraped)s'
- ' | '
- '
'
- ''
- ' %(tags)s | '
- '
'
- '
'
- ),
- (3, _('Normal'),
- ''
- '%(img)s |
'
- '%(name)s'
- ' |
'
- '%(birth_wraped)s |
'
- '%(death_wraped)s |
'
- '%(tags)s |
'
- '
'
- )]
-
- if index < 0:
- return person_themes
-
- if index < len(person_themes):
- return person_themes[index]
- else:
- return person_themes[0]
-
- def get_person_label(self, person):
- """
- Return person label string (with tags).
- """
- # Start an HTML table.
- # Remember to close the table afterwards!
- #
- # This isn't a free-form HTML format here...just a few keywords that
- # happen to be similar to keywords commonly seen in HTML.
- # For additional information on what is allowed, see:
- #
- # http://www.graphviz.org/info/shapes.html#html
- #
- # Will use html.escape to avoid '&', '<', '>' in the strings.
-
- # FIRST get all strings: img, name, dates, tags
-
- # see if we have an image to use for this person
- image = ''
- if self.show_images:
- image = self.view.graph_widget.get_person_image(person,
- kind='path')
- if not image and self.show_avatars:
- image = self.avatars.get_avatar(gender=person.gender)
-
- if image is not None:
- image = '' % image
- else:
- image = ''
-
- # get the person's name
- name = displayer.display_name(person.get_primary_name())
- # name string should not be empty
- name = escape(name) if name else ' '
-
- # birth, death is a lists [date, place]
- birth, death = self.get_date_strings(person)
-
- birth_str = ''
- death_str = ''
- birth_wraped = ''
- death_wraped = ''
-
- # There are two ways of displaying dates:
- # 1) full and on two lines:
- # b. 1890-12-31 - BirthPlace
- # d. 1960-01-02 - DeathPlace
- if self.show_full_dates or self.show_places:
- # add symbols
- if birth[0]:
- birth[0] = _('%s %s') % (self.bth, birth[0])
- birth_wraped = birth[0]
- birth_str = birth[0]
- if birth[1]:
- birth_wraped += '
'
- birth_str += ' '
- elif birth[1]:
- birth_wraped = _('%s ') % self.bth
- birth_str = _('%s ') % self.bth
- birth_wraped += birth[1]
- birth_str += birth[1]
-
- if death[0]:
- death[0] = _('%s %s') % (self.dth, death[0])
- death_wraped = death[0]
- death_str = death[0]
- if death[1]:
- death_wraped += '
'
- death_str += ' '
- elif death[1]:
- death_wraped = _('%s ') % self.dth
- death_str = _('%s ') % self.dth
- death_wraped += death[1]
- death_str += death[1]
-
- # 2) simple and on one line:
- # (1890 - 1960)
- else:
- if birth[0] or death[0]:
- birth_str = '(%s - %s)' % (birth[0], death[0])
- # add symbols
- if image:
- if birth[0]:
- birth_wraped = _('%s %s') % (self.bth, birth[0])
- if death[0]:
- death_wraped = _('%s %s') % (self.dth, death[0])
- else:
- birth_wraped = birth_str
-
- # get tags table for person and add tooltip for node
- tag_table = ''
- if self.show_tag_color:
- tags, tag_table = self.get_tags_and_table(person)
- if tag_table:
- self.add_tags_tooltip(person.handle, tags)
-
- # apply theme to person label
- if(image or self.person_theme_index == 0 or
- self.person_theme_index == 3):
- p_theme = self.get_person_themes(self.person_theme_index)
- else:
- # use default theme if no image
- p_theme = self.get_person_themes(3)
-
- label = p_theme[2] % {'img': image,
- 'name': name,
- 'birth_str': birth_str,
- 'death_str': death_str,
- 'birth_wraped': birth_wraped,
- 'death_wraped': death_wraped,
- 'tags': tag_table,
- 'bsize' : self.bold_size}
- return label
-
- def get_family_label(self, family):
- """
- Return family label string (with tags).
- """
- # start main html table
- label = ('')
-
- # add dates strtings to table
- event_str = ['', '']
- for event_ref in family.get_event_ref_list():
- event = self.database.get_event_from_handle(event_ref.ref)
- if (event.type == EventType.MARRIAGE and
- (event_ref.get_role() == EventRoleType.FAMILY or
- event_ref.get_role() == EventRoleType.PRIMARY)):
- event_str = self.get_event_string(event)
- break
- if event_str[0] and event_str[1]:
- event_str = '%s
%s' % (event_str[0], event_str[1])
- elif event_str[0]:
- event_str = event_str[0]
- elif event_str[1]:
- event_str = event_str[1]
- else:
- event_str = ''
-
- label += '%s |
' % event_str
-
- # add tags table for family and add tooltip for node
- if self.show_tag_color:
- tags, tag_table = self.get_tags_and_table(family)
-
- if tag_table:
- label += '%s |
' % tag_table
- self.add_tags_tooltip(family.handle, tags)
-
- # close main table
- label += '
'
-
- return label
-
- def get_date_strings(self, person):
- """
- Returns tuple of birth/christening and death/burying date strings.
- """
- birth_event = get_birth_or_fallback(self.database, person)
- if birth_event:
- birth = self.get_event_string(birth_event)
- else:
- birth = ['', '']
-
- death_event = get_death_or_fallback(self.database, person)
- if death_event:
- death = self.get_event_string(death_event)
- else:
- death = ['', '']
-
- return (birth, death)
-
- def get_event_string(self, event):
- """
- Return string for an event label.
-
- Based on the data availability and preferences, we select one
- of the following for a given event:
- year only
- complete date
- place name
- empty string
- """
- if event:
- place_title = place_displayer.display_event(self.database, event,
- fmt=self.place_format)
- date_object = event.get_date_object()
- date = ''
- place = ''
- # shall we display full date
- # or do we have a valid year to display only year
- if(self.show_full_dates and date_object.get_text() or
- date_object.get_year_valid()):
- if self.show_full_dates:
- date = '%s' % datehandler.get_date(event)
- else:
- date = '%i' % date_object.get_year()
- # shall we add the place?
- if self.show_places and place_title:
- place = place_title
- return [escape(date), escape(place)]
- else:
- if place_title and self.show_places:
- return ['', escape(place_title)]
- return ['', '']
-
def add_link(self, id1, id2, style="", head="", tail="", comment="",
bold=False, color=""):
"""
diff --git a/GraphView/theme.py b/GraphView/theme.py
new file mode 100644
index 000000000..7c47cb538
--- /dev/null
+++ b/GraphView/theme.py
@@ -0,0 +1,581 @@
+# -*- coding: utf-8 -*-
+
+import os
+from html import escape
+from importlib import import_module
+from gramps.gen.display.name import displayer
+from gramps.gen.utils.db import get_birth_or_fallback, get_death_or_fallback
+from gramps.gen.display.place import displayer as place_displayer
+from gramps.gen import datehandler
+from gramps.gen.lib import EventType, EventRoleType
+
+from gi.repository import Gdk
+
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+try:
+ _trans = glocale.get_addon_translator(__file__)
+except ValueError:
+ _trans = glocale.translation
+_ = _trans.gettext
+
+import sys
+theme_folder = os.path.join(os.path.dirname(__file__), 'themes')
+sys.path.append(os.path.abspath(theme_folder))
+
+
+class BaseTheme:
+ """
+ Base theme class.
+ """
+ def __init__(self, dot_generator, options, functions):
+ self.dot_generator = dot_generator
+ self.options = options
+ self.functions = functions
+ self.kind = '' # 'person', 'family'
+ self.index = None # unique integer
+ self.name = 'Base theme' # theme name
+ self.table = (
+ '')
+ self.html = '' # html table body
+ self.tags = None
+
+ self.table_row_fmt = '%s |
'
+ self.tag_fmt = self.table_row_fmt
+ self.date_fmt = self.table_row_fmt
+
+ def build(self, obj, html=None):
+ """
+ Build html table.
+ Should be implimented in theme and return html string.
+
+ This isn't a free-form HTML format here...just a few keywords that
+ happen to be similar to keywords commonly seen in HTML.
+ For additional information on what is allowed, see:
+ https://www.graphviz.org/doc/info/shapes.html#html
+ """
+ pass
+
+ def get_html(self, obj, html=None):
+ """
+ Return html table.
+ """
+ if html is None:
+ html = self.html
+ return self.table % self.build(obj, html)
+
+ def get_tags_str(self, obj):
+ """
+ Get formated tags string for person or family.
+ """
+ tag_table = ''
+ if self.options.show_tags:
+ self.tags, tag_table = self.functions.get_tags_and_table(obj)
+ return self.tag_fmt % tag_table if tag_table else ''
+
+
+class BasePersonTheme(BaseTheme):
+ """
+ Base person theme class.
+ """
+ def __init__(self, dot_generator, options, functions):
+ BaseTheme.__init__(self, dot_generator, options, functions)
+ self.kind = 'person'
+ self.wraped = False # wrap date place to new line
+
+ self.image_fmt = self.table_row_fmt
+ self.name_fmt = self.table_row_fmt
+
+ def get_attr_format(self):
+ """
+ Choose vertical or horizontal format for attributes cell.
+ """
+ if self.options.attrs_vertical:
+ attrs_fmt = (
+ ''
+ ''
+ ' |
')
+ else:
+ attrs_fmt = (
+ ''
+ ''
+ ' |
')
+ return attrs_fmt
+
+ def get_image(self, obj):
+ """
+ Get image for person or family.
+ For now is only for person.
+ """
+ image = ''
+ if self.kind=='person' and self.options.show_images:
+ image = self.functions.get_person_image(obj, kind='path')
+ if not image and self.options.show_avatars:
+ image = self.functions.get_avatar(gender=obj.gender)
+ image = '' % image if image is not None else ''
+ return image
+
+ def get_name(self, person):
+ """
+ Get the person's name.
+ """
+ if self.kind != 'person':
+ return ''
+ person_name = person.get_primary_name()
+ call_name = person_name.get_call_name()
+ name = displayer.display_name(person_name)
+
+ if not name:
+ # name string should not be empty
+ return ' '
+
+ # underline call name
+ if call_name and call_name in name:
+ name_list = name.split(call_name)
+ name = ''
+ for item in name_list:
+ if name:
+ name += '%s%s' % (escape(call_name), escape(item))
+ else:
+ name += escape(item)
+ name = ('%s'
+ % (self.options.bold_size, name))
+ return name
+
+ def get_dates(self, person, wraped=None):
+ """
+ Get birth and death dates.
+ """
+ if wraped is None:
+ wraped = self.wraped
+
+ # birth, death is a lists [date, place]
+ birth, death = self.functions.get_date_strings(person)
+
+ birth_str = ''
+ death_str = ''
+ birth_wraped = ''
+ death_wraped = ''
+
+ if self.options.show_full_dates or self.options.show_places:
+ if birth[0]:
+ birth[0] = _('%s %s') % (self.options.bth_sym, birth[0])
+ birth_wraped = birth[0]
+ birth_str = birth[0]
+ if birth[1]:
+ birth_wraped += '
'
+ birth_str += ' '
+ elif birth[1]:
+ birth_wraped = _('%s ') % self.options.bth_sym
+ birth_str = _('%s ') % self.options.bth_sym
+ birth_wraped += birth[1]
+ birth_str += birth[1]
+
+ if death[0]:
+ death[0] = _('%s %s') % (self.options.dth_sym, death[0])
+ death_wraped = death[0]
+ death_str = death[0]
+ if death[1]:
+ death_wraped += '
'
+ death_str += ' '
+ elif death[1]:
+ death_wraped = _('%s ') % self.options.dth_sym
+ death_str = _('%s ') % self.options.dth_sym
+ death_wraped += death[1]
+ death_str += death[1]
+
+ # 2) simple and on one line:
+ # (1890 - 1960)
+ else:
+ if birth[0] or death[0]:
+ # show dots if have no date
+ b = birth[0] if birth[0] else '...'
+ d = death[0] if death[0] else '...'
+ birth_str = _('(%s - %s)') % (b, d)
+ if birth[0]:
+ birth_wraped = _('%s %s') % (self.options.bth_sym, birth[0])
+ if death[0]:
+ death_wraped = _('%s %s') % (self.options.dth_sym, death[0])
+
+ if wraped:
+ return birth_wraped, death_wraped
+ else:
+ return birth_str, death_str
+
+ def get_image_str(self, person):
+ """
+ Get formated image string.
+ """
+ image = self.get_image(person)
+ return self.image_fmt % image if image else ''
+
+ def get_name_str(self, person):
+ """
+ Get formated name string.
+ """
+ name = self.get_name(person)
+ return self.name_fmt % name if name else ''
+
+ def get_dates_str(self, person, separated=False, default=''):
+ """
+ Get formated date strings.
+ """
+ birth, death = self.get_dates(person)
+
+ birth = self.date_fmt % (birth) if birth else default
+ death = self.date_fmt % (death) if death else default
+
+ if separated:
+ return birth, death
+ else:
+ return birth + death
+
+ def get_attrs_str(self, person):
+ """
+ Get attributes string.
+ """
+ if not self.options.show_attrs:
+ return ''
+ attrs_list = person.get_attribute_list()
+ if not attrs_list:
+ return ''
+
+ # symbols for eye and hair (font should be installed)
+ eye = u"\U0001F441"
+ hair = u"\U0001F9B3"
+ attr_symbol = {_('eye color') : eye,
+ _('hair color') : hair,
+ }
+
+ colors_dic = {
+ 'white' : 'white',
+ 'blond' : 'white',
+ 'amber' : 'gold',
+ 'blue' : 'blue',
+ 'grey' : 'grey',
+ 'green' : 'green',
+ 'brown' : 'saddlebrown',
+ 'black' : 'black',
+ 'yellow' : 'yellow',
+ 'red' : 'red',
+ 'auburn' : 'orangered',
+ # russian
+ 'белый' : 'white',
+ 'седой' : 'ivory',
+ 'синий' : 'blue',
+ 'голубой' : 'skyblue',
+ 'серый' : 'grey',
+ 'зеленый' : 'green',
+ 'янтарный' : 'gold',
+ 'болотный' : 'olive',
+ 'оливковый' : 'olive',
+ 'карий' : 'saddlebrown',
+ 'каштановый' : 'saddlebrown',
+ 'русый' : 'lightgoldenrod',
+ 'черный' : 'black',
+ 'желтый' : 'yellow',
+ 'рыжий' : 'orangered',
+ 'красный' : 'red',
+ }
+
+ # list of (attr_symbol, color)
+ attributes = []
+
+ for at in attrs_list:
+ symbol = attr_symbol.get(at.type.type2base().lower())
+ if symbol is not None:
+ attributes.append((symbol,
+ colors_dic.get(at.value.lower(), '')))
+
+ attrs = ''
+ if self.options.attrs_vertical:
+ attr_color = ' |
'
+ for at in attributes:
+ attrs += '%s |
' % at[0]
+ attrs += attr_color % at[1] if at[1] else ''
+ else:
+ for at in attributes:
+ if not attrs:
+ attrs = ''
+ attrs += '%s | ' % at[0]
+ if attrs:
+ attrs += '
'
+
+ colors = ''
+ attr_color = ' | '
+ for at in attributes:
+ if not colors:
+ colors = ''
+ colors += attr_color % at[1] if at[1] else ' | '
+ if colors:
+ attrs += colors + '
'
+
+ return self.get_attr_format() % attrs if attrs else ''
+
+
+class BaseFamilyTheme(BaseTheme):
+ """
+ Base family theme class.
+ """
+ def __init__(self, dot_generator, options, functions):
+ BaseTheme.__init__(self, dot_generator, options, functions)
+ self.kind = 'family'
+
+ def get_label(self, family):
+ """
+ Get lable for family (date and place).
+ """
+ event_str = ['', '']
+ for event_ref in family.get_event_ref_list():
+ event = self.dot_generator.database.get_event_from_handle(event_ref.ref)
+ if (event.type == EventType.MARRIAGE and
+ (event_ref.get_role() == EventRoleType.FAMILY or
+ event_ref.get_role() == EventRoleType.PRIMARY)):
+ event_str = self.functions.get_event_string(event)
+ break
+ if event_str[0] and event_str[1]:
+ event_str = '%s
%s' % (event_str[0], event_str[1])
+ elif event_str[0]:
+ event_str = event_str[0]
+ elif event_str[1]:
+ event_str = event_str[1]
+ else:
+ event_str = ''
+
+ return event_str
+
+ def get_label_str(self, family):
+ """
+ Get formated lable string for family (date and place).
+ """
+ label = self.get_label(family)
+ return self.date_fmt % label
+
+
+class Functions():
+ """
+ Functions from graphview to get data.
+ """
+ def __init__(self, graph_widget, options, kind):
+ self.dot_generator = options.dot_generator
+ self.dbstate = self.dot_generator.dbstate
+ self.options = options
+
+ if kind == 'person':
+ self.get_person_image = graph_widget.get_person_image
+ self.get_avatar = self.dot_generator.avatars.get_avatar
+
+ def get_tags_and_table(self, obj):
+ """
+ Return html tags table for obj (person or family).
+ """
+ tag_table = ''
+ tags = []
+
+ for tag_handle in obj.get_tag_list():
+ tags.append(self.dbstate.db.get_tag_from_handle(tag_handle))
+
+ # prepare html table of tags
+ if tags:
+ tag_table = ('')
+ for tag in tags:
+ rgba = Gdk.RGBA()
+ rgba.parse(tag.get_color())
+ value = '#%02x%02x%02x' % (int(rgba.red * 255),
+ int(rgba.green * 255),
+ int(rgba.blue * 255))
+ tag_table += ' | ' % value
+ tag_table += '
'
+
+ return tags, tag_table
+
+ def get_date_strings(self, person):
+ """
+ Returns tuple of birth/christening and death/burying date strings.
+ """
+ birth_event = get_birth_or_fallback(self.dot_generator.database,
+ person)
+ if birth_event:
+ birth = self.get_event_string(birth_event)
+ else:
+ birth = ['', '']
+
+ death_event = get_death_or_fallback(self.dot_generator.database,
+ person)
+ if death_event:
+ death = self.get_event_string(death_event)
+ else:
+ death = ['', '']
+
+ return (birth, death)
+
+ def get_event_string(self, event):
+ """
+ Return string for an event label.
+
+ Based on the data availability and preferences, we select one
+ of the following for a given event:
+ year only
+ complete date
+ place name
+ empty string
+ """
+ if event:
+ place_title = place_displayer.display_event(
+ self.dot_generator.database, event,
+ fmt=self.options.place_format)
+ date_object = event.get_date_object()
+ date = ''
+ place = ''
+ # shall we display full date
+ # or do we have a valid year to display only year
+ if(self.options.show_full_dates and date_object.get_text() or
+ date_object.get_year_valid()):
+ if self.options.show_full_dates:
+ date = '%s' % datehandler.get_date(event)
+ else:
+ date = '%i' % date_object.get_year()
+ # shall we add the place?
+ if self.options.show_places and place_title:
+ place = place_title
+ return [escape(date), escape(place)]
+ else:
+ if place_title and self.options.show_places:
+ return ['', escape(place_title)]
+ return ['', '']
+
+
+class Options():
+ """
+ Options from graphview config and DotSvgGenerator.
+ """
+ def __init__(self, dot_generator, kind):
+ self.config = dot_generator.view._config
+ self.dot_generator = dot_generator
+ self.kind = kind
+ # read data from config
+ self.update()
+
+ def update(self):
+ """
+ Update data.
+ """
+ self.show_tags = self.config.get('interface.graphview-show-tags')
+ self.show_full_dates = self.config.get(
+ 'interface.graphview-show-full-dates')
+ self.show_places = self.config.get('interface.graphview-show-places')
+ self.place_format = self.config.get(
+ 'interface.graphview-place-format') - 1
+ self.bold_size = self.dot_generator.bold_size
+
+ if self.kind == 'person':
+ self.show_images = self.config.get(
+ 'interface.graphview-show-images')
+ self.show_avatars = self.config.get(
+ 'interface.graphview-show-avatars')
+
+ attrs_opt = self.config.get('interface.graphview-attrs-direction')
+ self.show_attrs = attrs_opt != 0
+ self.attrs_vertical = attrs_opt == 1
+
+ self.bth_sym = self.dot_generator.bth
+ self.dth_sym = self.dot_generator.dth
+
+
+class Themes():
+ """
+ Main themes class.
+ """
+ def __init__(self, dot_generator, graph_widget):
+ self.config = dot_generator.view._config
+
+ self.person_opts = Options(dot_generator, 'person')
+ self.family_opts = Options(dot_generator, 'family')
+ self.person_funcs = Functions(graph_widget, self.person_opts, 'person')
+ self.family_funcs = Functions(graph_widget, self.family_opts, 'family')
+
+ self.list_person = [] # person theme objects list
+ self.list_family = []
+ self.person_themes = [] # person theme list [(index, name), ...]
+ self.family_themes = []
+
+ # current themes
+ self.person_theme = None
+ self.family_theme = None
+
+ self.person_theme_index = self.config.get(
+ 'interface.graphview-person-theme')
+
+ for module in os.listdir(theme_folder):
+ if module == '__init__.py' or module[-3:] != '.py':
+ continue
+ try:
+ item = import_module(module[:-3]).Theme
+ if item.THEME_KIND == 'person':
+ item = item(dot_generator,
+ self.person_opts, self.person_funcs)
+ self.list_person.append(item)
+ self.person_themes.append((item.index, item.name))
+ elif item.THEME_KIND == 'family':
+ item = item(dot_generator,
+ self.person_opts, self.person_funcs)
+ self.list_family.append(item)
+ else:
+ print('Wrong theme kind "%s" detected in module "%s"'
+ % (item.THEME_KIND, module))
+ except:
+ print('Found errors in theme module: ', module)
+
+ # sort theme lists by theme name
+ self.person_themes.sort(key=lambda tup: tup[1])
+ self.family_themes.sort(key=lambda tup: tup[1])
+
+ self.person_theme, self.family_theme = self.get_current()
+
+ def get_current(self):
+ """
+ Find current themes for person and family in lists by index.
+ """
+ self.person_theme_index = self.config.get(
+ 'interface.graphview-person-theme')
+ f_index = 0
+
+ p_theme = None
+ f_theme = None
+
+ for item in self.list_person:
+ if item.index == self.person_theme_index:
+ p_theme = item
+ break
+ # use default person theme if current does not found
+ if p_theme is None:
+ for item in self.list_person:
+ if item.index == 0:
+ p_theme = item
+ break
+
+ for item in self.list_family:
+ if item.index == f_index:
+ f_theme = item
+ break
+ # use default family theme if current does not found
+ if p_theme is None:
+ for item in self.list_family:
+ if item.index == 0:
+ f_theme = item
+ break
+
+ return p_theme, f_theme
+
+ def update_options(self):
+ """
+ Read current options.
+ """
+ self.person_opts.update()
+ self.family_opts.update()
+ self.person_theme, self.family_theme = self.get_current()
+
diff --git a/GraphView/themes/default_0.py b/GraphView/themes/default_0.py
new file mode 100644
index 000000000..8cfc48756
--- /dev/null
+++ b/GraphView/themes/default_0.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+
+from theme import BasePersonTheme
+
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+try:
+ _trans = glocale.get_addon_translator(__file__)
+except ValueError:
+ _trans = glocale.translation
+_ = _trans.gettext
+
+"""
+#################################
+# | --------- #
+# A | | Image | #
+# t | | | #
+# r | --------- #
+# r |---------------------------#
+# s | Name (bold font) #
+# |---------------------------#
+# | Birth and death dates # < - dates have short and long formats
+# |---------------------------#
+# | Tags line #
+#-------------------------------#
+# Attrs # < - attributes can be vertical or horizontal
+#################################
+
+ Long date format Short date format
+############################# #############################
+# Birth date and place # # (birth - death) #
+# Death date and place # #############################
+#############################
+"""
+
+class Theme(BasePersonTheme):
+ """
+ Default person theme.
+ """
+ THEME_KIND = 'person'
+
+ def __init__(self, dot_generator, options, functions):
+ BasePersonTheme.__init__(self, dot_generator, options, functions)
+ self.index = 0
+ self.name = _('Default')
+ self.wraped = False
+ # will be changed in "self.get_html" method (add attrs cell)
+ self.html = ('%(img)s'
+ '%(name)s'
+ '%(dates)s'
+ '%(tags)s')
+
+ self.full_date_fmt = ('%s |
')
+
+ def get_dates_str(self, person):
+ """
+ Get formated dates string.
+ """
+ if (self.options.show_full_dates or self.options.show_places
+ or self.wraped):
+ # change format to wrap birth and death dates
+ self.date_fmt = self.full_date_fmt
+ else:
+ self.date_fmt = self.table_row_fmt
+
+ # call original BasePersonTheme.get_dates_str
+ return super().get_dates_str(person)
+
+ def get_html(self, person):
+ """
+ Insert attributes cell to table.
+ """
+ if self.options.attrs_vertical:
+ html = '%(attrs)s' + self.html
+ else:
+ html = self.html + '%(attrs)s'
+
+ # call original "BaseTheme.get_html" with changed html
+ return super().get_html(person, html)
+
+ def build(self, person, html):
+ """
+ Build html table.
+ """
+ return html % {'img': self.get_image_str(person),
+ 'name': self.get_name_str(person),
+ 'dates': self.get_dates_str(person),
+ 'tags': self.get_tags_str(person),
+ 'attrs' : self.get_attrs_str(person)
+ }
+
diff --git a/GraphView/themes/default_family_0.py b/GraphView/themes/default_family_0.py
new file mode 100644
index 000000000..3fe53245e
--- /dev/null
+++ b/GraphView/themes/default_family_0.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+from theme import BaseFamilyTheme
+
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+try:
+ _trans = glocale.get_addon_translator(__file__)
+except ValueError:
+ _trans = glocale.translation
+_ = _trans.gettext
+
+"""
+#############################
+# Marrige date #
+#---------------------------#
+# Tags line #
+#############################
+"""
+
+class Theme(BaseFamilyTheme):
+ """
+ Default family theme.
+ """
+ THEME_KIND = 'family'
+
+ def __init__(self, dot_generator, options, functions):
+ BaseFamilyTheme.__init__(self, dot_generator, options, functions)
+ self.index = 0
+ self.name = _('Default')
+ self.wraped = False
+ self.html = ('%(dates)s'
+ '%(tags)s')
+
+ def build(self, family, html):
+ """
+ Build html table.
+ """
+ return html % {'dates': self.get_label_str(family),
+ 'tags': self.get_tags_str(family)}
+
diff --git a/GraphView/themes/left_image_2.py b/GraphView/themes/left_image_2.py
new file mode 100644
index 000000000..a4a8bcd4c
--- /dev/null
+++ b/GraphView/themes/left_image_2.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+from right_image_1 import Theme as RightImageTheme
+
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+try:
+ _trans = glocale.get_addon_translator(__file__)
+except ValueError:
+ _trans = glocale.translation
+_ = _trans.gettext
+
+"""
+#################################
+# | Name (bold font) #
+# A |---------------------------#
+# t | | Birth date #
+# t | Image | Birth place #
+# t | |---------------- #
+# r | | Death date #
+# s | | Death place #
+# |---------------------------#
+# | Tags line #
+#-------------------------------#
+# Attrs # < - attributes can be vertical or horizontal
+#################################
+"""
+
+
+class Theme(RightImageTheme):
+ """
+ Person theme with image on left side.
+ Use RightImageTheme as base.
+ """
+ THEME_KIND = 'person'
+
+ def __init__(self, dot_generator, options, functions):
+ RightImageTheme.__init__(self, dot_generator, options, functions)
+ self.index = 2
+ self.name = _('Image on left side')
+ self.html = (
+ '%(name)s'
+ ''
+ '%(img)s'
+ '%(birth)s'
+ '
'
+ '%(death)s
'
+ '%(tags)s'
+ )
+
diff --git a/GraphView/themes/normal_3.py b/GraphView/themes/normal_3.py
new file mode 100644
index 000000000..be4b73410
--- /dev/null
+++ b/GraphView/themes/normal_3.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+
+from default_0 import Theme as DefaultTheme
+
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+try:
+ _trans = glocale.get_addon_translator(__file__)
+except ValueError:
+ _trans = glocale.translation
+_ = _trans.gettext
+
+"""
+#################################
+# | --------- #
+# | | Image | #
+# A | | | #
+# t | --------- #
+# t |---------------------------#
+# r | Name (bold font) #
+# s |---------------------------#
+# | Birth date #
+# | Birth place #
+# |---------------------------#
+# | Death date #
+# | Death place #
+# |---------------------------#
+# | Tags line #
+# |---------------------------#
+# | Attrs # < - attributes can be vertical or horizontal
+#################################
+"""
+
+
+class Theme(DefaultTheme):
+ """
+ Use person DefaultTheme as base, but apply wrap for dates.
+ """
+ THEME_KIND = 'person'
+
+ def __init__(self, dot_generator, options, functions):
+ DefaultTheme.__init__(self, dot_generator, options, functions)
+ self.index = 3
+ self.name = _('Normal')
+ self.wraped = True
+
diff --git a/GraphView/themes/right_image_1.py b/GraphView/themes/right_image_1.py
new file mode 100644
index 000000000..5ffd735da
--- /dev/null
+++ b/GraphView/themes/right_image_1.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+
+from theme import BasePersonTheme
+
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+try:
+ _trans = glocale.get_addon_translator(__file__)
+except ValueError:
+ _trans = glocale.translation
+_ = _trans.gettext
+
+"""
+#################################
+# | Name (bold font) #
+# A |---------------------------#
+# t | Birth date | #
+# t | Birth place | Image #
+# r |----------------| #
+# s | Death date | #
+# | Death place | #
+# |---------------------------#
+# | Tags line #
+#-------------------------------#
+# Attrs # < - attributes can be vertical or horizontal
+#################################
+"""
+
+
+class Theme(BasePersonTheme):
+ """
+ Person theme with image on right side.
+ """
+ THEME_KIND = 'person'
+
+ def __init__(self, dot_generator, options, functions):
+ BasePersonTheme.__init__(self, dot_generator, options, functions)
+ self.index = 1
+ self.name = _('Image on right side')
+ self.wraped=True
+ self.html = (
+ '%(name)s'
+ ''
+ '%(birth)s'
+ '%(img)s'
+ '
'
+ '%(death)s
'
+ '%(tags)s'
+ )
+
+ self.name_fmt = '%s |
'
+ self.date_fmt = '%s | '
+ self.image_fmt = '%s | '
+ self.tag_fmt = '%s |
'
+
+ def get_html(self, person):
+ """
+ Insert attributes cell to table.
+ """
+ if self.options.attrs_vertical:
+ html = '%(attrs)s' + self.html
+ else:
+ html = self.html + '%(attrs)s'
+
+ # call original "BaseTheme.get_html" with changed html
+ return super().get_html(person, html)
+
+ def build(self, person, html):
+ """
+ Build html table.
+ """
+ # birth and death cells should be present in table.
+ # if no data - as empty cell without CELLPADDING
+ birth, death = self.get_dates_str(person, separated=True,
+ default=' | ')
+
+ return html % {'img': self.get_image_str(person),
+ 'name': self.get_name_str(person),
+ 'birth': birth,
+ 'death': death,
+ 'tags': self.get_tags_str(person),
+ 'attrs' : self.get_attrs_str(person)}
+