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 math
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:
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_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_FAMILY = "Sans"
FONT_SIZE_NORMAL = 11
@ -416,6 +424,8 @@ 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_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
self.filter_enabled: bool = False
@ -676,7 +686,7 @@ class MyTimelineView(NavigationView):
group_checkbox.set_inconsistent(True)
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.
@ -707,7 +717,7 @@ class MyTimelineView(NavigationView):
def _make_child_toggle_handler(self, group_checkbox: 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.
@ -916,6 +926,77 @@ class MyTimelineView(NavigationView):
scrolled.add(box)
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:
"""
Build the date range filter page with date chooser widgets.
@ -926,18 +1007,18 @@ class MyTimelineView(NavigationView):
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
box.set_margin_start(10)
box.set_margin_end(10)
box.set_margin_top(10)
box.set_margin_bottom(10)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=FILTER_PAGE_SPACING)
box.set_margin_start(FILTER_PAGE_MARGIN)
box.set_margin_end(FILTER_PAGE_MARGIN)
box.set_margin_top(FILTER_PAGE_MARGIN)
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.set_line_wrap(True)
box.pack_start(info_label, False, False, 0)
# 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)
# Year range for genealogical data
@ -946,86 +1027,32 @@ class MyTimelineView(NavigationView):
min_year = MIN_GENEALOGICAL_YEAR
max_year = current_year + 10
# From date calendar with year selector
from_frame = Gtk.Frame(label=_("From Date"))
from_frame.set_label_align(0.5, 0.5)
from_calendar_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
from_calendar_container.set_margin_start(5)
from_calendar_container.set_margin_end(5)
from_calendar_container.set_margin_top(5)
from_calendar_container.set_margin_bottom(5)
# Year selector for From date
from_year_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
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
# Create From date calendar widget
from_frame, from_calendar, from_year_spin = self._create_date_calendar_widget(
label=_("From Date"),
year_changed_handler=self._on_from_year_changed,
date_selected_handler=self._on_from_date_selected,
month_changed_handler=self._on_from_calendar_changed,
calendar_key='date_from_calendar',
spin_key='date_from_year_spin',
current_year=current_year,
min_year=min_year,
max_year=max_year
)
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)
# To date calendar with year selector
to_frame = Gtk.Frame(label=_("To Date"))
to_frame.set_label_align(0.5, 0.5)
to_calendar_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
to_calendar_container.set_margin_start(5)
to_calendar_container.set_margin_end(5)
to_calendar_container.set_margin_top(5)
to_calendar_container.set_margin_bottom(5)
# Year selector for To date
to_year_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
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
# Create To date calendar widget
to_frame, to_calendar, to_year_spin = self._create_date_calendar_widget(
label=_("To Date"),
year_changed_handler=self._on_to_year_changed,
date_selected_handler=self._on_to_date_selected,
month_changed_handler=self._on_to_calendar_changed,
calendar_key='date_to_calendar',
spin_key='date_to_year_spin',
current_year=current_year,
min_year=min_year,
max_year=max_year
)
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)
box.pack_start(calendar_box, True, True, 0)
@ -1043,28 +1070,37 @@ class MyTimelineView(NavigationView):
self.date_validation_label.set_markup("<span color='red'></span>")
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)
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
# Update event type checkboxes
if 'event_type_checkboxes' in self._filter_widgets:
for event_type, checkbox in self._filter_widgets['event_type_checkboxes'].items():
if not self.active_event_types:
checkbox.set_active(True) # All selected when filter is off
else:
checkbox.set_active(event_type in self.active_event_types)
for event_type, checkbox in self._filter_widgets['event_type_checkboxes'].items():
if not self.active_event_types:
checkbox.set_active(True) # All selected when filter is off
else:
checkbox.set_active(event_type in self.active_event_types)
# Update category checkboxes based on their children's states
if 'category_checkboxes' in self._filter_widgets and 'category_event_types' in self._filter_widgets:
@ -1079,165 +1115,171 @@ class MyTimelineView(NavigationView):
# Update category checkbox state based on children
self._update_group_checkbox_state(category_checkbox, child_checkboxes)
# Update person checkboxes with families
if 'person_checkboxes' in self._filter_widgets and 'person_container' in self._filter_widgets:
# Clear existing person checkboxes and family expanders
container = self._filter_widgets['person_container']
def _update_person_filter_widgets(self) -> None:
"""
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
# Remove all existing expanders
if 'family_expanders' in self._filter_widgets:
for expander in list(self._filter_widgets['family_expanders'].values()):
container.remove(expander)
expander.destroy()
self._filter_widgets['family_expanders'].clear()
# Clear existing person checkboxes and family expanders
container = self._filter_widgets['person_container']
# Remove all existing checkboxes
for checkbox in list(self._filter_widgets['person_checkboxes'].values()):
container.remove(checkbox)
checkbox.destroy()
self._filter_widgets['person_checkboxes'].clear()
# Remove all existing expanders
if 'family_expanders' in self._filter_widgets:
for expander in list(self._filter_widgets['family_expanders'].values()):
container.remove(expander)
expander.destroy()
self._filter_widgets['family_expanders'].clear()
# Collect all families and create expanders
if self.dbstate.is_open():
try:
# Initialize family_expanders if not exists
if 'family_expanders' not in self._filter_widgets:
self._filter_widgets['family_expanders'] = {}
# Remove all existing checkboxes
for checkbox in list(self._filter_widgets['person_checkboxes'].values()):
container.remove(checkbox)
checkbox.destroy()
self._filter_widgets['person_checkboxes'].clear()
# Iterate through all families
for family in self.dbstate.db.iter_families():
family_handle = family.get_handle()
# Collect all families and create expanders
if not self.dbstate.is_open():
return
# Get family display name
family_name = self._get_family_display_name(family)
try:
# Initialize family_expanders if not exists
if 'family_expanders' not in self._filter_widgets:
self._filter_widgets['family_expanders'] = {}
# Create container for family members
members_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
members_box.set_margin_start(20)
members_box.set_margin_top(5)
members_box.set_margin_bottom(5)
# Iterate through all families
for family in self.dbstate.db.iter_families():
family_handle = family.get_handle()
# Collect all child checkboxes for this family
child_checkboxes = []
# Get family display name
family_name = self._get_family_display_name(family)
# Helper function to add person checkbox
def add_person_checkbox(person_handle: Optional[str], role_label: str) -> None:
"""Add a checkbox for a person if handle is valid."""
if not person_handle:
return
person_name = self._get_person_display_name(person_handle)
if person_name:
label_text = f" {role_label}: {person_name}"
checkbox = Gtk.CheckButton(label=label_text)
checkbox.set_active(True if not self.person_filter else person_handle in self.person_filter)
self._filter_widgets['person_checkboxes'][person_handle] = checkbox
child_checkboxes.append(checkbox)
members_box.pack_start(checkbox, False, False, 0)
# Create container for family members
members_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
members_box.set_margin_start(20)
members_box.set_margin_top(5)
members_box.set_margin_bottom(5)
# Add father checkbox
add_person_checkbox(family.get_father_handle(), _('Father'))
# Collect all child checkboxes for this family
child_checkboxes = []
# Add mother checkbox
add_person_checkbox(family.get_mother_handle(), _('Mother'))
# Helper function to add person checkbox
def add_person_checkbox(person_handle: Optional[str], role_label: str) -> None:
"""Add a checkbox for a person if handle is valid."""
if not person_handle:
return
person_name = self._get_person_display_name(person_handle)
if person_name:
label_text = f" {role_label}: {person_name}"
checkbox = Gtk.CheckButton(label=label_text)
checkbox.set_active(True if not self.person_filter else person_handle in self.person_filter)
self._filter_widgets['person_checkboxes'][person_handle] = checkbox
child_checkboxes.append(checkbox)
members_box.pack_start(checkbox, False, False, 0)
# Add children checkboxes
for child_ref in family.get_child_ref_list():
add_person_checkbox(child_ref.ref, _('Child'))
# Add father checkbox
add_person_checkbox(family.get_father_handle(), _('Father'))
# Only add expander if there are members to show
if len(members_box.get_children()) > 0:
# Create family checkbox with three-state support
family_checkbox = Gtk.CheckButton(label=family_name)
# Add mother checkbox
add_person_checkbox(family.get_mother_handle(), _('Mother'))
# Create expander with checkbox as label
expander = Gtk.Expander()
# Set the checkbox as the label widget
expander.set_label_widget(family_checkbox)
expander.set_expanded(False)
# Add children checkboxes
for child_ref in family.get_child_ref_list():
add_person_checkbox(child_ref.ref, _('Child'))
# Store family checkbox
if 'family_checkboxes' not in self._filter_widgets:
self._filter_widgets['family_checkboxes'] = {}
self._filter_widgets['family_checkboxes'][family_handle] = family_checkbox
# Only add expander if there are members to show
if len(members_box.get_children()) > 0:
# Create family checkbox with three-state support
family_checkbox = Gtk.CheckButton(label=family_name)
# Flag to prevent recursion
updating_family = [False]
# Create expander with checkbox as label
expander = Gtk.Expander()
# Set the checkbox as the label widget
expander.set_label_widget(family_checkbox)
expander.set_expanded(False)
# Connect family checkbox to toggle all members
family_checkbox.connect("toggled",
self._make_group_toggle_handler(child_checkboxes, updating_family))
# Store family checkbox
if 'family_checkboxes' not in self._filter_widgets:
self._filter_widgets['family_checkboxes'] = {}
self._filter_widgets['family_checkboxes'][family_handle] = family_checkbox
# Connect child checkboxes to update family checkbox
for child_cb in child_checkboxes:
child_cb.connect("toggled",
self._make_child_toggle_handler(family_checkbox, child_checkboxes, updating_family))
# Flag to prevent recursion
updating_family = [False]
# Initialize family checkbox state
self._update_group_checkbox_state(family_checkbox, child_checkboxes)
# Connect family checkbox to toggle all members
family_checkbox.connect("toggled",
self._make_group_toggle_handler(child_checkboxes, updating_family))
expander.add(members_box)
self._filter_widgets['family_expanders'][family_handle] = expander
container.pack_start(expander, False, False, 0)
# Connect child checkboxes to update family checkbox
for child_cb in child_checkboxes:
child_cb.connect("toggled",
self._make_child_toggle_handler(family_checkbox, child_checkboxes, updating_family))
container.show_all()
except (AttributeError, KeyError) as e:
logger.warning(f"Error updating person filter: {e}", exc_info=True)
# Initialize family checkbox state
self._update_group_checkbox_state(family_checkbox, child_checkboxes)
# Update date range calendars and year selectors
if 'date_from_calendar' in self._filter_widgets and 'date_to_calendar' in self._filter_widgets:
from_calendar = self._filter_widgets['date_from_calendar']
to_calendar = self._filter_widgets['date_to_calendar']
expander.add(members_box)
self._filter_widgets['family_expanders'][family_handle] = expander
container.pack_start(expander, False, False, 0)
if self.date_range_filter and self.date_range_explicit:
min_sort, max_sort = self.date_range_filter
# Convert sort values back to dates for calendar display
# Approximate conversion: extract year from sort value
# Sort value is roughly: year * DATE_SORT_YEAR_MULTIPLIER + month * DATE_SORT_MONTH_MULTIPLIER + day
from_year = min_sort // DATE_SORT_YEAR_MULTIPLIER
to_year = max_sort // DATE_SORT_YEAR_MULTIPLIER
container.show_all()
except (AttributeError, KeyError) as e:
logger.warning(f"Error updating person filter: {e}", exc_info=True)
# Set calendar years (approximate)
current_from_year, current_from_month, current_from_day = from_calendar.get_date()
current_to_year, current_to_month, current_to_day = to_calendar.get_date()
def _update_date_range_widgets(self) -> None:
"""
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.select_month(current_from_month, from_year)
to_calendar.select_month(current_to_month, to_year)
from_calendar = self._filter_widgets['date_from_calendar']
to_calendar = self._filter_widgets['date_to_calendar']
# Update year spin buttons
if 'date_from_year_spin' in self._filter_widgets:
from_year_spin = self._filter_widgets['date_from_year_spin']
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 self.date_range_filter and self.date_range_explicit:
min_sort, max_sort = self.date_range_filter
# Convert sort values back to dates for calendar display
# Approximate conversion: extract year from sort value
# Sort value is roughly: year * DATE_SORT_YEAR_MULTIPLIER + month * DATE_SORT_MONTH_MULTIPLIER + day
from_year = min_sort // DATE_SORT_YEAR_MULTIPLIER
to_year = max_sort // DATE_SORT_YEAR_MULTIPLIER
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:
# Reset to current date
import datetime
now = datetime.date.today()
from_calendar.select_month(now.month - 1, now.year)
to_calendar.select_month(now.month - 1, now.year)
# Set calendar years (approximate)
current_from_year, current_from_month, current_from_day = from_calendar.get_date()
current_to_year, current_to_month, current_to_day = to_calendar.get_date()
# Reset year spin buttons
if 'date_from_year_spin' in self._filter_widgets:
from_year_spin = self._filter_widgets['date_from_year_spin']
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)
from_calendar.select_month(current_from_month, from_year)
to_calendar.select_month(current_to_month, to_year)
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)
# Update year spin buttons
self._update_year_spin_button('date_from_year_spin', from_year, self._on_from_year_changed)
self._update_year_spin_button('date_to_year_spin', to_year, self._on_to_year_changed)
else:
# Reset to current date
import datetime
now = datetime.date.today()
from_calendar.select_month(now.month - 1, now.year)
to_calendar.select_month(now.month - 1, now.year)
# Clear validation message
if hasattr(self, 'date_validation_label'):
self.date_validation_label.set_text("")
# Reset year spin buttons
self._update_year_spin_button('date_from_year_spin', now.year, self._on_from_year_changed)
self._update_year_spin_button('date_to_year_spin', now.year, self._on_to_year_changed)
# Clear validation message
if hasattr(self, 'date_validation_label'):
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:
"""
@ -1251,6 +1293,7 @@ class MyTimelineView(NavigationView):
# Clear all filters
self.filter_enabled = False
self.active_event_types = set()
self._update_normalized_active_event_types() # Invalidate cache
self.date_range_filter = None
self.date_range_explicit = False
self.person_filter = None
@ -1269,6 +1312,7 @@ class MyTimelineView(NavigationView):
if checkbox.get_active():
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._update_normalized_active_event_types() # Update cache
# Update category filter
if 'category_checkboxes' in self._filter_widgets:
@ -1867,6 +1911,52 @@ class MyTimelineView(NavigationView):
# 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]:
"""
Apply all active filters to events.
@ -1883,12 +1973,8 @@ class MyTimelineView(NavigationView):
filtered = []
for event in events:
# Check event type filter
if self.active_event_types:
# 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
if not self._apply_event_type_filter(event):
continue
# Check date range filter
if not self._is_date_in_range(event.date_sort):
@ -1900,8 +1986,7 @@ class MyTimelineView(NavigationView):
continue
# Check category filter
category = self._get_event_category(event.event_type)
if self.category_filter and category not in self.category_filter:
if not self._apply_category_filter(event):
continue
filtered.append(event)
@ -1911,6 +1996,7 @@ class MyTimelineView(NavigationView):
def _normalize_event_type(self, event_type: EventType) -> int:
"""
Normalize EventType to integer for comparison.
Uses caching to avoid repeated conversions.
Args:
event_type: The event type (may be EventType object or integer).
@ -1918,15 +2004,23 @@ class MyTimelineView(NavigationView):
Returns:
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:
if isinstance(event_type, int):
return event_type
normalized = event_type
elif hasattr(event_type, 'value'):
return event_type.value
normalized = event_type.value
else:
return int(event_type)
normalized = int(event_type)
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:
"""
@ -1940,10 +2034,12 @@ class MyTimelineView(NavigationView):
"""
if not self.active_event_types:
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_active = {self._normalize_event_type(et) for et in self.active_event_types}
return normalized_type in normalized_active
return normalized_type in self._normalized_active_event_types
def _is_date_in_range(self, date_sort: int) -> bool:
"""
@ -2580,6 +2676,28 @@ class MyTimelineView(NavigationView):
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:
"""
Format tooltip for person with multiple events.
@ -2605,15 +2723,9 @@ class MyTimelineView(NavigationView):
tooltip_text += f"{date_str} - {event_type_str}\n"
# Add place if available
place_handle = event_data.event.get_place_handle()
if place_handle:
try:
place = self.dbstate.db.get_place_from_handle(place_handle)
if place:
place_name = place.get_title()
tooltip_text += f" 📍 {place_name}\n"
except (AttributeError, KeyError) as e:
logger.debug(f"Error accessing place in tooltip: {e}")
place_name = self._get_place_name_for_event(event_data.event)
if place_name:
tooltip_text += f" 📍 {place_name}\n"
return tooltip_text
@ -2634,15 +2746,9 @@ class MyTimelineView(NavigationView):
tooltip_text = f"<b>{date_str}</b>\n{event_type_str}"
# Get place information
place_handle = event.get_place_handle()
if place_handle:
try:
place = self.dbstate.db.get_place_from_handle(place_handle)
if place:
place_name = place.get_title()
tooltip_text += f"\n📍 {place_name}"
except (AttributeError, KeyError) as e:
logger.debug(f"Error accessing place in tooltip: {e}")
place_name = self._get_place_name_for_event(event)
if place_name:
tooltip_text += f"\n📍 {place_name}"
# Get description
description = event.get_description()