1# Gramps - a GTK+/GNOME based genealogy program
2#
3# Copyright (C) 2001-2007  Donald N. Allingham
4# Copyright (C) 2009-2010  Gary Burton
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19#
20
21"""
22Relationship View
23"""
24
25#-------------------------------------------------------------------------
26#
27# Python modules
28#
29#-------------------------------------------------------------------------
30from html import escape
31import pickle
32
33#-------------------------------------------------------------------------
34#
35# Set up logging
36#
37#-------------------------------------------------------------------------
38import logging
39_LOG = logging.getLogger("plugin.relview")
40
41#-------------------------------------------------------------------------
42#
43# GTK/Gnome modules
44#
45#-------------------------------------------------------------------------
46from gi.repository import Gdk
47from gi.repository import Gtk
48from gi.repository import Pango
49
50#-------------------------------------------------------------------------
51#
52# Gramps Modules
53#
54#-------------------------------------------------------------------------
55from gramps.gen.const import GRAMPS_LOCALE as glocale
56_ = glocale.translation.sgettext
57ngettext = glocale.translation.ngettext # else "nearby" comments are ignored
58from gramps.gen.lib import (ChildRef, EventRoleType, EventType, Family,
59                            FamilyRelType, Name, Person, Surname)
60from gramps.gen.lib.date import Today
61from gramps.gen.db import DbTxn
62from gramps.gui.views.navigationview import NavigationView
63from gramps.gui.uimanager import ActionGroup
64from gramps.gui.editors import EditPerson, EditFamily
65from gramps.gui.editors import FilterEditor
66from gramps.gen.display.name import displayer as name_displayer
67from gramps.gen.display.place import displayer as place_displayer
68from gramps.gen.utils.file import media_path_full
69from gramps.gen.utils.alive import probably_alive
70from gramps.gui.utils import open_file_with_default_application
71from gramps.gen.datehandler import displayer, get_date
72from gramps.gen.utils.thumbnails import get_thumbnail_image
73from gramps.gen.config import config
74from gramps.gui import widgets
75from gramps.gui.widgets.reorderfam import Reorder
76from gramps.gui.selectors import SelectorFactory
77from gramps.gen.errors import WindowActiveError
78from gramps.gui.views.bookmarks import PersonBookmarks
79from gramps.gen.const import CUSTOM_FILTERS
80from gramps.gen.utils.db import (get_birth_or_fallback, get_death_or_fallback,
81                          preset_name)
82from gramps.gui.ddtargets import DdTargets
83from gramps.gen.utils.symbols import Symbols
84
85_NAME_START = 0
86_LABEL_START = 0
87_LABEL_STOP = 1
88_DATA_START = _LABEL_STOP
89_DATA_STOP = _DATA_START+1
90_BTN_START = _DATA_STOP
91_BTN_STOP = _BTN_START+2
92_PLABEL_START = 1
93_PLABEL_STOP = _PLABEL_START+1
94_PDATA_START = _PLABEL_STOP
95_PDATA_STOP = _PDATA_START+2
96_PDTLS_START = _PLABEL_STOP
97_PDTLS_STOP = _PDTLS_START+2
98_CLABEL_START = _PLABEL_START+1
99_CLABEL_STOP = _CLABEL_START+1
100_CDATA_START = _CLABEL_STOP
101_CDATA_STOP = _CDATA_START+1
102_CDTLS_START = _CDATA_START
103_CDTLS_STOP = _CDTLS_START+1
104_ALABEL_START = 0
105_ALABEL_STOP = _ALABEL_START+1
106_ADATA_START = _ALABEL_STOP
107_ADATA_STOP = _ADATA_START+3
108_SDATA_START = 2
109_SDATA_STOP = 4
110_RETURN = Gdk.keyval_from_name("Return")
111_KP_ENTER = Gdk.keyval_from_name("KP_Enter")
112_SPACE = Gdk.keyval_from_name("space")
113_LEFT_BUTTON = 1
114_RIGHT_BUTTON = 3
115
116class RelationshipView(NavigationView):
117    """
118    View showing a textual representation of the relationships of the
119    active person
120    """
121    #settings in the config file
122    CONFIGSETTINGS = (
123        ('preferences.family-siblings', True),
124        ('preferences.family-details', True),
125        ('preferences.relation-display-theme', "CLASSIC"),
126        ('preferences.relation-shade', True),
127        ('preferences.releditbtn', True),
128        )
129
130    def __init__(self, pdata, dbstate, uistate, nav_group=0):
131        NavigationView.__init__(self, _('Relationships'),
132                                      pdata, dbstate, uistate,
133                                      PersonBookmarks,
134                                      nav_group)
135
136        dbstate.connect('database-changed', self.change_db)
137        uistate.connect('nameformat-changed', self.build_tree)
138        uistate.connect('placeformat-changed', self.build_tree)
139        uistate.connect('font-changed', self.font_changed)
140        self.redrawing = False
141
142        self.child = None
143        self.old_handle = None
144
145        self.reorder_sensitive = False
146        self.collapsed_items = {}
147
148        self.additional_uis.append(self.additional_ui)
149
150        self.show_siblings = self._config.get('preferences.family-siblings')
151        self.show_details = self._config.get('preferences.family-details')
152        self.use_shade = self._config.get('preferences.relation-shade')
153        self.theme = self._config.get('preferences.relation-display-theme')
154        self.toolbar_visible = config.get('interface.toolbar-on')
155        self.age_precision = config.get('preferences.age-display-precision')
156        self.symbols = Symbols()
157        self.reload_symbols()
158
159    def get_handle_from_gramps_id(self, gid):
160        """
161        returns the handle of the specified object
162        """
163        obj = self.dbstate.db.get_person_from_gramps_id(gid)
164        if obj:
165            return obj.get_handle()
166        else:
167            return None
168
169    def _connect_db_signals(self):
170        """
171        implement from base class DbGUIElement
172        Register the callbacks we need.
173        """
174        # Add a signal to pick up event changes, bug #1416
175        self.callman.add_db_signal('event-update', self.family_update)
176
177        self.callman.add_db_signal('person-update', self.person_update)
178        self.callman.add_db_signal('person-rebuild', self.person_rebuild)
179        self.callman.add_db_signal('family-update', self.family_update)
180        self.callman.add_db_signal('family-add',    self.family_add)
181        self.callman.add_db_signal('family-delete', self.family_delete)
182        self.callman.add_db_signal('family-rebuild', self.family_rebuild)
183
184        self.callman.add_db_signal('person-delete', self.redraw)
185
186    def reload_symbols(self):
187        if self.uistate and self.uistate.symbols:
188            gsfs = self.symbols.get_symbol_for_string
189            self.male = gsfs(self.symbols.SYMBOL_MALE)
190            self.female = gsfs(self.symbols.SYMBOL_FEMALE)
191            self.bth = gsfs(self.symbols.SYMBOL_BIRTH)
192            self.bptsm = gsfs(self.symbols.SYMBOL_BAPTISM)
193            self.marriage = gsfs(self.symbols.SYMBOL_MARRIAGE)
194            self.marr = gsfs(self.symbols.SYMBOL_HETEROSEXUAL)
195            self.homom = gsfs(self.symbols.SYMBOL_MALE_HOMOSEXUAL)
196            self.homof = gsfs(self.symbols.SYMBOL_LESBIAN)
197            self.divorce = gsfs(self.symbols.SYMBOL_DIVORCE)
198            self.unmarr = gsfs(self.symbols.SYMBOL_UNMARRIED_PARTNERSHIP)
199            death_idx = self.uistate.death_symbol
200            self.dth = self.symbols.get_death_symbol_for_char(death_idx)
201            self.burial = gsfs(self.symbols.SYMBOL_BURIED)
202            self.cremation = gsfs(self.symbols.SYMBOL_CREMATED)
203        else:
204            gsf = self.symbols.get_symbol_fallback
205            self.male = gsf(self.symbols.SYMBOL_MALE)
206            self.female = gsf(self.symbols.SYMBOL_FEMALE)
207            self.bth = gsf(self.symbols.SYMBOL_BIRTH)
208            self.bptsm = gsf(self.symbols.SYMBOL_BAPTISM)
209            self.marriage = gsf(self.symbols.SYMBOL_MARRIAGE)
210            self.marr = gsf(self.symbols.SYMBOL_HETEROSEXUAL)
211            self.homom = gsf(self.symbols.SYMBOL_MALE_HOMOSEXUAL)
212            self.homof = gsf(self.symbols.SYMBOL_LESBIAN)
213            self.divorce = gsf(self.symbols.SYMBOL_DIVORCE)
214            self.unmarr = gsf(self.symbols.SYMBOL_UNMARRIED_PARTNERSHIP)
215            death_idx = self.symbols.DEATH_SYMBOL_LATIN_CROSS
216            self.dth = self.symbols.get_death_symbol_fallback(death_idx)
217            self.burial = gsf(self.symbols.SYMBOL_BURIED)
218            self.cremation = gsf(self.symbols.SYMBOL_CREMATED)
219
220    def font_changed(self):
221        self.reload_symbols()
222        self.build_tree()
223
224    def navigation_type(self):
225        return 'Person'
226
227    def can_configure(self):
228        """
229        See :class:`~gui.views.pageview.PageView
230        :return: bool
231        """
232        return True
233
234    def goto_handle(self, handle):
235        self.change_person(handle)
236
237    def shade_update(self, client, cnxn_id, entry, data):
238        self.use_shade = self._config.get('preferences.relation-shade')
239        self.toolbar_visible = config.get('interface.toolbar-on')
240        self.uistate.modify_statusbar(self.dbstate)
241        self.redraw()
242
243    def config_update(self, client, cnxn_id, entry, data):
244        self.show_siblings = self._config.get('preferences.family-siblings')
245        self.show_details = self._config.get('preferences.family-details')
246        self.redraw()
247
248    def build_tree(self):
249        self.redraw()
250
251    def person_update(self, handle_list):
252        if self.active:
253            person = self.get_active()
254            if person:
255                while not self.change_person(person):
256                    pass
257            else:
258                self.change_person(None)
259        else:
260            self.dirty = True
261
262    def person_rebuild(self):
263        """Large change to person database"""
264        if self.active:
265            self.bookmarks.redraw()
266            person = self.get_active()
267            if person:
268                while not self.change_person(person):
269                    pass
270            else:
271                self.change_person(None)
272        else:
273            self.dirty = True
274
275    def family_update(self, handle_list):
276        if self.active:
277            person = self.get_active()
278            if person:
279                while not self.change_person(person):
280                    pass
281            else:
282                self.change_person(None)
283        else:
284            self.dirty = True
285
286    def family_add(self, handle_list):
287        if self.active:
288            person = self.get_active()
289            if person:
290                while not self.change_person(person):
291                    pass
292            else:
293                self.change_person(None)
294        else:
295            self.dirty = True
296
297    def family_delete(self, handle_list):
298        if self.active:
299            person = self.get_active()
300            if person:
301                while not self.change_person(person):
302                    pass
303            else:
304                self.change_person(None)
305        else:
306            self.dirty = True
307
308    def family_rebuild(self):
309        if self.active:
310            person = self.get_active()
311            if person:
312                while not self.change_person(person):
313                    pass
314            else:
315                self.change_person(None)
316        else:
317            self.dirty = True
318
319    def change_page(self):
320        NavigationView.change_page(self)
321        self.uistate.clear_filter_results()
322
323    def get_stock(self):
324        """
325        Return the name of the stock icon to use for the display.
326        This assumes that this icon has already been registered with
327        GNOME as a stock icon.
328        """
329        return 'gramps-relation'
330
331    def get_viewtype_stock(self):
332        """Type of view in category
333        """
334        return 'gramps-relation'
335
336    def build_widget(self):
337        """
338        Build the widget that contains the view, see
339        :class:`~gui.views.pageview.PageView
340        """
341        container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
342        container.set_border_width(12)
343
344        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
345        self.vbox.show()
346
347        self.header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
348        self.header.show()
349
350        self.child = None
351
352        self.scroll = Gtk.ScrolledWindow()
353        self.scroll.set_policy(Gtk.PolicyType.AUTOMATIC,
354                               Gtk.PolicyType.AUTOMATIC)
355        self.scroll.show()
356
357        vp = Gtk.Viewport()
358        vp.set_shadow_type(Gtk.ShadowType.NONE)
359        vp.add(self.vbox)
360
361        self.scroll.add(vp)
362        self.scroll.show_all()
363
364        container.set_spacing(6)
365        container.pack_start(self.header, False, False, 0)
366        container.pack_start(Gtk.Separator(), False, False, 0)
367        container.pack_start(self.scroll, True, True, 0)
368        container.show_all()
369        return container
370
371    additional_ui = [  # Defines the UI string for UIManager
372        '''
373      <placeholder id="CommonGo">
374      <section>
375        <item>
376          <attribute name="action">win.Back</attribute>
377          <attribute name="label" translatable="yes">_Add Bookmark</attribute>
378        </item>
379        <item>
380          <attribute name="action">win.Forward</attribute>
381          <attribute name="label" translatable="yes">'''
382        '''Organize Bookmarks...</attribute>
383        </item>
384      </section>
385      <section>
386        <item>
387          <attribute name="action">win.HomePerson</attribute>
388          <attribute name="label" translatable="yes">_Home</attribute>
389        </item>
390      </section>
391      </placeholder>
392''',
393        '''
394      <placeholder id='otheredit'>
395        <item>
396          <attribute name="action">win.Edit</attribute>
397          <attribute name="label" translatable="yes">Edit...</attribute>
398        </item>
399        <item>
400          <attribute name="action">win.AddParents</attribute>
401          <attribute name="label" translatable="yes">'''
402        '''Add New Parents...</attribute>
403        </item>
404        <item>
405          <attribute name="action">win.ShareFamily</attribute>
406          <attribute name="label" translatable="yes">'''
407        '''Add Existing Parents...</attribute>
408        </item>
409        <item>
410          <attribute name="action">win.AddSpouse</attribute>
411          <attribute name="label" translatable="yes">Add Partner...</attribute>
412        </item>
413        <item>
414          <attribute name="action">win.ChangeOrder</attribute>
415          <attribute name="label" translatable="yes">_Reorder</attribute>
416        </item>
417        <item>
418          <attribute name="action">win.FilterEdit</attribute>
419          <attribute name="label" translatable="yes">'''
420        '''Person Filter Editor</attribute>
421        </item>
422      </placeholder>
423''',
424        '''
425      <section id="AddEditBook">
426        <item>
427          <attribute name="action">win.AddBook</attribute>
428          <attribute name="label" translatable="yes">_Add Bookmark</attribute>
429        </item>
430        <item>
431          <attribute name="action">win.EditBook</attribute>
432          <attribute name="label" translatable="no">%s...</attribute>
433        </item>
434      </section>
435''' % _('Organize Bookmarks'),  # Following are the Toolbar items
436        '''
437    <placeholder id='CommonNavigation'>
438    <child groups='RO'>
439      <object class="GtkToolButton">
440        <property name="icon-name">go-previous</property>
441        <property name="action-name">win.Back</property>
442        <property name="tooltip_text" translatable="yes">'''
443        '''Go to the previous object in the history</property>
444        <property name="label" translatable="yes">_Back</property>
445        <property name="use-underline">True</property>
446      </object>
447      <packing>
448        <property name="homogeneous">False</property>
449      </packing>
450    </child>
451    <child groups='RO'>
452      <object class="GtkToolButton">
453        <property name="icon-name">go-next</property>
454        <property name="action-name">win.Forward</property>
455        <property name="tooltip_text" translatable="yes">'''
456        '''Go to the next object in the history</property>
457        <property name="label" translatable="yes">_Forward</property>
458        <property name="use-underline">True</property>
459      </object>
460      <packing>
461        <property name="homogeneous">False</property>
462      </packing>
463    </child>
464    <child groups='RO'>
465      <object class="GtkToolButton">
466        <property name="icon-name">go-home</property>
467        <property name="action-name">win.HomePerson</property>
468        <property name="tooltip_text" translatable="yes">'''
469        '''Go to the home person</property>
470        <property name="label" translatable="yes">_Home</property>
471        <property name="use-underline">True</property>
472      </object>
473      <packing>
474        <property name="homogeneous">False</property>
475      </packing>
476    </child>
477    </placeholder>
478''',
479        '''
480    <placeholder id='BarCommonEdit'>
481    <child groups='RW'>
482      <object class="GtkToolButton">
483        <property name="icon-name">gtk-edit</property>
484        <property name="action-name">win.Edit</property>
485        <property name="tooltip_text" translatable="yes">'''
486        '''Edit the active person</property>
487        <property name="label" translatable="yes">Edit...</property>
488      </object>
489      <packing>
490        <property name="homogeneous">False</property>
491      </packing>
492    </child>
493    <child groups='RW'>
494      <object class="GtkToolButton">
495        <property name="icon-name">gramps-parents-add</property>
496        <property name="action-name">win.AddParents</property>
497        <property name="tooltip_text" translatable="yes">'''
498        '''Add a new set of parents</property>
499        <property name="label" translatable="yes">Add</property>
500      </object>
501      <packing>
502        <property name="homogeneous">False</property>
503      </packing>
504    </child>
505    <child groups='RW'>
506      <object class="GtkToolButton">
507        <property name="icon-name">gramps-parents-open</property>
508        <property name="action-name">win.ShareFamily</property>
509        <property name="tooltip_text" translatable="yes">'''
510        '''Add person as child to an existing family</property>
511        <property name="label" translatable="yes">Share</property>
512      </object>
513      <packing>
514        <property name="homogeneous">False</property>
515      </packing>
516    </child>
517    <child groups='RW'>
518      <object class="GtkToolButton">
519        <property name="icon-name">gramps-spouse</property>
520        <property name="action-name">win.AddSpouse</property>
521        <property name="tooltip_text" translatable="yes">'''
522        '''Add a new family with person as parent</property>
523        <property name="label" translatable="yes">Partner</property>
524      </object>
525      <packing>
526        <property name="homogeneous">False</property>
527      </packing>
528    </child>
529    <child groups='RW'>
530      <object class="GtkToolButton">
531        <property name="icon-name">view-sort-ascending</property>
532        <property name="action-name">win.ChangeOrder</property>
533        <property name="tooltip_text" translatable="yes">'''
534        '''Change order of parents and families</property>
535        <property name="label" translatable="yes">_Reorder</property>
536        <property name="use-underline">True</property>
537      </object>
538      <packing>
539        <property name="homogeneous">False</property>
540      </packing>
541    </child>
542    </placeholder>
543     ''']
544
545    def define_actions(self):
546        NavigationView.define_actions(self)
547
548        self.order_action = ActionGroup(name=self.title + '/ChangeOrder')
549        self.order_action.add_actions([
550            ('ChangeOrder', self.reorder)])
551
552        self.family_action = ActionGroup(name=self.title + '/Family')
553        self.family_action.add_actions([
554            ('Edit', self.edit_active, "<PRIMARY>Return"),
555            ('AddSpouse', self.add_spouse),
556            ('AddParents', self.add_parents),
557            ('ShareFamily', self.select_parents)])
558
559        self._add_action('FilterEdit', callback=self.filter_editor)
560        self._add_action('PRIMARY-J', self.jump, '<PRIMARY>J')
561
562        self._add_action_group(self.order_action)
563        self._add_action_group(self.family_action)
564
565        self.uimanager.set_actions_sensitive(self.order_action,
566                                             self.reorder_sensitive)
567        self.uimanager.set_actions_sensitive(self.family_action, False)
568
569    def filter_editor(self, *obj):
570        try:
571            FilterEditor('Person', CUSTOM_FILTERS,
572                         self.dbstate, self.uistate)
573        except WindowActiveError:
574            return
575
576    def change_db(self, db):
577        #reset the connects
578        self._change_db(db)
579        if self.child:
580            list(map(self.vbox.remove, self.vbox.get_children()))
581            list(map(self.header.remove, self.header.get_children()))
582            self.child = None
583        if self.active:
584                self.bookmarks.redraw()
585        self.redraw()
586
587    def get_name(self, handle, use_gender=False):
588        if handle:
589            person = self.dbstate.db.get_person_from_handle(handle)
590            name = name_displayer.display(person)
591            if use_gender:
592                gender = self.symbols.get_symbol_for_string(person.gender)
593            else:
594                gender = ""
595            return (name, gender)
596        else:
597            return (_("Unknown"), "")
598
599    def redraw(self, *obj):
600        active_person = self.get_active()
601        if active_person:
602            self.change_person(active_person)
603        else:
604            self.change_person(None)
605
606    def change_person(self, obj):
607        self.change_active(obj)
608        try:
609            return self._change_person(obj)
610        except AttributeError as msg:
611            import traceback
612            exc = traceback.format_exc()
613            _LOG.error(str(msg) +"\n" + exc)
614            from gramps.gui.dialog import RunDatabaseRepair
615            RunDatabaseRepair(str(msg),
616                              parent=self.uistate.window)
617            self.redrawing = False
618            return True
619
620    def _change_person(self, obj):
621        if obj == self.old_handle:
622            #same object, keep present scroll position
623            old_vadjust = self.scroll.get_vadjustment().get_value()
624            self.old_handle = obj
625        else:
626            #different object, scroll to top
627            old_vadjust = self.scroll.get_vadjustment().get_lower()
628            self.old_handle = obj
629        self.scroll.get_vadjustment().set_value(
630                            self.scroll.get_vadjustment().get_lower())
631        if self.redrawing:
632            return False
633        self.redrawing = True
634
635        for old_child in self.vbox.get_children():
636            self.vbox.remove(old_child)
637        for old_child in self.header.get_children():
638            self.header.remove(old_child)
639
640        person = None
641        if obj:
642            person = self.dbstate.db.get_person_from_handle(obj)
643        if not person:
644            self.uimanager.set_actions_sensitive(self.family_action, False)
645            self.uimanager.set_actions_sensitive(self.order_action, False)
646            self.redrawing = False
647            return
648        self.uimanager.set_actions_sensitive(self.family_action, True)
649
650        self.write_title(person)
651
652        self.child = Gtk.Grid()
653        self.child.set_border_width(12)
654        self.child.set_column_spacing(12)
655        self.child.set_row_spacing(0)
656        self.row = 0
657
658        family_handle_list = person.get_parent_family_handle_list()
659
660        self.reorder_sensitive = len(family_handle_list)> 1
661
662        if family_handle_list:
663            for family_handle in family_handle_list:
664                if family_handle:
665                    self.write_parents(family_handle, person)
666        else:
667            self.write_label(_("%s:") % _('Parents'), None, True, person)
668            self.row += 1
669
670        family_handle_list = person.get_family_handle_list()
671
672        if not self.reorder_sensitive:
673            self.reorder_sensitive = len(family_handle_list)> 1
674
675        if family_handle_list:
676            for family_handle in family_handle_list:
677                if family_handle:
678                    self.write_family(family_handle, person)
679
680        self.child.show_all()
681
682        self.vbox.pack_start(self.child, False, True, 0)
683        #reset scroll position as it was before
684        self.scroll.get_vadjustment().set_value(old_vadjust)
685        self.redrawing = False
686        self.uistate.modify_statusbar(self.dbstate)
687
688        self.uimanager.set_actions_sensitive(self.order_action,
689                                             self.reorder_sensitive)
690        self.dirty = False
691
692        return True
693
694    def write_title(self, person):
695
696        list(map(self.header.remove, self.header.get_children()))
697        grid = Gtk.Grid()
698        grid.set_column_spacing(12)
699        grid.set_row_spacing(0)
700
701        # name and edit button
702        name = name_displayer.display(person)
703        fmt = '<span size="larger" weight="bold">%s</span>'
704        text = fmt % escape(name)
705        gender_code = self.symbols.get_symbol_for_string(person.gender)
706        label = widgets.DualMarkupLabel(text, gender_code,
707                                        halign=Gtk.Align.END)
708        if self._config.get('preferences.releditbtn'):
709            button = widgets.IconButton(self.edit_button_press,
710                                        person.handle)
711            button.set_tooltip_text(_('Edit Person (%s)') % name)
712        else:
713            button = None
714        eventbox = Gtk.EventBox()
715        eventbox.set_visible_window(False)
716        self._set_draggable_person(eventbox, person.get_handle())
717        hbox = widgets.LinkBox(label, button)
718        eventbox.add(hbox)
719
720        grid.attach(eventbox, 0, 0, 2, 1)
721
722        eventbox = widgets.ShadeBox(self.use_shade)
723        grid.attach(eventbox, 1, 1, 1, 1)
724        subgrid = Gtk.Grid()
725        subgrid.set_column_spacing(12)
726        subgrid.set_row_spacing(0)
727        eventbox.add(subgrid)
728        self._set_draggable_person(eventbox, person.get_handle())
729        # Gramps ID
730
731        subgrid.attach(widgets.BasicLabel(_("%s:") % _('ID')), 1, 0, 1, 1)
732        label = widgets.BasicLabel(person.gramps_id)
733        label.set_hexpand(True)
734        subgrid.attach(label, 2, 0, 1, 1)
735
736        # Birth event.
737        birth = get_birth_or_fallback(self.dbstate.db, person)
738        if birth:
739            if birth.get_type() == EventType.BAPTISM:
740                birth_title = self.bptsm
741            else:
742                birth_title = self.bth
743        else:
744            birth_title = self.bth
745
746        subgrid.attach(widgets.BasicLabel(_("%s") % birth_title), 1, 1, 1, 1)
747        birthwidget = widgets.BasicLabel(self.format_event(birth))
748        birthwidget.set_selectable(True)
749        subgrid.attach(birthwidget, 2, 1, 1, 1)
750
751        death = get_death_or_fallback(self.dbstate.db, person)
752        if death:
753            if death.get_type() == EventType.BURIAL:
754                death_title = self.burial
755            elif death.get_type() == EventType.CREMATION:
756                death_title = self.cremation
757            else:
758                death_title = self.dth
759        else:
760            death_title = self.dth
761
762        showed_death = False
763        if birth:
764            birth_date = birth.get_date_object()
765            if (birth_date and birth_date.get_valid()):
766                if death:
767                    death_date = death.get_date_object()
768                    if (death_date and death_date.get_valid()):
769                        age = (death_date - birth_date).format(
770                            precision=self.age_precision)
771                        subgrid.attach(widgets.BasicLabel(
772                            _("%s") % death_title), 1, 2, 1, 1)
773                        deathwidget = widgets.BasicLabel(
774                            "%s (%s)" % (self.format_event(death), age),
775                            Pango.EllipsizeMode.END)
776                        deathwidget.set_selectable(True)
777                        subgrid.attach(deathwidget, 2, 2, 1, 1)
778                        showed_death = True
779                if not showed_death:
780                    age = (Today() - birth_date).format(
781                        precision=self.age_precision)
782                    if probably_alive(person, self.dbstate.db):
783                        subgrid.attach(widgets.BasicLabel(
784                            _("%s:") % _("Alive")), 1, 2, 1, 1)
785                        subgrid.attach(widgets.BasicLabel(
786                            "(%s)" % age, Pango.EllipsizeMode.END), 2, 2, 1, 1)
787                    else:
788                        subgrid.attach(widgets.BasicLabel(
789                            _("%s") % self.dth), 1, 2, 1, 1)
790                        subgrid.attach(widgets.BasicLabel(
791                            "%s (%s)" % (_("unknown"), age),
792                            Pango.EllipsizeMode.END), 2, 2, 1, 1)
793                    showed_death = True
794
795        if not showed_death:
796            subgrid.attach(widgets.BasicLabel(_("%s") % death_title),
797                          1, 2, 1, 1)
798            deathwidget = widgets.BasicLabel(self.format_event(death))
799            deathwidget.set_selectable(True)
800            subgrid.attach(deathwidget,
801                          2, 2, 1, 1)
802
803        mbox = Gtk.Box()
804        mbox.add(grid)
805
806        # image
807        image_list = person.get_media_list()
808        if image_list:
809            mobj = self.dbstate.db.get_media_from_handle(image_list[0].ref)
810            if mobj and mobj.get_mime_type()[0:5] == "image":
811                pixbuf = get_thumbnail_image(
812                                media_path_full(self.dbstate.db,
813                                                mobj.get_path()),
814                                rectangle=image_list[0].get_rectangle())
815                image = Gtk.Image()
816                image.set_from_pixbuf(pixbuf)
817                button = Gtk.Button()
818                button.add(image)
819                button.connect("clicked", lambda obj: self.view_photo(mobj))
820                mbox.pack_end(button, False, True, 0)
821
822        mbox.show_all()
823        self.header.pack_start(mbox, False, True, 0)
824
825    def view_photo(self, photo):
826        """
827        Open this picture in the default picture viewer.
828        """
829        photo_path = media_path_full(self.dbstate.db, photo.get_path())
830        open_file_with_default_application(photo_path, self.uistate)
831
832    def write_person_event(self, ename, event):
833        if event:
834            dobj = event.get_date_object()
835            phandle = event.get_place_handle()
836            if phandle:
837                pname = place_displayer.display_event(self.dbstate.db, event)
838            else:
839                pname = None
840
841            value = {
842                'date' : displayer.display(dobj),
843                'place' : pname,
844                }
845        else:
846            pname = None
847            dobj = None
848
849        if dobj:
850            if pname:
851                self.write_person_data(ename,
852                                       _('%(date)s in %(place)s') % value)
853            else:
854                self.write_person_data(ename, '%(date)s' % value)
855        elif pname:
856            self.write_person_data(ename, pname)
857        else:
858            self.write_person_data(ename, '')
859
860    def format_event(self, event):
861        if event:
862            dobj = event.get_date_object()
863            phandle = event.get_place_handle()
864            if phandle:
865                pname = place_displayer.display_event(self.dbstate.db, event)
866            else:
867                pname = None
868
869            value = {
870                'date' : displayer.display(dobj),
871                'place' : pname,
872                }
873        else:
874            pname = None
875            dobj = None
876
877        if dobj:
878            if pname:
879                return _('%(date)s in %(place)s') % value
880            else:
881                return '%(date)s' % value
882        elif pname:
883            return pname
884        else:
885            return ''
886
887    def write_person_data(self, title, data):
888        self.child.attach(widgets.BasicLabel(title), _ALABEL_START, self.row,
889                          _ALABEL_STOP-_ALABEL_START, 1)
890        self.child.attach(widgets.BasicLabel(data), _ADATA_START, self.row,
891                          _ADATA_STOP-_ADATA_START, 1)
892        self.row += 1
893
894    def marriage_symbol(self, family, markup=True):
895        if family:
896            father = mother = None
897            hdl1 = family.get_father_handle()
898            if hdl1:
899                father = self.dbstate.db.get_person_from_handle(hdl1).gender
900            hdl2 = family.get_mother_handle()
901            if hdl2:
902                mother = self.dbstate.db.get_person_from_handle(hdl2).gender
903            if father != mother:
904                symbol = self.marr
905            elif father == Person.MALE:
906                symbol = self.homom
907            else:
908                symbol = self.homof
909            if markup:
910                msg = '<span size="12000" >%s</span>' % symbol
911            else:
912                msg = symbol
913        else:
914            msg = ""
915        return msg
916
917    def write_label(self, title, family, is_parent, person = None):
918        """
919        Write a Family header row
920        Shows following elements:
921        (collapse/expand arrow, Parents/Family title label, Family gramps_id, and add-choose-edit-delete buttons)
922        """
923        msg = '<span style="italic" weight="heavy">%s</span>' % escape(title)
924        hbox = Gtk.Box()
925        label = widgets.MarkupLabel(msg, halign=Gtk.Align.END)
926        # Draw the collapse/expand button:
927        if family is not None:
928            if self.check_collapsed(person.handle, family.handle):
929                arrow = widgets.ExpandCollapseArrow(True,
930                                                    self.expand_collapse_press,
931                                                    (person, family.handle))
932            else:
933                arrow = widgets.ExpandCollapseArrow(False,
934                                                    self.expand_collapse_press,
935                                                    (person, family.handle))
936        else :
937            arrow = Gtk.Arrow(arrow_type=Gtk.ArrowType.RIGHT,
938                                        shadow_type=Gtk.ShadowType.OUT)
939        hbox.pack_start(arrow, False, True, 0)
940        hbox.pack_start(label, True, True, 0)
941        # Allow to drag this family
942        eventbox = Gtk.EventBox()
943        if family is not None:
944            self._set_draggable_family(eventbox, family.handle)
945        eventbox.add(hbox)
946        self.child.attach(eventbox, _LABEL_START, self.row,
947                          _LABEL_STOP-_LABEL_START, 1)
948
949        if family:
950            value = family.gramps_id
951        else:
952            value = ""
953        eventbox = Gtk.EventBox()
954        if family is not None:
955            self._set_draggable_family(eventbox, family.handle)
956        eventbox.add(widgets.BasicLabel(value))
957        self.child.attach(eventbox, _DATA_START, self.row,
958                          _DATA_STOP-_DATA_START, 1)
959
960        if family and self.check_collapsed(person.handle, family.handle):
961            # show family names later
962            pass
963        else:
964            hbox = Gtk.Box()
965            hbox.set_spacing(12)
966            hbox.set_hexpand(True)
967            if self.uistate and self.uistate.symbols:
968                msg = self.marriage_symbol(family)
969                marriage = widgets.MarkupLabel(msg)
970                hbox.pack_start(marriage, False, True, 0)
971            if is_parent:
972                call_fcn = self.add_parent_family
973                del_fcn = self.delete_parent_family
974                add_msg = _('Add a new set of parents')
975                sel_msg = _('Add person as child to an existing family')
976                edit_msg = _('Edit parents')
977                ord_msg = _('Reorder parents')
978                del_msg = _('Remove person as child of these parents')
979            else:
980                add_msg = _('Add a new family with person as parent')
981                sel_msg = None
982                edit_msg = _('Edit family')
983                ord_msg = _('Reorder families')
984                del_msg = _('Remove person as parent in this family')
985                call_fcn = self.add_family
986                del_fcn = self.delete_family
987
988            if not self.dbstate.db.readonly:
989                # Show edit-Buttons only if db is not readonly
990                if self.reorder_sensitive:
991                    add = widgets.IconButton(self.reorder_button_press, None,
992                                             'view-sort-ascending')
993                    add.set_tooltip_text(ord_msg)
994                    hbox.pack_start(add, False, True, 0)
995
996                add = widgets.IconButton(call_fcn, None, 'list-add')
997                add.set_tooltip_text(add_msg)
998                hbox.pack_start(add, False, True, 0)
999
1000                if is_parent:
1001                    add = widgets.IconButton(self.select_family, None,
1002                                             'gtk-index')
1003                    add.set_tooltip_text(sel_msg)
1004                    hbox.pack_start(add, False, True, 0)
1005
1006            if family:
1007                edit = widgets.IconButton(self.edit_family, family.handle,
1008                                          'gtk-edit')
1009                edit.set_tooltip_text(edit_msg)
1010                hbox.pack_start(edit, False, True, 0)
1011                if not self.dbstate.db.readonly:
1012                    delete = widgets.IconButton(del_fcn, family.handle,
1013                                                'list-remove')
1014                    delete.set_tooltip_text(del_msg)
1015                    hbox.pack_start(delete, False, True, 0)
1016
1017            eventbox = Gtk.EventBox()
1018            if family is not None:
1019                self._set_draggable_family(eventbox, family.handle)
1020            eventbox.add(hbox)
1021            self.child.attach(eventbox, _BTN_START, self.row,
1022                              _BTN_STOP-_BTN_START, 1)
1023        self.row += 1
1024
1025######################################################################
1026
1027    def write_parents(self, family_handle, person = None):
1028        family = self.dbstate.db.get_family_from_handle(family_handle)
1029        if not family:
1030            return
1031        if person and self.check_collapsed(person.handle, family_handle):
1032            # don't show rest
1033            self.write_label(_("%s:") % _('Parents'), family, True, person)
1034            self.row -= 1 # back up one row for summary names
1035            active = self.get_active()
1036            child_list = [ref.ref for ref in family.get_child_ref_list()
1037                          if ref.ref != active]
1038            if child_list:
1039                count = len(child_list)
1040            else:
1041                count = 0
1042            if count > 1 :
1043                # translators: leave all/any {...} untranslated
1044                childmsg = ngettext(" ({number_of} sibling)",
1045                                    " ({number_of} siblings)", count
1046                                   ).format(number_of=count)
1047            elif count == 1 :
1048                gender = self.dbstate.db.get_person_from_handle(
1049                                        child_list[0]).gender
1050                if gender == Person.MALE :
1051                    childmsg = _(" (1 brother)")
1052                elif gender == Person.FEMALE :
1053                    childmsg = _(" (1 sister)")
1054                else :
1055                    childmsg = _(" (1 sibling)")
1056            else :
1057                childmsg = _(" (only child)")
1058            self.family = family
1059            box = self.get_people_box(family.get_father_handle(),
1060                                      family.get_mother_handle(),
1061                                      post_msg=childmsg)
1062            eventbox = widgets.ShadeBox(self.use_shade)
1063            eventbox.add(box)
1064            self.child.attach(eventbox, _PDATA_START, self.row,
1065                               _PDATA_STOP-_PDATA_START, 1)
1066            self.row += 1 # now advance it
1067        else:
1068            self.write_label(_("%s:") % _('Parents'), family, True, person)
1069            self.write_person(_('Father'), family.get_father_handle())
1070            self.write_person(_('Mother'), family.get_mother_handle())
1071
1072            if self.show_siblings:
1073                active = self.get_active()
1074                hbox = Gtk.Box()
1075                if self.check_collapsed(person.handle, "SIBLINGS"):
1076                    arrow = widgets.ExpandCollapseArrow(True,
1077                                                        self.expand_collapse_press,
1078                                                        (person, "SIBLINGS"))
1079                else:
1080                    arrow = widgets.ExpandCollapseArrow(False,
1081                                                        self.expand_collapse_press,
1082                                                        (person, "SIBLINGS"))
1083                hbox.pack_start(arrow, False, True, 0)
1084                label_cell = self.build_label_cell(_('Siblings'))
1085                hbox.pack_start(label_cell, True, True, 0)
1086                self.child.attach(hbox, _CLABEL_START-1, self.row,
1087                                  _CLABEL_STOP-_CLABEL_START, 1)
1088
1089                if self.check_collapsed(person.handle, "SIBLINGS"):
1090                    hbox = Gtk.Box()
1091                    child_list = [ref.ref for ref in family.get_child_ref_list()
1092                                  if ref.ref != active]
1093                    if child_list:
1094                        count = len(child_list)
1095                    else:
1096                        count = 0
1097                    if count > 1 :
1098                        # translators: leave all/any {...} untranslated
1099                        childmsg = ngettext(" ({number_of} sibling)",
1100                                            " ({number_of} siblings)", count
1101                                           ).format(number_of=count)
1102                    elif count == 1 :
1103                        gender = self.dbstate.db.get_person_from_handle(
1104                                                child_list[0]).gender
1105                        if gender == Person.MALE :
1106                            childmsg = _(" (1 brother)")
1107                        elif gender == Person.FEMALE :
1108                            childmsg = _(" (1 sister)")
1109                        else :
1110                            childmsg = _(" (1 sibling)")
1111                    else :
1112                        childmsg = _(" (only child)")
1113                    self.family = None
1114                    box = self.get_people_box(post_msg=childmsg)
1115                    eventbox = widgets.ShadeBox(self.use_shade)
1116                    eventbox.add(box)
1117                    self.child.attach(eventbox, _PDATA_START, self.row,
1118                                      _PDATA_STOP-_PDATA_START, 1)
1119                    self.row += 1 # now advance it
1120                else:
1121                    hbox = Gtk.Box()
1122                    addchild = widgets.IconButton(self.add_child_to_fam,
1123                                                  family.handle,
1124                                                  'list-add')
1125                    addchild.set_tooltip_text(_('Add new child to family'))
1126                    selchild = widgets.IconButton(self.sel_child_to_fam,
1127                                                  family.handle,
1128                                                  'gtk-index')
1129                    selchild.set_tooltip_text(_('Add existing child to family'))
1130                    hbox.pack_start(addchild, False, True, 0)
1131                    hbox.pack_start(selchild, False, True, 0)
1132
1133                    self.child.attach(hbox, _CLABEL_START, self.row,
1134                                      _CLABEL_STOP-_CLABEL_START, 1)
1135                    self.row += 1
1136                    vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1137                    i = 1
1138                    child_list = [ref.ref for ref in family.get_child_ref_list()]
1139                    for child_handle in child_list:
1140                        child_should_be_linked = (child_handle != active)
1141                        self.write_child(vbox, child_handle, i, child_should_be_linked)
1142                        i += 1
1143                    eventbox = widgets.ShadeBox(self.use_shade)
1144                    eventbox.add(vbox)
1145                    self.child.attach(eventbox, _CDATA_START-1, self.row,
1146                                      _CDATA_STOP-_CDATA_START+1, 1)
1147
1148            self.row += 1
1149
1150    def get_people_box(self, *handles, **kwargs):
1151        hbox = Gtk.Box()
1152        initial_name = True
1153        if self.uistate and self.uistate.symbols:
1154            msg = self.marriage_symbol(self.family) + " "
1155            marriage = widgets.MarkupLabel(msg)
1156            hbox.pack_start(marriage, False, True, 0)
1157        for handle in handles:
1158            if not initial_name:
1159                link_label = Gtk.Label(label=" %s " % _('and'))
1160                link_label.show()
1161                hbox.pack_start(link_label, False, True, 0)
1162            initial_name = False
1163            if handle:
1164                name = self.get_name(handle, True)
1165                link_label = widgets.LinkLabel(name, self._button_press,
1166                                               handle, theme=self.theme)
1167                if self._config.get('preferences.releditbtn'):
1168                    button = widgets.IconButton(self.edit_button_press,
1169                                                handle)
1170                    button.set_tooltip_text(_('Edit Person (%s)') % name[0])
1171                else:
1172                    button = None
1173                hbox.pack_start(widgets.LinkBox(link_label, button),
1174                                False, True, 0)
1175            else:
1176                link_label = Gtk.Label(label=_('Unknown'))
1177                link_label.show()
1178                hbox.pack_start(link_label, False, True, 0)
1179        if "post_msg" in kwargs and kwargs["post_msg"]:
1180            link_label = Gtk.Label(label=kwargs["post_msg"])
1181            link_label.show()
1182            hbox.pack_start(link_label, False, True, 0)
1183        return hbox
1184
1185    def write_person(self, title, handle):
1186        """
1187        Create and show a person cell with a "Father/Mother/Spouse" label in the GUI at the current row
1188        @param title: left column label ("Father/Mother/Spouse")
1189        @param handle: person handle
1190        """
1191        if title:
1192            format = '<span weight="bold">%s: </span>'
1193        else:
1194            format = "%s"
1195
1196        label = widgets.MarkupLabel(format % escape(title),
1197                                    halign=Gtk.Align.END)
1198        if self._config.get('preferences.releditbtn'):
1199            label.set_padding(0, 5)
1200
1201        eventbox = Gtk.EventBox()
1202        if handle is not None:
1203            self._set_draggable_person(eventbox, handle)
1204        eventbox.add(label)
1205        self.child.attach(eventbox, _PLABEL_START, self.row,
1206                          _PLABEL_STOP-_PLABEL_START, 1)
1207
1208        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1209        eventbox = widgets.ShadeBox(self.use_shade)
1210        if handle:
1211            name = self.get_name(handle, True)
1212            person = self.dbstate.db.get_person_from_handle(handle)
1213            parent = len(person.get_parent_family_handle_list()) > 0
1214            format = ''
1215            relation_display_theme = self._config.get(
1216                                    'preferences.relation-display-theme')
1217            if parent:
1218                emph = True
1219            else:
1220                emph = False
1221            link_label = widgets.LinkLabel(name, self._button_press,
1222                                           handle, emph, theme=self.theme)
1223            if self._config.get('preferences.releditbtn'):
1224                button = widgets.IconButton(self.edit_button_press, handle)
1225                button.set_tooltip_text(_('Edit Person (%s)') % name[0])
1226            else:
1227                button = None
1228            vbox.pack_start(widgets.LinkBox(link_label, button), True, True, 0)
1229            self._set_draggable_person(eventbox, handle)
1230        else:
1231            link_label = Gtk.Label(label=_('Unknown'))
1232            link_label.set_halign(Gtk.Align.START)
1233            link_label.show()
1234            vbox.pack_start(link_label, True, True, 0)
1235
1236        if self.show_details and handle:
1237            value = self.info_string(handle)
1238            if value:
1239                vbox.pack_start(widgets.MarkupLabel(value), True, True, 0)
1240
1241        eventbox.add(vbox)
1242
1243        self.child.attach(eventbox, _PDATA_START, self.row,
1244                          _PDATA_STOP-_PDATA_START, 1)
1245        self.row += 1
1246        return vbox
1247
1248    def _set_draggable_person(self, eventbox, person_handle):
1249        """
1250        Register the given eventbox as a drag_source with given person_handle
1251        """
1252        self._set_draggable(eventbox, person_handle, DdTargets.PERSON_LINK, 'gramps-person')
1253
1254    def _set_draggable_family(self, eventbox, family_handle):
1255        """
1256        Register the given eventbox as a drag_source with given person_handle
1257        """
1258        self._set_draggable(eventbox, family_handle, DdTargets.FAMILY_LINK, 'gramps-family')
1259
1260    def _set_draggable(self, eventbox, object_h, dnd_type, stock_icon):
1261        """
1262        Register the given eventbox as a drag_source with given object_h
1263        """
1264        eventbox.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
1265                                 [dnd_type.target()], Gdk.DragAction.COPY)
1266        eventbox.drag_source_set_icon_name(stock_icon)
1267        eventbox.connect('drag_data_get',
1268                         self._make_drag_data_get_func(object_h, dnd_type))
1269
1270    def _make_drag_data_get_func(self, object_h, dnd_type):
1271        """
1272        Generate at runtime a drag_data_get function returning the given dnd_type and object_h
1273        """
1274        def drag_data_get(widget, context, sel_data, info, time):
1275            if info == dnd_type.app_id:
1276                data = (dnd_type.drag_type, id(self), object_h, 0)
1277                sel_data.set(dnd_type.atom_drag_type, 8, pickle.dumps(data))
1278        return drag_data_get
1279
1280    def build_label_cell(self, title):
1281        if title:
1282            format = '<span weight="bold">%s: </span>'
1283        else:
1284            format = "%s"
1285
1286        lbl = widgets.MarkupLabel(format % escape(title),
1287                                  halign=Gtk.Align.END)
1288        if self._config.get('preferences.releditbtn'):
1289            lbl.set_padding(0, 5)
1290        return lbl
1291
1292    def write_child(self, vbox, handle, index, child_should_be_linked):
1293        """
1294        Write a child cell (used for children and siblings of active person)
1295        """
1296        original_vbox = vbox
1297        # Always create a transparent eventbox to allow dnd drag
1298        ev = Gtk.EventBox()
1299        ev.set_visible_window(False)
1300        if handle:
1301            self._set_draggable_person(ev, handle)
1302        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1303        ev.add(vbox)
1304
1305        if not child_should_be_linked:
1306            frame = Gtk.Frame()
1307            frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
1308            frame.add(ev)
1309            original_vbox.pack_start(frame, True, True, 0)
1310        else:
1311            original_vbox.pack_start(ev, True, True, 0)
1312
1313        parent = has_children(self.dbstate.db,
1314                              self.dbstate.db.get_person_from_handle(handle))
1315
1316        format = ''
1317        relation_display_theme = self._config.get(
1318                                        'preferences.relation-display-theme')
1319        emph = False
1320        if child_should_be_linked and parent:
1321            emph = True
1322        elif child_should_be_linked and not parent:
1323            emph = False
1324        elif parent and not child_should_be_linked:
1325            emph = None
1326
1327        if child_should_be_linked:
1328            link_func = self._button_press
1329        else:
1330            link_func = None
1331
1332        name = self.get_name(handle, True)
1333        link_label = widgets.LinkLabel(name, link_func, handle, emph,
1334                                       theme=self.theme)
1335        link_label.set_padding(3, 0)
1336        if child_should_be_linked and self._config.get(
1337            'preferences.releditbtn'):
1338            button = widgets.IconButton(self.edit_button_press, handle)
1339            button.set_tooltip_text(_('Edit Person (%s)') % name[0])
1340        else:
1341            button = None
1342
1343        hbox = Gtk.Box()
1344        l = widgets.BasicLabel("%d." % index)
1345        l.set_width_chars(3)
1346        l.set_halign(Gtk.Align.END)
1347        hbox.pack_start(l, False, False, 0)
1348        hbox.pack_start(widgets.LinkBox(link_label, button),
1349                        False, False, 4)
1350        hbox.show()
1351        vbox.pack_start(hbox, True, True, 0)
1352
1353        if self.show_details:
1354            value = self.info_string(handle)
1355            if value:
1356                l = widgets.MarkupLabel(value)
1357                l.set_padding(48, 0)
1358                vbox.add(l)
1359
1360    def write_data(self, box, title, start_col=_SDATA_START,
1361                   stop_col=_SDATA_STOP, selectable=False):
1362        label=widgets.BasicLabel(title)
1363        label.set_selectable(selectable)
1364        box.add(label)
1365
1366    def info_string(self, handle):
1367        person = self.dbstate.db.get_person_from_handle(handle)
1368        if not person:
1369            return None
1370
1371        birth = get_birth_or_fallback(self.dbstate.db, person)
1372        if birth:
1373            if birth.get_type() == EventType.BAPTISM:
1374                s_birth = self.bptsm
1375            else:
1376                s_birth = self.bth
1377        if birth and birth.get_type() != EventType.BIRTH:
1378            sdate = get_date(birth)
1379            if sdate:
1380                bdate = "<i>%s</i>" % escape(sdate)
1381            else:
1382                bdate = ""
1383        elif birth:
1384            bdate = escape(get_date(birth))
1385        else:
1386            bdate = ""
1387
1388        death = get_death_or_fallback(self.dbstate.db, person)
1389        if death:
1390            if death.get_type() == EventType.BURIAL:
1391                s_death = self.burial
1392            elif death.get_type() == EventType.CREMATION:
1393                s_death = self.cremation
1394            else:
1395                s_death = self.dth
1396        if death and death.get_type() != EventType.DEATH:
1397            sdate = get_date(death)
1398            if sdate:
1399                ddate = "<i>%s</i>" % escape(sdate)
1400            else:
1401                ddate = ""
1402        elif death:
1403            ddate = escape(get_date(death))
1404        else:
1405            ddate = ""
1406
1407        if bdate and ddate:
1408            value = _("%(birthabbrev)s %(birthdate)s, %(deathabbrev)s %(deathdate)s") % {
1409                'birthabbrev': s_birth,
1410                'deathabbrev': s_death,
1411                'birthdate' : bdate,
1412                'deathdate' : ddate
1413                }
1414        elif bdate:
1415            value = _("%(event)s %(date)s") % {'event': s_birth, 'date': bdate}
1416        elif ddate:
1417            value = _("%(event)s %(date)s") % {'event': s_death, 'date': ddate}
1418        else:
1419            value = ""
1420        return value
1421
1422    def check_collapsed(self, person_handle, handle):
1423        """ Return true if collapsed. """
1424        return (handle in self.collapsed_items.get(person_handle, []))
1425
1426    def expand_collapse_press(self, obj, event, pair):
1427        """ Calback function for ExpandCollapseArrow, user param is
1428            pair, which is a tuple (object, handle) which handles the
1429            section of which the collapse state must change, so a
1430            parent, siblings id, family id, family children id, etc.
1431            NOTE: object must be a thing that has a handle field.
1432        """
1433        if button_activated(event, _LEFT_BUTTON):
1434            object, handle = pair
1435            if object.handle in self.collapsed_items:
1436                if handle in self.collapsed_items[object.handle]:
1437                    self.collapsed_items[object.handle].remove(handle)
1438                else:
1439                    self.collapsed_items[object.handle].append(handle)
1440            else:
1441                self.collapsed_items[object.handle] = [handle]
1442            self.redraw()
1443
1444    def _button_press(self, obj, event, handle):
1445        if button_activated(event, _LEFT_BUTTON):
1446            self.change_active(handle)
1447        elif button_activated(event, _RIGHT_BUTTON):
1448            self.my_menu = Gtk.Menu()
1449            self.my_menu.set_reserve_toggle_size(False)
1450            self.my_menu.append(self.build_menu_item(handle))
1451            self.my_menu.popup(None, None, None, None, event.button, event.time)
1452
1453    def build_menu_item(self, handle):
1454        person = self.dbstate.db.get_person_from_handle(handle)
1455        name = name_displayer.display(person)
1456
1457        item = Gtk.MenuItem()
1458        label = Gtk.Label(label=_("Edit Person (%s)") % name)
1459        label.show()
1460        label.set_halign(Gtk.Align.START)
1461
1462        item.add(label)
1463
1464        item.connect('activate', self.edit_menu, handle)
1465        item.show()
1466        return item
1467
1468    def edit_menu(self, obj, handle):
1469        person = self.dbstate.db.get_person_from_handle(handle)
1470        try:
1471            EditPerson(self.dbstate, self.uistate, [], person)
1472        except WindowActiveError:
1473            pass
1474
1475    def write_relationship(self, box, family):
1476        msg = _('Relationship type: %s') % escape(str(family.get_relationship()))
1477        box.add(widgets.MarkupLabel(msg))
1478
1479    def write_relationship_events(self, vbox, family):
1480        value = False
1481        for event_ref in family.get_event_ref_list():
1482            handle = event_ref.ref
1483            event = self.dbstate.db.get_event_from_handle(handle)
1484            if (event and event.get_type().is_relationship_event() and
1485                (event_ref.get_role() == EventRoleType.FAMILY or
1486                 event_ref.get_role() == EventRoleType.PRIMARY)):
1487                if event.get_type() ==  EventType.MARRIAGE:
1488                    etype = self.marriage
1489                elif event.get_type() ==  EventType.DIVORCE:
1490                    etype = self.divorce
1491                else:
1492                    etype = event.get_type().string
1493                self.write_event_ref(vbox, etype, event)
1494                value = True
1495        return value
1496
1497    def write_event_ref(self, vbox, ename, event, start_col=_SDATA_START,
1498                        stop_col=_SDATA_STOP):
1499        if event:
1500            dobj = event.get_date_object()
1501            phandle = event.get_place_handle()
1502            if phandle:
1503                pname = place_displayer.display_event(self.dbstate.db, event)
1504            else:
1505                pname = None
1506
1507            value = {
1508                'date' : displayer.display(dobj),
1509                'place' : pname,
1510                'event_type' : ename,
1511                }
1512        else:
1513            pname = None
1514            dobj = None
1515            value = { 'event_type' : ename, }
1516
1517        if dobj:
1518            if pname:
1519                self.write_data(
1520                    vbox, _('%(event_type)s %(date)s in %(place)s') %
1521                    value, start_col, stop_col, True)
1522            else:
1523                self.write_data(
1524                    vbox, _('%(event_type)s %(date)s') % value,
1525                    start_col, stop_col)
1526        elif pname:
1527            self.write_data(
1528                vbox, _('%(event_type)s %(place)s') % value,
1529                start_col, stop_col)
1530        else:
1531            self.write_data(
1532                vbox, '%(event_type)s' % value, start_col, stop_col)
1533
1534    def write_family(self, family_handle, person = None):
1535        family = self.dbstate.db.get_family_from_handle(family_handle)
1536        if family is None:
1537            from gramps.gui.dialog import WarningDialog
1538            WarningDialog(
1539                _('Broken family detected'),
1540                _('Please run the Check and Repair Database tool'),
1541                parent=self.uistate.window)
1542            return
1543
1544        father_handle = family.get_father_handle()
1545        mother_handle = family.get_mother_handle()
1546        if self.get_active() == father_handle:
1547            handle = mother_handle
1548        else:
1549            handle = father_handle
1550
1551        # collapse button
1552        if self.check_collapsed(person.handle, family_handle):
1553            # show "> Family: ..." and nothing else
1554            self.write_label(_("%s:") % _('Family'), family, False, person)
1555            self.row -= 1 # back up
1556            child_list = family.get_child_ref_list()
1557            if child_list:
1558                count = len(child_list)
1559            else:
1560                count = 0
1561            if count >= 1 :
1562                # translators: leave all/any {...} untranslated
1563                childmsg = ngettext(" ({number_of} child)",
1564                                    " ({number_of} children)", count
1565                                   ).format(number_of=count)
1566            else :
1567                childmsg = _(" (no children)")
1568            self.family = family
1569            box = self.get_people_box(handle, post_msg=childmsg)
1570            eventbox = widgets.ShadeBox(self.use_shade)
1571            eventbox.add(box)
1572            self.child.attach(eventbox, _PDATA_START, self.row,
1573                              _PDATA_STOP-_PDATA_START, 1)
1574            self.row += 1 # now advance it
1575        else:
1576            # show "V Family: ..." and the rest
1577            self.write_label(_("%s:") % _('Family'), family, False, person)
1578            if (handle or
1579                    family.get_relationship() != FamilyRelType.UNKNOWN):
1580                box = self.write_person(_('Spouse'), handle)
1581
1582                if not self.write_relationship_events(box, family):
1583                    self.write_relationship(box, family)
1584
1585            hbox = Gtk.Box()
1586            if self.check_collapsed(family.handle, "CHILDREN"):
1587                arrow = widgets.ExpandCollapseArrow(True,
1588                                                    self.expand_collapse_press,
1589                                                    (family, "CHILDREN"))
1590            else:
1591                arrow = widgets.ExpandCollapseArrow(False,
1592                                                    self.expand_collapse_press,
1593                                                    (family, "CHILDREN"))
1594            hbox.pack_start(arrow, False, True, 0)
1595            label_cell = self.build_label_cell(_('Children'))
1596            hbox.pack_start(label_cell, True, True, 0)
1597            self.child.attach(hbox, _CLABEL_START-1, self.row,
1598                              _CLABEL_STOP-_CLABEL_START, 1)
1599
1600            if self.check_collapsed(family.handle, "CHILDREN"):
1601                hbox = Gtk.Box()
1602                child_list = family.get_child_ref_list()
1603                if child_list:
1604                    count = len(child_list)
1605                else:
1606                    count = 0
1607                if count >= 1 :
1608                    # translators: leave all/any {...} untranslated
1609                    childmsg = ngettext(" ({number_of} child)",
1610                                        " ({number_of} children)", count
1611                                       ).format(number_of=count)
1612                else :
1613                    childmsg = _(" (no children)")
1614                self.family = None
1615                box = self.get_people_box(post_msg=childmsg)
1616                eventbox = widgets.ShadeBox(self.use_shade)
1617                eventbox.add(box)
1618                self.child.attach(eventbox, _PDATA_START, self.row,
1619                                  _PDATA_STOP-_PDATA_START, 1)
1620                self.row += 1 # now advance it
1621            else:
1622                hbox = Gtk.Box()
1623                addchild = widgets.IconButton(self.add_child_to_fam,
1624                                              family.handle,
1625                                              'list-add')
1626                addchild.set_tooltip_text(_('Add new child to family'))
1627                selchild = widgets.IconButton(self.sel_child_to_fam,
1628                                              family.handle,
1629                                              'gtk-index')
1630                selchild.set_tooltip_text(_('Add existing child to family'))
1631                hbox.pack_start(addchild, False, True, 0)
1632                hbox.pack_start(selchild, False, True, 0)
1633                self.child.attach(hbox, _CLABEL_START, self.row,
1634                                  _CLABEL_STOP-_CLABEL_START, 1)
1635
1636                vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1637                i = 1
1638                child_list = family.get_child_ref_list()
1639                for child_ref in child_list:
1640                    self.write_child(vbox, child_ref.ref, i, True)
1641                    i += 1
1642
1643                self.row += 1
1644                eventbox = widgets.ShadeBox(self.use_shade)
1645                eventbox.add(vbox)
1646                self.child.attach(eventbox, _CDATA_START-1, self.row,
1647                                  _CDATA_STOP-_CDATA_START+1, 1)
1648                self.row += 1
1649
1650    def edit_button_press(self, obj, event, handle):
1651        if button_activated(event, _LEFT_BUTTON):
1652            self.edit_person(obj, handle)
1653
1654    def edit_person(self, obj, handle):
1655        person = self.dbstate.db.get_person_from_handle(handle)
1656        try:
1657            EditPerson(self.dbstate, self.uistate, [], person)
1658        except WindowActiveError:
1659            pass
1660
1661    def edit_family(self, obj, event, handle):
1662        if button_activated(event, _LEFT_BUTTON):
1663            family = self.dbstate.db.get_family_from_handle(handle)
1664            try:
1665                EditFamily(self.dbstate, self.uistate, [], family)
1666            except WindowActiveError:
1667                pass
1668
1669    def add_family(self, obj, event, handle):
1670        if button_activated(event, _LEFT_BUTTON):
1671            family = Family()
1672            person = self.dbstate.db.get_person_from_handle(self.get_active())
1673            if not person:
1674                return
1675
1676            if person.gender == Person.MALE:
1677                family.set_father_handle(person.handle)
1678            else:
1679                family.set_mother_handle(person.handle)
1680
1681            try:
1682                EditFamily(self.dbstate, self.uistate, [], family)
1683            except WindowActiveError:
1684                pass
1685
1686    def add_spouse(self, *obj):
1687        family = Family()
1688        person = self.dbstate.db.get_person_from_handle(self.get_active())
1689
1690        if not person:
1691            return
1692
1693        if person.gender == Person.MALE:
1694            family.set_father_handle(person.handle)
1695        else:
1696            family.set_mother_handle(person.handle)
1697
1698        try:
1699            EditFamily(self.dbstate, self.uistate, [], family)
1700        except WindowActiveError:
1701            pass
1702
1703    def edit_active(self, obj, value):
1704        phandle = self.get_active()
1705        self.edit_person(obj, phandle)
1706
1707    def add_child_to_fam(self, obj, event, handle):
1708        if button_activated(event, _LEFT_BUTTON):
1709            callback = lambda x: self.callback_add_child(x, handle)
1710            person = Person()
1711            name = Name()
1712            #the editor requires a surname
1713            name.add_surname(Surname())
1714            name.set_primary_surname(0)
1715            family = self.dbstate.db.get_family_from_handle(handle)
1716            father_h = family.get_father_handle()
1717            if father_h:
1718                father = self.dbstate.db.get_person_from_handle(father_h)
1719                if father:
1720                    preset_name(father, name)
1721            person.set_primary_name(name)
1722            try:
1723                EditPerson(self.dbstate, self.uistate, [], person,
1724                           callback=callback)
1725            except WindowActiveError:
1726                pass
1727
1728    def callback_add_child(self, person, family_handle):
1729        ref = ChildRef()
1730        ref.ref = person.get_handle()
1731        family = self.dbstate.db.get_family_from_handle(family_handle)
1732        family.add_child_ref(ref)
1733
1734        with DbTxn(_("Add Child to Family"), self.dbstate.db) as trans:
1735            #add parentref to child
1736            person.add_parent_family_handle(family_handle)
1737            #default relationship is used
1738            self.dbstate.db.commit_person(person, trans)
1739            #add child to family
1740            self.dbstate.db.commit_family(family, trans)
1741
1742    def sel_child_to_fam(self, obj, event, handle, surname=None):
1743        if button_activated(event, _LEFT_BUTTON):
1744            SelectPerson = SelectorFactory('Person')
1745            family = self.dbstate.db.get_family_from_handle(handle)
1746            # it only makes sense to skip those who are already in the family
1747            skip_list = [family.get_father_handle(),
1748                         family.get_mother_handle()]
1749            skip_list.extend(x.ref for x in family.get_child_ref_list())
1750
1751            sel = SelectPerson(self.dbstate, self.uistate, [],
1752                               _("Select Child"), skip=skip_list)
1753            person = sel.run()
1754
1755            if person:
1756                self.callback_add_child(person, handle)
1757
1758    def select_family(self, obj, event, handle):
1759        if button_activated(event, _LEFT_BUTTON):
1760            SelectFamily = SelectorFactory('Family')
1761
1762            phandle = self.get_active()
1763            person = self.dbstate.db.get_person_from_handle(phandle)
1764            skip = set(person.get_family_handle_list())
1765
1766            dialog = SelectFamily(self.dbstate, self.uistate, skip=skip)
1767            family = dialog.run()
1768
1769            if family:
1770                child = self.dbstate.db.get_person_from_handle(self.get_active())
1771
1772                self.dbstate.db.add_child_to_family(family, child)
1773
1774    def select_parents(self, *obj):
1775        SelectFamily = SelectorFactory('Family')
1776
1777        phandle = self.get_active()
1778        person = self.dbstate.db.get_person_from_handle(phandle)
1779        skip = set(person.get_family_handle_list()+
1780                   person.get_parent_family_handle_list())
1781
1782        dialog = SelectFamily(self.dbstate, self.uistate, skip=skip)
1783        family = dialog.run()
1784
1785        if family:
1786            child = self.dbstate.db.get_person_from_handle(self.get_active())
1787
1788            self.dbstate.db.add_child_to_family(family, child)
1789
1790    def add_parents(self, *obj):
1791        family = Family()
1792        person = self.dbstate.db.get_person_from_handle(self.get_active())
1793
1794        if not person:
1795            return
1796
1797        ref = ChildRef()
1798        ref.ref = person.handle
1799        family.add_child_ref(ref)
1800
1801        try:
1802            EditFamily(self.dbstate, self.uistate, [], family)
1803        except WindowActiveError:
1804            pass
1805
1806    def add_parent_family(self, obj, event, handle):
1807        if button_activated(event, _LEFT_BUTTON):
1808            family = Family()
1809            person = self.dbstate.db.get_person_from_handle(self.get_active())
1810
1811            ref = ChildRef()
1812            ref.ref = person.handle
1813            family.add_child_ref(ref)
1814
1815            try:
1816                EditFamily(self.dbstate, self.uistate, [], family)
1817            except WindowActiveError:
1818                pass
1819
1820    def delete_family(self, obj, event, handle):
1821        if button_activated(event, _LEFT_BUTTON):
1822            self.dbstate.db.remove_parent_from_family(self.get_active(), handle)
1823
1824    def delete_parent_family(self, obj, event, handle):
1825        if button_activated(event, _LEFT_BUTTON):
1826            self.dbstate.db.remove_child_from_family(self.get_active(), handle)
1827
1828    def change_to(self, obj, handle):
1829        self.change_active(handle)
1830
1831    def reorder_button_press(self, obj, event, handle):
1832        if button_activated(event, _LEFT_BUTTON):
1833            self.reorder(obj)
1834
1835    def reorder(self, *obj):
1836        if self.get_active():
1837            try:
1838                Reorder(self.dbstate, self.uistate, [], self.get_active())
1839            except WindowActiveError:
1840                pass
1841
1842    def config_connect(self):
1843        """
1844        Overwriten from  :class:`~gui.views.pageview.PageView method
1845        This method will be called after the ini file is initialized,
1846        use it to monitor changes in the ini file
1847        """
1848        self._config.connect("preferences.relation-shade",
1849                          self.shade_update)
1850        self._config.connect("preferences.releditbtn",
1851                          self.config_update)
1852        self._config.connect("preferences.relation-display-theme",
1853                          self.config_update)
1854        self._config.connect("preferences.family-siblings",
1855                          self.config_update)
1856        self._config.connect("preferences.family-details",
1857                          self.config_update)
1858        config.connect("interface.toolbar-on",
1859                          self.shade_update)
1860
1861    def config_panel(self, configdialog):
1862        """
1863        Function that builds the widget in the configuration dialog
1864        """
1865        grid = Gtk.Grid()
1866        grid.set_border_width(12)
1867        grid.set_column_spacing(6)
1868        grid.set_row_spacing(6)
1869
1870        configdialog.add_checkbox(grid,
1871                _('Use shading'),
1872                0, 'preferences.relation-shade')
1873        configdialog.add_checkbox(grid,
1874                _('Display edit buttons'),
1875                1, 'preferences.releditbtn')
1876        checkbox = Gtk.CheckButton(label=_('View links as website links'))
1877        theme = self._config.get('preferences.relation-display-theme')
1878        checkbox.set_active(theme == 'WEBPAGE')
1879        checkbox.connect('toggled', self._config_update_theme)
1880        grid.attach(checkbox, 1, 2, 8, 1)
1881
1882        return _('Layout'), grid
1883
1884    def content_panel(self, configdialog):
1885        """
1886        Function that builds the widget in the configuration dialog
1887        """
1888        grid = Gtk.Grid()
1889        grid.set_border_width(12)
1890        grid.set_column_spacing(6)
1891        grid.set_row_spacing(6)
1892        configdialog.add_checkbox(grid,
1893                _('Show Details'),
1894                0, 'preferences.family-details')
1895        configdialog.add_checkbox(grid,
1896                _('Show Siblings'),
1897                1, 'preferences.family-siblings')
1898
1899        return _('Content'), grid
1900
1901    def _config_update_theme(self, obj):
1902        """
1903        callback from the theme checkbox
1904        """
1905        if obj.get_active():
1906            self.theme = 'WEBPAGE'
1907            self._config.set('preferences.relation-display-theme',
1908                              'WEBPAGE')
1909        else:
1910            self.theme = 'CLASSIC'
1911            self._config.set('preferences.relation-display-theme',
1912                              'CLASSIC')
1913
1914    def _get_configure_page_funcs(self):
1915        """
1916        Return a list of functions that create gtk elements to use in the
1917        notebook pages of the Configure dialog
1918
1919        :return: list of functions
1920        """
1921        return [self.content_panel, self.config_panel]
1922
1923#-------------------------------------------------------------------------
1924#
1925# Function to return if person has children
1926#
1927#-------------------------------------------------------------------------
1928def has_children(db,p):
1929    """
1930    Return if a person has children.
1931    """
1932    for family_handle in p.get_family_handle_list():
1933        family = db.get_family_from_handle(family_handle)
1934        if not family:
1935            continue
1936        childlist = family.get_child_ref_list()
1937        if childlist and len(childlist) > 0:
1938            return True
1939    return False
1940
1941def button_activated(event, mouse_button):
1942    if (event.type == Gdk.EventType.BUTTON_PRESS and
1943        event.button == mouse_button) or \
1944       (event.type == Gdk.EventType.KEY_PRESS and
1945        event.keyval in (_RETURN, _KP_ENTER, _SPACE)):
1946        return True
1947    else:
1948        return False
1949
1950