Add multiple person selection with colored connection lines
- Changed from single selected_person_handle to selected_person_handles set - Added color generation using HSV color space for distinct person colors - Added unique X positions for each person's vertical connection line - Updated click handler to toggle persons in/out of selection set - Refactored draw_person_connections to draw separate colored lines for each person - Each selected person gets their own colored vertical line at unique X position - Colors are consistent for each person based on handle hash
This commit is contained in:
parent
c9f6e7f8b8
commit
f6069dfd83
125
MyTimeline.py
125
MyTimeline.py
@ -28,6 +28,7 @@ MyTimeline View - A vertical timeline showing all events in the database
|
||||
#
|
||||
# -------------------------------------------------------------------------
|
||||
import cairo
|
||||
import colorsys
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("plugin.mytimeline")
|
||||
@ -109,6 +110,7 @@ BORDER_OPACITY = 0.3
|
||||
CONNECTION_LINE_COLOR = (0.2, 0.5, 1.0, 0.75) # Brighter, more opaque blue
|
||||
CONNECTION_LINE_WIDTH = 3.5
|
||||
CONNECTION_VERTICAL_LINE_X = 5 # Left of year markers
|
||||
CONNECTION_LINE_SPACING = 25 # Pixels between vertical lines for different persons
|
||||
|
||||
# Marker State Colors
|
||||
SELECTED_MARKER_COLOR = (0.2, 0.4, 0.9) # Blue highlight for selected person's events
|
||||
@ -381,7 +383,8 @@ class MyTimelineView(NavigationView):
|
||||
self.hovered_event_index = None
|
||||
self.tooltip_timeout_id = None
|
||||
self.tooltip_window = None
|
||||
self.selected_person_handle = None
|
||||
self.selected_person_handles: Set[str] = set() # Set of selected person handles
|
||||
self.person_colors: Dict[str, Tuple[float, float, float, float]] = {} # Person handle -> RGBA color
|
||||
self.mouse_x = 0
|
||||
self.mouse_y = 0
|
||||
|
||||
@ -493,6 +496,9 @@ class MyTimelineView(NavigationView):
|
||||
self.active_event_handle = None
|
||||
self.all_events = []
|
||||
self.events = []
|
||||
# Clear selected persons and colors
|
||||
self.selected_person_handles.clear()
|
||||
self.person_colors.clear()
|
||||
|
||||
# Connect to new database signals
|
||||
if db and db.is_open():
|
||||
@ -2124,6 +2130,56 @@ class MyTimelineView(NavigationView):
|
||||
"""
|
||||
return person is not None and person.get_handle() == handle
|
||||
|
||||
def _get_person_color(self, person_handle: str) -> Tuple[float, float, float, float]:
|
||||
"""
|
||||
Get or generate a distinct color for a person handle.
|
||||
Uses HSL color space to generate evenly distributed colors.
|
||||
|
||||
Args:
|
||||
person_handle: The handle of the person.
|
||||
|
||||
Returns:
|
||||
Tuple[float, float, float, float]: RGBA color tuple (values 0-1).
|
||||
"""
|
||||
# If color already assigned, return it
|
||||
if person_handle in self.person_colors:
|
||||
return self.person_colors[person_handle]
|
||||
|
||||
# Generate a new color based on the person's position in selection
|
||||
# Use hash of handle for consistent color assignment
|
||||
num_selected = len(self.selected_person_handles)
|
||||
if num_selected == 0:
|
||||
# Default color if no selection (shouldn't happen)
|
||||
color = (0.2, 0.5, 1.0, 0.75)
|
||||
else:
|
||||
# Generate color using HSL: vary hue, keep saturation and lightness constant
|
||||
# Use hash of handle for consistent color even when selection order changes
|
||||
handle_hash = hash(person_handle)
|
||||
hue = (abs(handle_hash) % 360) / 360.0 # 0-1 range
|
||||
saturation = 0.7 # High saturation for vibrant colors
|
||||
lightness = 0.5 # Medium lightness for good visibility
|
||||
|
||||
# Convert HSV to RGB (HSV is Hue, Saturation, Value)
|
||||
r, g, b = colorsys.hsv_to_rgb(hue, saturation, lightness)
|
||||
alpha = 0.75 # Semi-transparent
|
||||
color = (r, g, b, alpha)
|
||||
|
||||
# Store the color
|
||||
self.person_colors[person_handle] = color
|
||||
return color
|
||||
|
||||
def _get_person_vertical_line_x(self, person_index: int) -> float:
|
||||
"""
|
||||
Calculate the X position for a person's vertical connection line.
|
||||
|
||||
Args:
|
||||
person_index: The index of the person in the sorted selection (0-based).
|
||||
|
||||
Returns:
|
||||
float: The X coordinate for the vertical line.
|
||||
"""
|
||||
return CONNECTION_VERTICAL_LINE_X + (person_index * CONNECTION_LINE_SPACING)
|
||||
|
||||
def _calculate_y_position(self, date_sort: int, min_date: int, date_range: int,
|
||||
timeline_y_start: float, timeline_y_end: float) -> float:
|
||||
"""
|
||||
@ -2350,18 +2406,26 @@ class MyTimelineView(NavigationView):
|
||||
if self.active_event_handle == event_handle:
|
||||
# Deselect if clicking same event
|
||||
self.active_event_handle = None
|
||||
self.selected_person_handle = None
|
||||
else:
|
||||
# Select this event
|
||||
self.active_event_handle = event_handle
|
||||
# Set selected person based on this event's person
|
||||
if clicked_event_data.person:
|
||||
self.selected_person_handle = clicked_event_data.person.get_handle()
|
||||
else:
|
||||
self.selected_person_handle = None
|
||||
# Navigate to this event in the view
|
||||
self.uistate.set_active(event_handle, "Event")
|
||||
|
||||
# Toggle person selection (always toggle, even if event is same)
|
||||
if clicked_event_data.person:
|
||||
person_handle = clicked_event_data.person.get_handle()
|
||||
if person_handle in self.selected_person_handles:
|
||||
# Remove from selection
|
||||
self.selected_person_handles.remove(person_handle)
|
||||
# Clean up color if no longer selected
|
||||
if person_handle in self.person_colors:
|
||||
del self.person_colors[person_handle]
|
||||
else:
|
||||
# Add to selection
|
||||
self.selected_person_handles.add(person_handle)
|
||||
# Color will be assigned when drawing
|
||||
|
||||
self.drawing_area.queue_draw()
|
||||
return False
|
||||
|
||||
@ -2827,7 +2891,7 @@ class MyTimelineView(NavigationView):
|
||||
self._draw_events(context, events_with_y_pos, timeline_x)
|
||||
|
||||
# Draw visual connections for selected person (from selected event)
|
||||
if self.selected_person_handle is not None:
|
||||
if self.selected_person_handles:
|
||||
self.draw_person_connections(context, events_with_y_pos, timeline_x,
|
||||
timeline_y_start, timeline_y_end)
|
||||
|
||||
@ -3201,7 +3265,8 @@ class MyTimelineView(NavigationView):
|
||||
timeline_x: float, timeline_y_start: float,
|
||||
timeline_y_end: float) -> None:
|
||||
"""
|
||||
Draw visual connections between all events of the selected person.
|
||||
Draw visual connections between all events of selected persons.
|
||||
Each person gets their own colored vertical line at a unique X position.
|
||||
|
||||
Args:
|
||||
context: Cairo drawing context.
|
||||
@ -3210,33 +3275,39 @@ class MyTimelineView(NavigationView):
|
||||
timeline_y_start: Y coordinate of the timeline start.
|
||||
timeline_y_end: Y coordinate of the timeline end.
|
||||
"""
|
||||
if not self.selected_person_handle:
|
||||
if not self.selected_person_handles:
|
||||
return
|
||||
|
||||
# Find all events for the selected person
|
||||
context.save()
|
||||
context.set_line_width(CONNECTION_LINE_WIDTH)
|
||||
context.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
context.set_line_join(cairo.LINE_JOIN_ROUND)
|
||||
|
||||
# Sort person handles for consistent ordering (affects X position)
|
||||
sorted_person_handles = sorted(self.selected_person_handles)
|
||||
|
||||
# Draw connections for each selected person
|
||||
for person_index, person_handle in enumerate(sorted_person_handles):
|
||||
# Get color for this person
|
||||
person_color = self._get_person_color(person_handle)
|
||||
|
||||
# Calculate X position for this person's vertical line
|
||||
vertical_line_x = self._get_person_vertical_line_x(person_index)
|
||||
|
||||
# Find all events for this person
|
||||
person_events = [
|
||||
event_data for event_data in events_with_y_pos
|
||||
if self._person_matches_handle(event_data.person, self.selected_person_handle)
|
||||
if self._person_matches_handle(event_data.person, person_handle)
|
||||
]
|
||||
|
||||
if len(person_events) < 1:
|
||||
return
|
||||
continue
|
||||
|
||||
# Sort by Y position
|
||||
person_events.sort(key=lambda x: x.y_pos)
|
||||
|
||||
context.save()
|
||||
|
||||
# Position vertical line to the left of year markers
|
||||
# Year labels are positioned at timeline_x - 20 - text_width (around x=90-130)
|
||||
# Position vertical line at CONNECTION_VERTICAL_LINE_X to be clearly left of all year markers
|
||||
vertical_line_x = CONNECTION_VERTICAL_LINE_X
|
||||
|
||||
# Draw connecting lines - more visible with brighter color and increased opacity
|
||||
context.set_source_rgba(*CONNECTION_LINE_COLOR)
|
||||
context.set_line_width(CONNECTION_LINE_WIDTH)
|
||||
context.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
context.set_line_join(cairo.LINE_JOIN_ROUND)
|
||||
# Set color for this person's lines
|
||||
context.set_source_rgba(*person_color)
|
||||
|
||||
# Draw vertical connector line on the left side
|
||||
if len(person_events) > 1:
|
||||
@ -3246,15 +3317,11 @@ class MyTimelineView(NavigationView):
|
||||
|
||||
# Draw vertical line connecting all events
|
||||
if max_y - min_y > EVENT_MARKER_SIZE * 2:
|
||||
context.set_source_rgba(*CONNECTION_LINE_COLOR)
|
||||
context.set_line_width(CONNECTION_LINE_WIDTH)
|
||||
context.move_to(vertical_line_x, min_y)
|
||||
context.line_to(vertical_line_x, max_y)
|
||||
context.stroke()
|
||||
|
||||
# Draw horizontal lines connecting vertical line to each event marker
|
||||
context.set_source_rgba(*CONNECTION_LINE_COLOR)
|
||||
context.set_line_width(CONNECTION_LINE_WIDTH)
|
||||
for event_data in person_events:
|
||||
# Draw horizontal line from vertical line to event marker
|
||||
context.move_to(vertical_line_x, event_data.y_pos)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user