diff --git a/MyTimeline.py b/MyTimeline.py index dc7df9b..5594870 100644 --- a/MyTimeline.py +++ b/MyTimeline.py @@ -603,6 +603,48 @@ class MyTimelineView(NavigationView): self.filter_dialog.hide() + def _calculate_group_state(self, child_checkboxes: List[Gtk.CheckButton]) -> str: + """ + Calculate the state of a group based on child checkboxes. + + Args: + child_checkboxes: List of child checkboxes in the group. + + Returns: + str: 'all' if all selected, 'none' if none selected, 'some' if partially selected. + """ + if not child_checkboxes: + return 'none' + + active_count = sum(1 for cb in child_checkboxes if cb.get_active()) + + if active_count == 0: + return 'none' + elif active_count == len(child_checkboxes): + return 'all' + else: + return 'some' + + def _update_group_checkbox_state(self, group_checkbox: Gtk.CheckButton, + child_checkboxes: List[Gtk.CheckButton]) -> None: + """ + Update a group checkbox state based on child checkboxes. + + Args: + group_checkbox: The parent group checkbox to update. + child_checkboxes: List of child checkboxes. + """ + state = self._calculate_group_state(child_checkboxes) + + if state == 'all': + group_checkbox.set_active(True) + group_checkbox.set_inconsistent(False) + elif state == 'none': + group_checkbox.set_active(False) + group_checkbox.set_inconsistent(False) + else: # 'some' + group_checkbox.set_inconsistent(True) + def _build_filter_dialog(self) -> Gtk.Dialog: """ Build the filter dialog with all filter controls. @@ -682,34 +724,84 @@ class MyTimelineView(NavigationView): # Group event types by category event_type_checkboxes = {} category_boxes = {} + category_checkboxes = {} + category_event_types = {} # Map category to list of event types - # Iterate over event types from EVENT_COLORS (which uses EventType integers as keys) - # EventType members are already integers in Gramps + # First pass: collect event types by category for event_type_obj in EVENT_COLORS.keys(): - # EventType is already an integer, use it directly if event_type_obj not in EVENT_CATEGORIES: continue - category = EVENT_CATEGORIES[event_type_obj] - if category not in category_boxes: - # Create category section - category_label = Gtk.Label(label=f"{category}") - category_label.set_use_markup(True) - category_label.set_halign(Gtk.Align.START) - box.pack_start(category_label, False, False, 0) - - category_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) - category_box.set_margin_start(20) - category_boxes[category] = category_box - box.pack_start(category_box, False, False, 0) + if category not in category_event_types: + category_event_types[category] = [] + category_event_types[category].append(event_type_obj) + + # Second pass: create UI with category checkboxes + for category in sorted(category_event_types.keys()): + event_types_in_category = category_event_types[category] - # Create checkbox for event type with human-readable name - event_type_name = self._get_event_type_display_name(event_type_obj) - checkbox = Gtk.CheckButton(label=event_type_name) - event_type_checkboxes[event_type_obj] = checkbox - category_boxes[category].pack_start(checkbox, False, False, 0) + # Create category checkbox with three-state support + category_checkbox = Gtk.CheckButton(label=category) + category_checkboxes[category] = category_checkbox + box.pack_start(category_checkbox, False, False, 0) + + # Create container for event types in this category + category_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) + category_box.set_margin_start(20) + category_boxes[category] = category_box + box.pack_start(category_box, False, False, 0) + + # Create checkboxes for each event type in this category + child_checkboxes = [] + for event_type_obj in sorted(event_types_in_category, key=lambda x: self._get_event_type_display_name(x)): + event_type_name = self._get_event_type_display_name(event_type_obj) + checkbox = Gtk.CheckButton(label=event_type_name) + event_type_checkboxes[event_type_obj] = checkbox + child_checkboxes.append(checkbox) + category_box.pack_start(checkbox, False, False, 0) + + # Flag to prevent recursion between category and child checkboxes + 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 + + # 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._filter_widgets['event_type_checkboxes'] = event_type_checkboxes + self._filter_widgets['category_checkboxes'] = category_checkboxes + self._filter_widgets['category_event_types'] = category_event_types scrolled.add(box) return scrolled @@ -920,13 +1012,18 @@ class MyTimelineView(NavigationView): else: checkbox.set_active(event_type in self.active_event_types) - # Update category checkboxes - if 'category_checkboxes' in self._filter_widgets: - for category, checkbox in self._filter_widgets['category_checkboxes'].items(): - if not self.category_filter: - checkbox.set_active(True) # All selected when filter is off - else: - checkbox.set_active(category in self.category_filter) + # Update category checkboxes based on their children's states + if 'category_checkboxes' in self._filter_widgets and 'category_event_types' in self._filter_widgets: + for category, category_checkbox in self._filter_widgets['category_checkboxes'].items(): + # Get child checkboxes for this category + event_types_in_category = self._filter_widgets['category_event_types'].get(category, []) + child_checkboxes = [ + self._filter_widgets['event_type_checkboxes'][et] + for et in event_types_in_category + if et in self._filter_widgets['event_type_checkboxes'] + ] + # Update category checkbox state based on children + self._update_group_checkbox_state(category_checkbox, child_checkboxes) # Update person checkboxes with families if 'person_checkboxes' in self._filter_widgets and 'person_container' in self._filter_widgets: @@ -960,16 +1057,15 @@ class MyTimelineView(NavigationView): # 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) + # Collect all child checkboxes for this family + child_checkboxes = [] + # Add father checkbox father_handle = family.get_father_handle() if father_handle: @@ -980,6 +1076,7 @@ class MyTimelineView(NavigationView): 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 + child_checkboxes.append(checkbox) members_box.pack_start(checkbox, False, False, 0) except (AttributeError, KeyError): pass @@ -994,6 +1091,7 @@ class MyTimelineView(NavigationView): 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 + child_checkboxes.append(checkbox) members_box.pack_start(checkbox, False, False, 0) except (AttributeError, KeyError): pass @@ -1008,12 +1106,66 @@ class MyTimelineView(NavigationView): 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 + child_checkboxes.append(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: + # Create family checkbox with three-state support + family_checkbox = Gtk.CheckButton(label=family_name) + + # Create expander with checkbox as label + expander = Gtk.Expander() + # Set the checkbox as the label widget + expander.set_label_widget(family_checkbox) + expander.set_expanded(False) + + # Store family checkbox + if 'family_checkboxes' not in self._filter_widgets: + self._filter_widgets['family_checkboxes'] = {} + self._filter_widgets['family_checkboxes'][family_handle] = family_checkbox + + # Flag to prevent recursion + 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 + + # 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)) + + # Initialize family checkbox state + self._update_group_checkbox_state(family_checkbox, child_checkboxes) + expander.add(members_box) self._filter_widgets['family_expanders'][family_handle] = expander container.pack_start(expander, False, False, 0)