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 cairo
|
||||||
|
import colorsys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger("plugin.mytimeline")
|
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_COLOR = (0.2, 0.5, 1.0, 0.75) # Brighter, more opaque blue
|
||||||
CONNECTION_LINE_WIDTH = 3.5
|
CONNECTION_LINE_WIDTH = 3.5
|
||||||
CONNECTION_VERTICAL_LINE_X = 5 # Left of year markers
|
CONNECTION_VERTICAL_LINE_X = 5 # Left of year markers
|
||||||
|
CONNECTION_LINE_SPACING = 25 # Pixels between vertical lines for different persons
|
||||||
|
|
||||||
# Marker State Colors
|
# Marker State Colors
|
||||||
SELECTED_MARKER_COLOR = (0.2, 0.4, 0.9) # Blue highlight for selected person's events
|
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.hovered_event_index = None
|
||||||
self.tooltip_timeout_id = None
|
self.tooltip_timeout_id = None
|
||||||
self.tooltip_window = 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_x = 0
|
||||||
self.mouse_y = 0
|
self.mouse_y = 0
|
||||||
|
|
||||||
@ -493,6 +496,9 @@ class MyTimelineView(NavigationView):
|
|||||||
self.active_event_handle = None
|
self.active_event_handle = None
|
||||||
self.all_events = []
|
self.all_events = []
|
||||||
self.events = []
|
self.events = []
|
||||||
|
# Clear selected persons and colors
|
||||||
|
self.selected_person_handles.clear()
|
||||||
|
self.person_colors.clear()
|
||||||
|
|
||||||
# Connect to new database signals
|
# Connect to new database signals
|
||||||
if db and db.is_open():
|
if db and db.is_open():
|
||||||
@ -2124,6 +2130,56 @@ class MyTimelineView(NavigationView):
|
|||||||
"""
|
"""
|
||||||
return person is not None and person.get_handle() == handle
|
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,
|
def _calculate_y_position(self, date_sort: int, min_date: int, date_range: int,
|
||||||
timeline_y_start: float, timeline_y_end: float) -> float:
|
timeline_y_start: float, timeline_y_end: float) -> float:
|
||||||
"""
|
"""
|
||||||
@ -2350,18 +2406,26 @@ class MyTimelineView(NavigationView):
|
|||||||
if self.active_event_handle == event_handle:
|
if self.active_event_handle == event_handle:
|
||||||
# Deselect if clicking same event
|
# Deselect if clicking same event
|
||||||
self.active_event_handle = None
|
self.active_event_handle = None
|
||||||
self.selected_person_handle = None
|
|
||||||
else:
|
else:
|
||||||
# Select this event
|
# Select this event
|
||||||
self.active_event_handle = event_handle
|
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
|
# Navigate to this event in the view
|
||||||
self.uistate.set_active(event_handle, "Event")
|
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()
|
self.drawing_area.queue_draw()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -2827,7 +2891,7 @@ class MyTimelineView(NavigationView):
|
|||||||
self._draw_events(context, events_with_y_pos, timeline_x)
|
self._draw_events(context, events_with_y_pos, timeline_x)
|
||||||
|
|
||||||
# Draw visual connections for selected person (from selected event)
|
# 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,
|
self.draw_person_connections(context, events_with_y_pos, timeline_x,
|
||||||
timeline_y_start, timeline_y_end)
|
timeline_y_start, timeline_y_end)
|
||||||
|
|
||||||
@ -3201,7 +3265,8 @@ class MyTimelineView(NavigationView):
|
|||||||
timeline_x: float, timeline_y_start: float,
|
timeline_x: float, timeline_y_start: float,
|
||||||
timeline_y_end: float) -> None:
|
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:
|
Args:
|
||||||
context: Cairo drawing context.
|
context: Cairo drawing context.
|
||||||
@ -3210,33 +3275,39 @@ class MyTimelineView(NavigationView):
|
|||||||
timeline_y_start: Y coordinate of the timeline start.
|
timeline_y_start: Y coordinate of the timeline start.
|
||||||
timeline_y_end: Y coordinate of the timeline end.
|
timeline_y_end: Y coordinate of the timeline end.
|
||||||
"""
|
"""
|
||||||
if not self.selected_person_handle:
|
if not self.selected_person_handles:
|
||||||
return
|
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 = [
|
person_events = [
|
||||||
event_data for event_data in events_with_y_pos
|
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:
|
if len(person_events) < 1:
|
||||||
return
|
continue
|
||||||
|
|
||||||
# Sort by Y position
|
# Sort by Y position
|
||||||
person_events.sort(key=lambda x: x.y_pos)
|
person_events.sort(key=lambda x: x.y_pos)
|
||||||
|
|
||||||
context.save()
|
# Set color for this person's lines
|
||||||
|
context.set_source_rgba(*person_color)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Draw vertical connector line on the left side
|
# Draw vertical connector line on the left side
|
||||||
if len(person_events) > 1:
|
if len(person_events) > 1:
|
||||||
@ -3246,15 +3317,11 @@ class MyTimelineView(NavigationView):
|
|||||||
|
|
||||||
# Draw vertical line connecting all events
|
# Draw vertical line connecting all events
|
||||||
if max_y - min_y > EVENT_MARKER_SIZE * 2:
|
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.move_to(vertical_line_x, min_y)
|
||||||
context.line_to(vertical_line_x, max_y)
|
context.line_to(vertical_line_x, max_y)
|
||||||
context.stroke()
|
context.stroke()
|
||||||
|
|
||||||
# Draw horizontal lines connecting vertical line to each event marker
|
# 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:
|
for event_data in person_events:
|
||||||
# Draw horizontal line from vertical line to event marker
|
# Draw horizontal line from vertical line to event marker
|
||||||
context.move_to(vertical_line_x, event_data.y_pos)
|
context.move_to(vertical_line_x, event_data.y_pos)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user