Skip to content

Commit b9316fa

Browse files
First draft of tag_sort.py
Adds functionality to "group" tags based on an automatically generated "lineage" from a root grandparent tag to most given grandchild tags. Needs major improvements: * Better naming of variables. * Better encapsulation of sort information * Support for circular lineages * Comments * More?
1 parent ce87b11 commit b9316fa

File tree

3 files changed

+251
-2
lines changed

3 files changed

+251
-2
lines changed

tagstudio/src/core/tag_sort.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
from enum import Enum, Flag, auto
2+
from typing import Any, Callable
3+
4+
from src.core.constants import TAG_COLORS
5+
from src.core.library import Library
6+
7+
8+
class TagSortProperty(int, Enum):
9+
MATCHES_PREFIX = auto()
10+
CATEGORY = auto()
11+
LINEAGE_DEPTH = auto()
12+
DESCENDENT_COUNT = auto()
13+
NAME = auto()
14+
COLOR = auto()
15+
ID = auto()
16+
17+
18+
class TagSortDirection(Flag):
19+
ASC = False
20+
DESC = True
21+
22+
23+
Sort = list[tuple[TagSortProperty, TagSortDirection]]
24+
25+
# _default_sort must contain LINEAGE_DEPTH and ID
26+
_default_sort: Sort = [
27+
(TagSortProperty.LINEAGE_DEPTH, TagSortDirection.DESC),
28+
(TagSortProperty.DESCENDENT_COUNT, TagSortDirection.DESC),
29+
(TagSortProperty.NAME, TagSortDirection.ASC),
30+
(TagSortProperty.COLOR, TagSortDirection.ASC),
31+
(TagSortProperty.ID, TagSortDirection.ASC),
32+
]
33+
34+
def normalize_sort(old_sort: Sort = None) -> Sort:
35+
if old_sort is None:
36+
old_sort = []
37+
old_sort.extend(_default_sort)
38+
39+
sorted_properties: set[TagSortProperty] = set()
40+
new_sort = []
41+
for sort_property, sort_direction in old_sort:
42+
if sort_property not in sorted_properties:
43+
sorted_properties.add(sort_property)
44+
new_sort.append((sort_property, sort_direction))
45+
46+
return new_sort
47+
48+
49+
def add_sort_property(
50+
new_property: TagSortProperty,
51+
new_direction: TagSortDirection,
52+
old_sort: Sort = None,
53+
) -> Sort:
54+
new_sort = [(new_property, new_direction)]
55+
56+
if old_sort is not None:
57+
new_sort.extend(old_sort)
58+
59+
return normalize_sort(new_sort)
60+
61+
62+
def reverse_sort(old_sort: Sort) -> Sort:
63+
new_sort: Sort = []
64+
for sort_property, sort_direction in old_sort:
65+
if sort_direction is TagSortDirection.ASC:
66+
new_direction = TagSortDirection.DESC
67+
else:
68+
new_direction = TagSortDirection.ASC
69+
70+
new_sort.append((sort_property, new_direction))
71+
return new_sort
72+
73+
74+
def get_key(
75+
lib: Library, tag_id_list, sort: Sort = _default_sort
76+
) -> Callable[[int], list[Any]]:
77+
sort = normalize_sort(sort)
78+
79+
outer_sort: Sort = []
80+
lineage_direction: TagSortDirection
81+
inner_sort: Sort = []
82+
83+
outer = True
84+
for sort_property, sort_direction in sort:
85+
if sort_property is TagSortProperty.LINEAGE_DEPTH:
86+
outer = False
87+
lineage_direction = sort_direction
88+
continue
89+
90+
if outer:
91+
outer_sort.append((sort_property, sort_direction))
92+
else:
93+
inner_sort.append((sort_property, sort_direction))
94+
95+
if lineage_direction is TagSortDirection.DESC:
96+
inner_sort = reverse_sort(inner_sort)
97+
def key(tag_id: int) -> list[Any]:
98+
nonlocal lineage_direction
99+
canonical_lineage: Any = _get_canonical_lineage(
100+
lib, outer_sort, inner_sort, tag_id, tag_id_list
101+
)
102+
if lineage_direction is TagSortDirection.DESC:
103+
canonical_lineage = _ReverseComparison(canonical_lineage)
104+
105+
key_items = _get_basic_key_items(lib, tag_id, outer_sort)
106+
key_items.append(canonical_lineage)
107+
108+
print(key_items)
109+
return key_items
110+
111+
return key
112+
113+
114+
def _get_basic_key_items(
115+
lib: Library, tag_id: int, sort: Sort, tag_id_set: set[int] = None, prefix: str = None
116+
) -> list[Any]:
117+
key_items = []
118+
for sort_property, sort_direction in sort:
119+
key_item: Any = None
120+
match sort_property:
121+
case TagSortProperty.MATCHES_PREFIX:
122+
key_item = lib.get_tag(tag_id).name.lower().startswith(prefix.lower())
123+
# case TagSortProperty.CATEGORY:
124+
# case TagSortProperty.LINEAGE_DEPTH:
125+
case TagSortProperty.DESCENDENT_COUNT:
126+
tag_cluster = lib.get_tag_cluster(tag_id)
127+
if tag_id_set is not None:
128+
key_item = len(tag_id_set.intersection(set(tag_cluster)))
129+
else:
130+
key_item = len(tag_cluster)
131+
case TagSortProperty.NAME:
132+
key_item = lib.get_tag(tag_id).display_name(lib)
133+
case TagSortProperty.COLOR:
134+
key_item = TAG_COLORS.index(lib.get_tag(tag_id).color.lower())
135+
case TagSortProperty.ID:
136+
key_item = tag_id
137+
138+
if sort_direction is TagSortDirection.DESC:
139+
key_item = _ReverseComparison(key_item)
140+
141+
key_items.append(key_item)
142+
143+
return key_items
144+
145+
146+
class _ReverseComparison:
147+
def __init__(self, inner: Any):
148+
self.inner = inner
149+
150+
def __lt__(self, other):
151+
return other.inner.__lt__(self.inner)
152+
153+
def __le__(self, other):
154+
return other.inner.__le__(self.inner)
155+
156+
def __eq__(self, other):
157+
return other.inner.__eq__(self.inner)
158+
159+
def __ne__(self, other):
160+
return other.inner.__ne__(self.inner)
161+
162+
def __gt__(self, other):
163+
return other.inner.__gt__(self.inner)
164+
165+
def __ge__(self, other):
166+
return other.inner.__ge__(self.inner)
167+
168+
def __str__(self) -> str:
169+
return f"rev:{self.inner}"
170+
171+
def __repr__(self) -> str:
172+
return str(self)
173+
174+
def _get_canonical_lineage(
175+
lib: Library,
176+
outer_sort: Sort,
177+
inner_sort: Sort,
178+
tag_id: int,
179+
tag_id_list: list[int],
180+
last_generation_ids=set([-1]),
181+
first_gen=True,
182+
) -> list[list[Any]]:
183+
ancestor_id_queue: list[int] = [tag_id]
184+
encountered_tag_ids: set[int] = set(last_generation_ids)
185+
encountered_tag_ids.add(tag_id)
186+
187+
this_generation_ids: set[int] = set()
188+
189+
while ancestor_id_queue:
190+
next_ancestor_id = ancestor_id_queue.pop()
191+
parent_ids: set[int] = set(lib.get_tag(next_ancestor_id).subtag_ids)
192+
193+
if first_gen and not parent_ids:
194+
this_generation_ids.add(next_ancestor_id)
195+
196+
if not first_gen and last_generation_ids.intersection(parent_ids):
197+
this_generation_ids.add(next_ancestor_id)
198+
199+
#TODO: make this work for looping relationships
200+
for parent_id in parent_ids:
201+
if parent_id not in encountered_tag_ids:
202+
encountered_tag_ids.add(parent_id)
203+
ancestor_id_queue.append(parent_id)
204+
205+
outer_key = _get_basic_key_items(lib, tag_id, outer_sort)
206+
first_in_generation_id = None
207+
first_in_generation_inner_key_item = None
208+
for challenger_id in this_generation_ids:
209+
if challenger_id not in tag_id_list:
210+
continue
211+
212+
challenger_outer_key_item = _get_basic_key_items(lib, challenger_id, outer_sort)
213+
if challenger_outer_key_item != outer_key:
214+
continue
215+
216+
challenger_inner_key_item = _get_basic_key_items(lib, challenger_id, inner_sort)
217+
if (
218+
first_in_generation_id is None
219+
or challenger_inner_key_item < first_in_generation_inner_key_item
220+
):
221+
first_in_generation_id = challenger_id
222+
first_in_generation_inner_key_item = challenger_inner_key_item
223+
224+
lineage: list[list[Any]] = []
225+
if first_in_generation_id is not None:
226+
lineage = [first_in_generation_inner_key_item]
227+
lineage.extend(
228+
_get_canonical_lineage(
229+
lib,
230+
outer_sort,
231+
inner_sort,
232+
tag_id,
233+
tag_id_list,
234+
set([first_in_generation_id]),
235+
False,
236+
)
237+
)
238+
elif this_generation_ids:
239+
lineage = _get_canonical_lineage(
240+
lib, outer_sort, inner_sort, tag_id, tag_id_list, this_generation_ids, False
241+
)
242+
243+
return lineage

