Fix bug where unchecking all event types in a category re-checks them

When all event types in a category were unchecked, _update_group_checkbox_state
would set the category checkbox to False, which triggered its toggle handler.
This handler would then set all child checkboxes to match the category state,
causing them to be re-checked.

The fix adds an optional updating_flag parameter to _update_group_checkbox_state
that prevents the group toggle handler from firing during programmatic updates.
The flag is set before updating the checkbox state and cleared after, breaking
the recursion cycle.

Changes:
- Modified _update_group_checkbox_state to accept optional updating_flag parameter
- Updated _make_child_toggle_handler to pass updating_flag
- Store category updating flags in _filter_widgets for reuse
- Updated _update_event_type_widgets to use stored updating flags
- Fixed family checkbox initialization to use updating flag as well
This commit is contained in:
Daniel Viegas 2025-11-30 14:37:55 +01:00
parent cb572c4dc5
commit 0a9ecca878

View File

@ -431,7 +431,7 @@ class MyTimelineView(NavigationView):
""" """
View for displaying a vertical timeline of all events in the database. View for displaying a vertical timeline of all events in the database.
Shows all events with modern design and interactivity, allowing selection Shows all events with modern design and interactivity, allowing selection
and filtering of events by type, category, person, and date range. and filtering of events by type, category, and person.
""" """
def __init__(self, pdata: Any, dbstate: Any, uistate: Any, nav_group: int = 0) -> None: def __init__(self, pdata: Any, dbstate: Any, uistate: Any, nav_group: int = 0) -> None:
@ -486,8 +486,6 @@ class MyTimelineView(NavigationView):
# Filter state # Filter state
self.filter_enabled: bool = False self.filter_enabled: bool = False
self.active_event_types: Set[EventType] = set() # Empty = all enabled self.active_event_types: Set[EventType] = set() # Empty = all enabled
self.date_range_filter: Optional[Tuple[int, int]] = None # (min_date, max_date) in sort values
self.date_range_explicit: bool = False # Track if date range was explicitly set
self.person_filter: Optional[Set[str]] = None # Set of person handles to include, None = all self.person_filter: Optional[Set[str]] = None # Set of person handles to include, None = all
self.category_filter: Optional[Set[str]] = None # Set of event categories to include, None = all self.category_filter: Optional[Set[str]] = None # Set of event categories to include, None = all
self.all_events: List[TimelineEvent] = [] # Store all events before filtering self.all_events: List[TimelineEvent] = [] # Store all events before filtering
@ -690,9 +688,6 @@ class MyTimelineView(NavigationView):
response = self.filter_dialog.run() response = self.filter_dialog.run()
if response == Gtk.ResponseType.APPLY: if response == Gtk.ResponseType.APPLY:
self._apply_filter_dialog_settings() self._apply_filter_dialog_settings()
elif response == Gtk.ResponseType.CLOSE:
pass # Just close
self.filter_dialog.hide() self.filter_dialog.hide()
def _calculate_group_state(self, child_checkboxes: List[Gtk.CheckButton]) -> str: def _calculate_group_state(self, child_checkboxes: List[Gtk.CheckButton]) -> str:
@ -718,14 +713,22 @@ class MyTimelineView(NavigationView):
return 'some' return 'some'
def _update_group_checkbox_state(self, group_checkbox: Gtk.CheckButton, def _update_group_checkbox_state(self, group_checkbox: Gtk.CheckButton,
child_checkboxes: List[Gtk.CheckButton]) -> None: child_checkboxes: List[Gtk.CheckButton],
updating_flag: Optional[List[bool]] = None) -> None:
""" """
Update a group checkbox state based on child checkboxes. Update a group checkbox state based on child checkboxes.
Args: Args:
group_checkbox: The parent group checkbox to update. group_checkbox: The parent group checkbox to update.
child_checkboxes: List of child checkboxes. child_checkboxes: List of child checkboxes.
updating_flag: Optional list with single boolean to prevent recursion.
If provided, will be set to True during update to prevent
the group checkbox's toggle handler from firing.
""" """
# Set updating flag to prevent recursion if provided
if updating_flag is not None:
updating_flag[0] = True
state = self._calculate_group_state(child_checkboxes) state = self._calculate_group_state(child_checkboxes)
if state == 'all': if state == 'all':
@ -736,6 +739,10 @@ class MyTimelineView(NavigationView):
group_checkbox.set_inconsistent(False) group_checkbox.set_inconsistent(False)
else: # 'some' else: # 'some'
group_checkbox.set_inconsistent(True) group_checkbox.set_inconsistent(True)
# Clear updating flag after update
if updating_flag is not None:
updating_flag[0] = False
def _make_group_toggle_handler(self, child_checkboxes: List[Gtk.CheckButton], def _make_group_toggle_handler(self, child_checkboxes: List[Gtk.CheckButton],
updating_flag: List[bool]) -> Callable[[Gtk.Widget], None]: updating_flag: List[bool]) -> Callable[[Gtk.Widget], None]:
@ -784,7 +791,7 @@ class MyTimelineView(NavigationView):
def handler(widget: Gtk.Widget) -> None: def handler(widget: Gtk.Widget) -> None:
if updating_flag[0]: if updating_flag[0]:
return return
self._update_group_checkbox_state(group_checkbox, child_checkboxes) self._update_group_checkbox_state(group_checkbox, child_checkboxes, updating_flag)
return handler return handler
@ -831,10 +838,6 @@ class MyTimelineView(NavigationView):
person_page = self._build_person_filter_page() person_page = self._build_person_filter_page()
notebook.append_page(person_page, Gtk.Label(label=_("Persons"))) notebook.append_page(person_page, Gtk.Label(label=_("Persons")))
# Date Range Filter Page
date_page = self._build_date_range_filter_page()
notebook.append_page(date_page, Gtk.Label(label=_("Date Range")))
content_area.show_all() content_area.show_all()
return dialog return dialog
@ -869,6 +872,7 @@ class MyTimelineView(NavigationView):
category_boxes = {} category_boxes = {}
category_checkboxes = {} category_checkboxes = {}
category_event_types = {} # Map category to list of event types category_event_types = {} # Map category to list of event types
category_updating_flags = {} # Map category to updating flag list
# First pass: collect event types by category # First pass: collect event types by category
for event_type_obj in EVENT_COLORS: for event_type_obj in EVENT_COLORS:
@ -905,6 +909,7 @@ class MyTimelineView(NavigationView):
# Flag to prevent recursion between category and child checkboxes # Flag to prevent recursion between category and child checkboxes
updating_category = [False] updating_category = [False]
category_updating_flags[category] = updating_category
# Connect category checkbox to toggle all children # Connect category checkbox to toggle all children
category_checkbox.connect("toggled", category_checkbox.connect("toggled",
@ -918,6 +923,7 @@ class MyTimelineView(NavigationView):
self._filter_widgets['event_type_checkboxes'] = event_type_checkboxes self._filter_widgets['event_type_checkboxes'] = event_type_checkboxes
self._filter_widgets['category_checkboxes'] = category_checkboxes self._filter_widgets['category_checkboxes'] = category_checkboxes
self._filter_widgets['category_event_types'] = category_event_types self._filter_widgets['category_event_types'] = category_event_types
self._filter_widgets['category_updating_flags'] = category_updating_flags
scrolled.add(box) scrolled.add(box)
return scrolled return scrolled
@ -978,182 +984,6 @@ class MyTimelineView(NavigationView):
scrolled.add(box) scrolled.add(box)
return scrolled return scrolled
def _create_date_calendar_widget(self, label: str, year_changed_handler: Callable,
date_selected_handler: Callable,
month_changed_handler: Callable,
calendar_key: str, spin_key: str,
current_year: int, min_year: int, max_year: int) -> Tuple[Gtk.Frame, Gtk.Calendar, Gtk.SpinButton]:
"""
Create a date calendar widget with year selector.
Args:
label: Label for the frame.
year_changed_handler: Handler for year spin button changes.
date_selected_handler: Handler for calendar date selection.
month_changed_handler: Handler for calendar month changes.
calendar_key: Key to store calendar widget in _filter_widgets.
spin_key: Key to store spin button widget in _filter_widgets.
current_year: Initial year value.
min_year: Minimum year for the spin button.
max_year: Maximum year for the spin button.
Returns:
Tuple containing (frame, calendar, spin_button).
"""
frame = Gtk.Frame(label=label)
frame.set_label_align(0.5, 0.5)
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=CALENDAR_CONTAINER_SPACING)
container.set_margin_start(CALENDAR_CONTAINER_MARGIN)
container.set_margin_end(CALENDAR_CONTAINER_MARGIN)
container.set_margin_top(CALENDAR_CONTAINER_MARGIN)
container.set_margin_bottom(CALENDAR_CONTAINER_MARGIN)
# Year selector
year_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=YEAR_SELECTOR_SPACING)
year_label = Gtk.Label(label=_("Year:"))
year_adjustment = Gtk.Adjustment(
value=current_year,
lower=min_year,
upper=max_year,
step_increment=1,
page_increment=10
)
year_spin = Gtk.SpinButton()
year_spin.set_adjustment(year_adjustment)
year_spin.set_numeric(True)
year_spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID)
year_spin.set_width_chars(6)
year_spin.connect("value-changed", year_changed_handler)
year_box.pack_start(year_label, False, False, 0)
year_box.pack_start(year_spin, False, False, 0)
container.pack_start(year_box, False, False, 0)
# Calendar
calendar = Gtk.Calendar()
calendar.set_display_options(
Gtk.CalendarDisplayOptions.SHOW_HEADING |
Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES |
Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS
)
calendar.connect("day-selected", date_selected_handler)
calendar.connect("month-changed", month_changed_handler)
container.pack_start(calendar, True, True, 0)
frame.add(container)
# Store widgets
self._filter_widgets[calendar_key] = calendar
self._filter_widgets[spin_key] = year_spin
return frame, calendar, year_spin
def _build_date_range_filter_page(self) -> Gtk.Widget:
"""
Build the date range filter page with date chooser widgets.
Returns:
Gtk.Widget: The date range filter page.
"""
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
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=CALENDAR_HORIZONTAL_SPACING)
calendar_box.set_homogeneous(True)
# Year range for genealogical data
current_year = datetime.date.today().year
min_year = MIN_GENEALOGICAL_YEAR
max_year = current_year + 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
)
calendar_box.pack_start(from_frame, True, True, 0)
# 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
)
calendar_box.pack_start(to_frame, True, True, 0)
box.pack_start(calendar_box, True, True, 0)
# Clear button
clear_button = Gtk.Button(label=_("Clear Dates"))
clear_button.connect("clicked", self._on_clear_date_range)
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
button_box.pack_start(clear_button, False, False, 0)
box.pack_start(button_box, False, False, 0)
# Validation label
self.date_validation_label = Gtk.Label()
self.date_validation_label.set_line_wrap(True)
self.date_validation_label.set_markup("<span color='red'></span>")
box.pack_start(self.date_validation_label, False, False, 0)
scrolled.add(box)
return scrolled
def _set_validation_message(self, message: str) -> None:
"""
Set the date validation label message.
Args:
message: Message text to display (empty string to clear).
"""
if hasattr(self, 'date_validation_label'):
if message:
self.date_validation_label.set_markup(
f"<span color='red'>{message}</span>"
)
else:
self.date_validation_label.set_text("")
def _update_year_spin_button(self, spin_key: str, year: int, handler: Callable) -> None:
"""
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 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: def _update_event_type_widgets(self) -> None:
""" """
Update event type filter widgets to reflect current filter state. Update event type filter widgets to reflect current filter state.
@ -1170,6 +1000,7 @@ class MyTimelineView(NavigationView):
# Update category checkboxes based on their children's states # Update category checkboxes based on their children's states
if 'category_checkboxes' in self._filter_widgets and 'category_event_types' in self._filter_widgets: if 'category_checkboxes' in self._filter_widgets and 'category_event_types' in self._filter_widgets:
category_updating_flags = self._filter_widgets.get('category_updating_flags', {})
for category, category_checkbox in self._filter_widgets['category_checkboxes'].items(): for category, category_checkbox in self._filter_widgets['category_checkboxes'].items():
# Get child checkboxes for this category # Get child checkboxes for this category
event_types_in_category = self._filter_widgets['category_event_types'].get(category, []) event_types_in_category = self._filter_widgets['category_event_types'].get(category, [])
@ -1178,8 +1009,10 @@ class MyTimelineView(NavigationView):
for et in event_types_in_category for et in event_types_in_category
if et in self._filter_widgets['event_type_checkboxes'] if et in self._filter_widgets['event_type_checkboxes']
] ]
# Get the updating flag for this category to prevent recursion
updating_flag = category_updating_flags.get(category)
# Update category checkbox state based on children # Update category checkbox state based on children
self._update_group_checkbox_state(category_checkbox, child_checkboxes) self._update_group_checkbox_state(category_checkbox, child_checkboxes, updating_flag)
def _update_person_filter_widgets(self) -> None: def _update_person_filter_widgets(self) -> None:
""" """
@ -1282,7 +1115,7 @@ class MyTimelineView(NavigationView):
self._make_child_toggle_handler(family_checkbox, child_checkboxes, updating_family)) self._make_child_toggle_handler(family_checkbox, child_checkboxes, updating_family))
# Initialize family checkbox state # Initialize family checkbox state
self._update_group_checkbox_state(family_checkbox, child_checkboxes) self._update_group_checkbox_state(family_checkbox, child_checkboxes, updating_family)
expander.add(members_box) expander.add(members_box)
self._filter_widgets['family_expanders'][family_handle] = expander self._filter_widgets['family_expanders'][family_handle] = expander
@ -1292,47 +1125,6 @@ class MyTimelineView(NavigationView):
except (AttributeError, KeyError) as e: except (AttributeError, KeyError) as e:
logger.warning(f"Error updating person filter widgets in filter dialog: {e}", exc_info=True) logger.warning(f"Error updating person filter widgets in filter dialog: {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
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
self._set_validation_message("")
def _update_filter_dialog_state(self) -> None: def _update_filter_dialog_state(self) -> None:
""" """
Update the filter dialog widgets to reflect current filter state. Update the filter dialog widgets to reflect current filter state.
@ -1343,7 +1135,6 @@ class MyTimelineView(NavigationView):
# Update each filter type's widgets # Update each filter type's widgets
self._update_event_type_widgets() self._update_event_type_widgets()
self._update_person_filter_widgets() self._update_person_filter_widgets()
self._update_date_range_widgets()
def _on_filter_dialog_response(self, dialog: Gtk.Dialog, response_id: int) -> None: def _on_filter_dialog_response(self, dialog: Gtk.Dialog, response_id: int) -> None:
""" """
@ -1358,8 +1149,6 @@ class MyTimelineView(NavigationView):
self.filter_enabled = False self.filter_enabled = False
self.active_event_types = set() self.active_event_types = set()
self._update_normalized_active_event_types() # Invalidate cache self._update_normalized_active_event_types() # Invalidate cache
self.date_range_filter = None
self.date_range_explicit = False
self.person_filter = None self.person_filter = None
self.category_filter = None self.category_filter = None
self.apply_filters() self.apply_filters()
@ -1372,10 +1161,23 @@ class MyTimelineView(NavigationView):
# Update event type filter # Update event type filter
if 'event_type_checkboxes' in self._filter_widgets: if 'event_type_checkboxes' in self._filter_widgets:
active_types = set() active_types = set()
total_types = len(self._filter_widgets['event_type_checkboxes'])
for event_type, checkbox in self._filter_widgets['event_type_checkboxes'].items(): for event_type, checkbox in self._filter_widgets['event_type_checkboxes'].items():
if checkbox.get_active(): if checkbox.get_active():
active_types.add(event_type) active_types.add(event_type)
self.active_event_types = active_types if len(active_types) < len(self._filter_widgets['event_type_checkboxes']) else set()
# Logic:
# - If all types are selected: active_event_types = empty set (meaning "all enabled", no filtering)
# - If some types are unselected: active_event_types = set of selected types (filter to show only these)
# - If no types are selected: active_event_types = empty set, but filter_enabled will be False
# (unless other filters are active), so no events will show (correct behavior)
if len(active_types) == total_types:
# All types are selected - no filtering needed
self.active_event_types = set()
else:
# Some or no types are selected - filter to only show selected types
# (If active_types is empty, this means no events will match, which is correct)
self.active_event_types = active_types
self._update_normalized_active_event_types() # Update cache self._update_normalized_active_event_types() # Update cache
# Update category filter # Update category filter
@ -1396,54 +1198,9 @@ class MyTimelineView(NavigationView):
all_persons = set(self._filter_widgets['person_checkboxes']) all_persons = set(self._filter_widgets['person_checkboxes'])
self.person_filter = active_persons if active_persons != all_persons else None self.person_filter = active_persons if active_persons != all_persons else None
# Update date range filter from calendar widgets
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']
# Check if dates were explicitly set (stored in widget data)
# We'll use a simple approach: if user clicked calendars, use the dates
# For now, we'll always use calendar dates if they're valid
# The "Clear Dates" button will reset the explicit flag
# Get Date objects from calendar selections
from_date = self._create_date_from_calendar(from_calendar)
to_date = self._create_date_from_calendar(to_calendar)
if from_date is None or to_date is None:
# Show error message for invalid dates
self._set_validation_message(_('Error: Invalid date in date range filter'))
logger.warning("Error parsing date range in filter dialog: invalid date from calendar")
self.date_range_filter = None
self.date_range_explicit = False
return
min_sort = from_date.get_sort_value()
max_sort = to_date.get_sort_value()
# Validate date range
if min_sort > max_sort:
# Show error message
self._set_validation_message(_('Error: From date must be before To date'))
self.date_range_filter = None
self.date_range_explicit = False
return
else:
# Clear error message
self._set_validation_message("")
# Set filter - user has selected dates in calendars
self.date_range_filter = (min_sort, max_sort)
self.date_range_explicit = True
else:
# No calendar widgets, clear filter
self.date_range_filter = None
self.date_range_explicit = False
# Enable filter if any filter is active # Enable filter if any filter is active
self.filter_enabled = ( self.filter_enabled = (
self.active_event_types or self.active_event_types or
(self.date_range_filter is not None and self.date_range_explicit) or
self.person_filter is not None or self.person_filter is not None or
self.category_filter is not None self.category_filter is not None
) )
@ -1485,185 +1242,6 @@ class MyTimelineView(NavigationView):
for checkbox in self._filter_widgets['event_type_checkboxes'].values(): for checkbox in self._filter_widgets['event_type_checkboxes'].values():
checkbox.set_active(False) checkbox.set_active(False)
def _sync_year_spin_from_calendar(self, calendar: Gtk.Calendar, spin_key: str, handler: Callable) -> None:
"""
Update year spin button to match calendar date.
Args:
calendar: The calendar widget.
spin_key: Key to find the spin button in _filter_widgets.
handler: Handler function to block/unblock during update.
"""
if spin_key in self._filter_widgets:
year, month, day = calendar.get_date()
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 _sync_calendar_from_year_spin(self, calendar_key: str, new_year: int) -> None:
"""
Update calendar to match year spin button value.
Args:
calendar_key: Key to find the calendar in _filter_widgets.
new_year: The new year value to set.
"""
if calendar_key in self._filter_widgets:
calendar = self._filter_widgets[calendar_key]
current_year, current_month, current_day = calendar.get_date()
# Update calendar to new year, keeping same month and day
calendar.select_month(current_month, new_year)
def _create_date_from_calendar(self, calendar: Gtk.Calendar) -> Optional[Date]:
"""
Create a Date object from calendar selection.
Args:
calendar: The calendar widget.
Returns:
Optional[Date]: Date object if successful, None otherwise.
"""
try:
year, month, day = calendar.get_date()
# Note: month from calendar is 0-11, Date.set_yr_mon_day expects 1-12
date_obj = Date()
date_obj.set_yr_mon_day(year, month + 1, day)
return date_obj
except (ValueError, AttributeError, TypeError) as e:
logger.debug(f"Error creating date from calendar: {e}")
return None
def _on_from_date_selected(self, calendar: Gtk.Calendar) -> None:
"""
Handle From date calendar selection.
Args:
calendar: The calendar widget that was selected.
"""
self._sync_year_spin_from_calendar(calendar, 'date_from_year_spin', self._on_from_year_changed)
self._validate_date_range()
def _on_to_date_selected(self, calendar: Gtk.Calendar) -> None:
"""
Handle To date calendar selection.
Args:
calendar: The calendar widget that was selected.
"""
self._sync_year_spin_from_calendar(calendar, 'date_to_year_spin', self._on_to_year_changed)
self._validate_date_range()
def _on_from_calendar_changed(self, calendar: Gtk.Calendar) -> None:
"""
Handle From calendar month/year change.
Args:
calendar: The calendar widget that changed.
"""
self._sync_year_spin_from_calendar(calendar, 'date_from_year_spin', self._on_from_year_changed)
def _on_to_calendar_changed(self, calendar: Gtk.Calendar) -> None:
"""
Handle To calendar month/year change.
Args:
calendar: The calendar widget that changed.
"""
self._sync_year_spin_from_calendar(calendar, 'date_to_year_spin', self._on_to_year_changed)
def _on_from_year_changed(self, spin_button: Gtk.SpinButton) -> None:
"""
Handle From year spin button change.
Args:
spin_button: The year spin button that changed.
"""
new_year = int(spin_button.get_value())
self._sync_calendar_from_year_spin('date_from_calendar', new_year)
self._validate_date_range()
def _on_to_year_changed(self, spin_button: Gtk.SpinButton) -> None:
"""
Handle To year spin button change.
Args:
spin_button: The year spin button that changed.
"""
new_year = int(spin_button.get_value())
self._sync_calendar_from_year_spin('date_to_calendar', new_year)
self._validate_date_range()
def _on_clear_date_range(self, button: Gtk.Button) -> None:
"""
Clear the date range selection.
Args:
button: The clear button that was clicked.
"""
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']
# Reset to current date (calendars always show a date)
# Get current date and set calendars to it
now = datetime.date.today()
from_calendar.select_month(now.month - 1, now.year) # month is 0-11
from_calendar.select_day(now.day)
to_calendar.select_month(now.month - 1, now.year)
to_calendar.select_day(now.day)
# 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)
if 'date_to_year_spin' in self._filter_widgets:
to_year_spin = self._filter_widgets['date_to_year_spin']
to_year_spin.handler_block_by_func(self._on_to_year_changed)
to_year_spin.set_value(now.year)
to_year_spin.handler_unblock_by_func(self._on_to_year_changed)
# Clear validation message
self._set_validation_message("")
# Mark that date range should not be applied
self.date_range_explicit = False
def _validate_date_range(self) -> None:
"""
Validate that From date is not after To date.
"""
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']
from_year, from_month, from_day = from_calendar.get_date()
to_year, to_month, to_day = to_calendar.get_date()
# Use helper method to create dates
from_date = self._create_date_from_calendar(from_calendar)
to_date = self._create_date_from_calendar(to_calendar)
if from_date is None or to_date is None:
return
try:
from_sort = from_date.get_sort_value()
to_sort = to_date.get_sort_value()
if from_sort > to_sort:
self._set_validation_message(_('Warning: From date is after To date'))
else:
self._set_validation_message("")
except (ValueError, AttributeError, TypeError):
pass
def build_tree(self) -> None: def build_tree(self) -> None:
""" """
Rebuilds the current display. Called when the view becomes visible. Rebuilds the current display. Called when the view becomes visible.
@ -1986,9 +1564,15 @@ class MyTimelineView(NavigationView):
if not self.active_event_types: if not self.active_event_types:
self._normalized_active_event_types = None self._normalized_active_event_types = None
else: else:
self._normalized_active_event_types = { # Normalize all event types and create a set
self._normalize_event_type(et) for et in self.active_event_types # Ensure we have at least one normalized type
} normalized_set = {self._normalize_event_type(et) for et in self.active_event_types}
if normalized_set:
self._normalized_active_event_types = normalized_set
else:
# If normalization failed for all types, something is wrong
# Set to None to indicate no filtering (show all events)
self._normalized_active_event_types = None
def _apply_event_type_filter(self, event: TimelineEvent) -> bool: def _apply_event_type_filter(self, event: TimelineEvent) -> bool:
""" """
@ -2000,11 +1584,15 @@ class MyTimelineView(NavigationView):
Returns: Returns:
bool: True if event passes filter, False otherwise. bool: True if event passes filter, False otherwise.
""" """
# If no event type filter is active (empty set means "all enabled"), pass all events
if not self.active_event_types: if not self.active_event_types:
return True return True
# Use pre-computed normalized set if available, otherwise compute it # Use pre-computed normalized set if available, otherwise compute it
if self._normalized_active_event_types is None: if self._normalized_active_event_types is None:
self._update_normalized_active_event_types() self._update_normalized_active_event_types()
# If normalized set is still None or empty, something went wrong - pass all events
if self._normalized_active_event_types is None or not self._normalized_active_event_types:
return True
# Normalize event.event_type and compare with 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) event_type_normalized = self._normalize_event_type(event.event_type)
return event_type_normalized in self._normalized_active_event_types return event_type_normalized in self._normalized_active_event_types
@ -2043,10 +1631,6 @@ class MyTimelineView(NavigationView):
if not self._apply_event_type_filter(event): if not self._apply_event_type_filter(event):
continue continue
# Check date range filter
if not self._is_date_in_range(event.date_sort):
continue
# Check person filter # Check person filter
person_handle = event.person.get_handle() if event.person else None person_handle = event.person.get_handle() if event.person else None
if not self._is_person_included(person_handle): if not self._is_person_included(person_handle):
@ -2109,21 +1693,6 @@ class MyTimelineView(NavigationView):
normalized_type = self._normalize_event_type(event_type) normalized_type = self._normalize_event_type(event_type)
return normalized_type in self._normalized_active_event_types return normalized_type in self._normalized_active_event_types
def _is_date_in_range(self, date_sort: int) -> bool:
"""
Check if a date is within the filter range.
Args:
date_sort: The date sort value to check.
Returns:
bool: True if date is in range (or no filter active), False otherwise.
"""
if not self.date_range_filter or not self.date_range_explicit:
return True
min_date, max_date = self.date_range_filter
return min_date <= date_sort <= max_date
def _is_person_included(self, person_handle: Optional[str]) -> bool: def _is_person_included(self, person_handle: Optional[str]) -> bool:
""" """
Check if a person is included in the filter. Check if a person is included in the filter.