diff --git a/MyTimeline.py b/MyTimeline.py
index 00671b7..c074673 100644
--- a/MyTimeline.py
+++ b/MyTimeline.py
@@ -244,6 +244,7 @@ class MyTimelineView(NavigationView):
self.tooltip_timeout_id = None
self.tooltip_window = None
self.expanded_event_index = None
+ self.selected_person_handle = None
self.mouse_x = 0
self.mouse_y = 0
@@ -512,6 +513,53 @@ class MyTimelineView(NavigationView):
if self.drawing_area:
self.drawing_area.set_size_request(800, self.timeline_height)
+ def detect_label_overlaps(self, context, events_with_y_pos, timeline_y_start, timeline_y_end):
+ """Detect and adjust Y positions to prevent label overlaps."""
+ if not events_with_y_pos:
+ return events_with_y_pos
+
+ # Create a temporary layout to measure text
+ layout = PangoCairo.create_layout(context)
+ layout.set_font_description(Pango.font_description_from_string("Sans 11"))
+
+ adjusted_events = []
+ min_spacing = 30 # Minimum spacing between labels in pixels
+
+ for i, event_data in enumerate(events_with_y_pos):
+ date_sort, date_obj, event, person, event_type, expanded, y_pos = event_data
+
+ # Calculate label height
+ if person:
+ person_name = name_displayer.display(person)
+ date_str = get_date(event)
+ event_type_str = str(event_type)
+ label_text = f"{date_str} - {event_type_str} - {person_name}"
+ else:
+ date_str = get_date(event)
+ event_type_str = str(event_type)
+ label_text = f"{date_str} - {event_type_str}"
+
+ layout.set_text(label_text, -1)
+ text_width, text_height = layout.get_pixel_size()
+ label_height = text_height + 16 # Add padding
+
+ # Check for overlap with previous events
+ adjusted_y = y_pos
+ for prev_data in adjusted_events:
+ prev_y_pos = prev_data[6] # Get y_pos from previous event
+ # Check if labels would overlap
+ if abs(adjusted_y - prev_y_pos) < min_spacing:
+ # Adjust downward
+ adjusted_y = prev_y_pos + min_spacing
+
+ # Ensure adjusted position is within bounds
+ adjusted_y = max(timeline_y_start, min(adjusted_y, timeline_y_end))
+
+ # Create new event data with adjusted Y position
+ adjusted_events.append((date_sort, date_obj, event, person, event_type, expanded, adjusted_y))
+
+ return adjusted_events
+
def on_zoom_in(self, widget):
"""Zoom in."""
if self.zoom_level < self.max_zoom:
@@ -559,11 +607,33 @@ class MyTimelineView(NavigationView):
# Find which event was clicked
clicked_index = self.find_event_at_position(event.x, event.y)
if clicked_index is not None:
- # Toggle expansion
- if self.expanded_event_index == clicked_index:
- self.expanded_event_index = None
+ # Check if clicking on marker (for person selection) or label (for expansion)
+ date_sort, date_obj, clicked_event, clicked_person, event_type, expanded, _ = self.events[clicked_index]
+
+ # Check if click is on marker area (left side) or label area (right side)
+ timeline_x = TIMELINE_MARGIN_LEFT
+ marker_area_width = EVENT_MARKER_SIZE * self.zoom_level + 20
+
+ if event.x < timeline_x + marker_area_width:
+ # Click on marker - toggle person selection
+ if clicked_person:
+ person_handle = clicked_person.get_handle()
+ if self.selected_person_handle == person_handle:
+ # Deselect if clicking same person
+ self.selected_person_handle = None
+ else:
+ # Select this person
+ self.selected_person_handle = person_handle
+ else:
+ # No person for this event, deselect
+ self.selected_person_handle = None
else:
- self.expanded_event_index = clicked_index
+ # Click on label - toggle expansion
+ if self.expanded_event_index == clicked_index:
+ self.expanded_event_index = None
+ else:
+ self.expanded_event_index = clicked_index
+
self.drawing_area.queue_draw()
return False
@@ -649,37 +719,69 @@ class MyTimelineView(NavigationView):
return None
def show_tooltip(self, event_index, x_root, y_root):
- """Show tooltip for an event."""
+ """Show tooltip for an event, including all events for that person."""
if event_index is None or event_index >= len(self.events):
return False
date_sort, date_obj, event, person, event_type, expanded, y_pos = self.events[event_index]
- # Build tooltip text
- date_str = get_date(event)
- event_type_str = str(event_type)
-
- tooltip_text = f"{event_type_str}\n{date_str}"
-
+ # If event has a person, show all events for that person
if person:
+ person_handle = person.get_handle()
person_name = name_displayer.display(person)
- tooltip_text += f"\n{person_name}"
-
- # Get place information
- place_handle = event.get_place_handle()
- if place_handle:
- try:
- place = self.dbstate.db.get_place_from_handle(place_handle)
- if place:
- place_name = place.get_title()
- tooltip_text += f"\n{place_name}"
- except:
- pass
-
- # Get description
- description = event.get_description()
- if description:
- tooltip_text += f"\n{description}"
+
+ # Find all events for this person
+ person_events = []
+ for evt_data in self.events:
+ evt_date_sort, evt_date_obj, evt_event, evt_person, evt_event_type, evt_expanded, evt_y_pos = evt_data
+ if evt_person and evt_person.get_handle() == person_handle:
+ person_events.append((evt_date_sort, evt_date_obj, evt_event, evt_event_type))
+
+ # Sort by date
+ person_events.sort(key=lambda x: x[0])
+
+ # Build tooltip text with person name as header
+ tooltip_text = f"{person_name}\n"
+ tooltip_text += "ā" * 30 + "\n"
+
+ # List all events for this person
+ for evt_date_sort, evt_date_obj, evt_event, evt_event_type in person_events:
+ evt_date_str = get_date(evt_event)
+ evt_event_type_str = str(evt_event_type)
+ tooltip_text += f"{evt_event_type_str} - {evt_date_str}\n"
+
+ # Add place if available
+ evt_place_handle = evt_event.get_place_handle()
+ if evt_place_handle:
+ try:
+ evt_place = self.dbstate.db.get_place_from_handle(evt_place_handle)
+ if evt_place:
+ evt_place_name = evt_place.get_title()
+ tooltip_text += f" š {evt_place_name}\n"
+ except:
+ pass
+ else:
+ # Family event (no person) - show single event info
+ date_str = get_date(event)
+ event_type_str = str(event_type)
+
+ tooltip_text = f"{event_type_str}\n{date_str}"
+
+ # Get place information
+ place_handle = event.get_place_handle()
+ if place_handle:
+ try:
+ place = self.dbstate.db.get_place_from_handle(place_handle)
+ if place:
+ place_name = place.get_title()
+ tooltip_text += f"\nš {place_name}"
+ except:
+ pass
+
+ # Get description
+ description = event.get_description()
+ if description:
+ tooltip_text += f"\n{description}"
# Create tooltip window
self.tooltip_window = Gtk.Window(type=Gtk.WindowType.POPUP)
@@ -772,24 +874,35 @@ class MyTimelineView(NavigationView):
context.line_to(timeline_x, timeline_y_end)
context.stroke()
- # Draw events
+ # Calculate initial Y positions based on dates
+ events_with_y_pos = []
for i, event_data in enumerate(self.events):
- date_sort, date_obj, event, person, event_type, expanded, y_pos = event_data
+ date_sort, date_obj, event, person, event_type, expanded, _ = event_data
# Calculate Y position based on date
y_pos = TIMELINE_MARGIN_TOP + (
(date_sort - min_date) / date_range
) * (timeline_y_end - timeline_y_start)
- # Update y_pos in event data
- self.events[i] = event_data[:6] + (y_pos,)
+ events_with_y_pos.append((date_sort, date_obj, event, person, event_type, expanded, y_pos))
+
+ # Detect and fix label overlaps
+ events_with_y_pos = self.detect_label_overlaps(context, events_with_y_pos, timeline_y_start, timeline_y_end)
+
+ # Draw events
+ for i, event_data in enumerate(events_with_y_pos):
+ date_sort, date_obj, event, person, event_type, expanded, y_pos = event_data
# Check if this event is hovered or expanded
is_hovered = (i == self.hovered_event_index)
is_expanded = (i == self.expanded_event_index)
+ # Check if this event belongs to selected person
+ is_selected = (self.selected_person_handle is not None and
+ person and person.get_handle() == self.selected_person_handle)
+
# Draw event marker with modern styling
- self.draw_event_marker(context, timeline_x, y_pos, event_type, is_hovered)
+ self.draw_event_marker(context, timeline_x, y_pos, event_type, is_hovered, is_selected)
# Draw event label
label_x = timeline_x + 25
@@ -797,12 +910,16 @@ class MyTimelineView(NavigationView):
context, label_x, y_pos, date_obj, event, person, event_type, is_hovered, is_expanded
)
+ # Draw visual connections for selected person
+ if self.selected_person_handle is not None:
+ self.draw_person_connections(context, events_with_y_pos, timeline_x, timeline_y_start, timeline_y_end)
+
# Draw year markers on the left
self.draw_year_markers(context, timeline_x, timeline_y_start, timeline_y_end, min_date, max_date)
context.restore()
- def draw_event_marker(self, context, x, y, event_type, is_hovered=False):
+ def draw_event_marker(self, context, x, y, event_type, is_hovered=False, is_selected=False):
"""Draw a marker for an event with modern styling."""
context.save()
@@ -814,9 +931,15 @@ class MyTimelineView(NavigationView):
shape = EVENT_SHAPES.get(event_type_value, 'square')
marker_size = EVENT_MARKER_SIZE
- # Increase size if hovered
+ # Increase size if hovered or selected
if is_hovered:
marker_size *= 1.3
+ elif is_selected:
+ marker_size *= 1.2
+
+ # Use highlight color if selected
+ if is_selected:
+ color = (0.2, 0.4, 0.9) # Blue highlight for selected person's events
# Draw shadow
context.set_source_rgba(0.0, 0.0, 0.0, 0.3)
@@ -1064,6 +1187,57 @@ class MyTimelineView(NavigationView):
context.restore()
+ def draw_person_connections(self, context, events_with_y_pos, timeline_x, timeline_y_start, timeline_y_end):
+ """Draw visual connections between all events of the selected person."""
+ if not self.selected_person_handle:
+ return
+
+ # Find all events for the selected person
+ person_events = []
+ for event_data in events_with_y_pos:
+ date_sort, date_obj, event, person, event_type, expanded, y_pos = event_data
+ if person and person.get_handle() == self.selected_person_handle:
+ person_events.append((y_pos, event_data))
+
+ if len(person_events) < 1:
+ return
+
+ # Sort by Y position
+ person_events.sort(key=lambda x: x[0])
+
+ context.save()
+
+ # Draw connecting lines
+ context.set_source_rgba(0.2, 0.4, 0.9, 0.5) # Semi-transparent blue
+ context.set_line_width(2)
+ context.set_line_cap(cairo.LINE_CAP_ROUND)
+ context.set_line_join(cairo.LINE_JOIN_ROUND)
+
+ # Draw lines from timeline axis to each event marker
+ for y_pos, event_data in person_events:
+ context.move_to(timeline_x, y_pos)
+ context.line_to(timeline_x + EVENT_MARKER_SIZE * 1.5, y_pos)
+ context.stroke()
+
+ # Draw curved path connecting all events (optional - can be commented out if too cluttered)
+ if len(person_events) > 1:
+ # Draw a subtle curved path connecting all markers
+ context.set_source_rgba(0.2, 0.4, 0.9, 0.3) # More transparent
+ context.set_line_width(1.5)
+
+ # Create a smooth curve through all points
+ y_positions = [y for y, _ in person_events]
+ min_y = min(y_positions)
+ max_y = max(y_positions)
+
+ # Draw a vertical line on the left side connecting the range
+ if max_y - min_y > EVENT_MARKER_SIZE * 2:
+ context.move_to(timeline_x - 15, min_y)
+ context.line_to(timeline_x - 15, max_y)
+ context.stroke()
+
+ context.restore()
+
def get_stock(self):
"""Return the stock icon name."""
return "gramps-family"