Convert plugin to Event-based view and improve filter dialog
- Convert plugin from Family-based to Event-based view * Change category from Families to Events * Update navigation_type to 'Event' * Replace FamilyBookmarks with EventBookmarks * Rewrite collect_events() to show all events in database * Update goto_handle() to work with event handles - Update filter dialog to show families with members * Restructure person filter page with expandable families * Each family shows father, mother, and children * Add helper method to generate family display names - Restore person connection lines * Re-enable visual connections between events of selected person * Clicking an event selects the person and shows connections - Add uninstall script * Remove plugin files and backup directories * Clean up any plugin files in subdirectories
This commit is contained in:
parent
b32be12aea
commit
ce75cd55bb
@ -34,14 +34,14 @@ register(
|
||||
VIEW,
|
||||
id="mytimelineview",
|
||||
name=_("MyTimeline"),
|
||||
description=_("A vertical timeline view showing family events including birth, death, and marriage"),
|
||||
description=_("A vertical timeline view showing all events in the database"),
|
||||
version="1.0",
|
||||
gramps_target_version=MODULE_VERSION,
|
||||
status=STABLE,
|
||||
fname="MyTimeline.py",
|
||||
authors=["Daniel Viegas"],
|
||||
authors_email=["dlviegas@gmail.com"],
|
||||
category=("Families", _("Families")),
|
||||
category=("Events", _("Events")),
|
||||
viewclass="MyTimelineView",
|
||||
)
|
||||
|
||||
|
||||
421
MyTimeline.py
421
MyTimeline.py
@ -19,7 +19,7 @@
|
||||
#
|
||||
|
||||
"""
|
||||
MyTimeline View - A vertical timeline showing family events
|
||||
MyTimeline View - A vertical timeline showing all events in the database
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@ -29,6 +29,8 @@ MyTimeline View - A vertical timeline showing family events
|
||||
# -------------------------------------------------------------------------
|
||||
import cairo
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("plugin.mytimeline")
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Tuple, Any, Set, Dict
|
||||
@ -54,7 +56,7 @@ from gramps.gen.utils.db import (
|
||||
from gramps.gen.datehandler import get_date
|
||||
from gramps.gen.display.name import displayer as name_displayer
|
||||
from gramps.gui.views.navigationview import NavigationView
|
||||
from gramps.gui.views.bookmarks import FamilyBookmarks
|
||||
from gramps.gui.views.bookmarks import EventBookmarks
|
||||
|
||||
_ = glocale.translation.sgettext
|
||||
|
||||
@ -340,8 +342,9 @@ class TimelineEvent:
|
||||
# -------------------------------------------------------------------------
|
||||
class MyTimelineView(NavigationView):
|
||||
"""
|
||||
View for displaying a vertical timeline of family events.
|
||||
Shows all events for family members with modern design and interactivity.
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, pdata, dbstate, uistate, nav_group=0):
|
||||
@ -355,14 +358,14 @@ class MyTimelineView(NavigationView):
|
||||
nav_group: Navigation group identifier.
|
||||
"""
|
||||
NavigationView.__init__(
|
||||
self, _("MyTimeline"), pdata, dbstate, uistate, FamilyBookmarks, nav_group
|
||||
self, _("MyTimeline"), pdata, dbstate, uistate, EventBookmarks, nav_group
|
||||
)
|
||||
|
||||
self.dbstate = dbstate
|
||||
self.uistate = uistate
|
||||
|
||||
# Current family handle
|
||||
self.active_family_handle = None
|
||||
# Current event handle (for selection/highlighting)
|
||||
self.active_event_handle = None
|
||||
self.events: List[TimelineEvent] = [] # List of TimelineEvent objects
|
||||
|
||||
# UI components
|
||||
@ -419,20 +422,26 @@ class MyTimelineView(NavigationView):
|
||||
Return the navigation type for this view.
|
||||
|
||||
Returns:
|
||||
str: The navigation type, always "Family" for this view.
|
||||
str: The navigation type, always "Event" for this view.
|
||||
"""
|
||||
return "Family"
|
||||
return "Event"
|
||||
|
||||
def change_page(self) -> None:
|
||||
"""
|
||||
Called when the page changes.
|
||||
|
||||
Updates the view to show the active family's timeline.
|
||||
Updates the view to show all events and highlight the active event if selected.
|
||||
"""
|
||||
NavigationView.change_page(self)
|
||||
active_handle = self.get_active()
|
||||
if active_handle:
|
||||
self.goto_handle(active_handle)
|
||||
else:
|
||||
# If no event is selected, ensure timeline is displayed
|
||||
if not self.events and self.dbstate.is_open():
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
|
||||
def family_updated(self, handle_list: List[str]) -> None:
|
||||
"""
|
||||
@ -441,10 +450,10 @@ class MyTimelineView(NavigationView):
|
||||
Args:
|
||||
handle_list: List of family handles that were updated.
|
||||
"""
|
||||
if self.active_family_handle in handle_list:
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
# Refresh timeline since family updates may affect event display
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
|
||||
def person_updated(self, handle_list: List[str]) -> None:
|
||||
"""
|
||||
@ -453,25 +462,10 @@ class MyTimelineView(NavigationView):
|
||||
Args:
|
||||
handle_list: List of person handles that were updated.
|
||||
"""
|
||||
# Check if any updated person is related to current family
|
||||
if self.active_family_handle:
|
||||
try:
|
||||
family = self.dbstate.db.get_family_from_handle(self.active_family_handle)
|
||||
if family:
|
||||
father_handle = family.get_father_handle()
|
||||
mother_handle = family.get_mother_handle()
|
||||
child_handles = [ref.ref for ref in family.get_child_ref_list()]
|
||||
if (
|
||||
(father_handle and father_handle in handle_list)
|
||||
or (mother_handle and mother_handle in handle_list)
|
||||
or any(h in handle_list for h in child_handles)
|
||||
):
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
except (AttributeError, KeyError) as e:
|
||||
# Skip if family handle is invalid
|
||||
logger.warning(f"Error accessing family in person_updated: {e}", exc_info=True)
|
||||
# Refresh timeline since person updates may affect event display
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
|
||||
def event_updated(self, handle_list: List[str]) -> None:
|
||||
"""
|
||||
@ -480,11 +474,14 @@ class MyTimelineView(NavigationView):
|
||||
Args:
|
||||
handle_list: List of event handles that were updated.
|
||||
"""
|
||||
# Re-collect events if we have an active family
|
||||
if self.active_family_handle:
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
# Re-collect events when events are updated
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
|
||||
# If the active event was updated, ensure it's still highlighted
|
||||
if self.active_event_handle in handle_list:
|
||||
self._scroll_to_event(self.active_event_handle)
|
||||
|
||||
def change_db(self, db) -> None:
|
||||
"""
|
||||
@ -493,7 +490,7 @@ class MyTimelineView(NavigationView):
|
||||
Args:
|
||||
db: The new database object.
|
||||
"""
|
||||
self.active_family_handle = None
|
||||
self.active_event_handle = None
|
||||
self.all_events = []
|
||||
self.events = []
|
||||
|
||||
@ -745,7 +742,7 @@ class MyTimelineView(NavigationView):
|
||||
|
||||
def _build_person_filter_page(self) -> Gtk.Widget:
|
||||
"""
|
||||
Build the person filter page with checkboxes for each family member.
|
||||
Build the person filter page with expandable families showing their members.
|
||||
|
||||
Returns:
|
||||
Gtk.Widget: The person filter page.
|
||||
@ -763,8 +760,10 @@ class MyTimelineView(NavigationView):
|
||||
person_checkboxes = {}
|
||||
self._filter_widgets['person_checkboxes'] = person_checkboxes
|
||||
self._filter_widgets['person_container'] = box
|
||||
# Store expanders by family handle
|
||||
self._filter_widgets['family_expanders'] = {}
|
||||
|
||||
info_label = Gtk.Label(label=_("Select family members to include in the timeline."))
|
||||
info_label = Gtk.Label(label=_("Select families and their members to include in the timeline."))
|
||||
info_label.set_line_wrap(True)
|
||||
box.pack_start(info_label, False, False, 0)
|
||||
|
||||
@ -929,50 +928,97 @@ class MyTimelineView(NavigationView):
|
||||
else:
|
||||
checkbox.set_active(category in self.category_filter)
|
||||
|
||||
# Update person checkboxes
|
||||
# Update person checkboxes with families
|
||||
if 'person_checkboxes' in self._filter_widgets and 'person_container' in self._filter_widgets:
|
||||
# Clear existing person checkboxes
|
||||
# Clear existing person checkboxes and family expanders
|
||||
container = self._filter_widgets['person_container']
|
||||
|
||||
# Remove all existing expanders
|
||||
if 'family_expanders' in self._filter_widgets:
|
||||
for expander in list(self._filter_widgets['family_expanders'].values()):
|
||||
container.remove(expander)
|
||||
expander.destroy()
|
||||
self._filter_widgets['family_expanders'].clear()
|
||||
|
||||
# Remove all existing checkboxes
|
||||
for checkbox in list(self._filter_widgets['person_checkboxes'].values()):
|
||||
container.remove(checkbox)
|
||||
checkbox.destroy()
|
||||
self._filter_widgets['person_checkboxes'].clear()
|
||||
|
||||
# Add current family members
|
||||
if self.active_family_handle:
|
||||
# Collect all families and create expanders
|
||||
if self.dbstate.is_open():
|
||||
try:
|
||||
family = self.dbstate.db.get_family_from_handle(self.active_family_handle)
|
||||
if family:
|
||||
# Father
|
||||
# Initialize family_expanders if not exists
|
||||
if 'family_expanders' not in self._filter_widgets:
|
||||
self._filter_widgets['family_expanders'] = {}
|
||||
|
||||
# Iterate through all families
|
||||
for family in self.dbstate.db.iter_families():
|
||||
family_handle = family.get_handle()
|
||||
|
||||
# Get family display name
|
||||
family_name = self._get_family_display_name(family)
|
||||
|
||||
# Create expander for this family
|
||||
expander = Gtk.Expander(label=family_name)
|
||||
expander.set_expanded(False)
|
||||
|
||||
# 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)
|
||||
|
||||
# Add father checkbox
|
||||
father_handle = family.get_father_handle()
|
||||
if father_handle:
|
||||
father = self.dbstate.db.get_person_from_handle(father_handle)
|
||||
if father:
|
||||
checkbox = Gtk.CheckButton(label=name_displayer.display(father))
|
||||
checkbox.set_active(True if not self.person_filter else father_handle in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][father_handle] = checkbox
|
||||
container.pack_start(checkbox, False, False, 0)
|
||||
try:
|
||||
father = self.dbstate.db.get_person_from_handle(father_handle)
|
||||
if father:
|
||||
father_label = f" {_('Father')}: {name_displayer.display(father)}"
|
||||
checkbox = Gtk.CheckButton(label=father_label)
|
||||
checkbox.set_active(True if not self.person_filter else father_handle in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][father_handle] = checkbox
|
||||
members_box.pack_start(checkbox, False, False, 0)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
# Mother
|
||||
# Add mother checkbox
|
||||
mother_handle = family.get_mother_handle()
|
||||
if mother_handle:
|
||||
mother = self.dbstate.db.get_person_from_handle(mother_handle)
|
||||
if mother:
|
||||
checkbox = Gtk.CheckButton(label=name_displayer.display(mother))
|
||||
checkbox.set_active(True if not self.person_filter else mother_handle in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][mother_handle] = checkbox
|
||||
container.pack_start(checkbox, False, False, 0)
|
||||
try:
|
||||
mother = self.dbstate.db.get_person_from_handle(mother_handle)
|
||||
if mother:
|
||||
mother_label = f" {_('Mother')}: {name_displayer.display(mother)}"
|
||||
checkbox = Gtk.CheckButton(label=mother_label)
|
||||
checkbox.set_active(True if not self.person_filter else mother_handle in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][mother_handle] = checkbox
|
||||
members_box.pack_start(checkbox, False, False, 0)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
# Children
|
||||
# Add children checkboxes
|
||||
for child_ref in family.get_child_ref_list():
|
||||
child = self.dbstate.db.get_person_from_handle(child_ref.ref)
|
||||
if child:
|
||||
checkbox = Gtk.CheckButton(label=name_displayer.display(child))
|
||||
checkbox.set_active(True if not self.person_filter else child_ref.ref in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][child_ref.ref] = checkbox
|
||||
container.pack_start(checkbox, False, False, 0)
|
||||
child_handle = child_ref.ref
|
||||
try:
|
||||
child = self.dbstate.db.get_person_from_handle(child_handle)
|
||||
if child:
|
||||
child_label = f" {_('Child')}: {name_displayer.display(child)}"
|
||||
checkbox = Gtk.CheckButton(label=child_label)
|
||||
checkbox.set_active(True if not self.person_filter else child_handle in self.person_filter)
|
||||
self._filter_widgets['person_checkboxes'][child_handle] = checkbox
|
||||
members_box.pack_start(checkbox, False, False, 0)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
container.show_all()
|
||||
# Only add expander if there are members to show
|
||||
if len(members_box.get_children()) > 0:
|
||||
expander.add(members_box)
|
||||
self._filter_widgets['family_expanders'][family_handle] = expander
|
||||
container.pack_start(expander, False, False, 0)
|
||||
|
||||
container.show_all()
|
||||
except (AttributeError, KeyError) as e:
|
||||
logger.warning(f"Error updating person filter: {e}", exc_info=True)
|
||||
|
||||
@ -1357,24 +1403,71 @@ class MyTimelineView(NavigationView):
|
||||
"""
|
||||
Rebuilds the current display. Called when the view becomes visible.
|
||||
"""
|
||||
# Collect all events if not already done
|
||||
if not self.all_events and self.dbstate.is_open():
|
||||
self.collect_events()
|
||||
|
||||
# Highlight active event if one is selected
|
||||
active_handle = self.get_active()
|
||||
if active_handle:
|
||||
self.goto_handle(active_handle)
|
||||
elif self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
|
||||
def goto_handle(self, handle: str) -> None:
|
||||
"""
|
||||
Called when the active family changes.
|
||||
Called when the active event changes.
|
||||
|
||||
Args:
|
||||
handle: The handle of the family to display.
|
||||
handle: The handle of the event to highlight/select.
|
||||
"""
|
||||
if handle == self.active_family_handle:
|
||||
if handle == self.active_event_handle:
|
||||
return
|
||||
|
||||
self.active_family_handle = handle
|
||||
self.collect_events()
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
self.active_event_handle = handle
|
||||
|
||||
# Ensure events are collected
|
||||
if not self.all_events:
|
||||
self.collect_events()
|
||||
else:
|
||||
# Just refresh display to highlight the selected event
|
||||
if self.drawing_area:
|
||||
self.drawing_area.queue_draw()
|
||||
|
||||
# Scroll to the selected event if possible
|
||||
self._scroll_to_event(handle)
|
||||
|
||||
def _scroll_to_event(self, event_handle: str) -> None:
|
||||
"""
|
||||
Scroll the timeline to show the selected event.
|
||||
|
||||
Args:
|
||||
event_handle: The handle of the event to scroll to.
|
||||
"""
|
||||
if not self.scrolledwindow or not event_handle:
|
||||
return
|
||||
|
||||
# Find the event in our event list
|
||||
event_index = None
|
||||
for i, timeline_event in enumerate(self.events):
|
||||
if timeline_event.event.get_handle() == event_handle:
|
||||
event_index = i
|
||||
break
|
||||
|
||||
if event_index is None:
|
||||
return
|
||||
|
||||
# Calculate the y position of the event
|
||||
if event_index < len(self.events):
|
||||
event_y = TIMELINE_MARGIN_TOP + (event_index * EVENT_SPACING)
|
||||
|
||||
# Get the adjustment and scroll to the event
|
||||
vadj = self.scrolledwindow.get_vadjustment()
|
||||
if vadj:
|
||||
# Center the event in the viewport
|
||||
viewport_height = self.scrolledwindow.get_allocation().height
|
||||
scroll_to = max(0, event_y - (viewport_height / 2))
|
||||
vadj.set_value(scroll_to)
|
||||
|
||||
def _collect_person_event_refs(self, person) -> List:
|
||||
"""
|
||||
@ -1492,36 +1585,138 @@ class MyTimelineView(NavigationView):
|
||||
if self.drawing_area:
|
||||
self.drawing_area.set_size_request(800, self.timeline_height)
|
||||
|
||||
def _process_event(self, event, person_obj: Optional[Any] = None) -> Optional[TimelineEvent]:
|
||||
"""
|
||||
Process a single event and create a TimelineEvent.
|
||||
|
||||
Args:
|
||||
event: The event object to process.
|
||||
person_obj: Optional person object associated with this event.
|
||||
|
||||
Returns:
|
||||
Optional[TimelineEvent]: The created TimelineEvent, or None if invalid.
|
||||
"""
|
||||
try:
|
||||
if event and event.get_date_object():
|
||||
date_obj = event.get_date_object()
|
||||
event_type = event.get_type()
|
||||
return TimelineEvent(
|
||||
date_sort=date_obj.get_sort_value(),
|
||||
date_obj=date_obj,
|
||||
event=event,
|
||||
person=person_obj,
|
||||
event_type=event_type,
|
||||
y_pos=0.0
|
||||
)
|
||||
except (AttributeError, KeyError, ValueError) as e:
|
||||
logger.debug(f"Skipping invalid event: {e}")
|
||||
return None
|
||||
|
||||
def _find_person_for_event(self, event) -> Optional[Any]:
|
||||
"""
|
||||
Find a primary person associated with an event by searching through people.
|
||||
|
||||
Args:
|
||||
event: The event object to find a person for.
|
||||
|
||||
Returns:
|
||||
Optional person object, or None if not found or if event is family-only.
|
||||
"""
|
||||
if not event:
|
||||
return None
|
||||
|
||||
event_handle = event.get_handle()
|
||||
|
||||
# Search through people to find who references this event
|
||||
# For performance, we'll search through a limited set or use event references
|
||||
try:
|
||||
# Try to find a person who has this event as a primary event
|
||||
for person_handle in self.dbstate.db.get_person_handles():
|
||||
try:
|
||||
person = self.dbstate.db.get_person_from_handle(person_handle)
|
||||
if person:
|
||||
# Check primary events first
|
||||
for event_ref in person.get_primary_event_ref_list():
|
||||
if event_ref.ref == event_handle:
|
||||
return person
|
||||
# Check all events
|
||||
for event_ref in person.get_event_ref_list():
|
||||
if event_ref.ref == event_handle:
|
||||
return person
|
||||
except (AttributeError, KeyError):
|
||||
continue
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _get_family_display_name(self, family) -> str:
|
||||
"""
|
||||
Get a display name for a family showing parent names.
|
||||
|
||||
Args:
|
||||
family: The family object.
|
||||
|
||||
Returns:
|
||||
str: Display name for the family.
|
||||
"""
|
||||
if not family:
|
||||
return _("Unknown Family")
|
||||
|
||||
father_handle = family.get_father_handle()
|
||||
mother_handle = family.get_mother_handle()
|
||||
|
||||
father_name = None
|
||||
mother_name = None
|
||||
|
||||
if father_handle:
|
||||
try:
|
||||
father = self.dbstate.db.get_person_from_handle(father_handle)
|
||||
if father:
|
||||
father_name = name_displayer.display(father)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
if mother_handle:
|
||||
try:
|
||||
mother = self.dbstate.db.get_person_from_handle(mother_handle)
|
||||
if mother:
|
||||
mother_name = name_displayer.display(mother)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
if father_name and mother_name:
|
||||
return f"{father_name} & {mother_name}"
|
||||
elif father_name:
|
||||
return f"{father_name} & {_('Unknown')}"
|
||||
elif mother_name:
|
||||
return f"{_('Unknown')} & {mother_name}"
|
||||
else:
|
||||
return _("Unknown Family")
|
||||
|
||||
def collect_events(self) -> None:
|
||||
"""Collect all events for the active family."""
|
||||
"""Collect all events from the database."""
|
||||
self.all_events = []
|
||||
self.events = []
|
||||
|
||||
if not self.active_family_handle:
|
||||
if not self.dbstate.is_open():
|
||||
return
|
||||
|
||||
try:
|
||||
family = self.dbstate.db.get_family_from_handle(self.active_family_handle)
|
||||
# Iterate through all events in the database
|
||||
for event in self.dbstate.db.iter_events():
|
||||
# Try to find an associated person for this event
|
||||
person_obj = self._find_person_for_event(event)
|
||||
|
||||
# Process the event
|
||||
timeline_event = self._process_event(event, person_obj)
|
||||
if timeline_event:
|
||||
self.all_events.append(timeline_event)
|
||||
|
||||
except (AttributeError, KeyError) as e:
|
||||
logger.warning(f"Error accessing family: {e}", exc_info=True)
|
||||
logger.warning(f"Error collecting events: {e}", exc_info=True)
|
||||
return
|
||||
|
||||
if not family:
|
||||
return
|
||||
|
||||
# Get family events (marriage, divorce, etc.)
|
||||
self._collect_family_events(family)
|
||||
|
||||
# Get father's events
|
||||
self._collect_family_member_events(family.get_father_handle(), "father")
|
||||
|
||||
# Get mother's events
|
||||
self._collect_family_member_events(family.get_mother_handle(), "mother")
|
||||
|
||||
# Get children's events
|
||||
for child_ref in family.get_child_ref_list():
|
||||
self._collect_family_member_events(child_ref.ref, "child")
|
||||
|
||||
# Sort events by date
|
||||
self.all_events.sort(key=lambda x: x.date_sort)
|
||||
|
||||
@ -1997,19 +2192,23 @@ class MyTimelineView(NavigationView):
|
||||
clicked_index = self.find_event_at_position(event.x, event.y)
|
||||
if clicked_index is not None:
|
||||
clicked_event_data = self.events[clicked_index]
|
||||
event_handle = clicked_event_data.event.get_handle()
|
||||
|
||||
# Clicking anywhere on the event line selects the person (like selecting the event)
|
||||
if clicked_event_data.person:
|
||||
person_handle = clicked_event_data.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
|
||||
# Select/highlight this event
|
||||
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")
|
||||
|
||||
self.drawing_area.queue_draw()
|
||||
return False
|
||||
@ -2416,9 +2615,9 @@ class MyTimelineView(NavigationView):
|
||||
# Check if this event is hovered
|
||||
is_hovered = (i == self.hovered_event_index)
|
||||
|
||||
# Check if this event belongs to selected person
|
||||
is_selected = (self.selected_person_handle is not None and
|
||||
self._person_matches_handle(event_data.person, self.selected_person_handle))
|
||||
# Check if this event is the active/selected event
|
||||
is_selected = (self.active_event_handle is not None and
|
||||
event_data.event.get_handle() == self.active_event_handle)
|
||||
|
||||
# Draw event marker with modern styling
|
||||
self.draw_event_marker(context, timeline_x, event_data.y_pos,
|
||||
@ -2475,7 +2674,7 @@ class MyTimelineView(NavigationView):
|
||||
# Draw events
|
||||
self._draw_events(context, events_with_y_pos, timeline_x)
|
||||
|
||||
# Draw visual connections for selected person
|
||||
# Draw visual connections for selected person (from selected event)
|
||||
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)
|
||||
|
||||
154
uninstall_from_snap.sh
Executable file
154
uninstall_from_snap.sh
Executable file
@ -0,0 +1,154 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to uninstall MyTimeline plugin from snap-installed Gramps
|
||||
#
|
||||
# This script removes the MyTimeline plugin files from the Gramps snap plugin directory
|
||||
#
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Plugin files to remove
|
||||
PLUGIN_FILES=("MyTimeline.gpr.py" "MyTimeline.py")
|
||||
|
||||
# Target directory for snap-installed Gramps
|
||||
PLUGIN_DEST_DIR="$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins"
|
||||
|
||||
# Alternative locations to try
|
||||
ALTERNATIVE_DIRS=(
|
||||
"$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins"
|
||||
"$HOME/snap/gramps/current/.gramps/gramps60/plugins"
|
||||
"$HOME/snap/gramps/current/.gramps/plugins"
|
||||
"$HOME/snap/gramps/common/.local/share/gramps/gramps60/plugins"
|
||||
"$HOME/.local/share/gramps/gramps60/plugins"
|
||||
"$HOME/.gramps/gramps60/plugins"
|
||||
)
|
||||
|
||||
echo "=========================================="
|
||||
echo "MyTimeline Plugin Uninstaller for Snap Gramps"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Function to find the correct plugin directory
|
||||
find_plugin_dir() {
|
||||
# Try the primary location first
|
||||
if [ -d "$PLUGIN_DEST_DIR" ]; then
|
||||
echo "$PLUGIN_DEST_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try alternative locations
|
||||
for dir in "${ALTERNATIVE_DIRS[@]}"; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Find the target directory
|
||||
echo "Locating Gramps plugin directory..."
|
||||
TARGET_DIR=$(find_plugin_dir)
|
||||
|
||||
if [ -z "$TARGET_DIR" ]; then
|
||||
echo -e "${YELLOW}Warning: Plugin directory not found.${NC}"
|
||||
echo "The plugin may already be uninstalled or Gramps may not be installed via snap."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓${NC} Target directory: $TARGET_DIR"
|
||||
echo ""
|
||||
|
||||
# Check if plugin files exist
|
||||
FILES_FOUND=0
|
||||
FILES_REMOVED=0
|
||||
|
||||
for file in "${PLUGIN_FILES[@]}"; do
|
||||
if [ -f "$TARGET_DIR/$file" ]; then
|
||||
FILES_FOUND=$((FILES_FOUND + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FILES_FOUND -eq 0 ]; then
|
||||
echo -e "${YELLOW}No plugin files found. The plugin may already be uninstalled.${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found $FILES_FOUND plugin file(s)."
|
||||
echo ""
|
||||
|
||||
# Remove plugin files
|
||||
echo "Removing plugin files..."
|
||||
for file in "${PLUGIN_FILES[@]}"; do
|
||||
if [ -f "$TARGET_DIR/$file" ]; then
|
||||
rm "$TARGET_DIR/$file"
|
||||
echo -e "${GREEN}✓${NC} Removed: $file"
|
||||
FILES_REMOVED=$((FILES_REMOVED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove backup directories created by the installer
|
||||
BACKUP_DIRS_REMOVED=0
|
||||
echo ""
|
||||
echo "Searching for backup directories..."
|
||||
for backup_dir in "$TARGET_DIR"/mytimeline_backup_*; do
|
||||
if [ -d "$backup_dir" ]; then
|
||||
echo "Found backup directory: $(basename "$backup_dir")"
|
||||
rm -rf "$backup_dir"
|
||||
echo -e "${GREEN}✓${NC} Removed backup directory: $(basename "$backup_dir")"
|
||||
BACKUP_DIRS_REMOVED=$((BACKUP_DIRS_REMOVED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check for any plugin files in subdirectories that might be loaded
|
||||
echo ""
|
||||
echo "Checking for plugin files in subdirectories..."
|
||||
SUBDIR_FILES_REMOVED=0
|
||||
for subdir in "$TARGET_DIR"/*; do
|
||||
if [ -d "$subdir" ]; then
|
||||
for file in "${PLUGIN_FILES[@]}"; do
|
||||
if [ -f "$subdir/$file" ]; then
|
||||
echo -e "${YELLOW}Warning: Found $file in subdirectory: $(basename "$subdir")${NC}"
|
||||
rm -f "$subdir/$file"
|
||||
echo -e "${GREEN}✓${NC} Removed: $(basename "$subdir")/$file"
|
||||
SUBDIR_FILES_REMOVED=$((SUBDIR_FILES_REMOVED + 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
TOTAL_REMOVED=$((FILES_REMOVED + BACKUP_DIRS_REMOVED + SUBDIR_FILES_REMOVED))
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
if [ $TOTAL_REMOVED -gt 0 ]; then
|
||||
echo -e "${GREEN}Uninstallation completed successfully!${NC}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Removed:"
|
||||
echo " - $FILES_REMOVED plugin file(s) from main directory"
|
||||
if [ $BACKUP_DIRS_REMOVED -gt 0 ]; then
|
||||
echo " - $BACKUP_DIRS_REMOVED backup directory/directories"
|
||||
fi
|
||||
if [ $SUBDIR_FILES_REMOVED -gt 0 ]; then
|
||||
echo " - $SUBDIR_FILES_REMOVED plugin file(s) from subdirectories"
|
||||
fi
|
||||
echo ""
|
||||
echo "Location: $TARGET_DIR"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Restart Gramps if it's currently running"
|
||||
echo " 2. The plugin should no longer appear in the Plugins menu"
|
||||
else
|
||||
echo -e "${YELLOW}No files or directories were removed.${NC}"
|
||||
echo "=========================================="
|
||||
fi
|
||||
echo ""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user