diff --git a/MyTimeline.py b/MyTimeline.py
index 0daaae7..f20b59f 100644
--- a/MyTimeline.py
+++ b/MyTimeline.py
@@ -29,6 +29,7 @@ MyTimeline View - A vertical timeline showing all events in the database
# -------------------------------------------------------------------------
import cairo
import colorsys
+import datetime
import logging
import math
from dataclasses import dataclass
@@ -104,10 +105,16 @@ DATE_SORT_MONTH_MULTIPLIER = 100 # Month component in date sort value
# UI Layout Constants
FILTER_PAGE_MARGIN = 10 # Margin for filter page containers
FILTER_PAGE_SPACING = 10 # Spacing in filter page containers
+FILTER_PAGE_VERTICAL_SPACING = 5 # Vertical spacing within filter pages
+FILTER_CATEGORY_SPACING = 2 # Spacing within category containers
+FILTER_INDENT_MARGIN = 20 # Indent margin for nested elements
CALENDAR_CONTAINER_MARGIN = 5 # Margin for calendar containers
CALENDAR_CONTAINER_SPACING = 5 # Spacing in calendar containers
CALENDAR_HORIZONTAL_SPACING = 15 # Spacing between calendar frames
YEAR_SELECTOR_SPACING = 5 # Spacing in year selector boxes
+TOOLBAR_SPACING = 5 # Spacing in toolbar
+TOOLBAR_ITEM_MARGIN = 10 # Margin for toolbar items
+TOOLTIP_SEPARATOR_LENGTH = 30 # Length of tooltip separator line
# Font Constants
FONT_FAMILY = "Sans"
@@ -551,7 +558,7 @@ class MyTimelineView(NavigationView):
Gtk.Widget: The main container widget with toolbar and drawing area.
"""
# Main container
- main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
+ main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=TOOLBAR_SPACING)
# Toolbar with zoom controls
toolbar = Gtk.Toolbar()
@@ -565,8 +572,8 @@ class MyTimelineView(NavigationView):
# Zoom label
self.zoom_label = Gtk.Label(label="100%")
- self.zoom_label.set_margin_start(10)
- self.zoom_label.set_margin_end(10)
+ self.zoom_label.set_margin_start(TOOLBAR_ITEM_MARGIN)
+ self.zoom_label.set_margin_end(TOOLBAR_ITEM_MARGIN)
zoom_item = Gtk.ToolItem()
zoom_item.add(self.zoom_label)
toolbar.insert(zoom_item, 1)
@@ -796,14 +803,14 @@ class MyTimelineView(NavigationView):
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)
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=FILTER_PAGE_VERTICAL_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)
# Select All / Deselect All buttons
- button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
+ button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=FILTER_PAGE_VERTICAL_SPACING)
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"))
@@ -837,8 +844,8 @@ class MyTimelineView(NavigationView):
box.pack_start(category_checkbox, False, False, 0)
# Create container for event types in this category
- category_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
- category_box.set_margin_start(20)
+ category_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=FILTER_CATEGORY_SPACING)
+ category_box.set_margin_start(FILTER_INDENT_MARGIN)
category_boxes[category] = category_box
box.pack_start(category_box, False, False, 0)
@@ -877,11 +884,11 @@ class MyTimelineView(NavigationView):
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)
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=FILTER_PAGE_VERTICAL_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)
# Get unique categories
categories = sorted(set(EVENT_CATEGORIES.values()))
@@ -906,11 +913,11 @@ class MyTimelineView(NavigationView):
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)
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=FILTER_PAGE_VERTICAL_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)
# Person checkboxes container - will be populated when dialog is shown
person_checkboxes = {}
@@ -1022,7 +1029,6 @@ class MyTimelineView(NavigationView):
calendar_box.set_homogeneous(True)
# Year range for genealogical data
- import datetime
current_year = datetime.date.today().year
min_year = MIN_GENEALOGICAL_YEAR
max_year = current_year + 10
@@ -1155,10 +1161,10 @@ class MyTimelineView(NavigationView):
family_name = self._get_family_display_name(family)
# Create container for family members
- members_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
- members_box.set_margin_start(20)
- members_box.set_margin_top(5)
- members_box.set_margin_bottom(5)
+ members_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=FILTER_CATEGORY_SPACING + 1)
+ members_box.set_margin_start(FILTER_INDENT_MARGIN)
+ members_box.set_margin_top(CALENDAR_CONTAINER_MARGIN)
+ members_box.set_margin_bottom(CALENDAR_CONTAINER_MARGIN)
# Collect all child checkboxes for this family
child_checkboxes = []
@@ -1224,7 +1230,7 @@ class MyTimelineView(NavigationView):
container.show_all()
except (AttributeError, KeyError) as e:
- logger.warning(f"Error updating person filter: {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:
"""
@@ -1256,7 +1262,6 @@ class MyTimelineView(NavigationView):
self._update_year_spin_button('date_to_year_spin', to_year, self._on_to_year_changed)
else:
# Reset to current date
- import datetime
now = datetime.date.today()
from_calendar.select_month(now.month - 1, now.year)
to_calendar.select_month(now.month - 1, now.year)
@@ -1342,49 +1347,42 @@ class MyTimelineView(NavigationView):
# For now, we'll always use calendar dates if they're valid
# The "Clear Dates" button will reset the explicit flag
- # Get selected dates from calendars
- # Gtk.Calendar.get_date() returns (year, month, day) where month is 0-11
- from_year, from_month, from_day = from_calendar.get_date()
- to_year, to_month, to_day = to_calendar.get_date()
+ # Get Date objects from calendar selections
+ from_date = self._create_date_from_calendar(from_calendar)
+ to_date = self._create_date_from_calendar(to_calendar)
- try:
- # Create Date objects from calendar selections
- # Note: month from calendar is 0-11, Date.set_yr_mon_day expects 1-12
- from_date = Date()
- from_date.set_yr_mon_day(from_year, from_month + 1, from_day)
- min_sort = from_date.get_sort_value()
-
- to_date = Date()
- to_date.set_yr_mon_day(to_year, to_month + 1, to_day)
- max_sort = to_date.get_sort_value()
-
- # Validate date range
- if min_sort > max_sort:
- # Show error message
- if hasattr(self, 'date_validation_label'):
- self.date_validation_label.set_markup(
- f"{_('Error: From date must be before To date')}"
- )
- self.date_range_filter = None
- self.date_range_explicit = False
- return
- else:
- # Clear error message
- if hasattr(self, 'date_validation_label'):
- self.date_validation_label.set_text("")
-
- # Set filter - user has selected dates in calendars
- self.date_range_filter = (min_sort, max_sort)
- self.date_range_explicit = True
-
- except (ValueError, AttributeError, TypeError) as e:
- logger.warning(f"Error parsing date range: {e}", exc_info=True)
- self.date_range_filter = None
- self.date_range_explicit = False
+ if from_date is None or to_date is None:
+ # Show error message for invalid dates
if hasattr(self, 'date_validation_label'):
self.date_validation_label.set_markup(
- f"{_('Error: Invalid date')}"
+ f"{_('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
+ if hasattr(self, 'date_validation_label'):
+ self.date_validation_label.set_markup(
+ f"{_('Error: From date must be before To date')}"
+ )
+ self.date_range_filter = None
+ self.date_range_explicit = False
+ return
+ else:
+ # Clear error message
+ if hasattr(self, 'date_validation_label'):
+ self.date_validation_label.set_text("")
+
+ # 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
@@ -1435,6 +1433,56 @@ 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.
@@ -1442,14 +1490,7 @@ class MyTimelineView(NavigationView):
Args:
calendar: The calendar widget that was selected.
"""
- # Update year spin button to match calendar
- if 'date_from_year_spin' in self._filter_widgets:
- year, month, day = calendar.get_date()
- year_spin = self._filter_widgets['date_from_year_spin']
- year_spin.handler_block_by_func(self._on_from_year_changed)
- year_spin.set_value(year)
- year_spin.handler_unblock_by_func(self._on_from_year_changed)
-
+ 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:
@@ -1459,14 +1500,7 @@ class MyTimelineView(NavigationView):
Args:
calendar: The calendar widget that was selected.
"""
- # Update year spin button to match calendar
- if 'date_to_year_spin' in self._filter_widgets:
- year, month, day = calendar.get_date()
- year_spin = self._filter_widgets['date_to_year_spin']
- year_spin.handler_block_by_func(self._on_to_year_changed)
- year_spin.set_value(year)
- year_spin.handler_unblock_by_func(self._on_to_year_changed)
-
+ 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:
@@ -1476,13 +1510,7 @@ class MyTimelineView(NavigationView):
Args:
calendar: The calendar widget that changed.
"""
- # Update year spin button to match calendar
- if 'date_from_year_spin' in self._filter_widgets:
- year, month, day = calendar.get_date()
- year_spin = self._filter_widgets['date_from_year_spin']
- year_spin.handler_block_by_func(self._on_from_year_changed)
- year_spin.set_value(year)
- year_spin.handler_unblock_by_func(self._on_from_year_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:
"""
@@ -1491,13 +1519,7 @@ class MyTimelineView(NavigationView):
Args:
calendar: The calendar widget that changed.
"""
- # Update year spin button to match calendar
- if 'date_to_year_spin' in self._filter_widgets:
- year, month, day = calendar.get_date()
- year_spin = self._filter_widgets['date_to_year_spin']
- year_spin.handler_block_by_func(self._on_to_year_changed)
- year_spin.set_value(year)
- year_spin.handler_unblock_by_func(self._on_to_year_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:
"""
@@ -1506,14 +1528,9 @@ class MyTimelineView(NavigationView):
Args:
spin_button: The year spin button that changed.
"""
- if 'date_from_calendar' in self._filter_widgets:
- calendar = self._filter_widgets['date_from_calendar']
- new_year = int(spin_button.get_value())
- 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)
- # Trigger validation
- self._validate_date_range()
+ 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:
"""
@@ -1522,14 +1539,9 @@ class MyTimelineView(NavigationView):
Args:
spin_button: The year spin button that changed.
"""
- if 'date_to_calendar' in self._filter_widgets:
- calendar = self._filter_widgets['date_to_calendar']
- new_year = int(spin_button.get_value())
- 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)
- # Trigger validation
- self._validate_date_range()
+ 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:
"""
@@ -1544,7 +1556,6 @@ class MyTimelineView(NavigationView):
# Reset to current date (calendars always show a date)
# Get current date and set calendars to it
- import datetime
now = datetime.date.today()
from_calendar.select_month(now.month - 1, now.year) # month is 0-11
from_calendar.select_day(now.day)
@@ -1682,6 +1693,8 @@ class MyTimelineView(NavigationView):
self._cached_min_date = None
self._cached_max_date = None
self._event_to_person_cache.clear() # Clear event-to-person cache
+ # Clear event type normalization cache to prevent stale data
+ self._event_type_normalization_cache.clear()
def _calculate_timeline_height(self) -> None:
"""Calculate and set timeline height based on number of events and zoom."""
@@ -1798,7 +1811,7 @@ class MyTimelineView(NavigationView):
except (AttributeError, KeyError):
continue
except (AttributeError, KeyError) as e:
- logger.warning(f"Error building event-to-person index: {e}", exc_info=True)
+ logger.warning(f"Error building event-to-person index from database: {e}", exc_info=True)
def _find_person_for_event(self, event: 'Event') -> Optional['Person']:
"""
@@ -1893,7 +1906,7 @@ class MyTimelineView(NavigationView):
self.all_events.append(timeline_event)
except (AttributeError, KeyError) as e:
- logger.warning(f"Error collecting events: {e}", exc_info=True)
+ logger.warning(f"Error collecting events from database: {e}", exc_info=True)
return
# Sort events by date
@@ -2395,6 +2408,19 @@ class MyTimelineView(NavigationView):
self._adjusted_events_cache = None
self._cache_key = None
+ def _update_zoom(self, level: float) -> None:
+ """
+ Update zoom level and refresh the display.
+
+ Args:
+ level: The new zoom level to set.
+ """
+ self.zoom_level = level
+ self.update_zoom_display()
+ self._recalculate_timeline_height()
+ if self.drawing_area:
+ self.drawing_area.queue_draw()
+
def on_zoom_in(self, widget: Gtk.Widget) -> None:
"""
Zoom in.
@@ -2403,11 +2429,8 @@ class MyTimelineView(NavigationView):
widget: The widget that triggered the zoom (unused).
"""
if self.zoom_level < self.max_zoom:
- self.zoom_level = min(self.zoom_level + self.zoom_step, self.max_zoom)
- self.update_zoom_display()
- self._recalculate_timeline_height() # Only recalculate height, not events
- if self.drawing_area:
- self.drawing_area.queue_draw()
+ new_level = min(self.zoom_level + self.zoom_step, self.max_zoom)
+ self._update_zoom(new_level)
def on_zoom_out(self, widget: Gtk.Widget) -> None:
"""
@@ -2417,11 +2440,8 @@ class MyTimelineView(NavigationView):
widget: The widget that triggered the zoom (unused).
"""
if self.zoom_level > self.min_zoom:
- self.zoom_level = max(self.zoom_level - self.zoom_step, self.min_zoom)
- self.update_zoom_display()
- self._recalculate_timeline_height() # Only recalculate height, not events
- if self.drawing_area:
- self.drawing_area.queue_draw()
+ new_level = max(self.zoom_level - self.zoom_step, self.min_zoom)
+ self._update_zoom(new_level)
def on_zoom_reset(self, widget: Gtk.Widget) -> None:
"""
@@ -2430,11 +2450,7 @@ class MyTimelineView(NavigationView):
Args:
widget: The widget that triggered the reset (unused).
"""
- self.zoom_level = 1.0
- self.update_zoom_display()
- self._recalculate_timeline_height() # Only recalculate height, not events
- if self.drawing_area:
- self.drawing_area.queue_draw()
+ self._update_zoom(1.0)
def on_scroll(self, widget: Gtk.Widget, event: Gdk.Event) -> bool:
"""
@@ -2695,7 +2711,7 @@ class MyTimelineView(NavigationView):
if place:
return place.get_title()
except (AttributeError, KeyError) as e:
- logger.debug(f"Error accessing place for event: {e}")
+ logger.debug(f"Error accessing place information for event in tooltip: {e}")
return None
def _format_person_tooltip(self, person: 'Person', person_events: List[TimelineEvent]) -> str:
@@ -2711,7 +2727,7 @@ class MyTimelineView(NavigationView):
"""
person_name = name_displayer.display(person)
tooltip_text = f"{person_name}\n"
- tooltip_text += "─" * 30 + "\n"
+ tooltip_text += "─" * TOOLTIP_SEPARATOR_LENGTH + "\n"
# Sort by date
person_events_sorted = sorted(person_events, key=lambda x: x.date_sort)
@@ -3237,7 +3253,7 @@ class MyTimelineView(NavigationView):
if max_year is None or year > max_year:
max_year = year
except (AttributeError, ValueError) as e:
- logger.debug(f"Error accessing year from date: {e}")
+ logger.debug(f"Error extracting year from date for timeline year marker: {e}")
return (min_year, max_year)