tagstudio/src/qt/modals/tag_database.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414

1515
from src.core.library import Library
16+
from src.core.tag_sort import get_key
1617
from src.qt.widgets.panel import PanelWidget, PanelModal
1718
from src.qt.widgets.tag import TagWidget
1819
from src.qt.modals.build_tag import BuildTagPanel
@@ -103,8 +104,10 @@ def update_tags(self, query: str):
103104
# Get tag ids to keep this behaviorally identical
104105
tags = [t.id for t in self.lib.tags]
105106

107+
sorted_tags = sorted(tags, key=get_key(self.lib, tags))
108+
106109
first_id_set = False
107-
for tag_id in tags:
110+
for tag_id in sorted_tags:
108111
if not first_id_set:
109112
self.first_tag_id = tag_id
110113
first_id_set = True

tagstudio/src/qt/modals/tag_search.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919

2020
from src.core.library import Library
21+
from src.core.tag_sort import get_key
2122
from src.core.palette import ColorType, get_tag_color
2223
from src.qt.widgets.panel import PanelWidget
2324
from src.qt.widgets.tag import TagWidget
@@ -111,7 +112,9 @@ def update_tags(self, query: str = ""):
111112
found_tags = self.lib.search_tags(query, include_cluster=True)[: self.tag_limit]
112113
self.first_tag_id = found_tags[0] if found_tags else None
113114

114-
for tag_id in found_tags:
115+
sorted_tags = sorted(found_tags, key=get_key(self.lib, found_tags))
116+
117+
for tag_id in sorted_tags:
115118
c = QWidget()
116119
l = QHBoxLayout(c)
117120
l.setContentsMargins(0, 0, 0, 0)

0 commit comments

Comments
 (0)