Skip to content

Commit 83059e3

Browse files
committed
Do the filtering before building task trees for the UI
Introduce a FilteredTaskTreeManager class that prefilters the tasks before building a tree. Partially fixes GitHub issue #1207
1 parent d99b56e commit 83059e3

File tree

3 files changed

+158
-79
lines changed

3 files changed

+158
-79
lines changed

GTG/core/filters.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def match_tags(self, task: Task) -> bool:
103103

104104

105105
def do_match(self, item) -> bool:
106-
task = unwrap(item, Task)
106+
task = item if isinstance(item, Task) else unwrap(item, Task)
107107

108108
if self.pane == 'active':
109109
show = task.status is Status.ACTIVE
@@ -117,6 +117,7 @@ def do_match(self, item) -> bool:
117117
elif self.pane == 'closed':
118118
show = task.status is not Status.ACTIVE
119119

120+
120121
if show:
121122
if self.no_tags:
122123
current = not task.tags
@@ -159,7 +160,7 @@ def match_tags(self, task: Task) -> bool:
159160

160161

161162
def do_match(self, item) -> bool:
162-
task = unwrap(item, Task)
163+
task = item if isinstance(item, Task) else unwrap(item, Task)
163164

164165
if self.pane == 'active':
165166
show = task.status is Status.ACTIVE

GTG/core/tasks.py

Lines changed: 152 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,128 @@ def __hash__(self) -> int:
693693
# ------------------------------------------------------------------------------
694694
# STORE
695695
# ------------------------------------------------------------------------------
696+
class FilteredTaskTreeManager:
697+
698+
699+
def __init__(self,store:'TaskStore',task_filter:Gtk.Filter) -> None:
700+
self.root_model: Gio.ListStore = Gio.ListStore.new(Task)
701+
self.task_filter: Gtk.Filter = task_filter
702+
self.tid_to_subtask_model: Dict[UUID,Gio.ListStore] = dict()
703+
self.tid_to_containing_model: Dict[UUID,Gio.ListStore] = dict()
704+
self.tree_model = Gtk.TreeListModel.new(self.root_model, False, False, self._model_expand)
705+
self.store = store
706+
self._find_root_tasks()
707+
708+
709+
def get_tree_model(self):
710+
return self.tree_model
711+
712+
713+
def set_filter(self,new_filter:Gtk.Filter):
714+
self.task_filter = new_filter
715+
self.task_filter.connect('changed',self._on_changed)
716+
self._refilter_all_tasks()
717+
718+
719+
def _refilter_all_tasks(self) -> None:
720+
for t in self.store.data:
721+
self._update_with_descendants(t)
722+
723+
724+
def _on_changed(self,*args):
725+
self._refilter_all_tasks()
726+
727+
728+
def _find_root_tasks(self) -> None:
729+
self.root_model.remove_all()
730+
for t in self.store.lookup.values():
731+
if self._should_be_root_item(t):
732+
self.root_model.append(t)
733+
self.tid_to_containing_model[t.id] = self.root_model
734+
735+
736+
def _should_be_root_item(self,t:Task):
737+
if not self.task_filter.do_match(t):
738+
return False
739+
return t.parent is None or not self.task_filter.do_match(t.parent)
740+
741+
742+
def update_position_of(self,t:Task):
743+
if not self.task_filter.do_match(t):
744+
self.remove(t)
745+
return
746+
if not self._in_the_right_model(t):
747+
self.remove(t)
748+
self.add(t)
749+
750+
def _update_with_descendants(self,t:Task):
751+
self.update_position_of(t)
752+
for c in t.children:
753+
self._update_with_descendants(c)
754+
755+
756+
def _in_the_right_model(self,t:Task):
757+
"""Return true if and only if the task matching the filter is in the correct ListStore or not yet present."""
758+
current_model = self._get_containing_model(t)
759+
correct_model = self._get_correct_containing_model(t)
760+
761+
if current_model is None and correct_model is None:
762+
return True
763+
if current_model == correct_model:
764+
assert correct_model is not None
765+
pos = correct_model.find(t)
766+
return pos[0]
767+
return False
768+
769+
770+
def add(self,task:Task):
771+
"""Add the task to the correct ListStore."""
772+
model = self._get_correct_containing_model(task)
773+
if model is None:
774+
return
775+
model.append(task)
776+
self.tid_to_containing_model[task.id] = model
777+
778+
779+
def remove(self,task:Task):
780+
"""Remove the task from the containing ListStore."""
781+
model = self._get_containing_model(task)
782+
if model is None:
783+
return
784+
pos = model.find(task)
785+
if pos[0]:
786+
model.remove(pos[1])
787+
del self.tid_to_containing_model[task.id]
788+
789+
790+
def _get_correct_containing_model(self,task:Task) -> Optional[Gio.ListStore]:
791+
"""Return the ListStore that should contain the given task matching the filter."""
792+
if task.parent is None or not self.task_filter.do_match(task.parent):
793+
return self.root_model
794+
return self.tid_to_subtask_model.get(task.parent.id)
795+
796+
797+
def _get_containing_model(self,task:Task) -> Optional[Gio.ListStore]:
798+
"""Return the ListStore that currently contains the given task."""
799+
return self.tid_to_containing_model.get(task.id)
800+
801+
802+
def _model_expand(self, item):
803+
"""Return a ListStore with the matching children of the given task."""
804+
model = Gio.ListStore.new(Task)
805+
806+
if type(item) == Gtk.TreeListRow:
807+
item = item.get_item()
808+
809+
for child in item.children:
810+
if self.task_filter is None or self.task_filter.do_match(child):
811+
model.append(child)
812+
self.tid_to_containing_model[child.id] = model
813+
814+
self.tid_to_subtask_model[item.id] = model
815+
return Gtk.TreeListModel.new(model, False, False, self._model_expand)
816+
817+
696818

