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()