diff --git a/MyTimeline.py b/MyTimeline.py index d03fafa..5725dbd 100644 --- a/MyTimeline.py +++ b/MyTimeline.py @@ -32,7 +32,7 @@ import colorsys import logging import math from dataclasses import dataclass -from typing import Optional, List, Tuple, Any, Set, Dict, TYPE_CHECKING +from typing import Optional, List, Tuple, Any, Set, Dict, TYPE_CHECKING, Union if TYPE_CHECKING: from gramps.gen.lib import Event, Person, Family @@ -90,6 +90,16 @@ TOOLTIP_DELAY = 500 # milliseconds TOOLTIP_MAX_WIDTH = 500 LABEL_BACKGROUND_PADDING = 8 LABEL_BACKGROUND_RADIUS = 5 +DEFAULT_TIMELINE_HEIGHT = 1000 # Default height, will be recalculated +DEFAULT_TIMELINE_WIDTH = 800 +DEFAULT_DRAWING_AREA_HEIGHT = 600 +DEFAULT_DRAWING_AREA_WIDTH = 800 +EMPTY_TIMELINE_HEIGHT = 600 # Height when no events +FILTER_DIALOG_WIDTH = 600 +FILTER_DIALOG_HEIGHT = 700 +MIN_GENEALOGICAL_YEAR = 1000 # Minimum year for genealogical data +DATE_SORT_YEAR_MULTIPLIER = 10000 # Year component in date sort value +DATE_SORT_MONTH_MULTIPLIER = 100 # Month component in date sort value # Font Constants FONT_FAMILY = "Sans" @@ -360,7 +370,7 @@ class MyTimelineView(NavigationView): and filtering of events by type, category, person, and date range. """ - def __init__(self, pdata, dbstate, uistate, nav_group=0): + def __init__(self, pdata: Any, dbstate: Any, uistate: Any, nav_group: int = 0) -> None: """ Initialize the MyTimeline view. @@ -384,7 +394,7 @@ class MyTimelineView(NavigationView): # UI components self.scrolledwindow = None self.drawing_area = None - self.timeline_height = 1000 # Default height, will be recalculated + self.timeline_height = DEFAULT_TIMELINE_HEIGHT self.zoom_level = 1.0 # Zoom level (1.0 = 100%) self.min_zoom = 0.5 self.max_zoom = 3.0 @@ -498,7 +508,7 @@ class MyTimelineView(NavigationView): 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: Any) -> None: """ Called when the database changes. @@ -665,6 +675,57 @@ class MyTimelineView(NavigationView): else: # 'some' group_checkbox.set_inconsistent(True) + def _make_group_toggle_handler(self, child_checkboxes: List[Gtk.CheckButton], + updating_flag: List[bool]) -> Any: + """ + Create a handler for group checkbox toggle that toggles all children. + + Args: + child_checkboxes: List of child checkboxes to toggle. + updating_flag: List with single boolean to prevent recursion. + + Returns: + Callable handler function. + """ + def handler(widget: Gtk.Widget) -> None: + if updating_flag[0]: + return + + # Handle inconsistent state - make it consistent + if widget.get_inconsistent(): + widget.set_inconsistent(False) + widget.set_active(True) + + updating_flag[0] = True + # Toggle all children + is_active = widget.get_active() + for child_cb in child_checkboxes: + child_cb.set_active(is_active) + updating_flag[0] = False + + return handler + + def _make_child_toggle_handler(self, group_checkbox: Gtk.CheckButton, + child_checkboxes: List[Gtk.CheckButton], + updating_flag: List[bool]) -> Any: + """ + Create a handler for child checkbox toggle that updates group state. + + Args: + group_checkbox: The parent group checkbox to update. + child_checkboxes: List of all child checkboxes. + updating_flag: List with single boolean to prevent recursion. + + Returns: + Callable handler function. + """ + def handler(widget: Gtk.Widget) -> None: + if updating_flag[0]: + return + self._update_group_checkbox_state(group_checkbox, child_checkboxes) + + return handler + def _build_filter_dialog(self) -> Gtk.Dialog: """ Build the filter dialog with all filter controls. @@ -673,7 +734,7 @@ class MyTimelineView(NavigationView): Gtk.Dialog: The filter dialog widget. """ dialog = Gtk.Dialog(title=_("Filter Events"), parent=self.uistate.window) - dialog.set_default_size(600, 700) + dialog.set_default_size(FILTER_DIALOG_WIDTH, FILTER_DIALOG_HEIGHT) dialog.add_button(_("Clear All"), Gtk.ResponseType.REJECT) dialog.add_button(_("Close"), Gtk.ResponseType.CLOSE) dialog.add_button(_("Apply"), Gtk.ResponseType.APPLY) @@ -784,40 +845,13 @@ class MyTimelineView(NavigationView): updating_category = [False] # Connect category checkbox to toggle all children - def make_category_toggle_handler(cb_list, updating_flag): - def handler(widget): - if updating_flag[0]: - return - - # Handle inconsistent state - make it consistent - if widget.get_inconsistent(): - widget.set_inconsistent(False) - widget.set_active(True) - - updating_flag[0] = True - # Toggle all children - is_active = widget.get_active() - for child_cb in cb_list: - child_cb.set_active(is_active) - updating_flag[0] = False - return handler + category_checkbox.connect("toggled", + self._make_group_toggle_handler(child_checkboxes, updating_category)) # Connect child checkboxes to update category checkbox state - def make_child_toggle_handler(cat_cb, children, updating_flag): - def handler(widget): - if updating_flag[0]: - return - self._update_group_checkbox_state(cat_cb, children) - return handler - - # Connect category checkbox - category_checkbox.connect("toggled", - make_category_toggle_handler(child_checkboxes, updating_category)) - - # Connect child checkboxes for child_cb in child_checkboxes: child_cb.connect("toggled", - make_child_toggle_handler(category_checkbox, child_checkboxes, updating_category)) + self._make_child_toggle_handler(category_checkbox, child_checkboxes, updating_category)) self._filter_widgets['event_type_checkboxes'] = event_type_checkboxes self._filter_widgets['category_checkboxes'] = category_checkboxes @@ -906,10 +940,10 @@ class MyTimelineView(NavigationView): calendar_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15) calendar_box.set_homogeneous(True) - # Year range for genealogical data (1000 to current year + 10) + # Year range for genealogical data import datetime current_year = datetime.date.today().year - min_year = 1000 + min_year = MIN_GENEALOGICAL_YEAR max_year = current_year + 10 # From date calendar with year selector @@ -1130,37 +1164,13 @@ class MyTimelineView(NavigationView): updating_family = [False] # Connect family checkbox to toggle all members - def make_family_toggle_handler(cb_list, updating_flag): - def handler(widget): - if updating_flag[0]: - return - # Handle inconsistent state - if widget.get_inconsistent(): - widget.set_inconsistent(False) - widget.set_active(True) - updating_flag[0] = True - is_active = widget.get_active() - for child_cb in cb_list: - child_cb.set_active(is_active) - updating_flag[0] = False - return handler + family_checkbox.connect("toggled", + self._make_group_toggle_handler(child_checkboxes, updating_family)) # Connect child checkboxes to update family checkbox - def make_family_member_toggle_handler(fam_cb, children, updating_flag): - def handler(widget): - if updating_flag[0]: - return - self._update_group_checkbox_state(fam_cb, children) - return handler - - # Connect family checkbox - family_checkbox.connect("toggled", - make_family_toggle_handler(child_checkboxes, updating_family)) - - # Connect child checkboxes for child_cb in child_checkboxes: child_cb.connect("toggled", - make_family_member_toggle_handler(family_checkbox, child_checkboxes, updating_family)) + self._make_child_toggle_handler(family_checkbox, child_checkboxes, updating_family)) # Initialize family checkbox state self._update_group_checkbox_state(family_checkbox, child_checkboxes) @@ -1182,9 +1192,9 @@ class MyTimelineView(NavigationView): min_sort, max_sort = self.date_range_filter # Convert sort values back to dates for calendar display # Approximate conversion: extract year from sort value - # Sort value is roughly: year * 10000 + month * 100 + day - from_year = min_sort // 10000 - to_year = max_sort // 10000 + # Sort value is roughly: year * DATE_SORT_YEAR_MULTIPLIER + month * DATE_SORT_MONTH_MULTIPLIER + day + from_year = min_sort // DATE_SORT_YEAR_MULTIPLIER + to_year = max_sort // DATE_SORT_YEAR_MULTIPLIER # Set calendar years (approximate) current_from_year, current_from_month, current_from_day = from_calendar.get_date() @@ -1639,10 +1649,10 @@ class MyTimelineView(NavigationView): ) self.timeline_height = int(base_height * self.zoom_level) else: - self.timeline_height = int(600 * self.zoom_level) + self.timeline_height = int(EMPTY_TIMELINE_HEIGHT * self.zoom_level) if self.drawing_area: - self.drawing_area.set_size_request(800, self.timeline_height) + self.drawing_area.set_size_request(DEFAULT_DRAWING_AREA_WIDTH, self.timeline_height) def _create_timeline_event(self, event: 'Event', person_obj: Optional['Person'] = None, y_pos: float = 0.0) -> Optional[TimelineEvent]: """ @@ -2280,10 +2290,10 @@ class MyTimelineView(NavigationView): ) self.timeline_height = int(base_height * self.zoom_level) else: - self.timeline_height = int(600 * self.zoom_level) + self.timeline_height = int(EMPTY_TIMELINE_HEIGHT * self.zoom_level) if self.drawing_area: - self.drawing_area.set_size_request(800, self.timeline_height) + self.drawing_area.set_size_request(DEFAULT_DRAWING_AREA_WIDTH, self.timeline_height) # Invalidate cache when zoom changes self._adjusted_events_cache = None @@ -2570,7 +2580,7 @@ class MyTimelineView(NavigationView): return None - def _format_person_tooltip(self, person, person_events: List[TimelineEvent]) -> str: + def _format_person_tooltip(self, person: 'Person', person_events: List[TimelineEvent]) -> str: """ Format tooltip for person with multiple events. @@ -2607,7 +2617,7 @@ class MyTimelineView(NavigationView): return tooltip_text - def _format_single_event_tooltip(self, event, event_type: EventType) -> str: + def _format_single_event_tooltip(self, event: 'Event', event_type: EventType) -> str: """ Format tooltip for single event. diff --git a/generate_demo_family.py b/generate_demo_family.py index 114cc0d..b23e153 100644 --- a/generate_demo_family.py +++ b/generate_demo_family.py @@ -4,14 +4,25 @@ Generate a huge demo family for Gramps testing """ import random -from datetime import datetime, timedelta +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from datetime import datetime +from typing import Optional, List, Tuple, Dict # Set seed for deterministic event generation random.seed(42) -# Generate unique handles -def gen_handle(prefix, num): - return f"_{prefix}{num:08d}" +# Constants +EVENT_ID_OFFSET = 10 +FAMILY_ID_OFFSET = 100 +EVENT_ID_START_OFFSET = 2 +MIN_MONTH = 1 +MAX_MONTH = 12 +MIN_DAY = 1 +MAX_DAY = 28 +GRAMPS_XML_VERSION = "5.1.0" +GRAMPS_XML_NAMESPACE = "http://gramps-project.org/xml/1.7.1/" +GRAMPS_XML_DTD = "http://gramps-project.org/xml/1.7.1/grampsxml.dtd" # Event types to add EVENT_TYPES = [ @@ -29,10 +40,115 @@ EVENT_TYPES = [ ("Cremation", 0.2, None, None), # 20% chance if death exists, at death time ] -# Generate additional events for a person -def gen_additional_events(pid, first_name, surname, birth_year, death_year=None): - events = [] - event_id_offset = pid * 10 + 2 # Start after birth and death events +# Name lists +MALE_NAMES = [ + "James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", + "Thomas", "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", + "Paul", "Andrew", "Joshua", "Kenneth", "Kevin", "Brian", "George", "Timothy", + "Ronald", "Jason", "Edward", "Jeffrey", "Ryan", "Jacob", "Gary", "Nicholas", + "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon", "Benjamin" +] + +FEMALE_NAMES = [ + "Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan", + "Jessica", "Sarah", "Karen", "Nancy", "Lisa", "Betty", "Margaret", "Sandra", + "Ashley", "Kimberly", "Emily", "Donna", "Michelle", "Dorothy", "Carol", + "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura", + "Cynthia", "Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela", + "Emma", "Nicole", "Helen", "Samantha", "Katherine", "Christine", "Debra" +] + +SURNAMES = [ + "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", + "Rodriguez", "Martinez", "Hernandez", "Lopez", "Wilson", "Anderson", "Thomas", + "Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White", "Harris", + "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", + "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", "Adams" +] + +OCCUPATIONS = [ + "Farmer", "Teacher", "Engineer", "Doctor", "Lawyer", "Merchant", + "Carpenter", "Blacksmith", "Sailor", "Soldier", "Clerk", "Nurse" +] + +PLACES = [ + "New York", "London", "Paris", "Berlin", "Rome", "Madrid", "Amsterdam", + "Vienna", "Prague", "Warsaw", "Stockholm", "Copenhagen" +] + + +@dataclass +class EventData: + """Data structure for an event.""" + handle: str + event_type: str + year: int + month: int + day: int + description: str + event_id: int + + +@dataclass +class PersonData: + """Data structure for person information.""" + handle: str + name: str + surname: str + birth: int + death: Optional[int] + gender: str + parentin: List[str] + childof: List[str] + + +@dataclass +class FamilyData: + """Data structure for family information.""" + handle: str + father_handle: str + mother_handle: str + children_handles: List[str] + marriage_year: int + marriage_handle: str + family_id: int + + +def gen_handle(prefix: str, num: int) -> str: + """Generate unique handle.""" + return f"_{prefix}{num:08d}" + + +def create_event_element(event_data: EventData) -> ET.Element: + """Create an XML element for an event.""" + event_elem = ET.Element("event") + event_elem.set("handle", event_data.handle) + event_elem.set("change", str(int(datetime.now().timestamp()))) + event_elem.set("id", f"E{event_data.event_id:04d}") + + type_elem = ET.SubElement(event_elem, "type") + type_elem.text = event_data.event_type + + date_elem = ET.SubElement(event_elem, "dateval") + date_elem.set("val", f"{event_data.year}-{event_data.month:02d}-{event_data.day:02d}") + + if event_data.description: + desc_elem = ET.SubElement(event_elem, "description") + desc_elem.text = event_data.description + + return event_elem + + +def gen_additional_events( + pid: int, + first_name: str, + surname: str, + birth_year: int, + death_year: Optional[int] = None +) -> List[Tuple[str, EventData]]: + """Generate additional events for a person.""" + events: List[Tuple[str, EventData]] = [] + event_id_offset = pid * EVENT_ID_OFFSET + EVENT_ID_START_OFFSET for event_type, probability, min_years, max_years in EVENT_TYPES: if random.random() > probability: @@ -43,19 +159,18 @@ def gen_additional_events(pid, first_name, surname, birth_year, death_year=None) if not death_year: continue event_year = death_year - event_month = random.randint(1, 12) - event_day = random.randint(1, 28) + event_month = random.randint(MIN_MONTH, MAX_MONTH) + event_day = random.randint(MIN_DAY, MAX_DAY) else: if max_years is None: continue event_year = birth_year + random.randint(min_years, max_years) if death_year and event_year > death_year: continue - event_month = random.randint(1, 12) - event_day = random.randint(1, 28) + event_month = random.randint(MIN_MONTH, MAX_MONTH) + event_day = random.randint(MIN_DAY, MAX_DAY) event_handle = gen_handle("EVENT", event_id_offset) - event_id_offset += 1 # Generate description based on event type if event_type == "Education": @@ -63,16 +178,12 @@ def gen_additional_events(pid, first_name, surname, birth_year, death_year=None) elif event_type == "Graduation": description = f"Graduation - {first_name} {surname}" elif event_type == "Occupation": - occupations = ["Farmer", "Teacher", "Engineer", "Doctor", "Lawyer", "Merchant", - "Carpenter", "Blacksmith", "Sailor", "Soldier", "Clerk", "Nurse"] - occupation = random.choice(occupations) + occupation = random.choice(OCCUPATIONS) description = f"{occupation} - {first_name} {surname}" elif event_type == "Military Service": description = f"Military Service - {first_name} {surname}" elif event_type == "Residence": - places = ["New York", "London", "Paris", "Berlin", "Rome", "Madrid", "Amsterdam", - "Vienna", "Prague", "Warsaw", "Stockholm", "Copenhagen"] - place = random.choice(places) + place = random.choice(PLACES) description = f"Residence in {place} - {first_name} {surname}" elif event_type == "Emigration": description = f"Emigration - {first_name} {surname}" @@ -83,136 +194,218 @@ def gen_additional_events(pid, first_name, surname, birth_year, death_year=None) else: description = f"{event_type} of {surname}, {first_name}" - event_xml = f""" - {event_type} - - {description} - -""" - events.append((event_handle, event_xml)) + event_data = EventData( + handle=event_handle, + event_type=event_type, + year=event_year, + month=event_month, + day=event_day, + description=description, + event_id=event_id_offset + ) + + events.append((event_handle, event_data)) + event_id_offset += 1 return events -# Generate a person -def gen_person(pid, first_name, surname, birth_year, death_year=None, gender="M", - parentin_families=None, childof_families=None, reuse_additional_events=None): + +def gen_person( + pid: int, + first_name: str, + surname: str, + birth_year: int, + death_year: Optional[int] = None, + gender: str = "M", + parentin_families: Optional[List[str]] = None, + childof_families: Optional[List[str]] = None, + reuse_additional_events: Optional[List[Tuple[str, EventData]]] = None +) -> Tuple[ET.Element, ET.Element, Optional[ET.Element], List[ET.Element], List[Tuple[str, EventData]]]: + """Generate a person with all associated events.""" handle = gen_handle("PERSON", pid) - birth_handle = gen_handle("EVENT", pid * 10) - death_handle = gen_handle("EVENT", pid * 10 + 1) if death_year else None + birth_handle = gen_handle("EVENT", pid * EVENT_ID_OFFSET) + death_handle = gen_handle("EVENT", pid * EVENT_ID_OFFSET + 1) if death_year else None - person_xml = f""" - {gender} - - {first_name} - {surname} - - -""" + # Create person element + person_elem = ET.Element("person") + person_elem.set("handle", handle) + person_elem.set("change", str(int(datetime.now().timestamp()))) + person_elem.set("id", f"I{pid:04d}") + + gender_elem = ET.SubElement(person_elem, "gender") + gender_elem.text = gender + + name_elem = ET.SubElement(person_elem, "name") + name_elem.set("type", "Birth Name") + first_elem = ET.SubElement(name_elem, "first") + first_elem.text = first_name + surname_elem = ET.SubElement(name_elem, "surname") + surname_elem.text = surname + + # Birth event reference + birth_ref = ET.SubElement(person_elem, "eventref") + birth_ref.set("hlink", birth_handle) + birth_ref.set("role", "Primary") + + # Death event reference if death_handle: - person_xml += f""" -""" + death_ref = ET.SubElement(person_elem, "eventref") + death_ref.set("hlink", death_handle) + death_ref.set("role", "Primary") # Add additional events - reuse if provided, otherwise generate new if reuse_additional_events is not None: - # reuse_additional_events is a list of (handle, xml) tuples additional_events = reuse_additional_events else: additional_events = gen_additional_events(pid, first_name, surname, birth_year, death_year) for event_handle, _ in additional_events: - person_xml += f""" -""" + event_ref = ET.SubElement(person_elem, "eventref") + event_ref.set("hlink", event_handle) + event_ref.set("role", "Primary") - # Add parentin references (for fathers and mothers) + # Add parentin references if parentin_families: for family_handle in parentin_families: - person_xml += f""" -""" - # Add childof references (for children) + parentin_elem = ET.SubElement(person_elem, "parentin") + parentin_elem.set("hlink", family_handle) + + # Add childof references if childof_families: for family_handle in childof_families: - person_xml += f""" -""" - person_xml += """ -""" + childof_elem = ET.SubElement(person_elem, "childof") + childof_elem.set("hlink", family_handle) # Birth event - birth_month = random.randint(1, 12) - birth_day = random.randint(1, 28) - birth_event = f""" - Birth - - Birth of {surname}, {first_name} - -""" + birth_month = random.randint(MIN_MONTH, MAX_MONTH) + birth_day = random.randint(MIN_DAY, MAX_DAY) + birth_event_data = EventData( + handle=birth_handle, + event_type="Birth", + year=birth_year, + month=birth_month, + day=birth_day, + description=f"Birth of {surname}, {first_name}", + event_id=pid * EVENT_ID_OFFSET + ) + birth_event = create_event_element(birth_event_data) # Death event - death_event = "" + death_event: Optional[ET.Element] = None if death_handle and death_year: - death_month = random.randint(1, 12) - death_day = random.randint(1, 28) - death_event = f""" - Death - - Death of {surname}, {first_name} - -""" + death_month = random.randint(MIN_MONTH, MAX_MONTH) + death_day = random.randint(MIN_DAY, MAX_DAY) + death_event_data = EventData( + handle=death_handle, + event_type="Death", + year=death_year, + month=death_month, + day=death_day, + description=f"Death of {surname}, {first_name}", + event_id=pid * EVENT_ID_OFFSET + 1 + ) + death_event = create_event_element(death_event_data) - # Collect all additional events (return tuples for reuse, XML strings for output) - all_additional_events_xml = [event_xml for _, event_xml in additional_events] + # Convert additional events to XML elements + all_additional_events_xml = [create_event_element(event_data) for _, event_data in additional_events] - return person_xml, birth_event, death_event, all_additional_events_xml, additional_events + return person_elem, birth_event, death_event, all_additional_events_xml, additional_events -# Generate a family -def gen_family(fid, father_handle, mother_handle, marriage_year, children_handles): + +def gen_family( + fid: int, + father_handle: str, + mother_handle: str, + marriage_year: int, + children_handles: List[str] +) -> Tuple[ET.Element, ET.Element]: + """Generate a family with marriage event.""" handle = gen_handle("FAMILY", fid) - marriage_handle = gen_handle("EVENT", fid * 100) + marriage_handle = gen_handle("EVENT", fid * FAMILY_ID_OFFSET) + + # Create family element + family_elem = ET.Element("family") + family_elem.set("handle", handle) + family_elem.set("change", str(int(datetime.now().timestamp()))) + family_elem.set("id", f"F{fid:04d}") + + rel_elem = ET.SubElement(family_elem, "rel") + rel_elem.set("type", "Married") + + father_elem = ET.SubElement(family_elem, "father") + father_elem.set("hlink", father_handle) + + mother_elem = ET.SubElement(family_elem, "mother") + mother_elem.set("hlink", mother_handle) - family_xml = f""" - - - -""" for child_handle in children_handles: - family_xml += f""" -""" - family_xml += f""" - -""" + child_elem = ET.SubElement(family_elem, "childref") + child_elem.set("hlink", child_handle) + + marriage_ref = ET.SubElement(family_elem, "eventref") + marriage_ref.set("hlink", marriage_handle) + marriage_ref.set("role", "Family") # Marriage event - marriage_month = random.randint(1, 12) - marriage_day = random.randint(1, 28) - marriage_event = f""" - Marriage - - Marriage - -""" + marriage_month = random.randint(MIN_MONTH, MAX_MONTH) + marriage_day = random.randint(MIN_DAY, MAX_DAY) + marriage_event_data = EventData( + handle=marriage_handle, + event_type="Marriage", + year=marriage_year, + month=marriage_month, + day=marriage_day, + description="Marriage", + event_id=fid * FAMILY_ID_OFFSET + ) + marriage_event = create_event_element(marriage_event_data) - return family_xml, marriage_event + return family_elem, marriage_event -# First names -male_names = ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", - "Thomas", "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", - "Paul", "Andrew", "Joshua", "Kenneth", "Kevin", "Brian", "George", "Timothy", - "Ronald", "Jason", "Edward", "Jeffrey", "Ryan", "Jacob", "Gary", "Nicholas", - "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon", "Benjamin"] -female_names = ["Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan", - "Jessica", "Sarah", "Karen", "Nancy", "Lisa", "Betty", "Margaret", "Sandra", - "Ashley", "Kimberly", "Emily", "Donna", "Michelle", "Dorothy", "Carol", - "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura", - "Cynthia", "Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela", - "Emma", "Nicole", "Helen", "Samantha", "Katherine", "Christine", "Debra"] +def create_gramps_xml_document( + events: List[ET.Element], + people: List[ET.Element], + families: List[ET.Element] +) -> ET.ElementTree: + """Create the complete Gramps XML document.""" + # Create root element + database = ET.Element("database") + database.set("xmlns", GRAMPS_XML_NAMESPACE) + + # Header + header = ET.SubElement(database, "header") + created = ET.SubElement(header, "created") + created.set("date", datetime.now().strftime('%Y-%m-%d')) + created.set("version", GRAMPS_XML_VERSION) + + researcher = ET.SubElement(header, "researcher") + resname = ET.SubElement(researcher, "resname") + resname.text = "Demo Family Generator" + + # Tags (empty) + ET.SubElement(database, "tags") + + # Events + events_elem = ET.SubElement(database, "events") + for event in events: + events_elem.append(event) + + # People + people_elem = ET.SubElement(database, "people") + for person in people: + people_elem.append(person) + + # Families + families_elem = ET.SubElement(database, "families") + for family in families: + families_elem.append(family) + + return ET.ElementTree(database) -surnames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", - "Rodriguez", "Martinez", "Hernandez", "Lopez", "Wilson", "Anderson", "Thomas", - "Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White", "Harris", - "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", - "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", "Adams"] -def main(): +def main() -> None: + """Main function to generate the demo family.""" print("Generating huge demo family...") # Generate main family @@ -221,7 +414,7 @@ def main(): father_handle = gen_handle("PERSON", father_id) main_family_handle = gen_handle("FAMILY", 1) father_person, father_birth, father_death, father_additional_xml, _ = gen_person( - father_id, "John", "Smith", 1950, 2010, "M", + father_id, "John", "Smith", 1950, 2010, "M", parentin_families=[main_family_handle] ) @@ -234,17 +427,21 @@ def main(): ) all_additional_events = father_additional_xml + mother_additional_xml + all_events = [father_birth, mother_birth] + if father_death: + all_events.append(father_death) + if mother_death: + all_events.append(mother_death) # Generate 15 children - children = [] - child_handles = [] - child_events = [] - child_additional_events_map = {} # Store additional events by child_id + children: List[ET.Element] = [] + child_handles: List[str] = [] + child_additional_events_map: Dict[int, List[Tuple[str, EventData]]] = {} child_id = 3 for i in range(15): gender = "M" if i % 2 == 0 else "F" - first_name = random.choice(male_names if gender == "M" else female_names) + first_name = random.choice(MALE_NAMES if gender == "M" else FEMALE_NAMES) birth_year = 1970 + (i * 2) # Spread births from 1970 to 1998 death_year = birth_year + random.randint(60, 90) if random.random() < 0.3 else None # 30% chance of death @@ -256,9 +453,9 @@ def main(): children.append(child_person) child_handles.append(child_handle) - child_events.append(child_birth) + all_events.append(child_birth) if child_death: - child_events.append(child_death) + all_events.append(child_death) # Store tuples for reuse when regenerating child_additional_events_map[child_id] = child_additional_tuples all_additional_events.extend(child_additional_xml) @@ -266,40 +463,66 @@ def main(): # Generate family family_id = 1 - family_xml, marriage_event = gen_family(family_id, father_handle, mother_handle, 1969, child_handles) + family_elem, marriage_event = gen_family(family_id, father_handle, mother_handle, 1969, child_handles) + all_events.append(marriage_event) + families: List[ET.Element] = [family_elem] # Track person data for regeneration (needed for children who become parents) - import re - person_data = {} + person_data: Dict[int, PersonData] = {} # Store initial person data - person_data[father_id] = {"handle": father_handle, "name": "John", "surname": "Smith", - "birth": 1950, "death": 2010, "gender": "M", - "parentin": [main_family_handle], "childof": []} - person_data[mother_id] = {"handle": mother_handle, "name": "Mary", "surname": "Smith", - "birth": 1952, "death": 2015, "gender": "F", - "parentin": [main_family_handle], "childof": []} + person_data[father_id] = PersonData( + handle=father_handle, + name="John", + surname="Smith", + birth=1950, + death=2010, + gender="M", + parentin=[main_family_handle], + childof=[] + ) + person_data[mother_id] = PersonData( + handle=mother_handle, + name="Mary", + surname="Smith", + birth=1952, + death=2015, + gender="F", + parentin=[main_family_handle], + childof=[] + ) + for i, child_handle in enumerate(child_handles): child_pid = 3 + i gender = "M" if i % 2 == 0 else "F" # Extract name from generated child XML - child_xml = children[i] - name_match = re.search(r'([^<]+)', child_xml) - first_name = name_match.group(1) if name_match else random.choice(male_names if gender == "M" else female_names) + name_elem = children[i].find(".//first") + first_name = name_elem.text if name_elem is not None and name_elem.text else random.choice(MALE_NAMES if gender == "M" else FEMALE_NAMES) birth_year = 1970 + (i * 2) - # Extract death year from child_events if it exists + # Extract death year from events if it exists death_year = None - for event in child_events: - if f"id=\"E{child_pid*10+1:04d}\"" in event: - match = re.search(r'val="(\d{4})', event) - if match: - death_year = int(match.group(1)) - person_data[child_pid] = {"handle": child_handle, "name": first_name, "surname": "Smith", - "birth": birth_year, "death": death_year, "gender": gender, - "parentin": [], "childof": [main_family_handle]} + for event in all_events: + if event.get("id") == f"E{child_pid * EVENT_ID_OFFSET + 1:04d}": + date_elem = event.find(".//dateval") + if date_elem is not None: + date_val = date_elem.get("val", "") + if date_val: + try: + death_year = int(date_val.split("-")[0]) + except (ValueError, IndexError): + pass + person_data[child_pid] = PersonData( + handle=child_handle, + name=first_name, + surname="Smith", + birth=birth_year, + death=death_year, + gender=gender, + parentin=[], + childof=[main_family_handle] + ) # Generate grandchildren (children of first 5 children) - grandchildren = [] - grandchild_events = [] + grandchildren: List[ET.Element] = [] grandchild_id = child_id for i in range(5): # First 5 children have children @@ -309,41 +532,55 @@ def main(): spouse_gender = "F" if parent_gender == "M" else "M" # Create spouse - spouse_name = random.choice(female_names if spouse_gender == "F" else male_names) + spouse_name = random.choice(FEMALE_NAMES if spouse_gender == "F" else MALE_NAMES) spouse_birth = 1970 + (i * 2) + random.randint(-2, 2) spouse_handle = gen_handle("PERSON", grandchild_id) child_family_handle = gen_handle("FAMILY", family_id + 1) - person_data[grandchild_id] = {"handle": spouse_handle, "name": spouse_name, "surname": "Smith", - "birth": spouse_birth, "death": None, "gender": spouse_gender, - "parentin": [child_family_handle], "childof": []} + person_data[grandchild_id] = PersonData( + handle=spouse_handle, + name=spouse_name, + surname="Smith", + birth=spouse_birth, + death=None, + gender=spouse_gender, + parentin=[child_family_handle], + childof=[] + ) spouse_person, spouse_birth_event, spouse_death_event, spouse_additional_xml, _ = gen_person( grandchild_id, spouse_name, "Smith", spouse_birth, None, spouse_gender, parentin_families=[child_family_handle] ) grandchildren.append(spouse_person) - grandchild_events.append(spouse_birth_event) + all_events.append(spouse_birth_event) if spouse_death_event: - grandchild_events.append(spouse_death_event) + all_events.append(spouse_death_event) all_additional_events.extend(spouse_additional_xml) grandchild_id += 1 # Update parent to include parentin reference - person_data[parent_pid]["parentin"].append(child_family_handle) + person_data[parent_pid].parentin.append(child_family_handle) # Create 3-5 children per couple num_grandchildren = random.randint(3, 5) - grandchild_handles = [] + grandchild_handles: List[str] = [] for j in range(num_grandchildren): gchild_gender = "M" if j % 2 == 0 else "F" - gchild_name = random.choice(male_names if gchild_gender == "M" else female_names) + gchild_name = random.choice(MALE_NAMES if gchild_gender == "M" else FEMALE_NAMES) gchild_birth = 1995 + (i * 3) + j gchild_handle = gen_handle("PERSON", grandchild_id) - person_data[grandchild_id] = {"handle": gchild_handle, "name": gchild_name, "surname": "Smith", - "birth": gchild_birth, "death": None, "gender": gchild_gender, - "parentin": [], "childof": [child_family_handle]} + person_data[grandchild_id] = PersonData( + handle=gchild_handle, + name=gchild_name, + surname="Smith", + birth=gchild_birth, + death=None, + gender=gchild_gender, + parentin=[], + childof=[child_family_handle] + ) gchild_person, gchild_birth_event, gchild_death_event, gchild_additional_xml, _ = gen_person( grandchild_id, gchild_name, "Smith", gchild_birth, None, gchild_gender, @@ -351,17 +588,17 @@ def main(): ) grandchildren.append(gchild_person) grandchild_handles.append(gchild_handle) - grandchild_events.append(gchild_birth_event) + all_events.append(gchild_birth_event) if gchild_death_event: - grandchild_events.append(gchild_death_event) + all_events.append(gchild_death_event) all_additional_events.extend(gchild_additional_xml) grandchild_id += 1 # Create family for this couple family_id += 1 - fam_xml, fam_marriage = gen_family(family_id, parent_handle, spouse_handle, 1990 + i, grandchild_handles) - family_xml += fam_xml - child_events.append(fam_marriage) + fam_elem, fam_marriage = gen_family(family_id, parent_handle, spouse_handle, 1990 + i, grandchild_handles) + families.append(fam_elem) + all_events.append(fam_marriage) # Regenerate children XMLs with updated family references # We need to regenerate to update family references, but reuse the same events @@ -372,63 +609,41 @@ def main(): # Reuse the original additional events to ensure consistency original_additional_events = child_additional_events_map.get(child_pid, []) child_person, _, _, _, _ = gen_person( - child_pid, data["name"], data["surname"], data["birth"], data["death"], data["gender"], - parentin_families=data["parentin"], childof_families=data["childof"], + child_pid, data.name, data.surname, data.birth, data.death, data.gender, + parentin_families=data.parentin, childof_families=data.childof, reuse_additional_events=original_additional_events ) children.append(child_person) - # Write XML file - xml_content = f""" - - -
- - - Demo Family Generator - -
- - - -{father_birth} -{father_death} -{mother_birth} -{mother_death} -{marriage_event} -""" + # Add all additional events to events list + all_events.extend(all_additional_events) - for event in child_events: - xml_content += event - for event in grandchild_events: - xml_content += event - for event in all_additional_events: - xml_content += event + # Create complete XML document + people = [father_person, mother_person] + children + grandchildren + tree = create_gramps_xml_document(all_events, people, families) - xml_content += """ - -""" - xml_content += father_person - xml_content += mother_person - for child in children: - xml_content += child - for grandchild in grandchildren: - xml_content += grandchild + # Write XML file with proper formatting + # ET.indent is only available in Python 3.9+, so we'll format manually if needed + try: + ET.indent(tree, space=" ") + except AttributeError: + # Python < 3.9 doesn't have indent, will write without indentation + pass + tree.write("demo_family.gramps", encoding="utf-8", xml_declaration=True) - xml_content += """ - -""" - xml_content += family_xml + # Add DOCTYPE declaration (ElementTree doesn't support this directly) + with open("demo_family.gramps", "r", encoding="utf-8") as f: + content = f.read() - xml_content += """ -
-""" + # Insert DOCTYPE after XML declaration + doctype = f'\n' + content = content.replace('', + f'\n{doctype}', 1) with open("demo_family.gramps", "w", encoding="utf-8") as f: - f.write(xml_content) + f.write(content) - total_events = len(child_events) + len(grandchild_events) + len(all_additional_events) + total_events = len(all_events) print(f"Generated demo_family.gramps with:") print(f" - 2 parents (John and Mary Smith)") print(f" - 15 children") @@ -439,6 +654,6 @@ def main(): print(f" - {len(all_additional_events)} additional events (Baptism, Education, Occupation, etc.)") print(f" - Total events: {total_events}") + if __name__ == "__main__": main() - diff --git a/install_to_snap.sh b/install_to_snap.sh index 36c9b84..a83f078 100755 --- a/install_to_snap.sh +++ b/install_to_snap.sh @@ -6,12 +6,13 @@ # so it can be used in the snap-installed version of Gramps 6.0 # -set -e # Exit on error +set -euo pipefail # Exit on error, undefined vars, pipe failures # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +BLUE='\033[0;34m' NC='\033[0m' # No Color # Get the directory where this script is located @@ -40,6 +41,22 @@ echo "MyTimeline Plugin Installer for Snap Gramps" echo "==========================================" echo "" +# Function to check if Gramps is installed via snap +check_gramps_installed() { + if ! command -v gramps &> /dev/null && ! snap list gramps &> /dev/null; then + echo -e "${YELLOW}Warning: Gramps may not be installed via snap.${NC}" + echo "This script is designed for snap-installed Gramps." + echo "If Gramps is installed differently, you may need to manually install the plugin." + echo "" + read -p "Continue anyway? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 0 + fi + fi +} + # Function to find the correct plugin directory find_plugin_dir() { # Try the primary location first @@ -65,6 +82,9 @@ find_plugin_dir() { return 0 } +# Check if Gramps is installed +check_gramps_installed + # Verify source files exist echo "Checking source files..." MISSING_FILES=() @@ -82,6 +102,11 @@ if [ ${#MISSING_FILES[@]} -gt 0 ]; then for file in "${MISSING_FILES[@]}"; do echo " - $file" done + echo "" + echo -e "${YELLOW}Troubleshooting:${NC}" + echo " 1. Make sure you're running this script from the directory containing the plugin files" + echo " 2. Verify that MyTimeline.gpr.py and MyTimeline.py exist in: $PLUGIN_SRC_DIR" + echo " 3. Check file permissions: ls -la $PLUGIN_SRC_DIR" exit 1 fi @@ -92,7 +117,14 @@ TARGET_DIR=$(find_plugin_dir) if [ -z "$TARGET_DIR" ]; then echo -e "${RED}Error: Could not determine plugin directory location.${NC}" - echo "Please check if Gramps is installed via snap." + echo "" + echo -e "${YELLOW}Troubleshooting:${NC}" + echo " 1. Verify Gramps is installed: snap list gramps" + echo " 2. Check if snap directory exists: ls -la $HOME/snap/gramps/" + echo " 3. Try running Gramps at least once to create the plugin directory" + echo " 4. Manually create the directory: mkdir -p $PLUGIN_DEST_DIR" + echo "" + echo "If the issue persists, you may need to install the plugin manually." exit 1 fi @@ -126,7 +158,11 @@ fi # Copy plugin files echo "Installing plugin files..." for file in "${PLUGIN_FILES[@]}"; do - cp "$PLUGIN_SRC_DIR/$file" "$TARGET_DIR/" + if ! cp "$PLUGIN_SRC_DIR/$file" "$TARGET_DIR/"; then + echo -e "${RED}Error: Failed to copy $file${NC}" + echo "Check file permissions and disk space." + exit 1 + fi echo -e "${GREEN}✓${NC} Installed: $file" done diff --git a/uninstall_from_snap.sh b/uninstall_from_snap.sh index 2601f0e..73b5225 100755 --- a/uninstall_from_snap.sh +++ b/uninstall_from_snap.sh @@ -5,12 +5,13 @@ # This script removes the MyTimeline plugin files from the Gramps snap plugin directory # -set -e # Exit on error +set -euo pipefail # Exit on error, undefined vars, pipe failures # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +BLUE='\033[0;34m' NC='\033[0m' # No Color # Plugin files to remove @@ -60,6 +61,12 @@ 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." + echo "" + echo -e "${BLUE}Note:${NC} If you installed the plugin manually, you may need to remove it manually." + echo "Common locations to check:" + for dir in "${ALTERNATIVE_DIRS[@]}"; do + echo " - $dir" + done exit 0 fi @@ -88,7 +95,11 @@ echo "" echo "Removing plugin files..." for file in "${PLUGIN_FILES[@]}"; do if [ -f "$TARGET_DIR/$file" ]; then - rm "$TARGET_DIR/$file" + if ! rm "$TARGET_DIR/$file"; then + echo -e "${RED}Error: Failed to remove $file${NC}" + echo "Check file permissions." + exit 1 + fi echo -e "${GREEN}✓${NC} Removed: $file" FILES_REMOVED=$((FILES_REMOVED + 1)) fi