Fix marriage event display in timeline

- Add event-to-family cache for efficient family lookup
- Improve marriage event detection using multiple comparison methods
- Show both spouses for marriage events (e.g., 'Date - Marriage - Person1 & Person2')
- Fix label rendering by using set_text instead of set_markup for plain text
- Add fallback logic to ensure marriage events always show date and event type
- Improve family event indexing to associate family events with spouses
This commit is contained in:
Daniel Viegas 2025-11-30 16:30:27 +01:00
parent f0d52456bc
commit cdba794cda

View File

@ -480,6 +480,7 @@ class MyTimelineView(NavigationView):
self._cached_min_date: Optional[int] = None self._cached_min_date: Optional[int] = None
self._cached_max_date: Optional[int] = None self._cached_max_date: Optional[int] = None
self._event_to_person_cache: Dict[str, Optional['Person']] = {} # Event handle -> Person object (or None) self._event_to_person_cache: Dict[str, Optional['Person']] = {} # Event handle -> Person object (or None)
self._event_to_family_cache: Dict[str, Optional['Family']] = {} # Event handle -> Family object (or None)
self._event_type_normalization_cache: Dict[int, int] = {} # Cache for event type normalization (key: hash/id, value: normalized int) self._event_type_normalization_cache: Dict[int, int] = {} # Cache for event type normalization (key: hash/id, value: normalized int)
self._normalized_active_event_types: Optional[Set[int]] = None # Pre-computed normalized active types self._normalized_active_event_types: Optional[Set[int]] = None # Pre-computed normalized active types
@ -588,8 +589,9 @@ class MyTimelineView(NavigationView):
# Clear selected persons and colors # Clear selected persons and colors
self.selected_person_handles.clear() self.selected_person_handles.clear()
self.person_colors.clear() self.person_colors.clear()
# Clear event-to-person cache # Clear event-to-person and event-to-family caches
self._event_to_person_cache.clear() self._event_to_person_cache.clear()
self._event_to_family_cache.clear()
# Connect to new database signals # Connect to new database signals
if db and db.is_open(): if db and db.is_open():
@ -2236,6 +2238,7 @@ class MyTimelineView(NavigationView):
""" """
Build a reverse index mapping event handles to person objects. Build a reverse index mapping event handles to person objects.
This is much more efficient than searching through all persons for each event. This is much more efficient than searching through all persons for each event.
Also indexes family events (like marriages) to one of the spouses.
""" """
if not self.dbstate.is_open(): if not self.dbstate.is_open():
return return
@ -2258,6 +2261,49 @@ class MyTimelineView(NavigationView):
except (AttributeError, KeyError): except (AttributeError, KeyError):
continue continue
# Also index family events (marriages, etc.) to one of the spouses
# Also build event-to-family cache for efficient lookup
for family_handle in self.dbstate.db.get_family_handles():
try:
family = self.dbstate.db.get_family_from_handle(family_handle)
if not family:
continue
# Get family event references
family_event_refs = family.get_event_ref_list()
if not family_event_refs:
continue
# Cache event-to-family mapping
for event_ref in family_event_refs:
event_handle = event_ref.ref
if event_handle not in self._event_to_family_cache:
self._event_to_family_cache[event_handle] = family
# Try to get father first, then mother, as the person to associate with the event
person_for_family_event = None
father_handle = family.get_father_handle()
if father_handle:
try:
person_for_family_event = self.dbstate.db.get_person_from_handle(father_handle)
except (AttributeError, KeyError):
pass
if not person_for_family_event:
mother_handle = family.get_mother_handle()
if mother_handle:
try:
person_for_family_event = self.dbstate.db.get_person_from_handle(mother_handle)
except (AttributeError, KeyError):
pass
# Index family events to the person (if we found one)
if person_for_family_event:
self._index_event_refs(family_event_refs, person_for_family_event)
except (AttributeError, KeyError):
continue
except (AttributeError, KeyError) as e: except (AttributeError, KeyError) as e:
logger.warning(f"Error building event-to-person index from database: {e}", exc_info=True) logger.warning(f"Error building event-to-person index from database: {e}", exc_info=True)
@ -2785,6 +2831,7 @@ class MyTimelineView(NavigationView):
def _get_event_label_text(self, event: 'Event', person: Optional['Person'], event_type: EventType) -> str: def _get_event_label_text(self, event: 'Event', person: Optional['Person'], event_type: EventType) -> str:
""" """
Generate label text for an event. Centralized logic. Generate label text for an event. Centralized logic.
For marriage events, tries to show both spouses.
Args: Args:
event: The event object. event: The event object.
@ -2794,14 +2841,120 @@ class MyTimelineView(NavigationView):
Returns: Returns:
str: The formatted label text. str: The formatted label text.
""" """
date_str = get_date(event) if not event:
event_type_str = str(event_type) return "Event"
# Always get date and event type string first
date_str = get_date(event) or ""
# Get event type directly from event object to ensure accuracy
actual_event_type = event.get_type() if event else event_type
event_type_str = str(actual_event_type) if actual_event_type else (str(event_type) if event_type else "")
# Check if this is a marriage event by comparing the actual event type
# Try to get the integer value for comparison
is_marriage = False
try:
event_type_val = int(actual_event_type) if hasattr(actual_event_type, '__int__') else None
marriage_type_val = int(EventType.MARRIAGE) if hasattr(EventType.MARRIAGE, '__int__') else None
if event_type_val is not None and marriage_type_val is not None:
is_marriage = (event_type_val == marriage_type_val)
elif actual_event_type == EventType.MARRIAGE:
is_marriage = True
elif "Marriage" in event_type_str or "MARRIAGE" in event_type_str:
is_marriage = True
except (ValueError, TypeError, AttributeError):
# Fallback: string check
if "Marriage" in event_type_str or "MARRIAGE" in event_type_str:
is_marriage = True
# For marriage events, try to get both spouses from the family
if is_marriage and self.dbstate.is_open():
try:
event_handle = event.get_handle()
# Use cached family lookup
family = self._event_to_family_cache.get(event_handle)
# If not in cache, try to find the family by searching
if not family:
for family_handle in self.dbstate.db.get_family_handles():
try:
test_family = self.dbstate.db.get_family_from_handle(family_handle)
if not test_family:
continue
family_event_refs = test_family.get_event_ref_list()
for event_ref in family_event_refs:
if event_ref.ref == event_handle:
family = test_family
# Cache it for next time
self._event_to_family_cache[event_handle] = family
break
if family:
break
except (AttributeError, KeyError):
continue
# If we found a family, get both spouses
if family:
spouse_names = []
father_handle = family.get_father_handle()
mother_handle = family.get_mother_handle()
if father_handle:
try:
father = self.dbstate.db.get_person_from_handle(father_handle)
if father:
spouse_names.append(name_displayer.display(father))
except (AttributeError, KeyError):
pass
if mother_handle:
try:
mother = self.dbstate.db.get_person_from_handle(mother_handle)
if mother:
spouse_names.append(name_displayer.display(mother))
except (AttributeError, KeyError):
pass
# Build label with date, event type, and spouses
parts = []
if date_str:
parts.append(date_str)
if event_type_str:
parts.append(event_type_str)
if len(spouse_names) == 2:
parts.append(f"{spouse_names[0]} & {spouse_names[1]}")
elif len(spouse_names) == 1:
parts.append(spouse_names[0])
elif person:
# Fallback to associated person if no spouses found
person_name = name_displayer.display(person)
if person_name:
parts.append(person_name)
result = " - ".join(parts) if parts else (f"{date_str} - {event_type_str}" if date_str or event_type_str else "Marriage")
if result and result.strip():
return result
except Exception as e:
logger.debug(f"Error getting marriage event label: {e}")
# Default: show single person or just event type
# Always include date and event type
parts = []
if date_str:
parts.append(date_str)
if event_type_str:
parts.append(event_type_str)
if person: if person:
person_name = name_displayer.display(person) person_name = name_displayer.display(person)
return f"{date_str} - {event_type_str} - {person_name}" if person_name:
else: parts.append(person_name)
return f"{date_str} - {event_type_str}"
# Return joined parts, or at least event type if nothing else
result = " - ".join(parts) if parts else (event_type_str or "Event")
return result if result.strip() else "Event"
def _calculate_adjusted_positions(self, context: cairo.Context, def _calculate_adjusted_positions(self, context: cairo.Context,
events_with_y_pos: List[TimelineEvent], events_with_y_pos: List[TimelineEvent],
@ -3789,8 +3942,27 @@ class MyTimelineView(NavigationView):
# Build label text using centralized method # Build label text using centralized method
label_text = self._get_event_label_text(event, person, event_type) label_text = self._get_event_label_text(event, person, event_type)
layout.set_markup(label_text, -1) # Ensure we have a valid label text (should never be empty after our fixes, but just in case)
if not label_text or not label_text.strip():
# Fallback: create basic label
date_str = get_date(event) or ""
event_type_str = str(event.get_type()) if event else (str(event_type) if event_type else "Event")
if person:
person_name = name_displayer.display(person)
if date_str:
label_text = f"{date_str} - {event_type_str} - {person_name}"
else:
label_text = f"{event_type_str} - {person_name}"
else:
if date_str:
label_text = f"{date_str} - {event_type_str}"
else:
label_text = event_type_str
# Escape any markup characters in the text (set_markup expects markup, but we want plain text)
# Actually, let's use set_text instead of set_markup for plain text
layout.set_text(label_text, -1)
layout.set_width(-1) # No width limit layout.set_width(-1) # No width limit
# Draw background for hovered events # Draw background for hovered events