1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2007  Donald N. Allingham
5# Copyright (C) 2008       Raphael Ackermann
6# Copyright (C) 2010       Benny Malengier
7# Copyright (C) 2010       Nick Hall
8# Copyright (C) 2012       Doug Blank <doug.blank@gmail.com>
9# Copyright (C) 2015-      Serge Noiraud
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24#
25
26#-------------------------------------------------------------------------
27#
28# Standard python modules
29#
30#-------------------------------------------------------------------------
31import random
32import os
33from xml.sax.saxutils import escape
34from collections import abc
35
36#-------------------------------------------------------------------------
37#
38# GTK/Gnome modules
39#
40#-------------------------------------------------------------------------
41from gi.repository import GObject
42from gi.repository import Gdk
43from gi.repository import Gtk
44from gi.repository import Pango
45
46#-------------------------------------------------------------------------
47#
48# gramps modules
49#
50#-------------------------------------------------------------------------
51from gramps.gen.config import config
52from gramps.gen.const import GRAMPS_LOCALE as glocale
53from gramps.gen.const import HOME_DIR, URL_WIKISTRING, URL_MANUAL_PAGE
54from gramps.gen.datehandler import get_date_formats
55from gramps.gen.display.name import displayer as _nd
56from gramps.gen.display.name import NameDisplayError
57from gramps.gen.display.place import displayer as _pd
58from gramps.gen.utils.alive import update_constants
59from gramps.gen.utils.file import media_path
60from gramps.gen.utils.keyword import (get_keywords, get_translations,
61                                      get_translation_from_keyword,
62                                      get_keyword_from_translation)
63from gramps.gen.lib import Date, FamilyRelType
64from gramps.gen.lib import Name, Surname, NameOriginType
65from .managedwindow import ManagedWindow
66from .widgets import MarkupLabel, BasicLabel
67from .dialog import ErrorDialog, OkDialog
68from .editors.editplaceformat import EditPlaceFormat
69from .display import display_help
70from gramps.gen.plug.utils import available_updates
71from .plug import PluginWindows
72#from gramps.gen.errors import WindowActiveError
73from .spell import HAVE_GTKSPELL
74from gramps.gen.constfunc import win
75_ = glocale.translation.gettext
76from gramps.gen.utils.symbols import Symbols
77from gramps.gen.constfunc import get_env_var
78
79#-------------------------------------------------------------------------
80#
81# Constants
82#
83#-------------------------------------------------------------------------
84
85_surname_styles = [
86    _("Father's surname"),
87    _("None"),
88    _("Combination of mother's and father's surname"),
89    _("Icelandic style"),
90    ]
91
92# column numbers for the 'name format' model
93COL_NUM = 0
94COL_NAME = 1
95COL_FMT = 2
96COL_EXPL = 3
97
98WIKI_HELP_PAGE = URL_MANUAL_PAGE + "_-_Settings"
99WIKI_HELP_SEC = _('Preferences')
100
101#-------------------------------------------------------------------------
102#
103#
104#
105#-------------------------------------------------------------------------
106class DisplayNameEditor(ManagedWindow):
107    def __init__(self, uistate, dbstate, track, dialog):
108        # Assumes that there are two methods: dialog.name_changed_check(),
109        # and dialog._build_custom_name_ui()
110        ManagedWindow.__init__(self, uistate, track, DisplayNameEditor)
111        self.dialog = dialog
112        self.dbstate = dbstate
113        self.set_window(
114            Gtk.Dialog(title=_('Display Name Editor')),
115            None, _('Display Name Editor'), None)
116        self.window.add_button(_('_Close'), Gtk.ResponseType.CLOSE)
117        self.setup_configs('interface.displaynameeditor', 820, 550)
118        grid = self.dialog._build_custom_name_ui()
119        label = Gtk.Label(label=_("""The following keywords are replaced with the appropriate name parts:<tt>
120  <b>Given</b>   - given name (first name)     <b>Surname</b>  - surnames (with prefix and connectors)
121  <b>Title</b>   - title (Dr., Mrs.)           <b>Suffix</b>   - suffix (Jr., Sr.)
122  <b>Call</b>    - call name                   <b>Nickname</b> - nick name
123  <b>Initials</b>- first letters of given      <b>Common</b>   - nick name, call, or first of given
124  <b>Prefix</b>  - all prefixes (von, de)
125Surnames:
126  <b>Rest</b>      - non primary surnames    <b>Notpatronymic</b>- all surnames, except pa/matronymic &amp; primary
127  <b>Familynick</b>- family nick name        <b>Rawsurnames</b>  - surnames (no prefixes and connectors)
128  <b>Primary, Primary[pre] or [sur] or [con]</b>- full primary surname, prefix, surname only, connector
129  <b>Patronymic, or [pre] or [sur] or [con]</b> - full pa/matronymic surname, prefix, surname only, connector
130</tt>
131UPPERCASE keyword forces uppercase. Extra parentheses, commas are removed. Other text appears literally.
132
133<b>Example</b>: Dr. Edwin Jose von der Smith and Weston Wilson Sr ("Ed") - Underhills
134     <i>Edwin Jose</i>: Given, <i>von der</i>: Prefix, <i>Smith</i> and <i>Weston</i>: Primary, <i>and</i>: [con], <i>Wilson</i>: Patronymic,
135     <i>Dr.</i>: Title, <i>Sr</i>: Suffix, <i>Ed</i>: Nickname, <i>Underhills</i>: Familynick, <i>Jose</i>: Call.
136"""))
137        label.set_use_markup(True)
138        self.window.vbox.pack_start(label, False, True, 0)
139        self.window.vbox.pack_start(grid, True, True, 0)
140        self.window.connect('response', self.close)
141        self.show()
142
143    def close(self, *obj):
144        self.dialog.name_changed_check()
145        ManagedWindow.close(self, *obj)
146
147    def build_menu_names(self, obj):
148        return (_(" Name Editor"), None)
149
150
151#-------------------------------------------------------------------------
152#
153# ConfigureDialog
154#
155#-------------------------------------------------------------------------
156
157class ConfigureDialog(ManagedWindow):
158    """
159    Base class for configuration dialogs. They provide a Notebook, to which
160    pages are added with configuration options, and a Cancel and Save button.
161    On save, a config file on which the dialog works, is saved to disk, and
162    a callback called.
163    """
164    def __init__(self, uistate, dbstate, configure_page_funcs, configobj,
165                 configmanager,
166                 dialogtitle=_("Preferences"), on_close=None):
167        """
168        Set up a configuration dialog
169        :param uistate: a DisplayState instance
170        :param dbstate: a DbState instance
171        :param configure_page_funcs: a list of function that return a tuple
172            (str, Gtk.Widget). The string is used as label for the
173            configuration page, and the widget as the content of the
174            configuration page
175        :param configobj: the unique object that is configured, it must be
176            identifiable (id(configobj)). If the configure dialog of the
177            configobj is already open, a WindowActiveError will be
178            raised. Grab this exception in the calling method
179        :param configmanager: a configmanager object. Several convenience
180            methods are present in ConfigureDialog to set up widgets that
181            write changes directly via this configmanager.
182        :param dialogtitle: the title of the configuration dialog
183        :param on_close: callback that is called on close
184        """
185        self.dbstate = dbstate
186        self.__config = configmanager
187        ManagedWindow.__init__(self, uistate, [], configobj)
188        self.set_window(Gtk.Dialog(title=dialogtitle), None, dialogtitle, None)
189        self.window.add_button(_('_Close'), Gtk.ResponseType.CLOSE)
190        self.panel = Gtk.Notebook()
191        self.panel.set_scrollable(True)
192        self.window.vbox.pack_start(self.panel, True, True, 0)
193        self.__on_close = on_close
194        self.window.connect('response', self.done)
195
196        self.__setup_pages(configure_page_funcs)
197
198        self.show()
199
200    def __setup_pages(self, configure_page_funcs):
201        """
202        This method builds the notebook pages in the panel
203        """
204        if isinstance(configure_page_funcs, abc.Callable):
205            pages = configure_page_funcs()
206        else:
207            pages = configure_page_funcs
208        for func in pages:
209            labeltitle, widget = func(self)
210            self.panel.append_page(widget, MarkupLabel(labeltitle))
211
212    def done(self, obj, value):
213        if value == Gtk.ResponseType.HELP:
214            return
215        if self.__on_close:
216            self.__on_close()
217        self.close()
218
219    def update_int_entry(self, obj, constant):
220        """
221        :param obj: an object with get_text method that should contain an
222            integer
223        :param constant: the config setting to which the integer value must be
224            saved
225        """
226        try:
227            self.__config.set(constant, int(obj.get_text()))
228        except:
229            print("WARNING: ignoring invalid value for '%s'" % constant)
230
231    def update_markup_entry(self, obj, constant):
232        """
233        :param obj: an object with get_text method
234        :param constant: the config setting to which the text value must be
235            saved
236        """
237        try:
238            obj.get_text() % 'test_markup'
239        except TypeError:
240            print("WARNING: ignoring invalid value for '%s'" % constant)
241            ErrorDialog(
242                _("Invalid or incomplete format definition."),
243                obj.get_text(), parent=self.window)
244            obj.set_text('<b>%s</b>')
245        except ValueError:
246            print("WARNING: ignoring invalid value for '%s'" % constant)
247            ErrorDialog(
248                _("Invalid or incomplete format definition."),
249                obj.get_text(), parent=self.window)
250            obj.set_text('<b>%s</b>')
251
252        self.__config.set(constant, obj.get_text())
253
254    def update_entry(self, obj, constant):
255        """
256        :param obj: an object with get_text method
257        :param constant: the config setting to which the text value must be
258            saved
259        """
260        self.__config.set(constant, obj.get_text())
261
262    def update_color(self, obj, pspec, constant, color_hex_label):
263        """
264        Called on changing some color.
265        Either on programmatically color change.
266        """
267        rgba = obj.get_rgba()
268        hexval = "#%02x%02x%02x" % (int(rgba.red * 255),
269                                    int(rgba.green * 255),
270                                    int(rgba.blue * 255))
271        color_hex_label.set_text(hexval)
272        colors = self.__config.get(constant)
273        if isinstance(colors, list):
274            scheme = self.__config.get('colors.scheme')
275            colors[scheme] = hexval
276            self.__config.set(constant, colors)
277        else:
278            self.__config.set(constant, hexval)
279
280    def update_checkbox(self, obj, constant, config=None):
281        """
282        :param obj: the CheckButton object
283        :param constant: the config setting to which the value must be saved
284        """
285        if not config:
286            config = self.__config
287        config.set(constant, obj.get_active())
288
289    def update_radiobox(self, obj, constant):
290        """
291        :param obj: the RadioButton object
292        :param constant: the config setting to which the value must be saved
293        """
294        self.__config.set(constant, obj.get_active())
295
296    def update_combo(self, obj, constant):
297        """
298        :param obj: the ComboBox object
299        :param constant: the config setting to which the value must be saved
300        """
301        self.__config.set(constant, obj.get_active())
302
303    def update_slider(self, obj, constant):
304        """
305        :param obj: the HScale object
306        :param constant: the config setting to which the value must be saved
307        """
308        self.__config.set(constant, int(obj.get_value()))
309
310    def update_spinner(self, obj, constant):
311        """
312        :param obj: the SpinButton object
313        :param constant: the config setting to which the value must be saved
314        """
315        self.__config.set(constant, int(obj.get_value()))
316
317    def add_checkbox(self, grid, label, index, constant, start=1, stop=9,
318                     config=None, extra_callback=None, tooltip=''):
319        """
320        Adds checkbox option with tooltip.
321        """
322        if not config:
323            config = self.__config
324        checkbox = Gtk.CheckButton(label=label)
325        checkbox.set_active(config.get(constant))
326        checkbox.connect('toggled', self.update_checkbox, constant, config)
327        if extra_callback:
328            checkbox.connect('toggled', extra_callback)
329        if tooltip:
330            checkbox.set_tooltip_text(tooltip)
331        grid.attach(checkbox, start, index, stop - start, 1)
332        return checkbox
333
334    def add_radiobox(self, grid, label, index, constant, group, column,
335                     config=None):
336        """
337        Adds radiobox option.
338        """
339        if not config:
340            config = self.__config
341        radiobox = Gtk.RadioButton.new_with_mnemonic_from_widget(group, label)
342        if config.get(constant):
343            radiobox.set_active(True)
344        radiobox.connect('toggled', self.update_radiobox, constant)
345        grid.attach(radiobox, column, index, 1, 1)
346        return radiobox
347
348    def add_text(self, grid, label, index, config=None, line_wrap=True,
349                 start=1, stop=9, justify=Gtk.Justification.LEFT,
350                 align=Gtk.Align.START, bold=False):
351        """
352        Adds text with specified parameters.
353        """
354        if not config:
355            config = self.__config
356        text = Gtk.Label()
357        text.set_line_wrap(line_wrap)
358        text.set_halign(Gtk.Align.START)
359        if bold:
360            text.set_markup('<b>%s</b>' % label)
361        else:
362            text.set_text(label)
363        text.set_halign(align)
364        text.set_justify(justify)
365        text.set_hexpand(True)
366        grid.attach(text, start, index, stop - start, 1)
367        return text
368
369    def add_button(self, grid, label, index, constant, extra_callback=None, config=None):
370        if not config:
371            config = self.__config
372        button = Gtk.Button(label=label)
373        button.connect('clicked', extra_callback)
374        grid.attach(button, 1, index, 1, 1)
375        return button
376
377    def add_path_box(self, grid, label, index, entry, path, callback_label,
378                     callback_sel, config=None):
379        """
380        Add an entry to give in path and a select button to open a dialog.
381        Changing entry calls callback_label
382        Clicking open button call callback_sel
383        """
384        if not config:
385            config = self.__config
386        lwidget = BasicLabel(_("%s: ") % label)  # needed for French
387        hbox = Gtk.Box()
388        if path:
389            entry.set_text(path)
390        entry.connect('changed', callback_label)
391        btn = Gtk.Button()
392        btn.connect('clicked', callback_sel)
393        image = Gtk.Image()
394        image.set_from_icon_name('document-open', Gtk.IconSize.BUTTON)
395        image.show()
396        btn.add(image)
397        hbox.pack_start(entry, True, True, 0)
398        hbox.pack_start(btn, False, False, 0)
399        hbox.set_hexpand(True)
400        grid.attach(lwidget, 1, index, 1, 1)
401        grid.attach(hbox, 2, index, 1, 1)
402
403    def add_entry(self, grid, label, index, constant, callback=None,
404                  config=None, col_attach=0, localized_config=True):
405        """
406        Adds entry field.
407        """
408        if not config:
409            config = self.__config
410        if not callback:
411            callback = self.update_entry
412        if label:
413            lwidget = BasicLabel(_("%s: ") % label)  # translators: for French
414        entry = Gtk.Entry()
415        if localized_config:
416            entry.set_text(config.get(constant))
417        else:  # it needs localizing
418            entry.set_text(_(config.get(constant)))
419        entry.connect('changed', callback, constant)
420        entry.set_hexpand(True)
421        if label:
422            grid.attach(lwidget, col_attach, index, 1, 1)
423            grid.attach(entry, col_attach+1, index, 1, 1)
424        else:
425            grid.attach(entry, col_attach, index, 1, 1)
426        return entry
427
428    def add_pos_int_entry(self, grid, label, index, constant, callback=None,
429                          config=None, col_attach=1, helptext=''):
430        """
431        Adds entry field for positive integers.
432        """
433        if not config:
434            config = self.__config
435        lwidget = BasicLabel(_("%s: ") % label)  # needed for French
436        entry = Gtk.Entry()
437        entry.set_text(str(config.get(constant)))
438        entry.set_tooltip_markup(helptext)
439        entry.set_hexpand(True)
440        if callback:
441            entry.connect('changed', callback, constant)
442        grid.attach(lwidget, col_attach, index, 1, 1)
443        grid.attach(entry, col_attach+1, index, 1, 1)
444
445    def add_color(self, grid, label, index, constant, config=None, col=0):
446        """
447        Add color chooser widget with label and hex value to the grid.
448        """
449        if not config:
450            config = self.__config
451        lwidget = BasicLabel(_("%s: ") % label)  # needed for French
452        colors = config.get(constant)
453        if isinstance(colors, list):
454            scheme = config.get('colors.scheme')
455            hexval = colors[scheme]
456        else:
457            hexval = colors
458        color = Gdk.color_parse(hexval)
459        entry = Gtk.ColorButton(color=color)
460        color_hex_label = BasicLabel(hexval)
461        color_hex_label.set_hexpand(True)
462        entry.connect('notify::color', self.update_color, constant,
463                      color_hex_label)
464        grid.attach(lwidget, col, index, 1, 1)
465        grid.attach(entry, col+1, index, 1, 1)
466        grid.attach(color_hex_label, col+2, index, 1, 1)
467        return entry
468
469    def add_combo(self, grid, label, index, constant, opts, callback=None,
470                  config=None, valueactive=False, setactive=None):
471        """
472        A drop-down list allowing selection from a number of fixed options.
473        :param opts: A list of options.  Each option is a tuple containing an
474        integer code and a textual description.
475        If valueactive = True, the constant stores the value, not the position
476        in the list
477        """
478        if not config:
479            config = self.__config
480        if not callback:
481            callback = self.update_combo
482        lwidget = BasicLabel(_("%s: ") % label)  # needed for French
483        store = Gtk.ListStore(int, str)
484        for item in opts:
485            store.append(item)
486        combo = Gtk.ComboBox(model=store)
487        cell = Gtk.CellRendererText()
488        combo.pack_start(cell, True)
489        combo.add_attribute(cell, 'text', 1)
490        if valueactive:
491            val = config.get(constant)
492            pos = 0
493            for nr, item in enumerate(opts):
494                if item[-1] == val:
495                    pos = nr
496                    break
497            combo.set_active(pos)
498        else:
499            if setactive is None:
500                combo.set_active(config.get(constant))
501            else:
502                combo.set_active(setactive)
503        combo.connect('changed', callback, constant)
504        combo.set_hexpand(True)
505        grid.attach(lwidget, 1, index, 1, 1)
506        grid.attach(combo, 2, index, 1, 1)
507        return combo
508
509    def add_slider(self, grid, label, index, constant, range, callback=None,
510                   config=None, width=1):
511        """
512        Slider allowing the selection of an integer within a specified range.
513        :param range: Tuple containing the minimum and maximum allowed values.
514        """
515        if not config:
516            config = self.__config
517        if not callback:
518            callback = self.update_slider
519        lwidget = BasicLabel(_("%s: ") % label)  # needed for French
520        adj = Gtk.Adjustment(value=config.get(constant), lower=range[0],
521                             upper=range[1], step_increment=1,
522                             page_increment=0, page_size=0)
523        slider = Gtk.Scale(adjustment=adj)
524        slider.set_digits(0)
525        slider.set_value_pos(Gtk.PositionType.BOTTOM)
526        slider.connect('value-changed', callback, constant)
527        grid.attach(lwidget, 1, index, 1, 1)
528        grid.attach(slider, 2, index, width, 1)
529        return slider
530
531    def add_spinner(self, grid, label, index, constant, range, callback=None,
532                    config=None):
533        """
534        Spinner allowing the selection of an integer within a specified range.
535        :param range: Tuple containing the minimum and maximum allowed values.
536        """
537        if not config:
538            config = self.__config
539        if not callback:
540            callback = self.update_spinner
541        lwidget = BasicLabel(_("%s: ") % label)  # needed for French
542        adj = Gtk.Adjustment(value=config.get(constant), lower=range[0],
543                             upper=range[1], step_increment=1,
544                             page_increment=0, page_size=0)
545        spinner = Gtk.SpinButton(adjustment=adj, climb_rate=0.0, digits=0)
546        spinner.connect('value-changed', callback, constant)
547        spinner.set_hexpand(True)
548        grid.attach(lwidget, 1, index, 1, 1)
549        grid.attach(spinner, 2, index, 1, 1)
550        return spinner
551
552#-------------------------------------------------------------------------
553#
554# GrampsPreferences
555#
556#-------------------------------------------------------------------------
557class GrampsPreferences(ConfigureDialog):
558
559    def __init__(self, uistate, dbstate):
560        page_funcs = (
561            self.add_behavior_panel,
562            self.add_famtree_panel,
563            self.add_formats_panel,
564            self.add_text_panel,
565            self.add_prefix_panel,
566            self.add_date_panel,
567            self.add_researcher_panel,
568            self.add_advanced_panel,
569            self.add_color_panel,
570            self.add_symbols_panel
571            )
572        ConfigureDialog.__init__(self, uistate, dbstate, page_funcs,
573                                 GrampsPreferences, config,
574                                 on_close=update_constants)
575        help_btn = self.window.add_button(_('_Help'), Gtk.ResponseType.HELP)
576        help_btn.connect(
577            'clicked', lambda x: display_help(WIKI_HELP_PAGE, WIKI_HELP_SEC))
578        self.setup_configs('interface.grampspreferences', 700, 450)
579
580    def create_grid(self):
581        """
582        Gtk.Grid for config panels (tabs).
583        """
584        grid = Gtk.Grid()
585        grid.set_border_width(12)
586        grid.set_column_spacing(6)
587        grid.set_row_spacing(6)
588        return grid
589
590    def add_researcher_panel(self, configdialog):
591        """
592        Add the Researcher tab to the preferences.
593        """
594        grid = self.create_grid()
595        row = 0
596        self.add_text(
597            grid, _('Researcher information'), row,
598            line_wrap=True, start=0, stop=2, justify=Gtk.Justification.CENTER,
599            align=Gtk.Align.CENTER, bold=True)
600        row += 1
601        self.add_text(
602            grid, _('Enter information about yourself so people can contact '
603                    'you when you distribute your Family Tree'), row,
604            line_wrap=True, start=0, stop=2, justify=Gtk.Justification.CENTER,
605            align=Gtk.Align.CENTER)
606
607        row += 1
608        self.add_entry(grid, _('Name'), row, 'researcher.researcher-name')
609        row += 1
610        self.add_entry(grid, _('Address'), row, 'researcher.researcher-addr')
611        row += 1
612        self.add_entry(grid, _('Locality'), row,
613                       'researcher.researcher-locality')
614        row += 1
615        self.add_entry(grid, _('City'), row, 'researcher.researcher-city')
616        row += 1
617        self.add_entry(grid, _('State/County'), row,
618                       'researcher.researcher-state')
619        row += 1
620        self.add_entry(grid, _('Country'), row,
621                       'researcher.researcher-country')
622        row += 1
623        self.add_entry(grid, _('ZIP/Postal Code'), row,
624                       'researcher.researcher-postal')
625        row += 1
626        self.add_entry(grid, _('Phone'), row, 'researcher.researcher-phone')
627        row += 1
628        self.add_entry(grid, _('Email'), row, 'researcher.researcher-email')
629        return _('Researcher'), grid
630
631    def add_prefix_panel(self, configdialog):
632        """
633        Add the ID prefix tab to the preferences.
634        """
635        grid = self.create_grid()
636
637        self.add_text(
638            grid, _('Gramps ID format settings'), 0,
639            line_wrap=True, start=0, stop=2, justify=Gtk.Justification.CENTER,
640            align=Gtk.Align.CENTER, bold=True)
641
642        row = 1
643        self.add_entry(grid, _('Person'), row, 'preferences.iprefix',
644                       self.update_idformat_entry)
645        row += 1
646        self.add_entry(grid, _('Family'), row, 'preferences.fprefix',
647                       self.update_idformat_entry)
648        row += 1
649        self.add_entry(grid, _('Place'), row, 'preferences.pprefix',
650                       self.update_idformat_entry)
651        row += 1
652        self.add_entry(grid, _('Source'), row, 'preferences.sprefix',
653                       self.update_idformat_entry)
654        row += 1
655        self.add_entry(grid, _('Citation'), row, 'preferences.cprefix',
656                       self.update_idformat_entry)
657        row += 1
658        self.add_entry(grid, _('Media Object'), row, 'preferences.oprefix',
659                       self.update_idformat_entry)
660        row += 1
661        self.add_entry(grid, _('Event'), row, 'preferences.eprefix',
662                       self.update_idformat_entry)
663        row += 1
664        self.add_entry(grid, _('Repository'), row, 'preferences.rprefix',
665                       self.update_idformat_entry)
666        row += 1
667        self.add_entry(grid, _('Note'), row, 'preferences.nprefix',
668                       self.update_idformat_entry)
669        return _('ID Formats'), grid
670
671    def add_color_panel(self, configdialog):
672        """
673        Add the tab to set defaults colors for graph boxes.
674        """
675        grid = self.create_grid()
676        self.add_text(
677            grid, _('Colors used for boxes in the graphical views'),
678            0, line_wrap=True, start=0, stop=7, bold=True,
679            justify=Gtk.Justification.CENTER, align=Gtk.Align.CENTER)
680
681        hbox = Gtk.Box(spacing=12)
682        self.color_scheme_box = Gtk.ComboBoxText()
683        formats = [_("Light colors"),
684                   _("Dark colors")]
685        list(map(self.color_scheme_box.append_text, formats))
686        scheme = config.get('colors.scheme')
687        self.color_scheme_box.set_active(scheme)
688        self.color_scheme_box.connect('changed', self.color_scheme_changed)
689        lwidget = BasicLabel(_("%s: ") % _('Color scheme'))
690        hbox.pack_start(lwidget, False, False, 0)
691        hbox.pack_start(self.color_scheme_box, False, False, 0)
692
693        restore_btn = Gtk.Button(_('Restore to defaults'))
694        restore_btn.set_tooltip_text(
695            _('Restore colors for current theme to default.'))
696        restore_btn.connect('clicked', self.restore_colors)
697        hbox.pack_start(restore_btn, False, False, 0)
698        hbox.set_halign(Gtk.Align.CENTER)
699        grid.attach(hbox, 0, 1, 7, 1)
700
701        color_type = {'Male': _('Colors for Male persons'),
702                      'Female': _('Colors for Female persons'),
703                      'Unknown': _('Colors for Unknown persons'),
704                      'Family': _('Colors for Family nodes'),
705                      'Other': _('Other colors')}
706
707        bg_alive_text = _('Background for Alive')
708        bg_dead_text = _('Background for Dead')
709        brd_alive_text = _('Border for Alive')
710        brd_dead_text = _('Border for Dead')
711
712        # color label, config constant, group grid row, column, color type
713        color_list = [
714            # for male
715            (bg_alive_text, 'male-alive', 1, 1, 'Male'),
716            (bg_dead_text, 'male-dead', 2, 1, 'Male'),
717            (brd_alive_text, 'border-male-alive', 1, 4, 'Male'),
718            (brd_dead_text, 'border-male-dead', 2, 4, 'Male'),
719            # for female
720            (bg_alive_text, 'female-alive', 1, 1, 'Female'),
721            (bg_dead_text, 'female-dead', 2, 1, 'Female'),
722            (brd_alive_text, 'border-female-alive', 1, 4, 'Female'),
723            (brd_dead_text, 'border-female-dead', 2, 4, 'Female'),
724            # for unknown
725            (bg_alive_text, 'unknown-alive', 1, 1, 'Unknown'),
726            (bg_dead_text, 'unknown-dead', 2, 1, 'Unknown'),
727            (brd_alive_text, 'border-unknown-alive', 1, 4, 'Unknown'),
728            (brd_dead_text, 'border-unknown-dead', 2, 4, 'Unknown'),
729            # for family
730            (_('Default background'), 'family', 1, 1, 'Family'),
731            (_('Background for Married'), 'family-married', 3, 1, 'Family'),
732            (_('Background for Unmarried'),
733             'family-unmarried', 4, 1, 'Family'),
734            (_('Background for Civil union'),
735             'family-civil-union', 5, 1, 'Family'),
736            (_('Background for Unknown'), 'family-unknown', 6, 1, 'Family'),
737            (_('Background for Divorced'), 'family-divorced', 7, 1, 'Family'),
738            (_('Default border'), 'border-family', 1, 4, 'Family'),
739            (_('Border for Divorced'),
740             'border-family-divorced', 7, 4, 'Family'),
741            # for other
742            (_('Background for Home Person'), 'home-person', 1, 1, 'Other'),
743            ]
744
745        # prepare scrolled window for colors settings
746        scroll_window = Gtk.ScrolledWindow()
747        colors_grid = self.create_grid()
748        scroll_window.add(colors_grid)
749        scroll_window.set_vexpand(True)
750        scroll_window.set_policy(Gtk.PolicyType.NEVER,
751                                 Gtk.PolicyType.AUTOMATIC)
752        grid.attach(scroll_window, 0, 3, 7, 1)
753
754        # add color settings to scrolled window by groups
755        row = 0
756        self.colors = {}
757        for key, frame_lbl in color_type.items():
758            group_label = Gtk.Label()
759            group_label.set_halign(Gtk.Align.START)
760            group_label.set_margin_top(12)
761            group_label.set_markup(_('<b>%s</b>') % frame_lbl)
762            colors_grid.attach(group_label, 0, row, 3, 1)
763
764            row_added = 0
765            for color in color_list:
766                if color[4] == key:
767                    pref_name = 'colors.' + color[1]
768                    self.colors[pref_name] = self.add_color(
769                        colors_grid, color[0], row + color[2],
770                        pref_name, col=color[3])
771                    row_added += 1
772            row += row_added + 1
773
774        return _('Colors'), grid
775
776    def restore_colors(self, widget=None):
777        """
778        Restore colors of selected scheme to default.
779        """
780        scheme = config.get('colors.scheme')
781        for key, widget in self.colors.items():
782            color = Gdk.RGBA()
783            hexval = config.get_default(key)[scheme]
784            Gdk.RGBA.parse(color, hexval)
785            widget.set_rgba(color)
786
787    def add_advanced_panel(self, configdialog):
788        """
789        Config tab for Warnings and Error dialogs.
790        """
791        grid = self.create_grid()
792
793        self.add_text(
794            grid, _('Warnings and Error dialogs'), 0, line_wrap=True,
795            justify=Gtk.Justification.CENTER, align=Gtk.Align.CENTER,
796            bold=True)
797
798        row = 1
799        self.add_checkbox(
800            grid, _('Suppress warning when adding parents to a child'),
801            row, 'preferences.family-warn')
802        row += 1
803        self.add_checkbox(
804            grid, _('Suppress warning when canceling with changed data'),
805            row, 'interface.dont-ask')
806        row += 1
807        self.add_checkbox(
808            grid, _('Suppress warning about missing researcher when'
809                    ' exporting to GEDCOM'),
810            row, 'behavior.owner-warn')
811        row += 1
812        self.add_checkbox(
813            grid, _('Show plugin status dialog on plugin load error'),
814            row, 'behavior.pop-plugin-status')
815
816        return _('Warnings'), grid
817
818    def _build_name_format_model(self, active):
819        """
820        Create a common model for ComboBox and TreeView
821        """
822        name_format_model = Gtk.ListStore(GObject.TYPE_INT,
823                                          GObject.TYPE_STRING,
824                                          GObject.TYPE_STRING,
825                                          GObject.TYPE_STRING)
826        index = 0
827        the_index = 0
828        for num, name, fmt_str, act in _nd.get_name_format():
829            translation = fmt_str
830            for key in get_keywords():
831                if key in translation:
832                    translation = translation.replace(
833                        key, get_translation_from_keyword(key))
834            self.examplename.set_display_as(num)
835            name_format_model.append(row=[num, translation, fmt_str,
836                                          _nd.display_name(self.examplename)])
837            if num == active:
838                the_index = index
839            index += 1
840        return name_format_model, the_index
841
842    def __new_name(self, obj):
843        lyst = [
844            "%s, %s %s (%s)" % (_("Surname"), _("Given"), _("Suffix"),
845                                _("Common")),
846            "%s, %s %s (%s)" % (_("Surname"), _("Given"), _("Suffix"),
847                                _("Nickname")),
848            "%s, %s %s (%s)" % (_("Surname"), _("Name|Common"), _("Suffix"),
849                                _("Nickname")),
850            "%s, %s %s" % (_("Surname"), _("Name|Common"), _("Suffix")),
851            "%s, %s %s (%s)" % (_("SURNAME"), _("Given"), _("Suffix"),
852                                _("Call")),
853            "%s, %s (%s)" % (_("Surname"), _("Given"), _("Name|Common")),
854            "%s, %s (%s)" % (_("Surname"), _("Name|Common"), _("Nickname")),
855            "%s %s" % (_("Given"), _("Surname")),
856            "%s %s, %s" % (_("Given"), _("Surname"), _("Suffix")),
857            "%s %s %s" % (_("Given"), _("NotPatronymic"), _("Patronymic")),
858            "%s, %s %s (%s)" % (_("SURNAME"), _("Given"), _("Suffix"),
859                                _("Common")),
860            "%s, %s (%s)" % (_("SURNAME"), _("Given"), _("Name|Common")),
861            "%s, %s (%s)" % (_("SURNAME"), _("Given"), _("Nickname")),
862            "%s %s" % (_("Given"), _("SURNAME")),
863            "%s %s, %s" % (_("Given"), _("SURNAME"), _("Suffix")),
864            "%s /%s/" % (_("Given"), _("SURNAME")),
865            "%s %s, %s" % (_("Given"), _("Rawsurnames"), _("Suffix")),
866            ]
867        # repeat above list, but not translated.
868        fmtlyst = [
869            "%s, %s %s (%s)" % (("Surname"), ("Given"), ("Suffix"),
870                                ("Common")),
871            "%s, %s %s (%s)" % (("Surname"), ("Given"), ("Suffix"),
872                                ("Nickname")),
873            "%s, %s %s (%s)" % (("Surname"), ("Name|Common"), ("Suffix"),
874                                ("Nickname")),
875            "%s, %s %s" % (("Surname"), ("Name|Common"), ("Suffix")),
876            "%s, %s %s (%s)" % (("SURNAME"), ("Given"), ("Suffix"),
877                                ("Call")),
878            "%s, %s (%s)" % (("Surname"), ("Given"), ("Name|Common")),
879            "%s, %s (%s)" % (("Surname"), ("Name|Common"), ("Nickname")),
880            "%s %s" % (("Given"), ("Surname")),
881            "%s %s, %s" % (("Given"), ("Surname"), ("Suffix")),
882            "%s %s %s" % (("Given"), ("NotPatronymic"), ("Patronymic")),
883            "%s, %s %s (%s)" % (("SURNAME"), ("Given"), ("Suffix"),
884                                ("Common")),
885            "%s, %s (%s)" % (("SURNAME"), ("Given"), ("Name|Common")),
886            "%s, %s (%s)" % (("SURNAME"), ("Given"), ("Nickname")),
887            "%s %s" % (("Given"), ("SURNAME")),
888            "%s %s, %s" % (("Given"), ("SURNAME"), ("Suffix")),
889            "%s /%s/" % (("Given"), ("SURNAME")),
890            "%s %s, %s" % (("Given"), ("Rawsurnames"), ("Suffix")),
891            ]
892        rand = int(random.random() * len(lyst))
893        f = lyst[rand]
894        fmt = fmtlyst[rand]
895        i = _nd.add_name_format(f, fmt)
896        fmt_str = _nd.format_str(self.examplename, fmt)
897        node = self.fmt_model.append(row=[i, f, fmt, fmt_str])
898        path = self.fmt_model.get_path(node)
899        self.format_list.set_cursor(path, self.name_column, True)
900        self.edit_button.set_sensitive(False)
901        self.remove_button.set_sensitive(False)
902        self.insert_button.set_sensitive(False)
903
904    def __edit_name(self, obj):
905        store, node = self.format_list.get_selection().get_selected()
906        path = self.fmt_model.get_path(node)
907        self.edit_button.set_sensitive(False)
908        self.remove_button.set_sensitive(False)
909        self.insert_button.set_sensitive(False)
910        self.format_list.set_cursor(path, self.name_column, True)
911
912    def __check_for_name(self, name, oldnode):
913        """
914        Check to see if there is another name the same as name
915        in the format list. Don't compare with self (oldnode).
916        """
917        model = self.fmt_obox.get_model()
918        iter = model.get_iter_first()
919        while iter is not None:
920            othernum = model.get_value(iter, COL_NUM)
921            oldnum = model.get_value(oldnode, COL_NUM)
922            if othernum == oldnum:
923                pass  # skip comparison with self
924            else:
925                othername = model.get_value(iter, COL_NAME)
926                if othername == name:
927                    return True
928            iter = model.iter_next(iter)
929        return False
930
931    def __start_name_editing(self, dummy_renderer, dummy_editable, dummy_path):
932        """
933        Method called at the start of editing a name format.
934        """
935        self.format_list.set_tooltip_text(_("Enter to save, Esc to cancel "
936                                            "editing"))
937
938    def __cancel_change(self, dummy_renderer):
939        """
940        Break off the editing of a name format.
941        """
942        self.format_list.set_tooltip_text('')
943        num = self.selected_fmt[COL_NUM]
944        if any(fmt[COL_NUM] == num for fmt in self.dbstate.db.name_formats):
945            return
946        else:  # editing a new format not yet in db, cleanup is needed
947            self.fmt_model.remove(self.iter)
948            _nd.del_name_format(num)
949            self.insert_button.set_sensitive(True)
950
951    def __change_name(self, text, path, new_text):
952        """
953        Called when a name format changed and needs to be stored in the db.
954        """
955        self.format_list.set_tooltip_text('')
956        if len(new_text) > 0 and text != new_text:
957            # build a pattern from translated pattern:
958            pattern = new_text
959            if (len(new_text) > 2 and
960                    new_text[0] == '"' and
961                    new_text[-1] == '"'):
962                pass
963            else:
964                for key in get_translations():
965                    if key in pattern:
966                        pattern = pattern.replace(
967                            key, get_keyword_from_translation(key))
968            # now build up a proper translation:
969            translation = pattern
970            if (len(new_text) > 2 and
971                    new_text[0] == '"' and
972                    new_text[-1] == '"'):
973                pass
974            else:
975                for key in get_keywords():
976                    if key in translation:
977                        translation = translation.replace(
978                            key, get_translation_from_keyword(key))
979            num, name, fmt = self.selected_fmt[COL_NUM:COL_EXPL]
980            node = self.fmt_model.get_iter(path)
981            oldname = self.fmt_model.get_value(node, COL_NAME)
982            # check to see if this pattern already exists
983            if self.__check_for_name(translation, node):
984                ErrorDialog(_("This format exists already."),
985                            translation, parent=self.window)
986                self.edit_button.emit('clicked')
987                return
988            # else, change the name
989            self.edit_button.set_sensitive(True)
990            self.remove_button.set_sensitive(True)
991            self.insert_button.set_sensitive(True)
992            exmpl = _nd.format_str(self.examplename, pattern)
993            self.fmt_model.set(self.iter, COL_NAME, translation,
994                               COL_FMT, pattern,
995                               COL_EXPL, exmpl)
996            self.selected_fmt = (num, translation, pattern, exmpl)
997            _nd.edit_name_format(num, translation, pattern)
998            name_format = _nd.get_name_format(only_custom=True,
999                                              only_active=False)
1000            self.dbstate.db.name_formats = name_format
1001
1002    def __format_change(self, obj):
1003        try:
1004            t = (_nd.format_str(self.name, escape(obj.get_text())))
1005            self.valid = True
1006        except NameDisplayError:
1007            t = _("Invalid or incomplete format definition.")
1008            self.valid = False
1009        self.fmt_model.set(self.iter, COL_EXPL, t)
1010
1011    def _build_custom_name_ui(self):
1012        """
1013        UI to manage the custom name formats
1014        """
1015        grid = Gtk.Grid()
1016        grid.set_border_width(6)
1017        grid.set_column_spacing(6)
1018        grid.set_row_spacing(6)
1019
1020        # make a treeview for listing all the name formats
1021        format_tree = Gtk.TreeView(model=self.fmt_model)
1022        name_renderer = Gtk.CellRendererText()
1023        name_column = Gtk.TreeViewColumn(_('Format'),
1024                                         name_renderer,
1025                                         text=COL_NAME)
1026        name_renderer.set_property('editable', False)
1027        name_renderer.connect('editing-started', self.__start_name_editing)
1028        name_renderer.connect('edited', self.__change_name)
1029        name_renderer.connect('editing-canceled', self.__cancel_change)
1030        self.name_renderer = name_renderer
1031        format_tree.append_column(name_column)
1032        example_renderer = Gtk.CellRendererText()
1033        example_column = Gtk.TreeViewColumn(_('Example'),
1034                                            example_renderer,
1035                                            text=COL_EXPL)
1036        format_tree.append_column(example_column)
1037        format_tree.get_selection().connect('changed',
1038                                            self.cb_format_tree_select)
1039
1040        # ... and put it into a scrolled win
1041        format_sw = Gtk.ScrolledWindow()
1042        format_sw.set_policy(Gtk.PolicyType.AUTOMATIC,
1043                             Gtk.PolicyType.AUTOMATIC)
1044        format_sw.add(format_tree)
1045        format_sw.set_shadow_type(Gtk.ShadowType.IN)
1046        format_sw.set_hexpand(True)
1047        format_sw.set_vexpand(True)
1048        grid.attach(format_sw, 0, 0, 3, 1)
1049
1050        # to hold the values of the selected row of the tree and the iter
1051        self.selected_fmt = ()
1052        self.iter = None
1053
1054        self.insert_button = Gtk.Button.new_with_mnemonic(_('_Add'))
1055        self.insert_button.connect('clicked', self.__new_name)
1056
1057        self.edit_button = Gtk.Button.new_with_mnemonic(_('_Edit'))
1058        self.edit_button.connect('clicked', self.__edit_name)
1059        self.edit_button.set_sensitive(False)
1060
1061        self.remove_button = Gtk.Button.new_with_mnemonic(_('_Remove'))
1062        self.remove_button.connect('clicked', self.cb_del_fmt_str)
1063        self.remove_button.set_sensitive(False)
1064
1065        grid.attach(self.insert_button, 0, 1, 1, 1)
1066        grid.attach(self.remove_button, 1, 1, 1, 1)
1067        grid.attach(self.edit_button,   2, 1, 1, 1)
1068        self.format_list = format_tree
1069        self.name_column = name_column
1070        return grid
1071
1072    def name_changed_check(self):
1073        """
1074        Method to check for a name change. Called by Name Edit Dialog.
1075        """
1076        obj = self.fmt_obox
1077        the_list = obj.get_model()
1078        the_iter = obj.get_active_iter()
1079        format = the_list.get_value(the_iter, COL_FMT)
1080        if format != self.old_format:
1081            # Yes a change; call the callback
1082            self.cb_name_changed(obj)
1083
1084    def cb_name_changed(self, obj):
1085        """
1086        Preset name format ComboBox callback
1087        """
1088        the_list = obj.get_model()
1089        the_iter = obj.get_active_iter()
1090        new_idx = the_list.get_value(the_iter, COL_NUM)
1091        config.set('preferences.name-format', new_idx)
1092        _nd.set_default_format(new_idx)
1093        self.uistate.emit('nameformat-changed')
1094
1095    def cb_place_fmt_changed(self, obj):
1096        """
1097        Called when the place format is changed.
1098        """
1099        config.set('preferences.place-format', obj.get_active())
1100        self.uistate.emit('placeformat-changed')
1101
1102    def cb_pa_sur_changed(self, *args):
1103        """
1104        Checkbox patronymic as surname changed, propagate to namedisplayer
1105        """
1106        _nd.change_pa_sur()
1107        self.uistate.emit('nameformat-changed')
1108
1109    def cb_format_tree_select(self, tree_selection):
1110        """
1111        Name format editor TreeView callback
1112
1113        Remember the values of the selected row (self.selected_fmt, self.iter)
1114        and set the Remove and Edit button sensitivity
1115        """
1116        model, self.iter = tree_selection.get_selected()
1117        if self.iter is None:
1118            tree_selection.select_path(0)
1119            model, self.iter = tree_selection.get_selected()
1120        self.selected_fmt = model.get(self.iter, 0, 1, 2)
1121        idx = self.selected_fmt[COL_NUM] < 0
1122        self.remove_button.set_sensitive(idx)
1123        self.edit_button.set_sensitive(idx)
1124        self.name_renderer.set_property('editable', idx)
1125
1126    def cb_del_fmt_str(self, obj):
1127        """
1128        Name format editor Remove button callback
1129        """
1130        num = self.selected_fmt[COL_NUM]
1131
1132        if _nd.get_default_format() == num:
1133            self.fmt_obox.set_active(0)
1134
1135        self.fmt_model.remove(self.iter)
1136        _nd.set_format_inactive(num)
1137        self.dbstate.db.name_formats = _nd.get_name_format(only_custom=True,
1138                                                           only_active=False)
1139
1140    def cb_grampletbar_close(self, obj):
1141        """
1142        Gramplet bar close button preference callback
1143        """
1144        self.uistate.emit('grampletbar-close-changed')
1145
1146    def add_formats_panel(self, configdialog):
1147        """
1148        Config tab with Appearance and format settings.
1149        """
1150        grid = self.create_grid()
1151
1152        self.add_text(
1153            grid, _('Appearance and format settings'), 0,
1154            line_wrap=True, start=0, stop=3, justify=Gtk.Justification.CENTER,
1155            align=Gtk.Align.CENTER, bold=True)
1156
1157        row = 1
1158        # Display name:
1159        self.examplename = Name()
1160        examplesurname = Surname()
1161        examplesurnamesecond = Surname()
1162        examplesurnamepat = Surname()
1163        self.examplename.set_title('Dr.')
1164        self.examplename.set_first_name('Edwin Jose')
1165        examplesurname.set_prefix('von der')
1166        examplesurname.set_surname('Smith')
1167        examplesurname.set_connector('and')
1168        self.examplename.add_surname(examplesurname)
1169        examplesurnamesecond.set_surname('Weston')
1170        self.examplename.add_surname(examplesurnamesecond)
1171        examplesurnamepat.set_surname('Wilson')
1172        examplesurnamepat.set_origintype(
1173            NameOriginType(NameOriginType.PATRONYMIC))
1174        self.examplename.add_surname(examplesurnamepat)
1175        self.examplename.set_primary_surname(0)
1176        self.examplename.set_suffix('Sr')
1177        self.examplename.set_call_name('Jose')
1178        self.examplename.set_nick_name('Ed')
1179        self.examplename.set_family_nick_name('Underhills')
1180        # get the model for the combo and the treeview
1181        active = _nd.get_default_format()
1182        self.fmt_model, active = self._build_name_format_model(active)
1183        # set up the combo to choose the preset format
1184        self.fmt_obox = Gtk.ComboBox()
1185        cell = Gtk.CellRendererText()
1186        cell.set_property('ellipsize', Pango.EllipsizeMode.END)
1187        self.fmt_obox.pack_start(cell, True)
1188        self.fmt_obox.add_attribute(cell, 'text', 1)
1189        self.fmt_obox.set_model(self.fmt_model)
1190        # set the default value as active in the combo
1191        self.fmt_obox.set_active(active)
1192        self.fmt_obox.connect('changed', self.cb_name_changed)
1193        # label for the combo
1194        lwidget = BasicLabel(_("%s: ") % _('Name format'))
1195        lwidget.set_use_underline(True)
1196        lwidget.set_mnemonic_widget(self.fmt_obox)
1197        hbox = Gtk.Box()
1198        btn = Gtk.Button(label=("%s..." % _('Edit')))
1199        btn.connect('clicked', self.cb_name_dialog)
1200        hbox.pack_start(self.fmt_obox, True, True, 0)
1201        hbox.pack_start(btn, False, False, 0)
1202        grid.attach(lwidget, 0, row, 1, 1)
1203        grid.attach(hbox, 1, row, 2, 1)
1204
1205        row += 1
1206        # Pa/Matronymic surname handling
1207        self.add_checkbox(
1208            grid, _("Consider single pa/matronymic as surname"),
1209            row, 'preferences.patronimic-surname', start=0, stop=2,
1210            extra_callback=self.cb_pa_sur_changed)
1211
1212        row += 1
1213        # Date format:
1214        obox = Gtk.ComboBoxText()
1215        formats = get_date_formats()
1216        list(map(obox.append_text, formats))
1217        active = config.get('preferences.date-format')
1218        if active >= len(formats):
1219            active = 0
1220        obox.set_active(active)
1221        obox.connect('changed', self.date_format_changed)
1222        lwidget = BasicLabel(_("%s: ") % _('Date format'))
1223        grid.attach(lwidget, 0, row, 1, 1)
1224        grid.attach(obox, 1, row, 2, 1)
1225
1226        row += 1
1227        # Place format:
1228        self.pformat = Gtk.ComboBox()
1229        renderer = Gtk.CellRendererText()
1230        self.pformat.pack_start(renderer, True)
1231        self.pformat.add_attribute(renderer, "text", 0)
1232        self.cb_place_fmt_rebuild()
1233        active = config.get('preferences.place-format')
1234        self.pformat.set_active(active)
1235        self.pformat.connect('changed', self.cb_place_fmt_changed)
1236        hbox = Gtk.Box()
1237        self.fmt_btn = Gtk.Button(label=("%s..." % _('Edit')))
1238        self.fmt_btn.connect('clicked', self.cb_place_fmt_dialog)
1239        cb_widget = self.add_checkbox(
1240            grid, _("%s: ") % _('Place format (auto place title)'),
1241            row, 'preferences.place-auto', start=0, stop=1,
1242            extra_callback=self.auto_title_changed,
1243            tooltip=_("Enables automatic place title generation "
1244                      "using specified format."))
1245        self.auto_title_changed(cb_widget)
1246        hbox.pack_start(self.pformat, True, True, 0)
1247        hbox.pack_start(self.fmt_btn, False, False, 0)
1248        grid.attach(hbox, 1, row, 2, 1)
1249
1250        row += 1
1251        # Age precision:
1252        # precision=1 for "year", 2: "year, month" or 3: "year, month, days"
1253        obox = Gtk.ComboBoxText()
1254        age_precision = [_("Years"),
1255                         _("Years, Months"),
1256                         _("Years, Months, Days")]
1257        list(map(obox.append_text, age_precision))
1258        # Combo_box active index is from 0 to 2, we need values from 1 to 3
1259        active = config.get('preferences.age-display-precision') - 1
1260        if active >= 0 and active <= 2:
1261            obox.set_active(active)
1262        else:
1263            obox.set_active(0)
1264        obox.connect(
1265            'changed',
1266            lambda obj: config.set('preferences.age-display-precision',
1267                                   obj.get_active() + 1))
1268        lwidget = BasicLabel(_("%s: ")
1269                             % _('Age display precision (requires restart)'))
1270        grid.attach(lwidget, 0, row, 1, 1)
1271        grid.attach(obox, 1, row, 2, 1)
1272
1273        row += 1
1274        # Calendar format on report:
1275        obox = Gtk.ComboBoxText()
1276        list(map(obox.append_text, Date.ui_calendar_names))
1277        active = config.get('preferences.calendar-format-report')
1278        if active >= len(formats):
1279            active = 0
1280        obox.set_active(active)
1281        obox.connect('changed', self.date_calendar_changed)
1282        lwidget = BasicLabel(_("%s: ") % _('Calendar on reports'))
1283        grid.attach(lwidget, 0, row, 1, 1)
1284        grid.attach(obox, 1, row, 2, 1)
1285
1286        row += 1
1287        # Surname guessing:
1288        obox = Gtk.ComboBoxText()
1289        formats = _surname_styles
1290        list(map(obox.append_text, formats))
1291        obox.set_active(config.get('behavior.surname-guessing'))
1292        obox.connect('changed',
1293                     lambda obj: config.set('behavior.surname-guessing',
1294                                            obj.get_active()))
1295        lwidget = BasicLabel(_("%s: ") % _('Surname guessing'))
1296        grid.attach(lwidget, 0, row, 1, 1)
1297        grid.attach(obox, 1, row, 2, 1)
1298
1299        row += 1
1300        # Default Family Relationship
1301        obox = Gtk.ComboBoxText()
1302        formats = FamilyRelType().get_standard_names()
1303        list(map(obox.append_text, formats))
1304        obox.set_active(config.get('preferences.family-relation-type'))
1305        obox.connect('changed',
1306                     lambda obj: config.set('preferences.family-relation-type',
1307                                            obj.get_active()))
1308        lwidget = BasicLabel(_("%s: ") % _('Default family relationship'))
1309        grid.attach(lwidget, 0, row, 1, 1)
1310        grid.attach(obox, 1, row, 2, 1)
1311
1312        row += 1
1313        # height multiple surname table
1314        self.add_pos_int_entry(
1315            grid, _('Height multiple surname box (pixels)'),
1316            row, 'interface.surname-box-height', self.update_surn_height,
1317            col_attach=0)
1318
1319        row += 1
1320        # Status bar:
1321        obox = Gtk.ComboBoxText()
1322        formats = [_("Active person's name and ID"),
1323                   _("Relationship to home person")]
1324        list(map(obox.append_text, formats))
1325        active = config.get('interface.statusbar')
1326        if active < 2:
1327            obox.set_active(0)
1328        else:
1329            obox.set_active(1)
1330        obox.connect('changed',
1331                     lambda obj: config.set('interface.statusbar',
1332                                            2 * obj.get_active()))
1333        lwidget = BasicLabel(_("%s: ") % _('Status bar'))
1334        grid.attach(lwidget, 0, row, 1, 1)
1335        grid.attach(obox, 1, row, 2, 1)
1336
1337        row += 1
1338        # Text in sidebar:
1339        self.add_checkbox(
1340            grid, _("Show text label beside Navigator buttons "
1341                    "(requires restart)"),
1342            row, 'interface.sidebar-text', start=0, stop=2,
1343            tooltip=_("Show or hide text beside Navigator buttons "
1344                      "(People, Families, Events...).\n"
1345                      "Requires Gramps restart to apply."))
1346        row += 1
1347        # Gramplet bar close buttons:
1348        self.add_checkbox(
1349            grid, _("Show close button in gramplet bar tabs"),
1350            row, 'interface.grampletbar-close', start=0, stop=2,
1351            extra_callback=self.cb_grampletbar_close,
1352            tooltip=_("Show close button to simplify removing gramplets "
1353                      "from bars."))
1354        return _('Display'), grid
1355
1356    def auto_title_changed(self, obj):
1357        """
1358        Update sensitivity of place format widget.
1359        """
1360        state = obj.get_active()
1361        self.pformat.set_sensitive(state)
1362        self.fmt_btn.set_sensitive(state)
1363
1364    def add_text_panel(self, configdialog):
1365        """
1366        Config tab for Text settings.
1367        """
1368        grid = self.create_grid()
1369
1370        self.add_text(
1371            grid, _('Default text used for conditions'), 0,
1372            line_wrap=True, start=0, stop=2, justify=Gtk.Justification.CENTER,
1373            align=Gtk.Align.CENTER, bold=True)
1374
1375        row = 1
1376        self.add_entry(grid, _('Missing surname'), row,
1377                       'preferences.no-surname-text')
1378        row += 1
1379        self.add_entry(grid, _('Missing given name'), row,
1380                       'preferences.no-given-text')
1381        row += 1
1382        self.add_entry(grid, _('Missing record'), row,
1383                       'preferences.no-record-text')
1384        row += 1
1385        self.add_entry(grid, _('Private surname'), row,
1386                       'preferences.private-surname-text',
1387                       localized_config=False)
1388        row += 1
1389        self.add_entry(grid, _('Private given name'), row,
1390                       'preferences.private-given-text',
1391                       localized_config=False)
1392        row += 1
1393        self.add_entry(grid, _('Private record'), row,
1394                       'preferences.private-record-text')
1395        row += 1
1396        return _('Text'), grid
1397
1398    def cb_name_dialog(self, obj):
1399        the_list = self.fmt_obox.get_model()
1400        the_iter = self.fmt_obox.get_active_iter()
1401        self.old_format = the_list.get_value(the_iter, COL_FMT)
1402        win = DisplayNameEditor(self.uistate, self.dbstate, self.track, self)
1403
1404    def color_scheme_changed(self, obj):
1405        """
1406        Called on swiching color scheme.
1407        """
1408        scheme = obj.get_active()
1409        config.set('colors.scheme', scheme)
1410        for key, widget in self.colors.items():
1411            color = Gdk.RGBA()
1412            hexval = config.get(key)[scheme]
1413            Gdk.RGBA.parse(color, hexval)
1414            widget.set_rgba(color)
1415
1416    def cb_place_fmt_dialog(self, button):
1417        """
1418        Called to invoke the place format editor.
1419        """
1420        EditPlaceFormat(self.uistate, self.dbstate, self.track,
1421                        self.cb_place_fmt_rebuild)
1422
1423    def cb_place_fmt_rebuild(self):
1424        """
1425        Called to rebuild the place format list.
1426        """
1427        model = Gtk.ListStore(str)
1428        for fmt in _pd.get_formats():
1429            model.append([fmt.name])
1430        self.pformat.set_model(model)
1431        self.pformat.set_active(0)
1432
1433    def check_for_type_changed(self, obj):
1434        active = obj.get_active()
1435        if active == 0:  # update
1436            config.set('behavior.check-for-addon-update-types', ["update"])
1437        elif active == 1:  # update
1438            config.set('behavior.check-for-addon-update-types', ["new"])
1439        elif active == 2:  # update
1440            config.set('behavior.check-for-addon-update-types',
1441                       ["update", "new"])
1442
1443    def toggle_tag_on_import(self, obj):
1444        """
1445        Update Entry sensitive for tag on import.
1446        """
1447        self.tag_format_entry.set_sensitive(obj.get_active())
1448
1449    def check_for_updates_changed(self, obj):
1450        """
1451        Save "Check for addon updates" option.
1452        """
1453        active = obj.get_active()
1454        config.set('behavior.check-for-addon-updates', active)
1455
1456    def date_format_changed(self, obj):
1457        """
1458        Save "Date format" option.
1459        And show notify message to restart Gramps.
1460        """
1461        config.set('preferences.date-format', obj.get_active())
1462        OkDialog(_('Change is not immediate'),
1463                 _('Changing the date format will not take '
1464                   'effect until the next time Gramps is started.'),
1465                 parent=self.window)
1466
1467    def date_calendar_changed(self, obj):
1468        """
1469        Save "Date calendar" option.
1470        """
1471        config.set('preferences.calendar-format-report', obj.get_active())
1472
1473    def autobackup_changed(self, obj):
1474        """
1475        Save "Autobackup" option on change.
1476        """
1477        active = obj.get_active()
1478        config.set('database.autobackup', active)
1479        self.uistate.set_backup_timer()
1480
1481    def add_date_panel(self, configdialog):
1482        """
1483        Config tab with Dates settings.
1484        """
1485        grid = self.create_grid()
1486
1487        self.add_text(
1488            grid, _('Dates settings used for calculation operations'), 0,
1489            line_wrap=True, start=1, stop=3, justify=Gtk.Justification.CENTER,
1490            align=Gtk.Align.CENTER, bold=True)
1491
1492        row = 1
1493        self.add_pos_int_entry(
1494            grid, _('Markup for invalid date format'),
1495            row, 'preferences.invalid-date-format',
1496            self.update_markup_entry,
1497            helptext=_(
1498                'Convenience markups are:\n'
1499                '<b>&lt;b&gt;Bold&lt;/b&gt;</b>\n'
1500                '<big>&lt;big&gt;'
1501                'Makes font relatively larger&lt;/big&gt;</big>\n'
1502                '<i>&lt;i&gt;Italic&lt;/i&gt;</i>\n'
1503                '<s>&lt;s&gt;Strikethrough&lt;/s&gt;</s>\n'
1504                '<sub>&lt;sub&gt;Subscript&lt;/sub&gt;</sub>\n'
1505                '<sup>&lt;sup&gt;Superscript&lt;/sup&gt;</sup>\n'
1506                '<small>&lt;small&gt;'
1507                'Makes font relatively smaller&lt;/small&gt;</small>\n'
1508                '<tt>&lt;tt&gt;Monospace font&lt;/tt&gt;</tt>\n'
1509                '<u>&lt;u&gt;Underline&lt;/u&gt;</u>\n\n'
1510                'For example: &lt;u&gt;&lt;b&gt;%s&lt;/b&gt;&lt;/u&gt;\n'
1511                'will display <u><b>Underlined bold date</b></u>.\n'))
1512        row += 1
1513        self.add_spinner(
1514            grid, _('Date about range'),
1515            row, 'behavior.date-about-range', (1, 9999))
1516        row += 1
1517        self.add_spinner(
1518            grid, _('Date after range'),
1519            row, 'behavior.date-after-range', (1, 9999))
1520        row += 1
1521        self.add_spinner(
1522            grid, _('Date before range'),
1523            row, 'behavior.date-before-range', (1, 9999))
1524        row += 1
1525        self.add_spinner(
1526            grid, _('Maximum age probably alive'),
1527            row, 'behavior.max-age-prob-alive', (80, 140))
1528        row += 1
1529        self.add_spinner(
1530            grid, _('Maximum sibling age difference'),
1531            row, 'behavior.max-sib-age-diff', (10, 30))
1532        row += 1
1533        self.add_spinner(
1534            grid, _('Minimum years between generations'),
1535            row, 'behavior.min-generation-years', (5, 20))
1536        row += 1
1537        self.add_spinner(
1538            grid, _('Average years between generations'),
1539            row, 'behavior.avg-generation-gap', (10, 30))
1540
1541        return _('Dates'), grid
1542
1543    def add_behavior_panel(self, configdialog):
1544        """
1545        Config tab with 'General' settings.
1546        """
1547        grid = self.create_grid()
1548
1549        self.add_text(grid, _("General Gramps settings"), 0,
1550                      line_wrap=True, start=1, stop=3, bold=True,
1551                      justify=Gtk.Justification.CENTER, align=Gtk.Align.CENTER)
1552        current_line = 1
1553
1554        self.add_checkbox(grid, _('Add default source on GEDCOM import'),
1555                          current_line, 'preferences.default-source', stop=3)
1556
1557        current_line += 1
1558        cb_const = 'preferences.tag-on-import'
1559        # tag Entry
1560        self.tag_format_entry = Gtk.Entry()
1561        tag_const = 'preferences.tag-on-import-format'
1562        tag_data = _(config.get(tag_const))
1563        if not tag_data:
1564            # set default value if Entry is empty
1565            tag_data = config.get_default(tag_const)
1566            config.set(cb_const, False)
1567            config.set(tag_const, tag_data)
1568        self.tag_format_entry.set_text(tag_data)
1569        self.tag_format_entry.connect('changed', self.update_entry, tag_const)
1570        self.tag_format_entry.set_hexpand(True)
1571        self.tag_format_entry.set_sensitive(config.get(cb_const))
1572
1573        self.add_checkbox(grid, _('Add tag on import'), current_line,
1574                          cb_const, stop=2,
1575                          extra_callback=self.toggle_tag_on_import,
1576                          tooltip=_("Specified tag will be added on import.\n"
1577                                    "Clear to set default value."))
1578        grid.attach(self.tag_format_entry, 2, current_line, 1, 1)
1579
1580        current_line += 1
1581        obj = self.add_checkbox(grid, _('Enable spelling checker'),
1582                                current_line, 'behavior.spellcheck', stop=3)
1583        if not HAVE_GTKSPELL:
1584            obj.set_sensitive(False)
1585            spell_dict = {'gramps_wiki_build_spell_url':
1586                          URL_WIKISTRING +
1587                          "GEPS_029:_GTK3-GObject_introspection"
1588                          "_Conversion#Spell_Check_Install"}
1589            obj.set_tooltip_text(
1590                _("GtkSpell not loaded. "
1591                  "Spell checking will not be available.\n"
1592                  "To build it for Gramps see "
1593                  "%(gramps_wiki_build_spell_url)s") % spell_dict)
1594
1595        current_line += 1
1596        self.add_checkbox(grid, _('Display Tip of the Day'),
1597                          current_line, 'behavior.use-tips', stop=3,
1598                          tooltip=_("Show useful information about using "
1599                                    "Gramps on startup."))
1600        current_line += 1
1601        self.add_checkbox(grid, _('Remember last view displayed'),
1602                          current_line, 'preferences.use-last-view', stop=3,
1603                          tooltip=_("Remember last view displayed "
1604                                    "and open it next time."))
1605        current_line += 1
1606        self.add_spinner(grid, _('Max generations for relationships'),
1607                         current_line, 'behavior.generation-depth', (5, 50),
1608                         self.update_gendepth)
1609        current_line += 1
1610        self.path_entry = Gtk.Entry()
1611        self.add_path_box(
1612            grid, _('Base path for relative media paths'),
1613            current_line, self.path_entry, self.dbstate.db.get_mediapath(),
1614            self.set_mediapath, self.select_mediapath)
1615
1616        current_line += 1
1617        label = self.add_text(
1618            grid, _("Third party addons management"), current_line,
1619            line_wrap=True, start=1, stop=3, bold=True,
1620            justify=Gtk.Justification.CENTER, align=Gtk.Align.CENTER)
1621        label.set_margin_top(10)
1622
1623        current_line += 1
1624        # Check for addon updates:
1625        obox = Gtk.ComboBoxText()
1626        formats = [_("Never"),
1627                   _("Once a month"),
1628                   _("Once a week"),
1629                   _("Once a day"),
1630                   _("Always"), ]
1631        list(map(obox.append_text, formats))
1632        active = config.get('behavior.check-for-addon-updates')
1633        obox.set_active(active)
1634        obox.connect('changed', self.check_for_updates_changed)
1635        lwidget = BasicLabel(_("%s: ") % _('Check for addon updates'))
1636        grid.attach(lwidget, 1, current_line, 1, 1)
1637        grid.attach(obox, 2, current_line, 1, 1)
1638
1639        current_line += 1
1640        self.whattype_box = Gtk.ComboBoxText()
1641        formats = [_("Updated addons only"),
1642                   _("New addons only"),
1643                   _("New and updated addons")]
1644        list(map(self.whattype_box.append_text, formats))
1645        whattype = config.get('behavior.check-for-addon-update-types')
1646        if "new" in whattype and "update" in whattype:
1647            self.whattype_box.set_active(2)
1648        elif "new" in whattype:
1649            self.whattype_box.set_active(1)
1650        elif "update" in whattype:
1651            self.whattype_box.set_active(0)
1652        self.whattype_box.connect('changed', self.check_for_type_changed)
1653        lwidget = BasicLabel(_("%s: ") % _('What to check'))
1654        grid.attach(lwidget, 1, current_line, 1, 1)
1655        grid.attach(self.whattype_box, 2, current_line, 1, 1)
1656
1657        current_line += 1
1658        self.add_entry(grid, _('Where to check'), current_line,
1659                       'behavior.addons-url', col_attach=1)
1660
1661        current_line += 1
1662        self.add_checkbox(
1663            grid, _('Do not ask about previously notified addons'),
1664            current_line, 'behavior.do-not-show-previously-seen-addon-updates',
1665            stop=3)
1666
1667        current_line += 1
1668        button = Gtk.Button(label=_("Check for updated addons now"))
1669        button.connect("clicked", self.check_for_updates)
1670        button.set_hexpand(False)
1671        button.set_halign(Gtk.Align.CENTER)
1672        grid.attach(button, 1, current_line, 3, 1)
1673
1674        return _('General'), grid
1675
1676    def check_for_updates(self, button):
1677        try:
1678            addon_update_list = available_updates()
1679        except:
1680            OkDialog(_("Checking Addons Failed"),
1681                     _("The addon repository appears to be unavailable. "
1682                       "Please try again later."),
1683                     parent=self.window)
1684            return
1685
1686        if len(addon_update_list) > 0:
1687            rescan = PluginWindows.UpdateAddons(self.uistate, self.track,
1688                                                addon_update_list).rescan
1689            self.uistate.viewmanager.do_reg_plugins(self.dbstate, self.uistate,
1690                                                    rescan=rescan)
1691        else:
1692            check_types = config.get('behavior.check-for-addon-update-types')
1693            OkDialog(
1694                _("There are no available addons of this type"),
1695                _("Checked for '%s'") %
1696                _("' and '").join([_(t) for t in check_types]),
1697                parent=self.window)
1698
1699        # List of translated strings used here
1700        # Dead code for l10n
1701        _('new'), _('update')
1702
1703    def database_backend_changed(self, obj):
1704        """
1705        Update Database Backend.
1706        """
1707        the_list = obj.get_model()
1708        the_iter = obj.get_active_iter()
1709        db_choice = the_list.get_value(the_iter, 2)
1710        config.set('database.backend', db_choice)
1711        self.set_connection_widgets(db_choice)
1712
1713    def set_connection_widgets(self, db_choice):
1714        """
1715        Sets the connection widgets insensitive for embedded databases.
1716        """
1717        for widget in self.connection_widgets:
1718            if db_choice in ('bsddb', 'sqlite'):
1719                widget.set_sensitive(False)
1720            else:
1721                widget.set_sensitive(True)
1722
1723    def add_famtree_panel(self, configdialog):
1724        """
1725        Config tab for family tree and backup settings.
1726        """
1727        grid = self.create_grid()
1728
1729        self.add_text(
1730            grid, _("Family tree database settings and Backup "
1731                    "management"), 0, line_wrap=True, start=1, stop=3,
1732            justify=Gtk.Justification.CENTER, align=Gtk.Align.CENTER,
1733            bold=True)
1734
1735        current_line = 1
1736        lwidget = BasicLabel(_("%s: ") % _('Database backend'))
1737        grid.attach(lwidget, 1, current_line, 1, 1)
1738        obox = self.__create_backend_combo()
1739        grid.attach(obox, 2, current_line, 1, 1)
1740
1741        current_line += 1
1742        self.connection_widgets = []
1743        entry = self.add_entry(grid, _('Host'), current_line,
1744                               'database.host', col_attach=1)
1745        self.connection_widgets.append(entry)
1746
1747        current_line += 1
1748        entry = self.add_entry(grid, _('Port'), current_line,
1749                               'database.port', col_attach=1)
1750        self.connection_widgets.append(entry)
1751
1752        current_line += 1
1753        self.set_connection_widgets(config.get('database.backend'))
1754
1755        self.dbpath_entry = Gtk.Entry()
1756        self.add_path_box(grid, _('Family Tree Database path'), current_line,
1757                          self.dbpath_entry, config.get('database.path'),
1758                          self.set_dbpath, self.select_dbpath)
1759        current_line += 1
1760        self.add_checkbox(grid, _('Automatically load last Family Tree'),
1761                          current_line, 'behavior.autoload', stop=3,
1762                          tooltip=_("Don't open dialog to choose family "
1763                                    "tree to load on startup, "
1764                                    "just load last used."))
1765        current_line += 1
1766        self.backup_path_entry = Gtk.Entry()
1767        self.add_path_box(grid, _('Backup path'),
1768                          current_line, self.backup_path_entry,
1769                          config.get('database.backup-path'),
1770                          self.set_backup_path, self.select_backup_path)
1771        current_line += 1
1772        self.add_checkbox(grid, _('Backup on exit'), current_line,
1773                          'database.backup-on-exit', stop=3,
1774                          tooltip=_("Backup Your family tree on exit "
1775                                    "to Backup path specified above."))
1776        current_line += 1
1777        # Check for updates:
1778        obox = Gtk.ComboBoxText()
1779        formats = [_("Never"),
1780                   _("Every 15 minutes"),
1781                   _("Every 30 minutes"),
1782                   _("Every hour")]
1783        list(map(obox.append_text, formats))
1784        active = config.get('database.autobackup')
1785        obox.set_active(active)
1786        obox.connect('changed', self.autobackup_changed)
1787        lwidget = BasicLabel(_("%s: ") % _('Autobackup'))
1788        grid.attach(lwidget, 1, current_line, 1, 1)
1789        grid.attach(obox, 2, current_line, 1, 1)
1790
1791        return _('Family Tree'), grid
1792
1793    def __create_backend_combo(self):
1794        """
1795        Create backend selection widget.
1796        """
1797        backend_plugins = self.uistate.viewmanager._pmgr.get_reg_databases()
1798        obox = Gtk.ComboBox()
1799        cell = Gtk.CellRendererText()
1800        obox.pack_start(cell, True)
1801        obox.add_attribute(cell, 'text', 1)
1802        # Build model:
1803        model = Gtk.ListStore(GObject.TYPE_INT,
1804                              GObject.TYPE_STRING,
1805                              GObject.TYPE_STRING)
1806        count = 0
1807        active = 0
1808        default = config.get('database.backend')
1809        for plugin in sorted(backend_plugins, key=lambda plugin: plugin.name):
1810            if plugin.id == default:
1811                active = count
1812            model.append(row=[count, plugin.name, plugin.id])
1813            count += 1
1814        obox.set_model(model)
1815        # set the default value as active in the combo
1816        obox.set_active(active)
1817        obox.connect('changed', self.database_backend_changed)
1818        return obox
1819
1820    def set_mediapath(self, *obj):
1821        if self.path_entry.get_text().strip():
1822            self.dbstate.db.set_mediapath(self.path_entry.get_text())
1823        else:
1824            self.dbstate.db.set_mediapath(None)
1825
1826    def select_mediapath(self, *obj):
1827        """
1828        Show dialog to choose media directory.
1829        """
1830        f = Gtk.FileChooserDialog(title=_("Select media directory"),
1831                                  parent=self.window,
1832                                  action=Gtk.FileChooserAction.SELECT_FOLDER,
1833                                  buttons=(_('_Cancel'),
1834                                           Gtk.ResponseType.CANCEL,
1835                                           _('_Apply'),
1836                                           Gtk.ResponseType.OK))
1837        mpath = media_path(self.dbstate.db)
1838        f.set_current_folder(os.path.dirname(mpath))
1839
1840        status = f.run()
1841        if status == Gtk.ResponseType.OK:
1842            val = f.get_filename()
1843            if val:
1844                self.path_entry.set_text(val)
1845        f.destroy()
1846
1847    def set_dbpath(self, *obj):
1848        path = self.dbpath_entry.get_text().strip()
1849        config.set('database.path', path)
1850
1851    def select_dbpath(self, *obj):
1852        """
1853        Show dialog to choose database directory.
1854        """
1855        f = Gtk.FileChooserDialog(title=_("Select database directory"),
1856                                  transient_for=self.window,
1857                                  action=Gtk.FileChooserAction.SELECT_FOLDER)
1858        f.add_buttons(_('_Cancel'), Gtk.ResponseType.CANCEL,
1859                      _('_Apply'), Gtk.ResponseType.OK)
1860        dbpath = config.get('database.path')
1861        if not dbpath:
1862            dbpath = os.path.join(HOME_DIR, 'grampsdb')
1863        f.set_current_folder(os.path.dirname(dbpath))
1864
1865        status = f.run()
1866        if status == Gtk.ResponseType.OK:
1867            val = f.get_filename()
1868            if val:
1869                self.dbpath_entry.set_text(val)
1870        f.destroy()
1871
1872    def set_backup_path(self, *obj):
1873        path = self.backup_path_entry.get_text().strip()
1874        config.set('database.backup-path', path)
1875
1876    def select_backup_path(self, *obj):
1877        """
1878        Show dialog to choose backup directory.
1879        """
1880        f = Gtk.FileChooserDialog(title=_("Select backup directory"),
1881                                  parent=self.window,
1882                                  action=Gtk.FileChooserAction.SELECT_FOLDER,
1883                                  buttons=(_('_Cancel'),
1884                                           Gtk.ResponseType.CANCEL,
1885                                           _('_Apply'),
1886                                           Gtk.ResponseType.OK))
1887        backup_path = config.get('database.backup-path')
1888        if not backup_path:
1889            backup_path = config.get('database.path')
1890        f.set_current_folder(os.path.dirname(backup_path))
1891
1892        status = f.run()
1893        if status == Gtk.ResponseType.OK:
1894            val = f.get_filename()
1895            if val:
1896                self.backup_path_entry.set_text(val)
1897        f.destroy()
1898
1899    def update_idformat_entry(self, obj, constant):
1900        config.set(constant, obj.get_text())
1901        self.dbstate.db.set_prefixes(
1902            config.get('preferences.iprefix'),
1903            config.get('preferences.oprefix'),
1904            config.get('preferences.fprefix'),
1905            config.get('preferences.sprefix'),
1906            config.get('preferences.cprefix'),
1907            config.get('preferences.pprefix'),
1908            config.get('preferences.eprefix'),
1909            config.get('preferences.rprefix'),
1910            config.get('preferences.nprefix'))
1911
1912    def update_gendepth(self, obj, constant):
1913        """
1914        Called when the generation depth setting is changed.
1915        """
1916        intval = int(obj.get_value())
1917        config.set(constant, intval)
1918        # immediately use this value in displaystate.
1919        self.uistate.set_gendepth(intval)
1920
1921    def update_surn_height(self, obj, constant):
1922        ok = True
1923        if not obj.get_text():
1924            return
1925        try:
1926            intval = int(obj.get_text())
1927        except:
1928            intval = config.get(constant)
1929            ok = False
1930        if intval < 0:
1931            intval = config.get(constant)
1932            ok = False
1933        if ok:
1934            config.set(constant, intval)
1935        else:
1936            obj.set_text(str(intval))
1937
1938    def build_menu_names(self, obj):
1939        return (_('Preferences'), _('Preferences'))
1940
1941    def add_symbols_panel(self, configdialog):
1942        self.grid = Gtk.Grid()
1943        self.grid.set_border_width(12)
1944        self.grid.set_column_spacing(6)
1945        self.grid.set_row_spacing(6)
1946
1947        message = _('This tab gives you the possibility to use one font'
1948                    ' which is able to show all genealogical symbols\n\n'
1949                    'If you select the "use symbols" checkbox, '
1950                    'Gramps will use the selected font if it exists.'
1951                   )
1952        message += '\n'
1953        message += _('This can be useful if you want to add phonetic in '
1954                    'a note to show how to pronounce a name or if you mix'
1955                    ' multiple languages like greek and russian.'
1956                   )
1957        self.add_text(self.grid, message,
1958                0, line_wrap=True)
1959        self.add_checkbox(self.grid,
1960                _('Use symbols'),
1961                1,
1962                'utf8.in-use',
1963                extra_callback=self.activate_change_font
1964                )
1965        message = _('Be careful, if you click on the "Try to find" button, it can '
1966                    'take a while before you can continue (10 minutes or more). '
1967                    '\nIf you cancel the process, nothing will be changed.'
1968                   )
1969        self.add_text(self.grid, message,
1970                2, line_wrap=True)
1971        available_fonts = config.get('utf8.available-fonts')
1972        self.all_avail_fonts = list(enumerate(available_fonts))
1973        if len(available_fonts) > 0:
1974            self.add_text(self.grid,
1975                _('You have already run the tool to search for genealogy fonts.'
1976                  '\nRun it again only if you added fonts on your system.'
1977                 ),
1978                3, line_wrap=True)
1979        self.add_button(self.grid,
1980                _('Try to find'),
1981                4,
1982                'utf8.in-use',
1983                extra_callback=self.can_we_use_genealogical_fonts)
1984        sel_font = config.get('utf8.selected-font')
1985        if len(available_fonts) > 0:
1986            try:
1987                active_val = available_fonts.index(sel_font)
1988            except:
1989                active_val = 0
1990            self.add_combo(self.grid,
1991                _('Choose font'),
1992                5, 'utf8.selected-font',
1993                self.all_avail_fonts,
1994                callback=self.utf8_update_font,
1995                valueactive=True, setactive=active_val)
1996            symbols = Symbols()
1997            all_sbls = symbols.get_death_symbols()
1998            all_symbols = []
1999            for symbol in all_sbls:
2000                all_symbols.append(symbol[1] + " " + symbol[0])
2001            self.all_death_symbols = list(enumerate(all_symbols))
2002            pos = config.get('utf8.death-symbol')
2003            combo = self.add_combo(self.grid,
2004                _('Select default death symbol'),
2005                6, 'utf8.death-symbol',
2006                self.all_death_symbols,
2007                callback=self.utf8_update_death_symbol,
2008                valueactive=True, setactive='')
2009            combo.set_active(pos)
2010            if config.get('utf8.selected-font') != "":
2011                self.utf8_show_example()
2012
2013        return _('Genealogical Symbols'), self.grid
2014
2015    def can_we_use_genealogical_fonts(self, obj):
2016        try:
2017            import fontconfig
2018            from gramps.gui.utils import ProgressMeter
2019            from collections import defaultdict
2020        except:
2021            from gramps.gui.dialog import WarningDialog
2022            WarningDialog(_("Cannot look for genealogical fonts"),
2023                          _("I am not able to select genealogical fonts. "
2024                            "Please, install the module fontconfig for python 3."),
2025                          parent=self.uistate.window)
2026            return False
2027        try:
2028            # remove the old messages with old font
2029            self.grid.remove_row(8)
2030            self.grid.remove_row(7)
2031            self.grid.remove_row(6)
2032            self.grid.remove_row(5)
2033        except:
2034            pass
2035        fonts = fontconfig.query()
2036        all_fonts = defaultdict(set)
2037        symbols = Symbols()
2038        nb_symbols = symbols.get_how_many_symbols()
2039        self.in_progress = True
2040        self.progress = ProgressMeter(_('Checking available genealogical fonts'),
2041                                       can_cancel=True,
2042                                       cancel_callback=self.stop_looking_for_font,
2043                                       parent=self.uistate.window)
2044        self.progress.set_pass(_('Looking for all fonts with genealogical symbols.'), nb_symbols*len(fonts))
2045        for path in fonts:
2046            if not self.in_progress:
2047                return # We clicked on Cancel
2048            font = fontconfig.FcFont(path)
2049            local = get_env_var('LANGUAGE', 'en')
2050            if isinstance(font.family, list):
2051                fontname = None
2052                for lang,fam in font.family:
2053                    if lang == local:
2054                        fontname = fam
2055                if not fontname:
2056                    fontname = font.family[0][1]
2057            else:
2058                if local in font.family:
2059                    fontname = font.family[local]
2060                else:
2061                    for lang, name in font.family:     # version 0.6.0 use dict
2062                        fontname = name
2063                        break
2064            for rand in range(symbols.SYMBOL_MALE, symbols.SYMBOL_EXTINCT+1):
2065                string = symbols.get_symbol_for_html(rand)
2066                value = symbols.get_symbol_for_string(rand)
2067                if font.has_char(value):
2068                    all_fonts[fontname].add(value)
2069                self.progress.step()
2070            for rand in range(symbols.DEATH_SYMBOL_SKULL,
2071                              symbols.DEATH_SYMBOL_DEAD):
2072                value = symbols.get_death_symbol_for_char(rand)
2073                if font.has_char(value):
2074                    all_fonts[fontname].add(value)
2075                self.progress.step()
2076        self.progress.close()
2077        available_fonts = []
2078        for font, font_usage in all_fonts.items():
2079            if not font_usage:
2080               continue
2081            if len(font_usage) == nb_symbols: # If the font use all symbols
2082                available_fonts.append(font)
2083        config.set('utf8.available-fonts', available_fonts)
2084        sel_font = config.get('utf8.selected-font')
2085        try:
2086            active_val = available_fonts.index(sel_font)
2087        except:
2088            active_val = 0
2089        if len(available_fonts) > 0:
2090            self.all_avail_fonts = list(enumerate(available_fonts))
2091            choosefont = self.add_combo(self.grid,
2092                _('Choose font'),
2093                5, 'utf8.selected-font',
2094                self.all_avail_fonts, callback=self.utf8_update_font,
2095                valueactive=True, setactive=active_val)
2096            if len(available_fonts) == 1:
2097                single_font = self.all_avail_fonts[choosefont.get_active()][0]
2098                config.set('utf8.selected-font',
2099                           self.all_avail_fonts[single_font][1])
2100                self.utf8_show_example()
2101            symbols = Symbols()
2102            all_sbls = symbols.get_death_symbols()
2103            all_symbols = []
2104            for symbol in all_sbls:
2105                all_symbols.append(symbol[1] + " " + symbol[0])
2106            self.all_death_symbols = list(enumerate(all_symbols))
2107            pos = config.get('utf8.death-symbol')
2108            combo = self.add_combo(self.grid,
2109                _('Select default death symbol'),
2110                6, 'utf8.death-symbol',
2111                self.all_death_symbols,
2112                callback=self.utf8_update_death_symbol,
2113                valueactive=True, setactive='')
2114            combo.set_active(pos)
2115        else:
2116            self.add_text(self.grid,
2117                _('You have no font with genealogical symbols on your '
2118                  'system. Gramps will not be able to use symbols.'
2119                 ),
2120                6, line_wrap=True)
2121            config.set('utf8.selected-font',"")
2122        self.grid.show_all()
2123        self.in_progress = False
2124
2125    def utf8_update_font(self, obj, constant):
2126        entry = obj.get_active()
2127        config.set(constant, self.all_avail_fonts[entry][1])
2128        self.utf8_show_example()
2129
2130    def activate_change_font(self, obj=None):
2131        if obj:
2132            if not obj.get_active():
2133                # reset to the system default
2134                self.uistate.viewmanager.reset_font()
2135        font = config.get('utf8.selected-font')
2136        if not self.uistate.viewmanager.change_font(font):
2137            # We can't change the font, so reset the checkbox.
2138            if obj:
2139                obj.set_active(False)
2140        self.uistate.reload_symbols()
2141        self.uistate.emit('font-changed')
2142
2143    def utf8_show_example(self):
2144        from gi.repository import Pango
2145        from gramps.gen.utils.grampslocale import _LOCALE_NAMES as X
2146        from string import ascii_letters
2147        try:
2148            # remove the old messages with old font
2149            self.grid.remove_row(8)
2150            self.grid.remove_row(7)
2151        except:
2152            pass
2153        font = config.get('utf8.selected-font')
2154        symbols = Symbols()
2155        my_characters = _("What you will see") + " :\n"
2156        my_characters += ascii_letters
2157        my_characters += " àäâçùéèiïîêëiÉÀÈïÏËÄœŒÅåØøìòô ...\n"
2158        for k,v in sorted(X.items()):
2159            lang = Pango.Language.from_string(k)
2160            my_characters += v[2] + ":\t" + lang.get_sample_string() + "\n"
2161
2162        scrollw = Gtk.ScrolledWindow()
2163        scrollw.set_size_request(600, 100)
2164        text = Gtk.Label()
2165        text.set_line_wrap(True)
2166        font_description = Pango.font_description_from_string(font)
2167        text.modify_font(font_description)
2168        self.activate_change_font()
2169        text.set_halign(Gtk.Align.START)
2170        text.set_text(my_characters)
2171        scrollw.add(text)
2172        scrollw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
2173        self.grid.attach(scrollw, 1, 7, 8, 1)
2174
2175        my_characters = ""
2176        for idx in range(symbols.SYMBOL_FEMALE, symbols.SYMBOL_EXTINCT+1):
2177            my_characters += symbols.get_symbol_for_string(idx) + " "
2178
2179        death_symbl = config.get('utf8.death-symbol')
2180        my_characters += symbols.get_death_symbol_for_char(death_symbl)
2181        text = Gtk.Label()
2182        text.set_line_wrap(True)
2183        font_description = Pango.font_description_from_string(font)
2184        text.modify_font(font_description)
2185        text.set_halign(Gtk.Align.START)
2186        text.set_markup("<big><big><big><big>" +
2187                        my_characters +
2188                        "</big></big></big></big>")
2189        self.grid.attach(text, 1, 8, 8, 1)
2190        scrollw.show_all()
2191        text.show_all()
2192
2193    def stop_looking_for_font(self, *args, **kwargs):
2194        self.progress.close()
2195        self.in_progress = False
2196
2197    def utf8_update_death_symbol(self, obj, constant):
2198        entry = obj.get_active()
2199        config.set(constant, entry)
2200        self.utf8_show_example()
2201
2202