From 133d67cf5191d31e96b5127ce8c830e9053c99b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Csaba=20Gy=C3=B6rgyi?= <gycsaba96@gmail.com>
Date: Wed, 6 Mar 2024 11:10:09 +0100
Subject: [PATCH] Properly count tasks for tags

Consider the ancestors of a tag when calculating task counts.
Refresh task counts after drag and drop.

This fixes GitHub issue #404
---
 GTG/core/datastore.py      | 13 ++++++++++++-
 GTG/core/tags.py           | 15 ++++++++++++++-
 GTG/gtk/browser/sidebar.py |  7 +++++--
 3 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/GTG/core/datastore.py b/GTG/core/datastore.py
index df88580e4..b4a14b419 100644
--- a/GTG/core/datastore.py
+++ b/GTG/core/datastore.py
@@ -222,7 +222,7 @@ def count_tasks(count: dict, tasklist: list):
                 if not task.tags:
                     count['untagged'] += 1
 
-                for tag in task.tags:
+                for tag in { t for owned_tag in task.tags for t in [owned_tag] + owned_tag.get_ancestors() }:
                     val = count.get(tag.name, 0)
                     count[tag.name] = val + 1
 
@@ -246,6 +246,17 @@ def count_tasks(count: dict, tasklist: list):
                     self.tasks.filter(Filter.ACTIONABLE))
 
 
+    def refresh_tag_stats(self) -> None:
+        """
+        Refresh the number of tasks for each tag.
+        """
+        self.refresh_task_count()
+        for tag_name in self.tags.get_all_tag_names():
+            tag = self.tags.find(tag_name)
+            self.refresh_task_for_tag(tag)
+            self.notify_tag_change(tag)
+
+
     def notify_tag_change(self, tag) -> None:
         """Notify tasks that this tag has changed."""
         
diff --git a/GTG/core/tags.py b/GTG/core/tags.py
index 4b7b64571..71f0afb68 100644
--- a/GTG/core/tags.py
+++ b/GTG/core/tags.py
@@ -27,7 +27,7 @@
 import re
 
 from lxml.etree import Element, SubElement
-from typing import Any, Dict, Set
+from typing import Any, Dict, List, Set
 
 from GTG.core.base_store import BaseStore
 
@@ -163,6 +163,15 @@ def set_task_count_closed(self, value: int) -> None:
         self._task_count_closed = value
 
 
+    def get_ancestors(self) -> List['Tag']:
+        """Return all ancestors of this tag"""
+        ancestors = []
+        here = self
+        while here.parent:
+            here = here.parent
+            ancestors.append(here)
+        return ancestors
+
     def __hash__(self):
         return id(self)
         
@@ -207,6 +216,10 @@ def __str__(self) -> str:
 
         return f'Tag Store. Holds {len(self.lookup)} tag(s)'
 
+    def get_all_tag_names(self) -> List[str]:
+        """Return all tag names."""
+        return list(self.lookup_names.keys())
+    
 
     def find(self, name: str) -> Tag:
         """Get a tag by name."""
diff --git a/GTG/gtk/browser/sidebar.py b/GTG/gtk/browser/sidebar.py
index d6d781e71..78c166a2c 100644
--- a/GTG/gtk/browser/sidebar.py
+++ b/GTG/gtk/browser/sidebar.py
@@ -586,7 +586,6 @@ def check_parent(self, value, target) -> bool:
 
     def drag_drop(self, target, value, x, y):
         """Callback when dropping onto a target"""
-
         dropped = target.get_widget().props.tag
 
         if not self.check_parent(value, dropped):
@@ -596,7 +595,10 @@ def drag_drop(self, target, value, x, y):
             self.ds.tags.unparent(value.id, value.parent.id)
         
         self.ds.tags.parent(value.id, dropped.id)
+        self.ds.refresh_tag_stats()
         self.ds.tags.tree_model.emit('items-changed', 0, 0, 0)
+        self.refresh_tags()
+        
 
 
     def drop_enter(self, target, x, y, user_data=None):
@@ -646,7 +648,8 @@ def notify_task(self, task: Task) -> None:
     def on_toplevel_tag_drop(self, drop_target, tag, x, y):
         if tag.parent:
             self.ds.tags.unparent(tag.id, tag.parent.id)
-
+            self.ds.refresh_tag_stats()
+            self.refresh_tags()
             try:
                 for expander in self.expanders:
                     expander.activate_action('listitem.toggle-expand')