697819
class TaskStore(BaseStore[Task]):
698820
"""A tree of tasks."""
@@ -706,24 +828,26 @@ def __init__(self) -> None:
706828
super().__init__()
707829

708830
self.model = Gio.ListStore.new(Task)
709-
self.tree_model = Gtk.TreeListModel.new(self.model, False, False, self.model_expand)
710-
self.tid_to_subtask_model: Dict[UUID,Gio.ListStore] = dict()
831+
self.managers: list[FilteredTaskTreeManager] = []
711832

712833

713-
def model_expand(self, item):
714-
model = Gio.ListStore.new(Task)
834+
def get_filtered_tree_model(self,task_filter: Optional[Gtk.Filter]):
835+
manager = FilteredTaskTreeManager(self,task_filter)
836+
self.managers.append(manager)
837+
return manager.get_tree_model(), manager
715838

716-
if type(item) == Gtk.TreeListRow:
717-
item = item.get_item()
718-
719-
# open the first one
720-
if item.children:
721-
for child in item.children:
722-
model.append(child)
723839

724-
self.tid_to_subtask_model[item.id] = model
725-
return Gtk.TreeListModel.new(model, False, False, self.model_expand)
840+
def _update_task_in_managers(self,t:Optional[Task]):
841+
if t is None:
842+
return
843+
for m in self.managers:
844+
m.update_position_of(t)
726845

846+
def _remove_task_from_managers(self,t:Optional[Task]):
847+
if t is None:
848+
return
849+
for m in self.managers:
850+
m.remove(t)
727851

728852
def __str__(self) -> str:
729853
"""String representation."""
@@ -773,6 +897,8 @@ def new(self, title: str = '', parent: Optional[UUID] = None) -> Task: # type: i
773897
for tag in self.lookup[parent].tags:
774898
task.add_tag(tag)
775899

900+
self._update_task_in_managers(task)
901+
self._update_task_in_managers(task.parent)
776902
return task
777903

778904

@@ -921,47 +1047,13 @@ def to_xml(self) -> _Element:
9211047
return root
9221048

9231049

924-
def _remove_from_parent_model(self,task_id: UUID) -> None:
925-
"""
926-
Remove the task indicated by task_id from the model of its parent's subtasks.
927-
This is required to trigger a GUI update.
928-
"""
929-
item = self.lookup[task_id]
930-
if item.parent is None:
931-
return
932-
if item.parent.id not in self.tid_to_subtask_model:
933-
return
934-
model = self.tid_to_subtask_model[item.parent.id]
935-
pos = model.find(item)
936-
if pos[0]:
937-
model.remove(pos[1])
938-
939-
940-
def _append_to_parent_model(self,task_id: UUID) -> None:
941-
"""
942-
Appends the task indicated by task_id to the model of its parent's subtasks.
943-
This is required to trigger a GUI update.
944-
"""
945-
item = self.lookup[task_id]
946-
if item.parent is None:
947-
return
948-
if item.parent.id not in self.tid_to_subtask_model:
949-
return
950-
model = self.tid_to_subtask_model[item.parent.id]
951-
pos = model.find(item)
952-
if not pos[0]:
953-
model.append(item)
954-
955-
9561050
def add(self, item: Any, parent_id: Optional[UUID] = None) -> None:
9571051
"""Add a task to the taskstore."""
9581052

