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 += '' % 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 += '' % tag_table - self.add_tags_tooltip(family.handle, tags) - - # close main table - label += '
%s
%s
' - - 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 = ( + '' + '%s' + '
') + 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 = ( + '' + '%s
' + '') + else: + attrs_fmt = ( + '' + '%s
' + '') + 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)} +