diff --git a/MyTimeline.py b/MyTimeline.py
index 5725dbd..0daaae7 100644
--- a/MyTimeline.py
+++ b/MyTimeline.py
@@ -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("")
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:
@@ -1078,166 +1114,172 @@ class MyTimelineView(NavigationView):
]
# Update category checkbox state based on children
self._update_group_checkbox_state(category_checkbox, child_checkboxes)
+
+ 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
- # 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']
-
- # 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()
-
- # 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()
-
- # 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'] = {}
-
- # Iterate through all families
- for family in self.dbstate.db.iter_families():
- family_handle = family.get_handle()
-
- # Get family display name
- family_name = self._get_family_display_name(family)
-
- # 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)
-
- # Collect all child checkboxes for this family
- child_checkboxes = []
-
- # 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 father checkbox
- add_person_checkbox(family.get_father_handle(), _('Father'))
-
- # Add mother checkbox
- add_person_checkbox(family.get_mother_handle(), _('Mother'))
-
- # Add children checkboxes
- for child_ref in family.get_child_ref_list():
- add_person_checkbox(child_ref.ref, _('Child'))
-
- # 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)
-
- # 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)
-
- # 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
-
- # Flag to prevent recursion
- updating_family = [False]
-
- # Connect family checkbox to toggle all members
- family_checkbox.connect("toggled",
- self._make_group_toggle_handler(child_checkboxes, updating_family))
-
- # 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))
-
- # Initialize family checkbox state
- self._update_group_checkbox_state(family_checkbox, child_checkboxes)
-
- expander.add(members_box)
- self._filter_widgets['family_expanders'][family_handle] = expander
- container.pack_start(expander, False, False, 0)
-
- container.show_all()
- except (AttributeError, KeyError) as e:
- logger.warning(f"Error updating person filter: {e}", exc_info=True)
+ # Clear existing person checkboxes and family expanders
+ container = self._filter_widgets['person_container']
- # 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']
+ # 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()
+
+ # 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()
+
+ # Collect all families and create expanders
+ if not self.dbstate.is_open():
+ return
+
+ try:
+ # Initialize family_expanders if not exists
+ if 'family_expanders' not in self._filter_widgets:
+ self._filter_widgets['family_expanders'] = {}
- 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
+ # Iterate through all families
+ for family in self.dbstate.db.iter_families():
+ family_handle = family.get_handle()
- # 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()
+ # Get family display name
+ family_name = self._get_family_display_name(family)
- from_calendar.select_month(current_from_month, from_year)
- to_calendar.select_month(current_to_month, to_year)
+ # 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)
- # 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)
+ # Collect all child checkboxes for this family
+ child_checkboxes = []
- 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)
+ # 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)
- # 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)
+ # Add father checkbox
+ add_person_checkbox(family.get_father_handle(), _('Father'))
- 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)
+ # Add mother checkbox
+ add_person_checkbox(family.get_mother_handle(), _('Mother'))
+
+ # Add children checkboxes
+ for child_ref in family.get_child_ref_list():
+ add_person_checkbox(child_ref.ref, _('Child'))
+
+ # 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)
+
+ # 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)
+
+ # 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
+
+ # Flag to prevent recursion
+ updating_family = [False]
+
+ # Connect family checkbox to toggle all members
+ family_checkbox.connect("toggled",
+ self._make_group_toggle_handler(child_checkboxes, updating_family))
+
+ # 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))
+
+ # Initialize family checkbox state
+ self._update_group_checkbox_state(family_checkbox, child_checkboxes)
+
+ expander.add(members_box)
+ self._filter_widgets['family_expanders'][family_handle] = expander
+ container.pack_start(expander, False, False, 0)
- # Clear validation message
- if hasattr(self, 'date_validation_label'):
- self.date_validation_label.set_text("")
+ container.show_all()
+ except (AttributeError, KeyError) as e:
+ logger.warning(f"Error updating person filter: {e}", exc_info=True)
+
+ 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 = self._filter_widgets['date_from_calendar']
+ to_calendar = self._filter_widgets['date_to_calendar']
+
+ 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
+
+ # 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()
+
+ from_calendar.select_month(current_from_month, from_year)
+ to_calendar.select_month(current_to_month, to_year)
+
+ # 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)
+
+ # 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"{date_str}\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()