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,
|
VIEW,
|
||||||
id="mytimelineview",
|
id="mytimelineview",
|
||||||
name=_("MyTimeline"),
|
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",
|
version="1.0",
|
||||||
gramps_target_version=MODULE_VERSION,
|
gramps_target_version=MODULE_VERSION,
|
||||||
status=STABLE,
|
status=STABLE,
|
||||||
fname="MyTimeline.py",
|
fname="MyTimeline.py",
|
||||||
authors=["Daniel Viegas"],
|
authors=["Daniel Viegas"],
|
||||||
authors_email=["dlviegas@gmail.com"],
|
authors_email=["dlviegas@gmail.com"],
|
||||||
category=("Families", _("Families")),
|
category=("Events", _("Events")),
|
||||||
viewclass="MyTimelineView",
|
viewclass="MyTimelineView",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
373
MyTimeline.py
373
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 cairo
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("plugin.mytimeline")
|
||||||
import math
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, List, Tuple, Any, Set, Dict
|
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.datehandler import get_date
|
||||||
from gramps.gen.display.name import displayer as name_displayer
|
from gramps.gen.display.name import displayer as name_displayer
|
||||||
from gramps.gui.views.navigationview import NavigationView
|
from gramps.gui.views.navigationview import NavigationView
|
||||||
from gramps.gui.views.bookmarks import FamilyBookmarks
|
from gramps.gui.views.bookmarks import EventBookmarks
|
||||||
|
|
||||||
_ = glocale.translation.sgettext
|
_ = glocale.translation.sgettext
|
||||||
|
|
||||||
@ -340,8 +342,9 @@ class TimelineEvent:
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
class MyTimelineView(NavigationView):
|
class MyTimelineView(NavigationView):
|
||||||
"""
|
"""
|
||||||
View for displaying a vertical timeline of family events.
|
View for displaying a vertical timeline of all events in the database.
|
||||||
Shows all events for family members with modern design and interactivity.
|
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):
|
def __init__(self, pdata, dbstate, uistate, nav_group=0):
|
||||||
@ -355,14 +358,14 @@ class MyTimelineView(NavigationView):
|
|||||||
nav_group: Navigation group identifier.
|
nav_group: Navigation group identifier.
|
||||||
"""
|
"""
|
||||||
NavigationView.__init__(
|
NavigationView.__init__(
|
||||||
self, _("MyTimeline"), pdata, dbstate, uistate, FamilyBookmarks, nav_group
|
self, _("MyTimeline"), pdata, dbstate, uistate, EventBookmarks, nav_group
|
||||||
)
|
)
|
||||||
|
|
||||||
self.dbstate = dbstate
|
self.dbstate = dbstate
|
||||||
self.uistate = uistate
|
self.uistate = uistate
|
||||||
|
|
||||||
# Current family handle
|
# Current event handle (for selection/highlighting)
|
||||||
self.active_family_handle = None
|
self.active_event_handle = None
|
||||||
self.events: List[TimelineEvent] = [] # List of TimelineEvent objects
|
self.events: List[TimelineEvent] = [] # List of TimelineEvent objects
|
||||||
|
|
||||||
# UI components
|
# UI components
|
||||||
@ -419,20 +422,26 @@ class MyTimelineView(NavigationView):
|
|||||||
Return the navigation type for this view.
|
Return the navigation type for this view.
|
||||||
|
|
||||||
Returns:
|
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:
|
def change_page(self) -> None:
|
||||||
"""
|
"""
|
||||||
Called when the page changes.
|
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)
|
NavigationView.change_page(self)
|
||||||
active_handle = self.get_active()
|
active_handle = self.get_active()
|
||||||
if active_handle:
|
if active_handle:
|
||||||
self.goto_handle(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:
|
def family_updated(self, handle_list: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -441,7 +450,7 @@ class MyTimelineView(NavigationView):
|
|||||||
Args:
|
Args:
|
||||||
handle_list: List of family handles that were updated.
|
handle_list: List of family handles that were updated.
|
||||||
"""
|
"""
|
||||||
if self.active_family_handle in handle_list:
|
# Refresh timeline since family updates may affect event display
|
||||||
self.collect_events()
|
self.collect_events()
|
||||||
if self.drawing_area:
|
if self.drawing_area:
|
||||||
self.drawing_area.queue_draw()
|
self.drawing_area.queue_draw()
|
||||||
@ -453,25 +462,10 @@ class MyTimelineView(NavigationView):
|
|||||||
Args:
|
Args:
|
||||||
handle_list: List of person handles that were updated.
|
handle_list: List of person handles that were updated.
|
||||||
"""
|
"""
|
||||||
# Check if any updated person is related to current family
|
# Refresh timeline since person updates may affect event display
|
||||||
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()
|
self.collect_events()
|
||||||
if self.drawing_area:
|
if self.drawing_area:
|
||||||
self.drawing_area.queue_draw()
|
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)
|
|
||||||
|
|
||||||
def event_updated(self, handle_list: List[str]) -> None:
|
def event_updated(self, handle_list: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -480,12 +474,15 @@ class MyTimelineView(NavigationView):
|
|||||||
Args:
|
Args:
|
||||||
handle_list: List of event handles that were updated.
|
handle_list: List of event handles that were updated.
|
||||||
"""
|
"""
|
||||||
# Re-collect events if we have an active family
|
# Re-collect events when events are updated
|
||||||
if self.active_family_handle:
|
|
||||||
self.collect_events()
|
self.collect_events()
|
||||||
if self.drawing_area:
|
if self.drawing_area:
|
||||||
self.drawing_area.queue_draw()
|
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:
|
def change_db(self, db) -> None:
|
||||||
"""
|
"""
|
||||||
Called when the database changes.
|
Called when the database changes.
|
||||||
@ -493,7 +490,7 @@ class MyTimelineView(NavigationView):
|
|||||||
Args:
|
Args:
|
||||||
db: The new database object.
|
db: The new database object.
|
||||||
"""
|
"""
|
||||||
self.active_family_handle = None
|
self.active_event_handle = None
|
||||||
self.all_events = []
|
self.all_events = []
|
||||||
self.events = []
|
self.events = []
|
||||||
|
|
||||||
@ -745,7 +742,7 @@ class MyTimelineView(NavigationView):
|
|||||||
|
|
||||||
def _build_person_filter_page(self) -> Gtk.Widget:
|
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:
|
Returns:
|
||||||
Gtk.Widget: The person filter page.
|
Gtk.Widget: The person filter page.
|
||||||
@ -763,8 +760,10 @@ class MyTimelineView(NavigationView):
|
|||||||
person_checkboxes = {}
|
person_checkboxes = {}
|
||||||
self._filter_widgets['person_checkboxes'] = person_checkboxes
|
self._filter_widgets['person_checkboxes'] = person_checkboxes
|
||||||
self._filter_widgets['person_container'] = box
|
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)
|
info_label.set_line_wrap(True)
|
||||||
box.pack_start(info_label, False, False, 0)
|
box.pack_start(info_label, False, False, 0)
|
||||||
|
|
||||||
@ -929,48 +928,95 @@ class MyTimelineView(NavigationView):
|
|||||||
else:
|
else:
|
||||||
checkbox.set_active(category in self.category_filter)
|
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:
|
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']
|
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()):
|
for checkbox in list(self._filter_widgets['person_checkboxes'].values()):
|
||||||
container.remove(checkbox)
|
container.remove(checkbox)
|
||||||
checkbox.destroy()
|
checkbox.destroy()
|
||||||
self._filter_widgets['person_checkboxes'].clear()
|
self._filter_widgets['person_checkboxes'].clear()
|
||||||
|
|
||||||
# Add current family members
|
# Collect all families and create expanders
|
||||||
if self.active_family_handle:
|
if self.dbstate.is_open():
|
||||||
try:
|
try:
|
||||||
family = self.dbstate.db.get_family_from_handle(self.active_family_handle)
|
# Initialize family_expanders if not exists
|
||||||
if family:
|
if 'family_expanders' not in self._filter_widgets:
|
||||||
# Father
|
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()
|
father_handle = family.get_father_handle()
|
||||||
if father_handle:
|
if father_handle:
|
||||||
|
try:
|
||||||
father = self.dbstate.db.get_person_from_handle(father_handle)
|
father = self.dbstate.db.get_person_from_handle(father_handle)
|
||||||
if father:
|
if father:
|
||||||
checkbox = Gtk.CheckButton(label=name_displayer.display(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)
|
checkbox.set_active(True if not self.person_filter else father_handle in self.person_filter)
|
||||||
self._filter_widgets['person_checkboxes'][father_handle] = checkbox
|
self._filter_widgets['person_checkboxes'][father_handle] = checkbox
|
||||||
container.pack_start(checkbox, False, False, 0)
|
members_box.pack_start(checkbox, False, False, 0)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
# Mother
|
# Add mother checkbox
|
||||||
mother_handle = family.get_mother_handle()
|
mother_handle = family.get_mother_handle()
|
||||||
if mother_handle:
|
if mother_handle:
|
||||||
|
try:
|
||||||
mother = self.dbstate.db.get_person_from_handle(mother_handle)
|
mother = self.dbstate.db.get_person_from_handle(mother_handle)
|
||||||
if mother:
|
if mother:
|
||||||
checkbox = Gtk.CheckButton(label=name_displayer.display(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)
|
checkbox.set_active(True if not self.person_filter else mother_handle in self.person_filter)
|
||||||
self._filter_widgets['person_checkboxes'][mother_handle] = checkbox
|
self._filter_widgets['person_checkboxes'][mother_handle] = checkbox
|
||||||
container.pack_start(checkbox, False, False, 0)
|
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():
|
for child_ref in family.get_child_ref_list():
|
||||||
child = self.dbstate.db.get_person_from_handle(child_ref.ref)
|
child_handle = child_ref.ref
|
||||||
|
try:
|
||||||
|
child = self.dbstate.db.get_person_from_handle(child_handle)
|
||||||
if child:
|
if child:
|
||||||
checkbox = Gtk.CheckButton(label=name_displayer.display(child))
|
child_label = f" {_('Child')}: {name_displayer.display(child)}"
|
||||||
checkbox.set_active(True if not self.person_filter else child_ref.ref in self.person_filter)
|
checkbox = Gtk.CheckButton(label=child_label)
|
||||||
self._filter_widgets['person_checkboxes'][child_ref.ref] = checkbox
|
checkbox.set_active(True if not self.person_filter else child_handle in self.person_filter)
|
||||||
container.pack_start(checkbox, False, False, 0)
|
self._filter_widgets['person_checkboxes'][child_handle] = checkbox
|
||||||
|
members_box.pack_start(checkbox, False, False, 0)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 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()
|
container.show_all()
|
||||||
except (AttributeError, KeyError) as e:
|
except (AttributeError, KeyError) as e:
|
||||||
@ -1357,25 +1403,72 @@ class MyTimelineView(NavigationView):
|
|||||||
"""
|
"""
|
||||||
Rebuilds the current display. Called when the view becomes visible.
|
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()
|
active_handle = self.get_active()
|
||||||
if active_handle:
|
if active_handle:
|
||||||
self.goto_handle(active_handle)
|
self.goto_handle(active_handle)
|
||||||
|
elif self.drawing_area:
|
||||||
|
self.drawing_area.queue_draw()
|
||||||
|
|
||||||
def goto_handle(self, handle: str) -> None:
|
def goto_handle(self, handle: str) -> None:
|
||||||
"""
|
"""
|
||||||
Called when the active family changes.
|
Called when the active event changes.
|
||||||
|
|
||||||
Args:
|
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
|
return
|
||||||
|
|
||||||
self.active_family_handle = handle
|
self.active_event_handle = handle
|
||||||
|
|
||||||
|
# Ensure events are collected
|
||||||
|
if not self.all_events:
|
||||||
self.collect_events()
|
self.collect_events()
|
||||||
|
else:
|
||||||
|
# Just refresh display to highlight the selected event
|
||||||
if self.drawing_area:
|
if self.drawing_area:
|
||||||
self.drawing_area.queue_draw()
|
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:
|
def _collect_person_event_refs(self, person) -> List:
|
||||||
"""
|
"""
|
||||||
Collect event references from a person.
|
Collect event references from a person.
|
||||||
@ -1492,36 +1585,138 @@ class MyTimelineView(NavigationView):
|
|||||||
if self.drawing_area:
|
if self.drawing_area:
|
||||||
self.drawing_area.set_size_request(800, self.timeline_height)
|
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:
|
def collect_events(self) -> None:
|
||||||
"""Collect all events for the active family."""
|
"""Collect all events from the database."""
|
||||||
self.all_events = []
|
self.all_events = []
|
||||||
self.events = []
|
self.events = []
|
||||||
|
|
||||||
if not self.active_family_handle:
|
if not self.dbstate.is_open():
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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:
|
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
|
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
|
# Sort events by date
|
||||||
self.all_events.sort(key=lambda x: x.date_sort)
|
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)
|
clicked_index = self.find_event_at_position(event.x, event.y)
|
||||||
if clicked_index is not None:
|
if clicked_index is not None:
|
||||||
clicked_event_data = self.events[clicked_index]
|
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)
|
# 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:
|
if clicked_event_data.person:
|
||||||
person_handle = clicked_event_data.person.get_handle()
|
self.selected_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:
|
else:
|
||||||
# Select this person
|
|
||||||
self.selected_person_handle = person_handle
|
|
||||||
else:
|
|
||||||
# No person for this event, deselect
|
|
||||||
self.selected_person_handle = None
|
self.selected_person_handle = None
|
||||||
|
# Navigate to this event in the view
|
||||||
|
self.uistate.set_active(event_handle, "Event")
|
||||||
|
|
||||||
self.drawing_area.queue_draw()
|
self.drawing_area.queue_draw()
|
||||||
return False
|
return False
|
||||||
@ -2416,9 +2615,9 @@ class MyTimelineView(NavigationView):
|
|||||||
# Check if this event is hovered
|
# Check if this event is hovered
|
||||||
is_hovered = (i == self.hovered_event_index)
|
is_hovered = (i == self.hovered_event_index)
|
||||||
|
|
||||||
# Check if this event belongs to selected person
|
# Check if this event is the active/selected event
|
||||||
is_selected = (self.selected_person_handle is not None and
|
is_selected = (self.active_event_handle is not None and
|
||||||
self._person_matches_handle(event_data.person, self.selected_person_handle))
|
event_data.event.get_handle() == self.active_event_handle)
|
||||||
|
|
||||||
# Draw event marker with modern styling
|
# Draw event marker with modern styling
|
||||||
self.draw_event_marker(context, timeline_x, event_data.y_pos,
|
self.draw_event_marker(context, timeline_x, event_data.y_pos,
|
||||||
@ -2475,7 +2674,7 @@ class MyTimelineView(NavigationView):
|
|||||||
# Draw events
|
# Draw events
|
||||||
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
|
# Draw visual connections for selected person (from selected event)
|
||||||
if self.selected_person_handle is not None:
|
if self.selected_person_handle is not None:
|
||||||
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)
|
||||||
|
|||||||
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