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:
parent
f0d52456bc
commit
cdba794cda
188
MyTimeline.py
188
MyTimeline.py
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user