From cdba794cda8c4df1f792c6a2f46d2b7ec5044153 Mon Sep 17 00:00:00 2001 From: Daniel Viegas Date: Sun, 30 Nov 2025 16:30:27 +0100 Subject: [PATCH] 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 --- MyTimeline.py | 188 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 180 insertions(+), 8 deletions(-) diff --git a/MyTimeline.py b/MyTimeline.py index 9665b5f..078d200 100644 --- a/MyTimeline.py +++ b/MyTimeline.py @@ -480,6 +480,7 @@ class MyTimelineView(NavigationView): self._cached_min_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_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._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 self.selected_person_handles.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_family_cache.clear() # Connect to new database signals if db and db.is_open(): @@ -2236,6 +2238,7 @@ class MyTimelineView(NavigationView): """ Build a reverse index mapping event handles to person objects. 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(): return @@ -2258,6 +2261,49 @@ class MyTimelineView(NavigationView): except (AttributeError, KeyError): 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: 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: """ Generate label text for an event. Centralized logic. + For marriage events, tries to show both spouses. Args: event: The event object. @@ -2794,14 +2841,120 @@ class MyTimelineView(NavigationView): Returns: str: The formatted label text. """ - date_str = get_date(event) - event_type_str = str(event_type) + if not event: + 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: person_name = name_displayer.display(person) - return f"{date_str} - {event_type_str} - {person_name}" - else: - return f"{date_str} - {event_type_str}" + if person_name: + parts.append(person_name) + + # 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, events_with_y_pos: List[TimelineEvent], @@ -3789,8 +3942,27 @@ class MyTimelineView(NavigationView): # Build label text using centralized method 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 # Draw background for hovered events