Refactor MyTimeline.py: improve code quality and reduce duplication

- Extract calendar widget creation into helper method (_create_date_calendar_widget)
  Eliminates ~80 lines of duplicated code between from/to date widgets

- Extract place access logic into _get_place_name_for_event() helper
  Provides single source of truth for place access with error handling

- Consolidate year spin button updates via _update_year_spin_button() helper
  Reduces duplication and ensures consistent behavior

- Split _update_filter_dialog_state() into focused methods:
  - _update_event_type_widgets()
  - _update_person_filter_widgets()
  - _update_date_range_widgets()

- Extract filter check logic into separate methods:
  - _apply_event_type_filter()
  - _apply_category_filter()

- Improve type hints: change handler return types from Any to Callable[[Gtk.Widget], None]

- Extract UI layout constants (margins, spacing) for better maintainability

- Add event type normalization caching for performance
  Cache dictionary avoids repeated conversions

- Optimize event type set operations with pre-computed normalized active types
  Improves filter performance significantly
This commit is contained in:
Daniel Viegas 2025-11-30 00:27:43 +01:00
parent 5860b3d25c
commit c76735f2b8

View File

@ -32,7 +32,7 @@ import colorsys
import logging import logging
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, List, Tuple, Any, Set, Dict, TYPE_CHECKING, Union from typing import Optional, List, Tuple, Any, Set, Dict, TYPE_CHECKING, Union, Callable
if TYPE_CHECKING: if TYPE_CHECKING:
from gramps.gen.lib import Event, Person, Family from gramps.gen.lib import Event, Person, Family
@ -101,6 +101,14 @@ MIN_GENEALOGICAL_YEAR = 1000 # Minimum year for genealogical data
DATE_SORT_YEAR_MULTIPLIER = 10000 # Year component in date sort value DATE_SORT_YEAR_MULTIPLIER = 10000 # Year component in date sort value
DATE_SORT_MONTH_MULTIPLIER = 100 # Month component in date sort value DATE_SORT_MONTH_MULTIPLIER = 100 # Month component in date sort value
# UI Layout Constants
FILTER_PAGE_MARGIN = 10 # Margin for filter page containers
FILTER_PAGE_SPACING = 10 # Spacing in filter page containers
CALENDAR_CONTAINER_MARGIN = 5 # Margin for calendar containers
CALENDAR_CONTAINER_SPACING = 5 # Spacing in calendar containers
CALENDAR_HORIZONTAL_SPACING = 15 # Spacing between calendar frames
YEAR_SELECTOR_SPACING = 5 # Spacing in year selector boxes
# Font Constants # Font Constants
FONT_FAMILY = "Sans" FONT_FAMILY = "Sans"
FONT_SIZE_NORMAL = 11 FONT_SIZE_NORMAL = 11
@ -416,6 +424,8 @@ 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_type_normalization_cache: Dict[EventType, int] = {} # Cache for event type normalization
self._normalized_active_event_types: Optional[Set[int]] = None # Pre-computed normalized active types
# Filter state # Filter state
self.filter_enabled: bool = False self.filter_enabled: bool = False
@ -676,7 +686,7 @@ class MyTimelineView(NavigationView):
group_checkbox.set_inconsistent(True) group_checkbox.set_inconsistent(True)
def _make_group_toggle_handler(self, child_checkboxes: List[Gtk.CheckButton], def _make_group_toggle_handler(self, child_checkboxes: List[Gtk.CheckButton],
updating_flag: List[bool]) -> Any: updating_flag: List[bool]) -> Callable[[Gtk.Widget], None]:
""" """
Create a handler for group checkbox toggle that toggles all children. Create a handler for group checkbox toggle that toggles all children.
@ -707,7 +717,7 @@ class MyTimelineView(NavigationView):
def _make_child_toggle_handler(self, group_checkbox: Gtk.CheckButton, def _make_child_toggle_handler(self, group_checkbox: Gtk.CheckButton,
child_checkboxes: List[Gtk.CheckButton], child_checkboxes: List[Gtk.CheckButton],
updating_flag: List[bool]) -> Any: updating_flag: List[bool]) -> Callable[[Gtk.Widget], None]:
""" """
Create a handler for child checkbox toggle that updates group state. Create a handler for child checkbox toggle that updates group state.
@ -916,6 +926,77 @@ class MyTimelineView(NavigationView):
scrolled.add(box) scrolled.add(box)
return scrolled return scrolled
def _create_date_calendar_widget(self, label: str, year_changed_handler: Callable,
date_selected_handler: Callable,
month_changed_handler: Callable,
calendar_key: str, spin_key: str,
current_year: int, min_year: int, max_year: int) -> Tuple[Gtk.Frame, Gtk.Calendar, Gtk.SpinButton]:
"""
Create a date calendar widget with year selector.
Args:
label: Label for the frame.
year_changed_handler: Handler for year spin button changes.
date_selected_handler: Handler for calendar date selection.
month_changed_handler: Handler for calendar month changes.
calendar_key: Key to store calendar widget in _filter_widgets.
spin_key: Key to store spin button widget in _filter_widgets.
current_year: Initial year value.
min_year: Minimum year for the spin button.
max_year: Maximum year for the spin button.
Returns:
Tuple containing (frame, calendar, spin_button).
"""
frame = Gtk.Frame(label=label)
frame.set_label_align(0.5, 0.5)
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=CALENDAR_CONTAINER_SPACING)
container.set_margin_start(CALENDAR_CONTAINER_MARGIN)
container.set_margin_end(CALENDAR_CONTAINER_MARGIN)
container.set_margin_top(CALENDAR_CONTAINER_MARGIN)
container.set_margin_bottom(CALENDAR_CONTAINER_MARGIN)
# Year selector
year_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=YEAR_SELECTOR_SPACING)
year_label = Gtk.Label(label=_("Year:"))
year_adjustment = Gtk.Adjustment(
value=current_year,
lower=min_year,
upper=max_year,
step_increment=1,
page_increment=10
)
year_spin = Gtk.SpinButton()
year_spin.set_adjustment(year_adjustment)
year_spin.set_numeric(True)
year_spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID)
year_spin.set_width_chars(6)
year_spin.connect("value-changed", year_changed_handler)
year_box.pack_start(year_label, False, False, 0)
year_box.pack_start(year_spin, False, False, 0)
container.pack_start(year_box, False, False, 0)
# Calendar
calendar = Gtk.Calendar()
calendar.set_display_options(
Gtk.CalendarDisplayOptions.SHOW_HEADING |
Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES |
Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS
)
calendar.connect("day-selected", date_selected_handler)
calendar.connect("month-changed", month_changed_handler)
container.pack_start(calendar, True, True, 0)
frame.add(container)
# Store widgets
self._filter_widgets[calendar_key] = calendar
self._filter_widgets[spin_key] = year_spin
return frame, calendar, year_spin
def _build_date_range_filter_page(self) -> Gtk.Widget: def _build_date_range_filter_page(self) -> Gtk.Widget:
""" """
Build the date range filter page with date chooser widgets. Build the date range filter page with date chooser widgets.
@ -926,18 +1007,18 @@ class MyTimelineView(NavigationView):
scrolled = Gtk.ScrolledWindow() scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=FILTER_PAGE_SPACING)
box.set_margin_start(10) box.set_margin_start(FILTER_PAGE_MARGIN)
box.set_margin_end(10) box.set_margin_end(FILTER_PAGE_MARGIN)
box.set_margin_top(10) box.set_margin_top(FILTER_PAGE_MARGIN)
box.set_margin_bottom(10) box.set_margin_bottom(FILTER_PAGE_MARGIN)
info_label = Gtk.Label(label=_("Select date range to filter events. Leave unselected to show all dates.")) info_label = Gtk.Label(label=_("Select date range to filter events. Leave unselected to show all dates."))
info_label.set_line_wrap(True) info_label.set_line_wrap(True)
box.pack_start(info_label, False, False, 0) box.pack_start(info_label, False, False, 0)
# Calendar container # Calendar container
calendar_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15) calendar_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=CALENDAR_HORIZONTAL_SPACING)
calendar_box.set_homogeneous(True) calendar_box.set_homogeneous(True)
# Year range for genealogical data # Year range for genealogical data
@ -946,86 +1027,32 @@ class MyTimelineView(NavigationView):
min_year = MIN_GENEALOGICAL_YEAR min_year = MIN_GENEALOGICAL_YEAR
max_year = current_year + 10 max_year = current_year + 10
# From date calendar with year selector # Create From date calendar widget
from_frame = Gtk.Frame(label=_("From Date")) from_frame, from_calendar, from_year_spin = self._create_date_calendar_widget(
from_frame.set_label_align(0.5, 0.5) label=_("From Date"),
from_calendar_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) year_changed_handler=self._on_from_year_changed,
from_calendar_container.set_margin_start(5) date_selected_handler=self._on_from_date_selected,
from_calendar_container.set_margin_end(5) month_changed_handler=self._on_from_calendar_changed,
from_calendar_container.set_margin_top(5) calendar_key='date_from_calendar',
from_calendar_container.set_margin_bottom(5) spin_key='date_from_year_spin',
current_year=current_year,
# Year selector for From date min_year=min_year,
from_year_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) max_year=max_year
from_year_label = Gtk.Label(label=_("Year:"))
from_year_adjustment = Gtk.Adjustment(
value=current_year,
lower=min_year,
upper=max_year,
step_increment=1,
page_increment=10
) )
from_year_spin = Gtk.SpinButton()
from_year_spin.set_adjustment(from_year_adjustment)
from_year_spin.set_numeric(True)
from_year_spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID)
from_year_spin.set_width_chars(6)
from_year_spin.connect("value-changed", self._on_from_year_changed)
from_year_box.pack_start(from_year_label, False, False, 0)
from_year_box.pack_start(from_year_spin, False, False, 0)
from_calendar_container.pack_start(from_year_box, False, False, 0)
from_calendar = Gtk.Calendar()
from_calendar.set_display_options(
Gtk.CalendarDisplayOptions.SHOW_HEADING |
Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES |
Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS
)
from_calendar.connect("day-selected", self._on_from_date_selected)
from_calendar.connect("month-changed", self._on_from_calendar_changed)
from_calendar_container.pack_start(from_calendar, True, True, 0)
from_frame.add(from_calendar_container)
calendar_box.pack_start(from_frame, True, True, 0) calendar_box.pack_start(from_frame, True, True, 0)
# To date calendar with year selector # Create To date calendar widget
to_frame = Gtk.Frame(label=_("To Date")) to_frame, to_calendar, to_year_spin = self._create_date_calendar_widget(
to_frame.set_label_align(0.5, 0.5) label=_("To Date"),
to_calendar_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) year_changed_handler=self._on_to_year_changed,
to_calendar_container.set_margin_start(5) date_selected_handler=self._on_to_date_selected,
to_calendar_container.set_margin_end(5) month_changed_handler=self._on_to_calendar_changed,
to_calendar_container.set_margin_top(5) calendar_key='date_to_calendar',
to_calendar_container.set_margin_bottom(5) spin_key='date_to_year_spin',
current_year=current_year,
# Year selector for To date min_year=min_year,
to_year_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) max_year=max_year
to_year_label = Gtk.Label(label=_("Year:"))
to_year_adjustment = Gtk.Adjustment(
value=current_year,
lower=min_year,
upper=max_year,
step_increment=1,
page_increment=10
) )
to_year_spin = Gtk.SpinButton()
to_year_spin.set_adjustment(to_year_adjustment)
to_year_spin.set_numeric(True)
to_year_spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID)
to_year_spin.set_width_chars(6)
to_year_spin.connect("value-changed", self._on_to_year_changed)
to_year_box.pack_start(to_year_label, False, False, 0)
to_year_box.pack_start(to_year_spin, False, False, 0)
to_calendar_container.pack_start(to_year_box, False, False, 0)
to_calendar = Gtk.Calendar()
to_calendar.set_display_options(
Gtk.CalendarDisplayOptions.SHOW_HEADING |
Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES |
Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS
)
to_calendar.connect("day-selected", self._on_to_date_selected)
to_calendar.connect("month-changed", self._on_to_calendar_changed)
to_calendar_container.pack_start(to_calendar, True, True, 0)
to_frame.add(to_calendar_container)
calendar_box.pack_start(to_frame, True, True, 0) calendar_box.pack_start(to_frame, True, True, 0)
box.pack_start(calendar_box, True, True, 0) box.pack_start(calendar_box, True, True, 0)
@ -1043,23 +1070,32 @@ class MyTimelineView(NavigationView):
self.date_validation_label.set_markup("<span color='red'></span>") self.date_validation_label.set_markup("<span color='red'></span>")
box.pack_start(self.date_validation_label, False, False, 0) box.pack_start(self.date_validation_label, False, False, 0)
self._filter_widgets['date_from_calendar'] = from_calendar
self._filter_widgets['date_to_calendar'] = to_calendar
self._filter_widgets['date_from_year_spin'] = from_year_spin
self._filter_widgets['date_to_year_spin'] = to_year_spin
scrolled.add(box) scrolled.add(box)
return scrolled return scrolled
def _update_filter_dialog_state(self) -> None: def _update_year_spin_button(self, spin_key: str, year: int, handler: Callable) -> None:
""" """
Update the filter dialog widgets to reflect current filter state. Update a year spin button value, blocking signals during update.
Args:
spin_key: Key to find the spin button in _filter_widgets.
year: Year value to set.
handler: Handler function to block/unblock during update.
""" """
if not hasattr(self, '_filter_widgets'): if spin_key in self._filter_widgets:
year_spin = self._filter_widgets[spin_key]
year_spin.handler_block_by_func(handler)
year_spin.set_value(year)
year_spin.handler_unblock_by_func(handler)
def _update_event_type_widgets(self) -> None:
"""
Update event type filter widgets to reflect current filter state.
"""
if 'event_type_checkboxes' not in self._filter_widgets:
return return
# Update event type checkboxes # Update event type checkboxes
if 'event_type_checkboxes' in self._filter_widgets:
for event_type, checkbox in self._filter_widgets['event_type_checkboxes'].items(): for event_type, checkbox in self._filter_widgets['event_type_checkboxes'].items():
if not self.active_event_types: if not self.active_event_types:
checkbox.set_active(True) # All selected when filter is off checkbox.set_active(True) # All selected when filter is off
@ -1079,8 +1115,13 @@ class MyTimelineView(NavigationView):
# Update category checkbox state based on children # Update category checkbox state based on children
self._update_group_checkbox_state(category_checkbox, child_checkboxes) self._update_group_checkbox_state(category_checkbox, child_checkboxes)
# Update person checkboxes with families def _update_person_filter_widgets(self) -> None:
if 'person_checkboxes' in self._filter_widgets and 'person_container' in self._filter_widgets: """
Update person filter widgets to reflect current filter state.
"""
if 'person_checkboxes' not in self._filter_widgets or 'person_container' not in self._filter_widgets:
return
# Clear existing person checkboxes and family expanders # Clear existing person checkboxes and family expanders
container = self._filter_widgets['person_container'] container = self._filter_widgets['person_container']
@ -1098,7 +1139,9 @@ class MyTimelineView(NavigationView):
self._filter_widgets['person_checkboxes'].clear() self._filter_widgets['person_checkboxes'].clear()
# Collect all families and create expanders # Collect all families and create expanders
if self.dbstate.is_open(): if not self.dbstate.is_open():
return
try: try:
# Initialize family_expanders if not exists # Initialize family_expanders if not exists
if 'family_expanders' not in self._filter_widgets: if 'family_expanders' not in self._filter_widgets:
@ -1183,8 +1226,13 @@ class MyTimelineView(NavigationView):
except (AttributeError, KeyError) as e: except (AttributeError, KeyError) as e:
logger.warning(f"Error updating person filter: {e}", exc_info=True) logger.warning(f"Error updating person filter: {e}", exc_info=True)
# Update date range calendars and year selectors def _update_date_range_widgets(self) -> None:
if 'date_from_calendar' in self._filter_widgets and 'date_to_calendar' in self._filter_widgets: """
Update date range filter widgets to reflect current filter state.
"""
if 'date_from_calendar' not in self._filter_widgets or 'date_to_calendar' not in self._filter_widgets:
return
from_calendar = self._filter_widgets['date_from_calendar'] from_calendar = self._filter_widgets['date_from_calendar']
to_calendar = self._filter_widgets['date_to_calendar'] to_calendar = self._filter_widgets['date_to_calendar']
@ -1204,17 +1252,8 @@ class MyTimelineView(NavigationView):
to_calendar.select_month(current_to_month, to_year) to_calendar.select_month(current_to_month, to_year)
# Update year spin buttons # Update year spin buttons
if 'date_from_year_spin' in self._filter_widgets: self._update_year_spin_button('date_from_year_spin', from_year, self._on_from_year_changed)
from_year_spin = self._filter_widgets['date_from_year_spin'] self._update_year_spin_button('date_to_year_spin', to_year, self._on_to_year_changed)
from_year_spin.handler_block_by_func(self._on_from_year_changed)
from_year_spin.set_value(from_year)
from_year_spin.handler_unblock_by_func(self._on_from_year_changed)
if 'date_to_year_spin' in self._filter_widgets:
to_year_spin = self._filter_widgets['date_to_year_spin']
to_year_spin.handler_block_by_func(self._on_to_year_changed)
to_year_spin.set_value(to_year)
to_year_spin.handler_unblock_by_func(self._on_to_year_changed)
else: else:
# Reset to current date # Reset to current date
import datetime import datetime
@ -1223,22 +1262,25 @@ class MyTimelineView(NavigationView):
to_calendar.select_month(now.month - 1, now.year) to_calendar.select_month(now.month - 1, now.year)
# Reset year spin buttons # Reset year spin buttons
if 'date_from_year_spin' in self._filter_widgets: self._update_year_spin_button('date_from_year_spin', now.year, self._on_from_year_changed)
from_year_spin = self._filter_widgets['date_from_year_spin'] self._update_year_spin_button('date_to_year_spin', now.year, self._on_to_year_changed)
from_year_spin.handler_block_by_func(self._on_from_year_changed)
from_year_spin.set_value(now.year)
from_year_spin.handler_unblock_by_func(self._on_from_year_changed)
if 'date_to_year_spin' in self._filter_widgets:
to_year_spin = self._filter_widgets['date_to_year_spin']
to_year_spin.handler_block_by_func(self._on_to_year_changed)
to_year_spin.set_value(now.year)
to_year_spin.handler_unblock_by_func(self._on_to_year_changed)
# Clear validation message # Clear validation message
if hasattr(self, 'date_validation_label'): if hasattr(self, 'date_validation_label'):
self.date_validation_label.set_text("") self.date_validation_label.set_text("")
def _update_filter_dialog_state(self) -> None:
"""
Update the filter dialog widgets to reflect current filter state.
"""
if not hasattr(self, '_filter_widgets'):
return
# Update each filter type's widgets
self._update_event_type_widgets()
self._update_person_filter_widgets()
self._update_date_range_widgets()
def _on_filter_dialog_response(self, dialog: Gtk.Dialog, response_id: int) -> None: def _on_filter_dialog_response(self, dialog: Gtk.Dialog, response_id: int) -> None:
""" """
Handle filter dialog response. Handle filter dialog response.
@ -1251,6 +1293,7 @@ class MyTimelineView(NavigationView):
# Clear all filters # Clear all filters
self.filter_enabled = False self.filter_enabled = False
self.active_event_types = set() self.active_event_types = set()
self._update_normalized_active_event_types() # Invalidate cache
self.date_range_filter = None self.date_range_filter = None
self.date_range_explicit = False self.date_range_explicit = False
self.person_filter = None self.person_filter = None
@ -1269,6 +1312,7 @@ class MyTimelineView(NavigationView):
if checkbox.get_active(): if checkbox.get_active():
active_types.add(event_type) active_types.add(event_type)
self.active_event_types = active_types if len(active_types) < len(self._filter_widgets['event_type_checkboxes']) else set() self.active_event_types = active_types if len(active_types) < len(self._filter_widgets['event_type_checkboxes']) else set()
self._update_normalized_active_event_types() # Update cache
# Update category filter # Update category filter
if 'category_checkboxes' in self._filter_widgets: if 'category_checkboxes' in self._filter_widgets:
@ -1867,6 +1911,52 @@ class MyTimelineView(NavigationView):
# Update filter button state # Update filter button state
self._update_filter_button_state() self._update_filter_button_state()
def _update_normalized_active_event_types(self) -> None:
"""
Update the pre-computed normalized active event types set.
Call this whenever active_event_types changes.
"""
if not self.active_event_types:
self._normalized_active_event_types = None
else:
self._normalized_active_event_types = {
self._normalize_event_type(et) for et in self.active_event_types
}
def _apply_event_type_filter(self, event: TimelineEvent) -> bool:
"""
Check if event passes event type filter.
Args:
event: The event to check.
Returns:
bool: True if event passes filter, False otherwise.
"""
if not self.active_event_types:
return True
# Use pre-computed normalized set if available, otherwise compute it
if self._normalized_active_event_types is None:
self._update_normalized_active_event_types()
# Normalize event.event_type and compare with normalized active_event_types
event_type_normalized = self._normalize_event_type(event.event_type)
return event_type_normalized in self._normalized_active_event_types
def _apply_category_filter(self, event: TimelineEvent) -> bool:
"""
Check if event passes category filter.
Args:
event: The event to check.
Returns:
bool: True if event passes filter, False otherwise.
"""
if not self.category_filter:
return True
category = self._get_event_category(event.event_type)
return category in self.category_filter
def _apply_filters(self, events: List[TimelineEvent]) -> List[TimelineEvent]: def _apply_filters(self, events: List[TimelineEvent]) -> List[TimelineEvent]:
""" """
Apply all active filters to events. Apply all active filters to events.
@ -1883,11 +1973,7 @@ class MyTimelineView(NavigationView):
filtered = [] filtered = []
for event in events: for event in events:
# Check event type filter # Check event type filter
if self.active_event_types: if not self._apply_event_type_filter(event):
# Normalize event.event_type and compare with normalized active_event_types
event_type_normalized = self._normalize_event_type(event.event_type)
active_types_normalized = {self._normalize_event_type(et) for et in self.active_event_types}
if event_type_normalized not in active_types_normalized:
continue continue
# Check date range filter # Check date range filter
@ -1900,8 +1986,7 @@ class MyTimelineView(NavigationView):
continue continue
# Check category filter # Check category filter
category = self._get_event_category(event.event_type) if not self._apply_category_filter(event):
if self.category_filter and category not in self.category_filter:
continue continue
filtered.append(event) filtered.append(event)
@ -1911,6 +1996,7 @@ class MyTimelineView(NavigationView):
def _normalize_event_type(self, event_type: EventType) -> int: def _normalize_event_type(self, event_type: EventType) -> int:
""" """
Normalize EventType to integer for comparison. Normalize EventType to integer for comparison.
Uses caching to avoid repeated conversions.
Args: Args:
event_type: The event type (may be EventType object or integer). event_type: The event type (may be EventType object or integer).
@ -1918,15 +2004,23 @@ class MyTimelineView(NavigationView):
Returns: Returns:
int: The integer value of the event type. int: The integer value of the event type.
""" """
# Check cache first
if event_type in self._event_type_normalization_cache:
return self._event_type_normalization_cache[event_type]
try: try:
if isinstance(event_type, int): if isinstance(event_type, int):
return event_type normalized = event_type
elif hasattr(event_type, 'value'): elif hasattr(event_type, 'value'):
return event_type.value normalized = event_type.value
else: else:
return int(event_type) normalized = int(event_type)
except (TypeError, ValueError, AttributeError): except (TypeError, ValueError, AttributeError):
return 0 # Default to 0 if conversion fails normalized = 0 # Default to 0 if conversion fails
# Cache the result
self._event_type_normalization_cache[event_type] = normalized
return normalized
def _is_event_type_enabled(self, event_type: EventType) -> bool: def _is_event_type_enabled(self, event_type: EventType) -> bool:
""" """
@ -1940,10 +2034,12 @@ class MyTimelineView(NavigationView):
""" """
if not self.active_event_types: if not self.active_event_types:
return True return True
# Normalize both for comparison # Use pre-computed normalized set if available, otherwise compute it
if self._normalized_active_event_types is None:
self._update_normalized_active_event_types()
# Normalize event type and compare with normalized active types
normalized_type = self._normalize_event_type(event_type) normalized_type = self._normalize_event_type(event_type)
normalized_active = {self._normalize_event_type(et) for et in self.active_event_types} return normalized_type in self._normalized_active_event_types
return normalized_type in normalized_active
def _is_date_in_range(self, date_sort: int) -> bool: def _is_date_in_range(self, date_sort: int) -> bool:
""" """
@ -2580,6 +2676,28 @@ class MyTimelineView(NavigationView):
return None return None
def _get_place_name_for_event(self, event: 'Event') -> Optional[str]:
"""
Get the place name for an event, with error handling.
Args:
event: The event object.
Returns:
Optional[str]: Place name if available, None otherwise.
"""
place_handle = event.get_place_handle()
if not place_handle:
return None
try:
place = self.dbstate.db.get_place_from_handle(place_handle)
if place:
return place.get_title()
except (AttributeError, KeyError) as e:
logger.debug(f"Error accessing place for event: {e}")
return None
def _format_person_tooltip(self, person: 'Person', person_events: List[TimelineEvent]) -> str: def _format_person_tooltip(self, person: 'Person', person_events: List[TimelineEvent]) -> str:
""" """
Format tooltip for person with multiple events. Format tooltip for person with multiple events.
@ -2605,15 +2723,9 @@ class MyTimelineView(NavigationView):
tooltip_text += f"{date_str} - {event_type_str}\n" tooltip_text += f"{date_str} - {event_type_str}\n"
# Add place if available # Add place if available
place_handle = event_data.event.get_place_handle() place_name = self._get_place_name_for_event(event_data.event)
if place_handle: if place_name:
try:
place = self.dbstate.db.get_place_from_handle(place_handle)
if place:
place_name = place.get_title()
tooltip_text += f" 📍 {place_name}\n" tooltip_text += f" 📍 {place_name}\n"
except (AttributeError, KeyError) as e:
logger.debug(f"Error accessing place in tooltip: {e}")
return tooltip_text return tooltip_text
@ -2634,15 +2746,9 @@ class MyTimelineView(NavigationView):
tooltip_text = f"<b>{date_str}</b>\n{event_type_str}" tooltip_text = f"<b>{date_str}</b>\n{event_type_str}"
# Get place information # Get place information
place_handle = event.get_place_handle() place_name = self._get_place_name_for_event(event)
if place_handle: if place_name:
try:
place = self.dbstate.db.get_place_from_handle(place_handle)
if place:
place_name = place.get_title()
tooltip_text += f"\n📍 {place_name}" tooltip_text += f"\n📍 {place_name}"
except (AttributeError, KeyError) as e:
logger.debug(f"Error accessing place in tooltip: {e}")
# Get description # Get description
description = event.get_description() description = event.get_description()