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:
parent
ce75cd55bb
commit
c9f6e7f8b8
212
MyTimeline.py
212
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"<b>{category}</b>")
|
||||
category_label.set_use_markup(True)
|
||||
category_label.set_halign(Gtk.Align.START)
|
||||
box.pack_start(category_label, False, False, 0)
|
||||
if category not in category_event_types:
|
||||
category_event_types[category] = []
|
||||
category_event_types[category].append(event_type_obj)
|
||||
|
||||
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)
|
||||
# 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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user