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:
parent
cb572c4dc5
commit
0a9ecca878
531
MyTimeline.py
531
MyTimeline.py
@ -431,7 +431,7 @@ class MyTimelineView(NavigationView):
|
||||
"""
|
||||
View for displaying a vertical timeline of all events in the database.
|
||||
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:
|
||||
@ -486,8 +486,6 @@ class MyTimelineView(NavigationView):
|
||||
# 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.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.category_filter: Optional[Set[str]] = None # Set of event categories to include, None = all
|
||||
self.all_events: List[TimelineEvent] = [] # Store all events before filtering
|
||||
@ -690,9 +688,6 @@ class MyTimelineView(NavigationView):
|
||||
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 _calculate_group_state(self, child_checkboxes: List[Gtk.CheckButton]) -> str:
|
||||
@ -718,14 +713,22 @@ class MyTimelineView(NavigationView):
|
||||
return 'some'
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
group_checkbox: The parent group checkbox to update.
|
||||
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)
|
||||
|
||||
if state == 'all':
|
||||
@ -737,6 +740,10 @@ class MyTimelineView(NavigationView):
|
||||
else: # 'some'
|
||||
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],
|
||||
updating_flag: List[bool]) -> Callable[[Gtk.Widget], None]:
|
||||
"""
|
||||
@ -784,7 +791,7 @@ class MyTimelineView(NavigationView):
|
||||
def handler(widget: Gtk.Widget) -> None:
|
||||
if updating_flag[0]:
|
||||
return
|
||||
self._update_group_checkbox_state(group_checkbox, child_checkboxes)
|
||||
self._update_group_checkbox_state(group_checkbox, child_checkboxes, updating_flag)
|
||||
|
||||
return handler
|
||||
|
||||
@ -831,10 +838,6 @@ class MyTimelineView(NavigationView):
|
||||
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
|
||||
|
||||
@ -869,6 +872,7 @@ class MyTimelineView(NavigationView):
|
||||
category_boxes = {}
|
||||
category_checkboxes = {}
|
||||
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
|
||||
for event_type_obj in EVENT_COLORS:
|
||||
@ -905,6 +909,7 @@ class MyTimelineView(NavigationView):
|
||||
|
||||
# Flag to prevent recursion between category and child checkboxes
|
||||
updating_category = [False]
|
||||
category_updating_flags[category] = updating_category
|
||||
|
||||
# Connect category checkbox to toggle all children
|
||||
category_checkbox.connect("toggled",
|
||||
@ -918,6 +923,7 @@ class MyTimelineView(NavigationView):
|
||||
self._filter_widgets['event_type_checkboxes'] = event_type_checkboxes
|
||||
self._filter_widgets['category_checkboxes'] = category_checkboxes
|
||||
self._filter_widgets['category_event_types'] = category_event_types
|
||||
self._filter_widgets['category_updating_flags'] = category_updating_flags
|
||||
|
||||
scrolled.add(box)
|
||||
return scrolled
|
||||
@ -978,182 +984,6 @@ 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.
|
||||
|
||||
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:
|
||||
"""
|
||||
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
|
||||
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():
|
||||
# Get child checkboxes for this 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
|
||||
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
|
||||
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:
|
||||
"""
|
||||
@ -1282,7 +1115,7 @@ class MyTimelineView(NavigationView):
|
||||
self._make_child_toggle_handler(family_checkbox, child_checkboxes, updating_family))
|
||||
|
||||
# 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)
|
||||
self._filter_widgets['family_expanders'][family_handle] = expander
|
||||
@ -1292,47 +1125,6 @@ class MyTimelineView(NavigationView):
|
||||
except (AttributeError, KeyError) as e:
|
||||
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:
|
||||
"""
|
||||
Update the filter dialog widgets to reflect current filter state.
|
||||
@ -1343,7 +1135,6 @@ class MyTimelineView(NavigationView):
|
||||
# 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:
|
||||
"""
|
||||
@ -1358,8 +1149,6 @@ class MyTimelineView(NavigationView):
|
||||
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
|
||||
self.category_filter = None
|
||||
self.apply_filters()
|
||||
@ -1372,10 +1161,23 @@ class MyTimelineView(NavigationView):
|
||||
# Update event type filter
|
||||
if 'event_type_checkboxes' in self._filter_widgets:
|
||||
active_types = set()
|
||||
total_types = len(self._filter_widgets['event_type_checkboxes'])
|
||||
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()
|
||||
|
||||
# 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
|
||||
|
||||
# Update category filter
|
||||
@ -1396,54 +1198,9 @@ class MyTimelineView(NavigationView):
|
||||
all_persons = set(self._filter_widgets['person_checkboxes'])
|
||||
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
|
||||
self.filter_enabled = (
|
||||
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.category_filter is not None
|
||||
)
|
||||
@ -1485,185 +1242,6 @@ class MyTimelineView(NavigationView):
|
||||
for checkbox in self._filter_widgets['event_type_checkboxes'].values():
|
||||
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:
|
||||
"""
|
||||
Rebuilds the current display. Called when the view becomes visible.
|
||||
@ -1986,9 +1564,15 @@ class MyTimelineView(NavigationView):
|
||||
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
|
||||
}
|
||||
# Normalize all event types and create a set
|
||||
# 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:
|
||||
"""
|
||||
@ -2000,11 +1584,15 @@ class MyTimelineView(NavigationView):
|
||||
Returns:
|
||||
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:
|
||||
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()
|
||||
# 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
|
||||
event_type_normalized = self._normalize_event_type(event.event_type)
|
||||
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):
|
||||
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):
|
||||
@ -2109,21 +1693,6 @@ class MyTimelineView(NavigationView):
|
||||
normalized_type = self._normalize_event_type(event_type)
|
||||
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:
|
||||
"""
|
||||
Check if a person is included in the filter.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user