Skip to content

Commit 38036e5

Browse files
Sync addons-source@maintenance/gramps60 with upstream/gramps-project (2026-07-27)
2 parents 93bf864 + 359bbbb commit 38036e5

32 files changed

Lines changed: 5524 additions & 478 deletions

CONTRIBUTING.md

Lines changed: 143 additions & 476 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#
2+
# Gramps - a GTK+/GNOME based genealogy program
3+
#
4+
# Copyright (C) 2015 Nick Hall
5+
# Copyright (C) 2024 Paul Womack (BugBear)
6+
# Copyright (C) 2026 Doug Blank
7+
#
8+
# This program is free software; you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License as published by
10+
# the Free Software Foundation; either version 2 of the License, or
11+
# (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program; if not, write to the Free Software
20+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21+
22+
register(
23+
GRAMPLET,
24+
id="Person Relationship Filter",
25+
name=_("Person Relationship Filter"),
26+
description=_("Gramplet providing a person filter on relationships"),
27+
version = '1.0.1',
28+
gramps_target_version="6.0",
29+
status=STABLE,
30+
fname="PersonRelationshipFilter.py",
31+
height=200,
32+
gramplet="PersonRelationshipFilter",
33+
gramplet_title=_("Relationship Filter"),
34+
navtypes=["Person"],
35+
authors=["Paul Womack", "Doug Blank"],
36+
authors_email=["doug.blank@gmail.com"],
37+
help_url="Addon:PersonRelationshipFilter",
38+
)
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
# Gramps - a GTK+/GNOME based genealogy program
2+
#
3+
# Copyright (C) 2010 Doug Blank <doug.blank@gmail.com>
4+
# Copyright (C) 2011 Nick Hall
5+
# Copyright (C) 2011 Tim G L Lyons
6+
# Copyright (C) 2024 Paul Womack (BugBear)
7+
#
8+
# This program is free software; you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License as published by
10+
# the Free Software Foundation; either version 2 of the License, or
11+
# (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program; if not, write to the Free Software
20+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21+
#
22+
23+
# -------------------------------------------------------------------------
24+
#
25+
# Gramps modules
26+
#
27+
# -------------------------------------------------------------------------
28+
29+
from gi.repository import Gtk
30+
31+
from gramps.gen.const import GRAMPS_LOCALE as glocale
32+
from gramps.gen.plug import Gramplet
33+
34+
from gramps.gen.filters.rules import Rule
35+
from gramps.gen.filters.rules.person import ProbablyAlive
36+
from gramps.gen.lib.person import Person
37+
from gramps.gen.lib import Date
38+
from gramps.gen.datehandler import displayer
39+
from gramps.gen.filters import GenericFilter
40+
from gramps.gui import widgets
41+
from gramps.gui.filters.sidebar import SidebarFilter
42+
43+
_ = glocale.translation.gettext
44+
45+
46+
class _RegExpNameList(Rule):
47+
"""Rule that checks for full or partial name matches"""
48+
49+
labels = [_("Text:")]
50+
name = _("People with a name matching <text>")
51+
description = _(
52+
"Matches people's names containing a substring or "
53+
"matching a regular expression"
54+
)
55+
category = _("General filters")
56+
allow_regex = True
57+
58+
def field_list(self, name):
59+
raise NotImplementedError
60+
61+
def apply_to_one(self, db, person):
62+
for name in [person.primary_name] + person.alternate_names:
63+
for field in self.field_list(name):
64+
if self.match_substring(0, field):
65+
return True
66+
else:
67+
return False
68+
69+
70+
class RegExpPersonal(_RegExpNameList):
71+
def field_list(self, name):
72+
return [name.first_name, name.title, name.call, name.nick]
73+
74+
75+
class RegExpFamily(_RegExpNameList):
76+
def field_list(self, name):
77+
return [name.get_surname(), name.suffix, name.famnick]
78+
79+
80+
class _HasNamedRelation(Rule):
81+
labels = [_("Filter name:")]
82+
name = _("Children of name match")
83+
category = _("Family filters")
84+
description = _("Matches children of anybody with a given name")
85+
86+
def __init__(self, arg, name_matcher, use_regex=False):
87+
super().__init__(arg, use_regex)
88+
self.name_matcher = name_matcher(arg, use_regex)
89+
90+
def prepare(self, db, user):
91+
self.name_matcher.requestprepare(db, user)
92+
93+
def reset(self):
94+
self.name_matcher.requestreset()
95+
96+
def get_rel_list(self, db, person):
97+
raise NotImplementedError
98+
99+
def get_spouse_list(self, db, person):
100+
handles = []
101+
for fam_id in person.family_list:
102+
fam = db.get_raw_family_data(fam_id)
103+
if fam:
104+
for spouse_id in [fam.father_handle, fam.mother_handle]:
105+
if not spouse_id:
106+
continue
107+
if spouse_id == person.handle:
108+
continue
109+
handles.append(spouse_id)
110+
return handles
111+
112+
def apply_to_one(self, db, person):
113+
for rel_id in self.get_rel_list(db, person):
114+
if rel_id:
115+
rel = db.get_raw_person_data(rel_id)
116+
if self.name_matcher.apply_to_one(db, rel):
117+
return True
118+
return False
119+
120+
121+
class _HasNamedParent(_HasNamedRelation):
122+
def get_parent_families(self, db, person):
123+
families = []
124+
for fam_id in person.parent_family_list:
125+
fam = db.get_family_from_handle(fam_id)
126+
if fam:
127+
families.append(fam)
128+
return families
129+
130+
131+
class HasNamedFather(_HasNamedParent):
132+
def get_rel_list(self, db, person):
133+
return map(
134+
lambda fam: fam.get_father_handle(), self.get_parent_families(db, person)
135+
)
136+
137+
138+
class HasNamedMother(_HasNamedParent):
139+
def get_rel_list(self, db, person):
140+
return map(
141+
lambda fam: fam.get_mother_handle(), self.get_parent_families(db, person)
142+
)
143+
144+
145+
class IsSiblingofNamedSibling(_HasNamedRelation):
146+
def get_rel_list(self, db, person):
147+
handles = []
148+
fam_id = person.get_main_parents_family_handle() # or all families, per above?
149+
fam = db.get_family_from_handle(fam_id) if fam_id else None
150+
if fam:
151+
for child_ref in fam.get_child_ref_list():
152+
if child_ref and child_ref.ref != person.handle:
153+
handles.append(child_ref.ref)
154+
return handles
155+
156+
157+
class HasNamedChild(_HasNamedRelation):
158+
def get_rel_list(self, db, person):
159+
handles = []
160+
for fam_id in person.family_list:
161+
fam = db.get_family_from_handle(fam_id)
162+
if fam:
163+
for child_ref in fam.get_child_ref_list():
164+
if child_ref:
165+
handles.append(child_ref.ref)
166+
return handles
167+
168+
169+
class HasNamedSpouse(_HasNamedRelation):
170+
def get_rel_list(self, db, person):
171+
return self.get_spouse_list(db, person)
172+
173+
174+
class HasName(_HasNamedRelation):
175+
def get_rel_list(self, db, person):
176+
if person.gender == Person.FEMALE and isinstance(
177+
self.name_matcher, RegExpFamily
178+
):
179+
# for female surnames, we want to trawl the spouses surnames
180+
handles = self.get_spouse_list(db, person)
181+
handles.append(person.handle)
182+
return handles
183+
else:
184+
return [person.handle]
185+
186+
187+
def extract_text(entry_widget):
188+
"""
189+
Extract the text from the entry widget, strips off any extra spaces.
190+
"""
191+
return str(entry_widget.get_text().strip())
192+
193+
194+
# leverage to split the name into fore and aft
195+
class SearchableNamePair:
196+
def __init__(self, label, rule_class):
197+
self.widget_personal = widgets.BasicEntry()
198+
self.widget_personal.set_placeholder_text(_("given"))
199+
self.widget_family = widgets.BasicEntry()
200+
self.widget_family.set_placeholder_text(_("surname"))
201+
self.label = label
202+
self.rule_class = rule_class
203+
204+
def place(self, sidebar):
205+
# container.add_text_entry(self.label, self.widget_personal)
206+
# self.add_text_entry(container, self.label, self.widget_personal)
207+
# unrolled
208+
209+
sidebar.grid.attach(widgets.BasicLabel(self.label), 1, sidebar.position, 1, 1)
210+
211+
self.widget_personal.set_hexpand(True)
212+
sidebar.grid.attach(self.widget_personal, 2, sidebar.position, 1, 1)
213+
self.widget_personal.connect("key-press-event", sidebar.key_press)
214+
215+
self.widget_family.set_hexpand(True)
216+
sidebar.grid.attach(self.widget_family, 3, sidebar.position, 1, 1)
217+
self.widget_family.connect("key-press-event", sidebar.key_press)
218+
sidebar.position += 1
219+
220+
def clear(self):
221+
self.widget_personal.set_text("")
222+
self.widget_family.set_text("")
223+
224+
def _add_to_filter(self, generic_filter, regex, widget, search_class):
225+
v = extract_text(widget)
226+
if v:
227+
rule = self.rule_class([v], search_class, use_regex=regex)
228+
generic_filter.add_rule(rule)
229+
230+
def add_to_filter(self, generic_filter, regex):
231+
self._add_to_filter(generic_filter, regex, self.widget_personal, RegExpPersonal)
232+
self._add_to_filter(generic_filter, regex, self.widget_family, RegExpFamily)
233+
234+
235+
# -------------------------------------------------------------------------
236+
#
237+
# PersonSidebarFilter class
238+
#
239+
# -------------------------------------------------------------------------
240+
class PersonSidebarFilter(SidebarFilter):
241+
242+
def __init__(self, dbstate, uistate, clicked):
243+
self.clicked_func = clicked
244+
self.sensitive_regex = False
245+
246+
self.names = [
247+
SearchableNamePair(_("Person"), HasName),
248+
SearchableNamePair(_("Father"), HasNamedFather),
249+
SearchableNamePair(_("Mother"), HasNamedMother),
250+
SearchableNamePair(_("Spouse"), HasNamedSpouse),
251+
SearchableNamePair(_("Sibling 1"), IsSiblingofNamedSibling),
252+
SearchableNamePair(_("Sibling 2"), IsSiblingofNamedSibling),
253+
SearchableNamePair(_("Child 1"), HasNamedChild),
254+
SearchableNamePair(_("Child 2"), HasNamedChild),
255+
]
256+
self.filter_alive = widgets.DateEntry(uistate, [])
257+
258+
self.filter_regex = Gtk.CheckButton(label=_("Use regular expressions"))
259+
260+
SidebarFilter.__init__(self, dbstate, uistate, "Person")
261+
262+
def create_widget(self):
263+
exdate1 = Date()
264+
exdate2 = Date()
265+
exdate1.set(
266+
Date.QUAL_NONE,
267+
Date.MOD_RANGE,
268+
Date.CAL_GREGORIAN,
269+
(0, 0, 1800, False, 0, 0, 1900, False),
270+
)
271+
exdate2.set(
272+
Date.QUAL_NONE, Date.MOD_BEFORE, Date.CAL_GREGORIAN, (0, 0, 1850, False)
273+
)
274+
275+
msg1 = displayer.display(exdate1)
276+
msg2 = displayer.display(exdate2)
277+
278+
for w in self.names:
279+
w.place(self)
280+
281+
self.add_text_entry(
282+
_("Probably Alive"),
283+
self.filter_alive,
284+
_("example: '%(msg1)s' or '%(msg2)s'") % {"msg1": msg1, "msg2": msg2},
285+
)
286+
self.add_regex_entry(self.filter_regex)
287+
288+
def clear(self, obj):
289+
for w in self.names:
290+
w.clear()
291+
self.filter_alive.set_text("")
292+
293+
def get_filter(self):
294+
"""
295+
Extracts the text strings from the sidebar, and uses them to build up
296+
a new filter.
297+
"""
298+
299+
regex = self.filter_regex.get_active()
300+
301+
# build a GenericFilter
302+
generic_filter = GenericFilter()
303+
for w in self.names:
304+
w.add_to_filter(generic_filter, regex)
305+
306+
alive = extract_text(self.filter_alive)
307+
if alive:
308+
rule = ProbablyAlive([alive])
309+
generic_filter.add_rule(rule)
310+
311+
return generic_filter
312+
313+
314+
# -------------------------------------------------------------------------
315+
#
316+
# Filter class
317+
#
318+
# -------------------------------------------------------------------------
319+
class Filter(Gramplet):
320+
"""
321+
The base class for all filter gramplets.
322+
"""
323+
324+
FILTER_CLASS: type[SidebarFilter] | None = None
325+
326+
def init(self):
327+
self.filter = self.FILTER_CLASS(
328+
self.dbstate, self.uistate, self.__filter_clicked
329+
)
330+
self.widget = self.filter.get_widget()
331+
self.gui.get_container_widget().remove(self.gui.textview)
332+
self.gui.get_container_widget().add(self.widget)
333+
self.widget.show_all()
334+
335+
def __filter_clicked(self):
336+
"""
337+
Called when the filter apply button is clicked.
338+
"""
339+
self.gui.view.generic_filter = self.filter.get_filter()
340+
self.gui.view.build_tree()
341+
342+
343+
# -------------------------------------------------------------------------
344+
#
345+
# PersonFilter class
346+
#
347+
# -------------------------------------------------------------------------
348+
class PersonRelationshipFilter(Filter):
349+
"""
350+
A gramplet providing a Person Filter.
351+
"""
352+
353+
FILTER_CLASS = PersonSidebarFilter

0 commit comments

Comments
 (0)