Add comprehensive event filtering to MyTimeline view
- Add filter dialog with tabs for event types, categories, persons, and date range - Implement EVENT_CATEGORIES mapping for event categorization - Add filter state management (event types, date range, person, category filters) - Fix EventType normalization for dictionary lookups and comparisons - Add _normalize_event_type() helper method for consistent EventType handling - Update event collection to store all events and apply filters - Add filter button to toolbar with active state indication - Support multiple filter types that can be combined - Fix event collection bug (events now stored in all_events before filtering)
This commit is contained in:
parent
891a9664d8
commit
1b6d76583c
691
MyTimeline.py
691
MyTimeline.py
@ -31,7 +31,7 @@ import cairo
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Tuple, Any
|
||||
from typing import Optional, List, Tuple, Any, Set, Dict
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
@ -248,6 +248,74 @@ EVENT_SHAPES = {
|
||||
EventType.CUSTOM: 'square',
|
||||
}
|
||||
|
||||
# Event category mapping for filtering (using EventType objects as keys, like EVENT_COLORS and EVENT_SHAPES)
|
||||
# Note: EventType members are already integers in Gramps, not enum objects with .value
|
||||
EVENT_CATEGORIES = {
|
||||
# Life Events
|
||||
EventType.BIRTH: "Life Events",
|
||||
EventType.DEATH: "Life Events",
|
||||
EventType.BURIAL: "Life Events",
|
||||
EventType.CREMATION: "Life Events",
|
||||
EventType.ADOPT: "Life Events",
|
||||
|
||||
# Family Events
|
||||
EventType.MARRIAGE: "Family Events",
|
||||
EventType.DIVORCE: "Family Events",
|
||||
EventType.ENGAGEMENT: "Family Events",
|
||||
EventType.MARR_SETTL: "Family Events",
|
||||
EventType.MARR_LIC: "Family Events",
|
||||
EventType.MARR_CONTR: "Family Events",
|
||||
EventType.MARR_BANNS: "Family Events",
|
||||
EventType.DIV_FILING: "Family Events",
|
||||
EventType.ANNULMENT: "Family Events",
|
||||
EventType.MARR_ALT: "Family Events",
|
||||
|
||||
# Religious Events
|
||||
EventType.BAPTISM: "Religious Events",
|
||||
EventType.ADULT_CHRISTEN: "Religious Events",
|
||||
EventType.CONFIRMATION: "Religious Events",
|
||||
EventType.CHRISTEN: "Religious Events",
|
||||
EventType.FIRST_COMMUN: "Religious Events",
|
||||
EventType.BLESS: "Religious Events",
|
||||
EventType.BAR_MITZVAH: "Religious Events",
|
||||
EventType.BAS_MITZVAH: "Religious Events",
|
||||
EventType.RELIGION: "Religious Events",
|
||||
EventType.ORDINATION: "Religious Events",
|
||||
|
||||
# Vocational Events
|
||||
EventType.OCCUPATION: "Vocational Events",
|
||||
EventType.RETIREMENT: "Vocational Events",
|
||||
EventType.ELECTED: "Vocational Events",
|
||||
EventType.MILITARY_SERV: "Vocational Events",
|
||||
|
||||
# Academic Events
|
||||
EventType.EDUCATION: "Academic Events",
|
||||
EventType.GRADUATION: "Academic Events",
|
||||
EventType.DEGREE: "Academic Events",
|
||||
|
||||
# Travel Events
|
||||
EventType.EMIGRATION: "Travel Events",
|
||||
EventType.IMMIGRATION: "Travel Events",
|
||||
EventType.NATURALIZATION: "Travel Events",
|
||||
|
||||
# Legal Events
|
||||
EventType.PROBATE: "Legal Events",
|
||||
EventType.WILL: "Legal Events",
|
||||
|
||||
# Residence Events
|
||||
EventType.RESIDENCE: "Residence Events",
|
||||
EventType.CENSUS: "Residence Events",
|
||||
EventType.PROPERTY: "Residence Events",
|
||||
|
||||
# Other Events
|
||||
EventType.CAUSE_DEATH: "Other Events",
|
||||
EventType.MED_INFO: "Other Events",
|
||||
EventType.NOB_TITLE: "Other Events",
|
||||
EventType.NUM_MARRIAGES: "Other Events",
|
||||
EventType.UNKNOWN: "Other Events",
|
||||
EventType.CUSTOM: "Other Events",
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
#
|
||||
@ -321,6 +389,19 @@ class MyTimelineView(NavigationView):
|
||||
self._cached_min_date: Optional[int] = None
|
||||
self._cached_max_date: Optional[int] = None
|
||||
|
||||
# Filter state
|
||||
self.filter_enabled: bool = False
|
||||
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.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.all_events: List[TimelineEvent] = [] # Store all events before filtering
|
||||
|
||||
# Filter UI components
|
||||
self.filter_button = None
|
||||
self.filter_dialog = None
|
||||
self._filter_widgets = {}
|
||||
|
||||
# Initialize temporary surface for text measurement (used in find_event_at_position)
|
||||
self._temp_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
|
||||
|
||||
@ -412,6 +493,7 @@ class MyTimelineView(NavigationView):
|
||||
db: The new database object.
|
||||
"""
|
||||
self.active_family_handle = None
|
||||
self.all_events = []
|
||||
self.events = []
|
||||
|
||||
# Connect to new database signals
|
||||
@ -465,6 +547,12 @@ class MyTimelineView(NavigationView):
|
||||
|
||||
toolbar.insert(Gtk.SeparatorToolItem(), 4)
|
||||
|
||||
# Filter button
|
||||
self.filter_button = Gtk.ToolButton(icon_name="view-filter-symbolic")
|
||||
self.filter_button.set_tooltip_text(_("Filter Events"))
|
||||
self.filter_button.connect("clicked", self.on_filter_button_clicked)
|
||||
toolbar.insert(self.filter_button, 5)
|
||||
|
||||
main_box.pack_start(toolbar, False, False, 0)
|
||||
|
||||
# Scrolled window
|
||||
@ -495,6 +583,457 @@ class MyTimelineView(NavigationView):
|
||||
|
||||
return main_box
|
||||
|
||||
def on_filter_button_clicked(self, button: Gtk.ToolButton) -> None:
|
||||
"""
|
||||
Handle filter button click - show filter dialog.
|
||||
|
||||
Args:
|
||||
button: The filter button that was clicked.
|
||||
"""
|
||||
if self.filter_dialog is None:
|
||||
self.filter_dialog = self._build_filter_dialog()
|
||||
|
||||
# Update filter dialog state
|
||||
self._update_filter_dialog_state()
|
||||
|
||||
# Show dialog
|
||||
response = self.filter_dialog.run()
|
||||
if response == Gtk.ResponseType.APPLY:
|
||||
self._apply_filter_dialog_settings()
|
||||
elif response == Gtk.ResponseType.CLOSE:
|
||||
pass # Just close
|
||||
|
||||
self.filter_dialog.hide()
|
||||
|
||||
def _build_filter_dialog(self) -> Gtk.Dialog:
|
||||
"""
|
||||
Build the filter dialog with all filter controls.
|
||||
|
||||
Returns:
|
||||
Gtk.Dialog: The filter dialog widget.
|
||||
"""
|
||||
dialog = Gtk.Dialog(title=_("Filter Events"), parent=self.uistate.window)
|
||||
dialog.set_default_size(600, 700)
|
||||
dialog.add_button(_("Clear All"), Gtk.ResponseType.REJECT)
|
||||
dialog.add_button(_("Close"), Gtk.ResponseType.CLOSE)
|
||||
dialog.add_button(_("Apply"), Gtk.ResponseType.APPLY)
|
||||
|
||||
# Connect to clear button
|
||||
dialog.connect("response", self._on_filter_dialog_response)
|
||||
|
||||
# Main content area
|
||||
content_area = dialog.get_content_area()
|
||||
content_area.set_spacing(10)
|
||||
content_area.set_margin_start(10)
|
||||
content_area.set_margin_end(10)
|
||||
content_area.set_margin_top(10)
|
||||
content_area.set_margin_bottom(10)
|
||||
|
||||
# Notebook for organizing filters
|
||||
notebook = Gtk.Notebook()
|
||||
content_area.pack_start(notebook, True, True, 0)
|
||||
|
||||
# Store widget references for later access
|
||||
self._filter_widgets = {}
|
||||
|
||||
# Event Type Filter Page
|
||||
event_type_page = self._build_event_type_filter_page()
|
||||
notebook.append_page(event_type_page, Gtk.Label(label=_("Event Types")))
|
||||
|
||||
# Category Filter Page
|
||||
category_page = self._build_category_filter_page()
|
||||
notebook.append_page(category_page, Gtk.Label(label=_("Categories")))
|
||||
|
||||
# Person Filter Page
|
||||
person_page = self._build_person_filter_page()
|
||||
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()
|
||||
return dialog
|
||||
|
||||
def _build_event_type_filter_page(self) -> Gtk.Widget:
|
||||
"""
|
||||
Build the event type filter page with checkboxes for each event type.
|
||||
|
||||
Returns:
|
||||
Gtk.Widget: The event type filter page.
|
||||
"""
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
box.set_margin_start(10)
|
||||
box.set_margin_end(10)
|
||||
box.set_margin_top(10)
|
||||
box.set_margin_bottom(10)
|
||||
|
||||
# Select All / Deselect All buttons
|
||||
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
select_all_btn = Gtk.Button(label=_("Select All"))
|
||||
select_all_btn.connect("clicked", self._on_select_all_event_types)
|
||||
deselect_all_btn = Gtk.Button(label=_("Deselect All"))
|
||||
deselect_all_btn.connect("clicked", self._on_deselect_all_event_types)
|
||||
button_box.pack_start(select_all_btn, False, False, 0)
|
||||
button_box.pack_start(deselect_all_btn, False, False, 0)
|
||||
box.pack_start(button_box, False, False, 0)
|
||||
|
||||
# Group event types by category
|
||||
event_type_checkboxes = {}
|
||||
category_boxes = {}
|
||||
|
||||
# Iterate over event types from EVENT_COLORS (which uses EventType integers as keys)
|
||||
# EventType members are already integers in Gramps
|
||||
for event_type_obj in EVENT_COLORS.keys():
|
||||
# EventType is already an integer, use it directly
|
||||
if event_type_obj not in EVENT_CATEGORIES:
|
||||
continue
|
||||
|
||||
category = EVENT_CATEGORIES[event_type_obj]
|
||||
if category not in category_boxes:
|
||||
# Create category section
|
||||
category_label = Gtk.Label(label=f"<b>{category}</b>")
|
||||
category_label.set_use_markup(True)
|
||||
category_label.set_halign(Gtk.Align.START)
|
||||
box.pack_start(category_label, False, False, 0)
|
||||
|
||||
category_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
category_box.set_margin_start(20)
|
||||
category_boxes[category] = category_box
|
||||
box.pack_start(category_box, False, False, 0)
|
||||
|
||||
# Create checkbox for event type
|
||||
checkbox = Gtk.CheckButton(label=str(event_type_obj))
|
||||
event_type_checkboxes[event_type_obj] = checkbox
|
||||
category_boxes[category].pack_start(checkbox, False, False, 0)
|
||||
|
||||
self._filter_widgets['event_type_checkboxes'] = event_type_checkboxes
|
||||
|
||||
scrolled.add(box)
|
||||
return scrolled
|
||||
|
||||
def _build_category_filter_page(self) -> Gtk.Widget:
|
||||
"""
|
||||
Build the category filter page with checkboxes for each category.
|
||||
|
||||
Returns:
|
||||
Gtk.Widget: The category filter page.
|
||||
"""
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
box.set_margin_start(10)
|
||||
box.set_margin_end(10)
|
||||
box.set_margin_top(10)
|
||||
box.set_margin_bottom(10)
|
||||
|
||||
# Get unique categories
|
||||
categories = sorted(set(EVENT_CATEGORIES.values()))
|
||||
|
||||
category_checkboxes = {}
|
||||
for category in categories:
|
||||
checkbox = Gtk.CheckButton(label=category)
|
||||
category_checkboxes[category] = checkbox
|
||||
box.pack_start(checkbox, False, False, 0)
|
||||
|
||||
self._filter_widgets['category_checkboxes'] = category_checkboxes
|
||||
|
||||
return box
|
||||
|
||||
def _build_person_filter_page(self) -> Gtk.Widget:
|
||||
"""
|
||||
Build the person filter page with checkboxes for each family member.
|
||||
|
||||
Returns:
|
||||
Gtk.Widget: The person filter page.
|
||||
"""
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
box.set_margin_start(10)
|
||||
box.set_margin_end(10)
|
||||
box.set_margin_top(10)
|
||||
box.set_margin_bottom(10)
|
||||
|
||||
# Person checkboxes container - will be populated when dialog is shown
|
||||
person_checkboxes = {}
|
||||
self._filter_widgets['person_checkboxes'] = person_checkboxes
|
||||
self._filter_widgets['person_container'] = box
|
||||
|
||||
info_label = Gtk.Label(label=_("Select family members to include in the timeline."))
|
||||
info_label.set_line_wrap(True)
|
||||
box.pack_start(info_label, False, False, 0)
|
||||
|
||||
scrolled.add(box)
|
||||
return scrolled
|
||||
|
||||
def _build_date_range_filter_page(self) -> Gtk.Widget:
|
||||
"""
|
||||
Build the date range filter page with date entry fields.
|
||||
|
||||
Returns:
|
||||
Gtk.Widget: The date range filter page.
|
||||
"""
|
||||
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)
|
||||
|
||||
info_label = Gtk.Label(label=_("Enter date range to filter events. Leave empty to show all dates."))
|
||||
info_label.set_line_wrap(True)
|
||||
box.pack_start(info_label, False, False, 0)
|
||||
|
||||
# From date
|
||||
from_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
from_label = Gtk.Label(label=_("From:"))
|
||||
from_label.set_size_request(80, -1)
|
||||
from_entry = Gtk.Entry()
|
||||
from_entry.set_placeholder_text(_("YYYY-MM-DD or YYYY"))
|
||||
from_box.pack_start(from_label, False, False, 0)
|
||||
from_box.pack_start(from_entry, True, True, 0)
|
||||
box.pack_start(from_box, False, False, 0)
|
||||
|
||||
# To date
|
||||
to_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
to_label = Gtk.Label(label=_("To:"))
|
||||
to_label.set_size_request(80, -1)
|
||||
to_entry = Gtk.Entry()
|
||||
to_entry.set_placeholder_text(_("YYYY-MM-DD or YYYY"))
|
||||
to_box.pack_start(to_label, False, False, 0)
|
||||
to_box.pack_start(to_entry, True, True, 0)
|
||||
box.pack_start(to_box, False, False, 0)
|
||||
|
||||
self._filter_widgets['date_from_entry'] = from_entry
|
||||
self._filter_widgets['date_to_entry'] = to_entry
|
||||
|
||||
return box
|
||||
|
||||
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 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)
|
||||
|
||||
# Update category checkboxes
|
||||
if 'category_checkboxes' in self._filter_widgets:
|
||||
for category, checkbox in self._filter_widgets['category_checkboxes'].items():
|
||||
if not self.category_filter:
|
||||
checkbox.set_active(True) # All selected when filter is off
|
||||
else:
|
||||
checkbox.set_active(category in self.category_filter)
|
||||
|
||||
# Update person checkboxes
|
||||
if 'person_checkboxes' in self._filter_widgets and 'person_container' in self._filter_widgets:
|
||||
# Clear existing person checkboxes
|
||||
container = self._filter_widgets['person_container']
|
||||
for checkbox in list(self._filter_widgets['person_checkboxes'].values()):
|
||||
container.remove(checkbox)
|
||||
checkbox.destroy()
|
||||
self._filter_widgets['person_checkboxes'].clear()
|
||||
|
||||
# Add current family members
|
||||
if self.active_family_handle:
|
||||
try:
|
||||
family = self.dbstate.db.get_family_from_handle(self.active_family_handle)
|
||||
if family:
|
||||
# Father
|
||||
father_handle = family.get_father_handle()
|
||||
if father_handle:
|
||||
father = self.dbstate.db.get_person_from_handle(father_handle)
|
||||
if father:
|
||||
checkbox = Gtk.CheckButton(label=name_displayer.display(father))
|
||||
checkbox.set_active(True if not self.person_filter else father_handle in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][father_handle] = checkbox
|
||||
container.pack_start(checkbox, False, False, 0)
|
||||
|
||||
# Mother
|
||||
mother_handle = family.get_mother_handle()
|
||||
if mother_handle:
|
||||
mother = self.dbstate.db.get_person_from_handle(mother_handle)
|
||||
if mother:
|
||||
checkbox = Gtk.CheckButton(label=name_displayer.display(mother))
|
||||
checkbox.set_active(True if not self.person_filter else mother_handle in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][mother_handle] = checkbox
|
||||
container.pack_start(checkbox, False, False, 0)
|
||||
|
||||
# Children
|
||||
for child_ref in family.get_child_ref_list():
|
||||
child = self.dbstate.db.get_person_from_handle(child_ref.ref)
|
||||
if child:
|
||||
checkbox = Gtk.CheckButton(label=name_displayer.display(child))
|
||||
checkbox.set_active(True if not self.person_filter else child_ref.ref in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][child_ref.ref] = checkbox
|
||||
container.pack_start(checkbox, False, False, 0)
|
||||
|
||||
container.show_all()
|
||||
except (AttributeError, KeyError) as e:
|
||||
logger.warning(f"Error updating person filter: {e}", exc_info=True)
|
||||
|
||||
# Update date range entries
|
||||
if 'date_from_entry' in self._filter_widgets and 'date_to_entry' in self._filter_widgets:
|
||||
from_entry = self._filter_widgets['date_from_entry']
|
||||
to_entry = self._filter_widgets['date_to_entry']
|
||||
if self.date_range_filter:
|
||||
min_date, max_date = self.date_range_filter
|
||||
# Convert sort values back to dates for display (simplified)
|
||||
from_entry.set_text("")
|
||||
to_entry.set_text("")
|
||||
else:
|
||||
from_entry.set_text("")
|
||||
to_entry.set_text("")
|
||||
|
||||
def _on_filter_dialog_response(self, dialog: Gtk.Dialog, response_id: int) -> None:
|
||||
"""
|
||||
Handle filter dialog response.
|
||||
|
||||
Args:
|
||||
dialog: The filter dialog.
|
||||
response_id: The response ID (APPLY, CLOSE, REJECT).
|
||||
"""
|
||||
if response_id == Gtk.ResponseType.REJECT:
|
||||
# Clear all filters
|
||||
self.filter_enabled = False
|
||||
self.active_event_types = set()
|
||||
self.date_range_filter = None
|
||||
self.person_filter = None
|
||||
self.category_filter = None
|
||||
self.apply_filters()
|
||||
self._update_filter_button_state()
|
||||
|
||||
def _apply_filter_dialog_settings(self) -> None:
|
||||
"""
|
||||
Apply filter settings from the dialog to the filter state.
|
||||
"""
|
||||
# Update event type filter
|
||||
if 'event_type_checkboxes' in self._filter_widgets:
|
||||
active_types = set()
|
||||
for event_type, checkbox in self._filter_widgets['event_type_checkboxes'].items():
|
||||
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()
|
||||
|
||||
# Update category filter
|
||||
if 'category_checkboxes' in self._filter_widgets:
|
||||
active_categories = set()
|
||||
for category, checkbox in self._filter_widgets['category_checkboxes'].items():
|
||||
if checkbox.get_active():
|
||||
active_categories.add(category)
|
||||
all_categories = set(self._filter_widgets['category_checkboxes'].keys())
|
||||
self.category_filter = active_categories if active_categories != all_categories else None
|
||||
|
||||
# Update person filter
|
||||
if 'person_checkboxes' in self._filter_widgets:
|
||||
active_persons = set()
|
||||
for person_handle, checkbox in self._filter_widgets['person_checkboxes'].items():
|
||||
if checkbox.get_active():
|
||||
active_persons.add(person_handle)
|
||||
all_persons = set(self._filter_widgets['person_checkboxes'].keys())
|
||||
self.person_filter = active_persons if active_persons != all_persons else None
|
||||
|
||||
# Update date range filter
|
||||
if 'date_from_entry' in self._filter_widgets and 'date_to_entry' in self._filter_widgets:
|
||||
from_text = self._filter_widgets['date_from_entry'].get_text().strip()
|
||||
to_text = self._filter_widgets['date_to_entry'].get_text().strip()
|
||||
|
||||
if from_text or to_text:
|
||||
# Parse dates using Gramps Date objects
|
||||
try:
|
||||
min_sort = None
|
||||
max_sort = None
|
||||
|
||||
if from_text:
|
||||
# Try to parse the date string
|
||||
# Support formats: YYYY, YYYY-MM, YYYY-MM-DD
|
||||
parts = from_text.split('-')
|
||||
year = int(parts[0])
|
||||
month = int(parts[1]) if len(parts) > 1 else 1
|
||||
day = int(parts[2]) if len(parts) > 2 else 1
|
||||
|
||||
from_date = Date()
|
||||
from_date.set_yr_mon_day(year, month, day)
|
||||
min_sort = from_date.get_sort_value()
|
||||
|
||||
if to_text:
|
||||
# Try to parse the date string
|
||||
parts = to_text.split('-')
|
||||
year = int(parts[0])
|
||||
month = int(parts[1]) if len(parts) > 1 else 12
|
||||
day = int(parts[2]) if len(parts) > 2 else 31
|
||||
|
||||
to_date = Date()
|
||||
to_date.set_yr_mon_day(year, month, day)
|
||||
max_sort = to_date.get_sort_value()
|
||||
|
||||
# If only one date is provided, set reasonable defaults
|
||||
if min_sort is None:
|
||||
min_sort = 0
|
||||
if max_sort is None:
|
||||
max_sort = 99999999
|
||||
|
||||
self.date_range_filter = (min_sort, max_sort) if (from_text or to_text) else None
|
||||
except (ValueError, AttributeError, TypeError) as e:
|
||||
logger.warning(f"Error parsing date range: {e}", exc_info=True)
|
||||
self.date_range_filter = None
|
||||
else:
|
||||
self.date_range_filter = None
|
||||
|
||||
# Enable filter if any filter is active
|
||||
self.filter_enabled = (
|
||||
self.active_event_types or
|
||||
self.date_range_filter is not None or
|
||||
self.person_filter is not None or
|
||||
self.category_filter is not None
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
self.apply_filters()
|
||||
self._update_filter_button_state()
|
||||
|
||||
def _update_filter_button_state(self) -> None:
|
||||
"""
|
||||
Update the filter button visual state to indicate if filters are active.
|
||||
"""
|
||||
if self.filter_button:
|
||||
if self.filter_enabled:
|
||||
# Highlight button when filters are active
|
||||
self.filter_button.set_tooltip_text(_("Filter Events (Active)"))
|
||||
else:
|
||||
self.filter_button.set_tooltip_text(_("Filter Events"))
|
||||
|
||||
def _on_select_all_event_types(self, button: Gtk.Button) -> None:
|
||||
"""
|
||||
Select all event type checkboxes.
|
||||
|
||||
Args:
|
||||
button: The button that was clicked.
|
||||
"""
|
||||
if 'event_type_checkboxes' in self._filter_widgets:
|
||||
for checkbox in self._filter_widgets['event_type_checkboxes'].values():
|
||||
checkbox.set_active(True)
|
||||
|
||||
def _on_deselect_all_event_types(self, button: Gtk.Button) -> None:
|
||||
"""
|
||||
Deselect all event type checkboxes.
|
||||
|
||||
Args:
|
||||
button: The button that was clicked.
|
||||
"""
|
||||
if 'event_type_checkboxes' in self._filter_widgets:
|
||||
for checkbox in self._filter_widgets['event_type_checkboxes'].values():
|
||||
checkbox.set_active(False)
|
||||
|
||||
def build_tree(self) -> None:
|
||||
"""
|
||||
Rebuilds the current display. Called when the view becomes visible.
|
||||
@ -579,7 +1118,7 @@ class MyTimelineView(NavigationView):
|
||||
for event_ref in event_refs:
|
||||
timeline_event = self._process_event_ref(event_ref, person_obj)
|
||||
if timeline_event:
|
||||
self.events.append(timeline_event)
|
||||
self.all_events.append(timeline_event)
|
||||
|
||||
def _collect_family_member_events(self, handle: Optional[str], role: str) -> None:
|
||||
"""
|
||||
@ -609,7 +1148,7 @@ class MyTimelineView(NavigationView):
|
||||
for event_ref in family.get_event_ref_list():
|
||||
timeline_event = self._process_event_ref(event_ref, None)
|
||||
if timeline_event:
|
||||
self.events.append(timeline_event)
|
||||
self.all_events.append(timeline_event)
|
||||
|
||||
def _invalidate_cache(self) -> None:
|
||||
"""Invalidate all caches when events change."""
|
||||
@ -636,6 +1175,7 @@ class MyTimelineView(NavigationView):
|
||||
|
||||
def collect_events(self) -> None:
|
||||
"""Collect all events for the active family."""
|
||||
self.all_events = []
|
||||
self.events = []
|
||||
|
||||
if not self.active_family_handle:
|
||||
@ -664,7 +1204,10 @@ class MyTimelineView(NavigationView):
|
||||
self._collect_family_member_events(child_ref.ref, "child")
|
||||
|
||||
# Sort events by date
|
||||
self.events.sort(key=lambda x: x.date_sort)
|
||||
self.all_events.sort(key=lambda x: x.date_sort)
|
||||
|
||||
# Apply filters
|
||||
self.events = self._apply_filters(self.all_events)
|
||||
|
||||
# Invalidate cache when events change
|
||||
self._invalidate_cache()
|
||||
@ -672,6 +1215,146 @@ class MyTimelineView(NavigationView):
|
||||
# Calculate timeline height
|
||||
self._calculate_timeline_height()
|
||||
|
||||
# Update filter button state
|
||||
self._update_filter_button_state()
|
||||
|
||||
def _apply_filters(self, events: List[TimelineEvent]) -> List[TimelineEvent]:
|
||||
"""
|
||||
Apply all active filters to events.
|
||||
|
||||
Args:
|
||||
events: List of TimelineEvent objects to filter.
|
||||
|
||||
Returns:
|
||||
List[TimelineEvent]: Filtered list of events.
|
||||
"""
|
||||
if not self.filter_enabled:
|
||||
return events
|
||||
|
||||
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
|
||||
|
||||
# Check date range filter
|
||||
if not self._is_date_in_range(event.date_sort):
|
||||
continue
|
||||
|
||||
# Check person filter
|
||||
person_handle = event.person.get_handle() if event.person else None
|
||||
if not self._is_person_included(person_handle):
|
||||
continue
|
||||
|
||||
# Check category filter
|
||||
category = self._get_event_category(event.event_type)
|
||||
if self.category_filter and category not in self.category_filter:
|
||||
continue
|
||||
|
||||
filtered.append(event)
|
||||
|
||||
return filtered
|
||||
|
||||
def _normalize_event_type(self, event_type: EventType) -> int:
|
||||
"""
|
||||
Normalize EventType to integer for comparison.
|
||||
|
||||
Args:
|
||||
event_type: The event type (may be EventType object or integer).
|
||||
|
||||
Returns:
|
||||
int: The integer value of the event type.
|
||||
"""
|
||||
try:
|
||||
if isinstance(event_type, int):
|
||||
return event_type
|
||||
elif hasattr(event_type, 'value'):
|
||||
return event_type.value
|
||||
else:
|
||||
return int(event_type)
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
return 0 # Default to 0 if conversion fails
|
||||
|
||||
def _is_event_type_enabled(self, event_type: EventType) -> bool:
|
||||
"""
|
||||
Check if an event type is enabled in the filter.
|
||||
|
||||
Args:
|
||||
event_type: The event type to check.
|
||||
|
||||
Returns:
|
||||
bool: True if event type is enabled (or no filter active), False otherwise.
|
||||
"""
|
||||
if not self.active_event_types:
|
||||
return True
|
||||
# Normalize both for comparison
|
||||
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
|
||||
|
||||
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:
|
||||
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:
|
||||
"""
|
||||
Check if a person is included in the filter.
|
||||
|
||||
Args:
|
||||
person_handle: The person handle to check (None for family events).
|
||||
|
||||
Returns:
|
||||
bool: True if person is included (or no filter active), False otherwise.
|
||||
"""
|
||||
if self.person_filter is None:
|
||||
return True
|
||||
# Family events (person_handle is None) are included if person_filter is not None
|
||||
# but we might want to exclude them - for now, include them
|
||||
if person_handle is None:
|
||||
return True
|
||||
return person_handle in self.person_filter
|
||||
|
||||
def _get_event_category(self, event_type: EventType) -> str:
|
||||
"""
|
||||
Get the category for an event type.
|
||||
|
||||
Args:
|
||||
event_type: The event type (may be EventType object or integer).
|
||||
|
||||
Returns:
|
||||
str: The category name, or "Other Events" if not found.
|
||||
"""
|
||||
# Normalize EventType to integer for dictionary lookup
|
||||
event_type_value = self._normalize_event_type(event_type)
|
||||
return EVENT_CATEGORIES.get(event_type_value, "Other Events")
|
||||
|
||||
def apply_filters(self) -> None:
|
||||
"""
|
||||
Apply current filters and update the view.
|
||||
Public method to trigger filter application.
|
||||
"""
|
||||
if self.all_events:
|
||||
self.events = self._apply_filters(self.all_events)
|
||||
self._invalidate_cache()
|
||||
self._calculate_timeline_height()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
|
||||
def _calculate_date_range(self) -> Tuple[int, int, int]:
|
||||
"""
|
||||
Calculate date range from events.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user