Add three-state toggles to filter dialog groups

- Added helper methods for three-state checkbox management
  (_calculate_group_state, _update_group_checkbox_state)
- Replaced category labels with three-state checkboxes in Event Types tab
- Added three-state checkboxes to family expander labels in Persons tab
- Implemented bidirectional state synchronization between group and child checkboxes
- Group checkboxes show checked (all), unchecked (none), or inconsistent (some) states
This commit is contained in:
Daniel Viegas 2025-11-29 21:58:35 +01:00
parent ce75cd55bb
commit c9f6e7f8b8

View File

@ -603,6 +603,48 @@ class MyTimelineView(NavigationView):
self.filter_dialog.hide() 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: def _build_filter_dialog(self) -> Gtk.Dialog:
""" """
Build the filter dialog with all filter controls. Build the filter dialog with all filter controls.
@ -682,34 +724,84 @@ class MyTimelineView(NavigationView):
# Group event types by category # Group event types by category
event_type_checkboxes = {} event_type_checkboxes = {}
category_boxes = {} 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) # First pass: collect event types by category
# EventType members are already integers in Gramps
for event_type_obj in EVENT_COLORS.keys(): for event_type_obj in EVENT_COLORS.keys():
# EventType is already an integer, use it directly
if event_type_obj not in EVENT_CATEGORIES: if event_type_obj not in EVENT_CATEGORIES:
continue continue
category = EVENT_CATEGORIES[event_type_obj] category = EVENT_CATEGORIES[event_type_obj]
if category not in category_boxes: if category not in category_event_types:
# Create category section category_event_types[category] = []
category_label = Gtk.Label(label=f"<b>{category}</b>") category_event_types[category].append(event_type_obj)
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) # Second pass: create UI with category checkboxes
category_box.set_margin_start(20) for category in sorted(category_event_types.keys()):
category_boxes[category] = category_box event_types_in_category = category_event_types[category]
box.pack_start(category_box, False, False, 0)
# Create checkbox for event type with human-readable name # Create category checkbox with three-state support
event_type_name = self._get_event_type_display_name(event_type_obj) category_checkbox = Gtk.CheckButton(label=category)
checkbox = Gtk.CheckButton(label=event_type_name) category_checkboxes[category] = category_checkbox
event_type_checkboxes[event_type_obj] = checkbox box.pack_start(category_checkbox, False, False, 0)
category_boxes[category].pack_start(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['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) scrolled.add(box)
return scrolled return scrolled
@ -920,13 +1012,18 @@ class MyTimelineView(NavigationView):
else: else:
checkbox.set_active(event_type in self.active_event_types) checkbox.set_active(event_type in self.active_event_types)
# Update category checkboxes # Update category checkboxes based on their children's states
if 'category_checkboxes' in self._filter_widgets: if 'category_checkboxes' in self._filter_widgets and 'category_event_types' in self._filter_widgets:
for category, checkbox in self._filter_widgets['category_checkboxes'].items(): for category, category_checkbox in self._filter_widgets['category_checkboxes'].items():
if not self.category_filter: # Get child checkboxes for this category
checkbox.set_active(True) # All selected when filter is off event_types_in_category = self._filter_widgets['category_event_types'].get(category, [])
else: child_checkboxes = [
checkbox.set_active(category in self.category_filter) 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 # 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:
@ -960,16 +1057,15 @@ class MyTimelineView(NavigationView):
# Get family display name # Get family display name
family_name = self._get_family_display_name(family) 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 # Create container for family members
members_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) members_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
members_box.set_margin_start(20) members_box.set_margin_start(20)
members_box.set_margin_top(5) members_box.set_margin_top(5)
members_box.set_margin_bottom(5) members_box.set_margin_bottom(5)
# Collect all child checkboxes for this family
child_checkboxes = []
# Add father checkbox # Add father checkbox
father_handle = family.get_father_handle() father_handle = family.get_father_handle()
if father_handle: if father_handle:
@ -980,6 +1076,7 @@ class MyTimelineView(NavigationView):
checkbox = Gtk.CheckButton(label=father_label) 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
child_checkboxes.append(checkbox)
members_box.pack_start(checkbox, False, False, 0) members_box.pack_start(checkbox, False, False, 0)
except (AttributeError, KeyError): except (AttributeError, KeyError):
pass pass
@ -994,6 +1091,7 @@ class MyTimelineView(NavigationView):
checkbox = Gtk.CheckButton(label=mother_label) 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
child_checkboxes.append(checkbox)
members_box.pack_start(checkbox, False, False, 0) members_box.pack_start(checkbox, False, False, 0)
except (AttributeError, KeyError): except (AttributeError, KeyError):
pass pass
@ -1008,12 +1106,66 @@ class MyTimelineView(NavigationView):
checkbox = Gtk.CheckButton(label=child_label) checkbox = Gtk.CheckButton(label=child_label)
checkbox.set_active(True if not self.person_filter else child_handle in self.person_filter) checkbox.set_active(True if not self.person_filter else child_handle in self.person_filter)
self._filter_widgets['person_checkboxes'][child_handle] = checkbox self._filter_widgets['person_checkboxes'][child_handle] = checkbox
child_checkboxes.append(checkbox)
members_box.pack_start(checkbox, False, False, 0) members_box.pack_start(checkbox, False, False, 0)
except (AttributeError, KeyError): except (AttributeError, KeyError):
pass pass
# Only add expander if there are members to show # Only add expander if there are members to show
if len(members_box.get_children()) > 0: 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) expander.add(members_box)
self._filter_widgets['family_expanders'][family_handle] = expander self._filter_widgets['family_expanders'][family_handle] = expander
container.pack_start(expander, False, False, 0) container.pack_start(expander, False, False, 0)