Skip to content

Commit d1b006a

Browse files
authored
feat(ui): re-implement tag display names on sql (#747)
* feat: add disambiguation_tag column to tags table * feat(ui): re-add tag display names * fix(ui): allow empty disambiguation selection * ui: restore basic tab functionality * fix: don't set disambiguation_id for self-parented JSON tags * fix: return consistent search results
1 parent 54b8397 commit d1b006a

File tree

9 files changed

+168
-24
lines changed

9 files changed

+168
-24
lines changed

tagstudio/resources/translations/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,9 @@
211211
"tag.choose_color": "Choose Tag Color",
212212
"tag.color": "Color",
213213
"tag.confirm_delete": "Are you sure you want to delete the tag \"{tag_name}\"?",
214-
"tag.create": "Create Tag",
215214
"tag.create_add": "Create && Add \"{query}\"",
215+
"tag.create": "Create Tag",
216+
"tag.disambiguation.tooltip": "Use this tag for disambiguation",
216217
"tag.edit": "Edit Tag",
217218
"tag.name": "Name",
218219
"tag.new": "New Tag",

tagstudio/src/core/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ class LibraryPrefs(DefaultEnum):
7171
IS_EXCLUDE_LIST = True
7272
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
7373
PAGE_SIZE: int = 500
74-
DB_VERSION: int = 5
74+
DB_VERSION: int = 6

tagstudio/src/core/library/alchemy/library.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,17 @@ def migrate_json_to_sqlite(self, json_lib: JsonLibrary):
185185
# Tags
186186
for tag in json_lib.tags:
187187
color_namespace, color_slug = default_color_groups.json_to_sql_color(tag.color)
188+
disambiguation_id: int | None = None
189+
if tag.subtag_ids and tag.subtag_ids[0] != tag.id:
190+
disambiguation_id = tag.subtag_ids[0]
188191
self.add_tag(
189192
Tag(
190193
id=tag.id,
191194
name=tag.name,
192195
shorthand=tag.shorthand,
193196
color_namespace=color_namespace,
194197
color_slug=color_slug,
198+
disambiguation_id=disambiguation_id,
195199
)
196200
)
197201
# Apply user edits to built-in JSON tags.
@@ -263,6 +267,23 @@ def get_field_name_from_id(self, field_id: int) -> _FieldID:
263267
return f
264268
return None
265269

270+
def tag_display_name(self, tag_id: int) -> str:
271+
with Session(self.engine) as session:
272+
tag = session.scalar(select(Tag).where(Tag.id == tag_id))
273+
if not tag:
274+
return "<NO TAG>"
275+
276+
if tag.disambiguation_id:
277+
disam_tag = session.scalar(select(Tag).where(Tag.id == tag.disambiguation_id))
278+
if not disam_tag:
279+
return "<NO DISAM TAG>"
280+
disam_name = disam_tag.shorthand
281+
if not disam_name:
282+
disam_name = disam_tag.name
283+
return f"{tag.name} ({disam_name})"
284+
else:
285+
return tag.name
286+
266287
def open_library(self, library_dir: Path, storage_path: str | None = None) -> LibraryStatus:
267288
is_new: bool = True
268289
if storage_path == ":memory:":
@@ -1162,10 +1183,13 @@ def update_aliases(self, tag, alias_ids, alias_names, session):
11621183
alias = TagAlias(alias_name, tag.id)
11631184
session.add(alias)
11641185

1165-
def update_parent_tags(self, tag, parent_ids, session):
1186+
def update_parent_tags(self, tag: Tag, parent_ids: list[int] | set[int], session):
11661187
if tag.id in parent_ids:
11671188
parent_ids.remove(tag.id)
11681189

1190+
if tag.disambiguation_id not in parent_ids:
1191+
tag.disambiguation_id = None
1192+
11691193
# load all tag's parent tags to know which to remove
11701194
prev_parent_tags = session.scalars(
11711195
select(TagParent).where(TagParent.parent_id == tag.id)

tagstudio/src/core/library/alchemy/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class Tag(Base):
100100
secondaryjoin="Tag.id == TagParent.child_id",
101101
back_populates="parent_tags",
102102
)
103+
disambiguation_id: Mapped[int | None]
103104

104105
__table_args__ = (
105106
ForeignKeyConstraint(
@@ -130,6 +131,7 @@ def __init__(
130131
icon: str | None = None,
131132
color_namespace: str | None = None,
132133
color_slug: str | None = None,
134+
disambiguation_id: int | None = None,
133135
is_category: bool = False,
134136
):
135137
self.name = name
@@ -139,6 +141,7 @@ def __init__(
139141
self.color_slug = color_slug
140142
self.icon = icon
141143
self.shorthand = shorthand
144+
self.disambiguation_id = disambiguation_id
142145
self.is_category = is_category
143146
assert not self.id
144147
self.id = id

tagstudio/src/qt/modals/build_tag.py

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88

99
import structlog
1010
from PySide6.QtCore import Qt, Signal
11+
from PySide6.QtGui import QColor
1112
from PySide6.QtWidgets import (
1213
QApplication,
14+
QButtonGroup,
1315
QCheckBox,
1416
QFrame,
1517
QHBoxLayout,
1618
QLabel,
1719
QLineEdit,
1820
QPushButton,
21+
QRadioButton,
1922
QScrollArea,
2023
QTableWidget,
2124
QVBoxLayout,
@@ -28,7 +31,13 @@
2831
from src.qt.modals.tag_search import TagSearchPanel
2932
from src.qt.translations import Translations
3033
from src.qt.widgets.panel import PanelModal, PanelWidget
31-
from src.qt.widgets.tag import TagWidget
34+
from src.qt.widgets.tag import (
35+
TagWidget,
36+
get_border_color,
37+
get_highlight_color,
38+
get_primary_color,
39+
get_text_color,
40+
)
3241
from src.qt.widgets.tag_color_preview import TagColorPreview
3342

3443
logger = structlog.get_logger(__name__)
@@ -62,6 +71,7 @@ def __init__(self, library: Library, tag: Tag | None = None):
6271
self.tag: Tag # NOTE: This gets set at the end of the init.
6372
self.tag_color_namespace: str | None
6473
self.tag_color_slug: str | None
74+
self.disambiguation_id: int | None
6575

6676
self.setMinimumSize(300, 400)
6777
self.root_layout = QVBoxLayout(self)
@@ -129,6 +139,8 @@ def __init__(self, library: Library, tag: Tag | None = None):
129139
self.parent_tags_layout.setContentsMargins(0, 0, 0, 0)
130140
self.parent_tags_layout.setSpacing(0)
131141
self.parent_tags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
142+
self.disam_button_group = QButtonGroup(self)
143+
self.disam_button_group.setExclusive(False)
132144

133145
self.parent_tags_title = QLabel()
134146
Translations.translate_qobject(self.parent_tags_title, "tag.parent_tags")
@@ -314,22 +326,111 @@ def set_parent_tags(self):
314326
while self.parent_tags_scroll_layout.itemAt(0):
315327
self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater()
316328

317-
last: QWidget = self.aliases_table.cellWidget(self.aliases_table.rowCount() - 1, 1)
318329
c = QWidget()
319330
layout = QVBoxLayout(c)
320331
layout.setContentsMargins(0, 0, 0, 0)
321332
layout.setSpacing(3)
322-
for tag_id in self.parent_ids:
323-
tag = self.lib.get_tag(tag_id)
324-
tw = TagWidget(tag, has_edit=False, has_remove=True)
325-
tw.on_remove.connect(lambda t=tag_id: self.remove_parent_tag_callback(t))
326-
layout.addWidget(tw)
327-
self.setTabOrder(last, tw.bg_button)
328-
last = tw.bg_button
329-
self.setTabOrder(last, self.name_field)
330-
333+
last_tab: QWidget = self.aliases_table.cellWidget(self.aliases_table.rowCount() - 1, 1)
334+
next_tab: QWidget = last_tab
335+
336+
for parent_id in self.parent_ids:
337+
tag = self.lib.get_tag(parent_id)
338+
if not tag:
339+
continue
340+
is_disam = parent_id == self.disambiguation_id
341+
last_tab, next_tab, container = self.__build_row_item_widget(tag, parent_id, is_disam)
342+
layout.addWidget(container)
343+
# TODO: Disam buttons after the first currently can't be added due to this error:
344+
# QWidget::setTabOrder: 'first' and 'second' must be in the same window
345+
self.setTabOrder(last_tab, next_tab)
346+
347+
self.setTabOrder(next_tab, self.name_field)
331348
self.parent_tags_scroll_layout.addWidget(c)
332349

350+
def __build_row_item_widget(self, tag: Tag, parent_id: int, is_disambiguation: bool):
351+
container = QWidget()
352+
row = QHBoxLayout(container)
353+
row.setContentsMargins(0, 0, 0, 0)
354+
row.setSpacing(3)
355+
356+
# Init Colors
357+
primary_color = get_primary_color(tag)
358+
border_color = (
359+
get_border_color(primary_color)
360+
if not (tag.color and tag.color.secondary)
361+
else (QColor(tag.color.secondary))
362+
)
363+
highlight_color = get_highlight_color(
364+
primary_color
365+
if not (tag.color and tag.color.secondary)
366+
else QColor(tag.color.secondary)
367+
)
368+
text_color: QColor
369+
if tag.color and tag.color.secondary:
370+
text_color = QColor(tag.color.secondary)
371+
else:
372+
text_color = get_text_color(primary_color, highlight_color)
373+
374+
# Add Disambiguation Tag Button
375+
disam_button = QRadioButton()
376+
disam_button.setObjectName(f"disambiguationButton.{parent_id}")
377+
disam_button.setFixedSize(22, 22)
378+
disam_button.setToolTip(Translations.translate_formatted("tag.disambiguation.tooltip"))
379+
disam_button.setStyleSheet(
380+
f"QRadioButton{{"
381+
f"background: rgba{primary_color.toTuple()};"
382+
f"color: rgba{text_color.toTuple()};"
383+
f"border-color: rgba{border_color.toTuple()};"
384+
f"border-radius: 6px;"
385+
f"border-style:solid;"
386+
f"border-width: 2px;"
387+
f"}}"
388+
f"QRadioButton::indicator{{"
389+
f"width: 10px;"
390+
f"height: 10px;"
391+
f"border-radius: 2px;"
392+
f"margin: 4px;"
393+
f"}}"
394+
f"QRadioButton::indicator:checked{{"
395+
f"background: rgba{text_color.toTuple()};"
396+
f"}}"
397+
f"QRadioButton::hover{{"
398+
f"border-color: rgba{highlight_color.toTuple()};"
399+
f"}}"
400+
)
401+
402+
self.disam_button_group.addButton(disam_button)
403+
if is_disambiguation:
404+
disam_button.setChecked(True)
405+
406+
disam_button.clicked.connect(lambda checked=False: self.toggle_disam_id(parent_id))
407+
row.addWidget(disam_button)
408+
409+
# Add Tag Widget
410+
tag_widget = TagWidget(
411+
tag,
412+
library=self.lib,
413+
has_edit=False,
414+
has_remove=True,
415+
)
416+
417+
tag_widget.on_remove.connect(lambda t=parent_id: self.remove_parent_tag_callback(t))
418+
row.addWidget(tag_widget)
419+
420+
return disam_button, tag_widget.bg_button, container
421+
422+
def toggle_disam_id(self, disambiguation_id: int | None):
423+
if self.disambiguation_id == disambiguation_id:
424+
self.disambiguation_id = None
425+
else:
426+
self.disambiguation_id = disambiguation_id
427+
428+
for button in self.disam_button_group.buttons():
429+
if button.objectName() == f"disambiguationButton.{self.disambiguation_id}":
430+
button.setChecked(True)
431+
else:
432+
button.setChecked(False)
433+
333434
def add_aliases(self):
334435
names: set[str] = set()
335436
for i in range(0, self.aliases_table.rowCount()):
@@ -406,6 +507,7 @@ def set_tag(self, tag: Tag):
406507
self.alias_ids.append(alias_id)
407508
self._set_aliases()
408509

510+
self.disambiguation_id = tag.disambiguation_id
409511
for parent_id in tag.parent_ids:
410512
self.parent_ids.add(parent_id)
411513
self.set_parent_tags()
@@ -440,10 +542,10 @@ def build_tag(self) -> Tag:
440542

441543
tag.name = self.name_field.text()
442544
tag.shorthand = self.shorthand_field.text()
443-
tag.is_category = self.cat_checkbox.isChecked()
444-
545+
tag.disambiguation_id = self.disambiguation_id
445546
tag.color_namespace = self.tag_color_namespace
446547
tag.color_slug = self.tag_color_slug
548+
tag.is_category = self.cat_checkbox.isChecked()
447549

448550
logger.info("built tag", tag=tag)
449551
return tag

tagstudio/src/qt/modals/tag_database.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ def remove_tag(self, tag: Tag):
6363

6464
message_box = QMessageBox()
6565
Translations.translate_with_setter(message_box.setWindowTitle, "tag.remove")
66-
Translations.translate_qobject(message_box, "tag.confirm_delete", tag_name=tag.name)
66+
Translations.translate_qobject(
67+
message_box, "tag.confirm_delete", tag_name=self.lib.tag_display_name(tag.id)
68+
)
6769
message_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore
6870
message_box.setIcon(QMessageBox.Question) # type: ignore
6971

tagstudio/src/qt/modals/tag_search.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __build_row_item_widget(self, tag: Tag):
8686

8787
tag_widget = TagWidget(
8888
tag,
89+
library=self.lib,
8990
has_edit=True,
9091
has_remove=has_remove_button,
9192
)
@@ -221,8 +222,9 @@ def update_tags(self, query: str | None = None):
221222
results_1.append(tag)
222223
else:
223224
results_2.append(tag)
224-
results_1.sort(key=lambda tag: len(tag.name))
225-
results_2.sort()
225+
results_1.sort(key=lambda tag: self.lib.tag_display_name(tag.id))
226+
results_1.sort(key=lambda tag: len(self.lib.tag_display_name(tag.id)))
227+
results_2.sort(key=lambda tag: self.lib.tag_display_name(tag.id))
226228
self.first_tag_id = results_1[0].id if len(results_1) > 0 else tag_results[0].id
227229
for tag in results_1 + results_2:
228230
self.scroll_layout.addWidget(self.__build_row_item_widget(tag))
@@ -266,7 +268,7 @@ def callback(btp: build_tag.BuildTagPanel):
266268

267269
self.edit_modal = PanelModal(
268270
build_tag_panel,
269-
tag.name,
271+
self.lib.tag_display_name(tag.id),
270272
done_callback=(self.update_tags(self.search_field.text())),
271273
has_save=True,
272274
)

tagstudio/src/qt/widgets/tag.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
44

55

6+
import typing
67
from types import FunctionType
78

89
import structlog
@@ -22,6 +23,10 @@
2223

2324
logger = structlog.get_logger(__name__)
2425

26+
# Only import for type checking/autocompletion, will not be imported at runtime.
27+
if typing.TYPE_CHECKING:
28+
from src.core.library.alchemy import Library
29+
2530

2631
class TagAliasWidget(QWidget):
2732
on_remove = Signal()
@@ -102,12 +107,14 @@ def __init__(
102107
tag: Tag,
103108
has_edit: bool,
104109
has_remove: bool,
110+
library: "Library | None" = None,
105111
on_remove_callback: FunctionType = None,
106112
on_click_callback: FunctionType = None,
107113
on_edit_callback: FunctionType = None,
108114
) -> None:
109115
super().__init__()
110116
self.tag = tag
117+
self.lib: Library | None = library
111118
self.has_edit = has_edit
112119
self.has_remove = has_remove
113120

@@ -119,7 +126,10 @@ def __init__(
119126

120127
self.bg_button = QPushButton(self)
121128
self.bg_button.setFlat(True)
122-
self.bg_button.setText(tag.name)
129+
if self.lib:
130+
self.bg_button.setText(self.lib.tag_display_name(tag.id))
131+
else:
132+
self.bg_button.setText(tag.name)
123133
if has_edit:
124134
edit_action = QAction(self)
125135
edit_action.setText(Translations.translate_formatted("generic.edit"))

0 commit comments

Comments
 (0)