9591053
super().add(item, parent_id)
9601054

961-
if not parent_id:
962-
self.model.append(item)
963-
else:
964-
self._append_to_parent_model(item.id)
1055+
self._update_task_in_managers(item)
1056+
self._update_task_in_managers(item.parent)
9651057

9661058
item.duplicate_cb = self.duplicate_for_recurrent
9671059
self.notify('task_count_all')
@@ -975,14 +1067,13 @@ def remove(self, item_id: UUID) -> None:
9751067

9761068
# Remove from UI
9771069
item = self.lookup[item_id]
978-
if item.parent is not None:
979-
self._remove_from_parent_model(item.id)
980-
else:
981-
pos = self.model.find(item)
982-
self.model.remove(pos[1])
1070+
parent = item.parent
1071+
self._remove_task_from_managers(item)
9831072

9841073
super().remove(item_id)
9851074

1075+
self._update_task_in_managers(parent)
1076+
9861077
self.notify('task_count_all')
9871078
self.notify('task_count_no_tags')
9881079

@@ -991,36 +1082,26 @@ def parent(self, item_id: UUID, parent_id: UUID) -> None:
9911082

9921083
item = self.lookup[item_id]
9931084

994-
# Remove from UI
995-
if item.parent is not None:
996-
self._remove_from_parent_model(item_id)
997-
else:
998-
pos = self.model.find(item)
999-
self.model.remove(pos[1])
1000-
10011085
super().parent(item_id, parent_id)
10021086

1003-
# Add back to UI
1004-
self._append_to_parent_model(item_id)
1087+
self._update_task_in_managers(item)
1088+
self._update_task_in_managers(item.parent)
10051089

10061090

10071091
def unparent(self, item_id: UUID) -> None:
10081092

10091093
item = self.lookup[item_id]
1010-
parent = item.parent
1011-
if parent is None:
1094+
old_parent = item.parent
1095+
if old_parent is None:
10121096
return
10131097

1014-
# Remove from UI
1015-
self._remove_from_parent_model(item_id)
1016-
10171098
super().unparent(item_id)
10181099

10191100
# remove inline references to the former subtask
1020-
parent.content = re.sub(r'\{\!\s*'+str(item_id)+r'\s*\!\}','',parent.content)
1101+
old_parent.content = re.sub(r'\{\!\s*'+str(item_id)+r'\s*\!\}','',old_parent.content)
10211102

1022-
# Add back to UI
1023-
self.model.append(item)
1103+
self._update_task_in_managers(item)
1104+
self._update_task_in_managers(old_parent)
10241105

10251106

10261107
def filter(self, filter_type: Filter, arg: Union[Tag,List[Tag],None] = None) -> List[Task]:

GTG/gtk/browser/task_pane.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,7 @@ def __init__(self, browser, pane):
152152

153153
self.search_filter = SearchTaskFilter(self.ds, pane)
154154
self.task_filter = TaskPaneFilter(self.app.ds, pane)
155-
156-
self.filtered = Gtk.FilterListModel()
157-
self.filtered.set_model(self.app.ds.tasks.tree_model)
158-
self.filtered.set_filter(self.task_filter)
155+
self.filtered, self.filter_manager = self.app.ds.tasks.get_filtered_tree_model(self.task_filter)
159156

160157
self.sort_model = Gtk.TreeListRowSorter()
161158
self.sort_model.set_sorter(TaskTitleSorter())
@@ -223,7 +220,7 @@ def set_title(self) -> None:
223220
def set_search_query(self, query) -> None:
224221
"""Change tasks filter."""
225222

226-
self.filtered.set_filter(self.search_filter)
223+
self.filter_manager.set_filter(self.search_filter)
227224
self.search_filter.set_query(query)
228225
self.search_filter.pane = self.pane
229226
self.search_filter.changed(Gtk.FilterChange.DIFFERENT)
@@ -235,7 +232,7 @@ def set_filter_pane(self, pane) -> None:
235232

236233
if self.searching:
237234
self.searching = False
238-
self.filtered.set_filter(self.task_filter)
235+
self.filter_manager.set_filter(self.task_filter)
239236

240237
self.pane = pane
241238
self.task_filter.pane = pane

0 commit comments

Comments
 (0)