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
This commit is contained in:
Travis Abendshien
2025-01-29 19:42:55 -08:00
committed by GitHub
parent 54b8397e92
commit d1b006a897
9 changed files with 167 additions and 23 deletions

View File

@@ -211,8 +211,9 @@
"tag.choose_color": "Choose Tag Color",
"tag.color": "Color",
"tag.confirm_delete": "Are you sure you want to delete the tag \"{tag_name}\"?",
"tag.create": "Create Tag",
"tag.create_add": "Create && Add \"{query}\"",
"tag.create": "Create Tag",
"tag.disambiguation.tooltip": "Use this tag for disambiguation",
"tag.edit": "Edit Tag",
"tag.name": "Name",
"tag.new": "New Tag",

View File

@@ -71,4 +71,4 @@ class LibraryPrefs(DefaultEnum):
IS_EXCLUDE_LIST = True
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
PAGE_SIZE: int = 500
DB_VERSION: int = 5
DB_VERSION: int = 6

View File

@@ -185,6 +185,9 @@ class Library:
# Tags
for tag in json_lib.tags:
color_namespace, color_slug = default_color_groups.json_to_sql_color(tag.color)
disambiguation_id: int | None = None
if tag.subtag_ids and tag.subtag_ids[0] != tag.id:
disambiguation_id = tag.subtag_ids[0]
self.add_tag(
Tag(
id=tag.id,
@@ -192,6 +195,7 @@ class Library:
shorthand=tag.shorthand,
color_namespace=color_namespace,
color_slug=color_slug,
disambiguation_id=disambiguation_id,
)
)
# Apply user edits to built-in JSON tags.
@@ -263,6 +267,23 @@ class Library:
return f
return None
def tag_display_name(self, tag_id: int) -> str:
with Session(self.engine) as session:
tag = session.scalar(select(Tag).where(Tag.id == tag_id))
if not tag:
return "<NO TAG>"
if tag.disambiguation_id:
disam_tag = session.scalar(select(Tag).where(Tag.id == tag.disambiguation_id))
if not disam_tag:
return "<NO DISAM TAG>"
disam_name = disam_tag.shorthand
if not disam_name:
disam_name = disam_tag.name
return f"{tag.name} ({disam_name})"
else:
return tag.name
def open_library(self, library_dir: Path, storage_path: str | None = None) -> LibraryStatus:
is_new: bool = True
if storage_path == ":memory:":
@@ -1162,10 +1183,13 @@ class Library:
alias = TagAlias(alias_name, tag.id)
session.add(alias)
def update_parent_tags(self, tag, parent_ids, session):
def update_parent_tags(self, tag: Tag, parent_ids: list[int] | set[int], session):
if tag.id in parent_ids:
parent_ids.remove(tag.id)
if tag.disambiguation_id not in parent_ids:
tag.disambiguation_id = None
# load all tag's parent tags to know which to remove
prev_parent_tags = session.scalars(
select(TagParent).where(TagParent.parent_id == tag.id)

View File

@@ -100,6 +100,7 @@ class Tag(Base):
secondaryjoin="Tag.id == TagParent.child_id",
back_populates="parent_tags",
)
disambiguation_id: Mapped[int | None]
__table_args__ = (
ForeignKeyConstraint(
@@ -130,6 +131,7 @@ class Tag(Base):
icon: str | None = None,
color_namespace: str | None = None,
color_slug: str | None = None,
disambiguation_id: int | None = None,
is_category: bool = False,
):
self.name = name
@@ -139,6 +141,7 @@ class Tag(Base):
self.color_slug = color_slug
self.icon = icon
self.shorthand = shorthand
self.disambiguation_id = disambiguation_id
self.is_category = is_category
assert not self.id
self.id = id

View File

@@ -8,14 +8,17 @@ from typing import cast
import structlog
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QColor
from PySide6.QtWidgets import (
QApplication,
QButtonGroup,
QCheckBox,
QFrame,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QRadioButton,
QScrollArea,
QTableWidget,
QVBoxLayout,
@@ -28,7 +31,13 @@ from src.qt.modals.tag_color_selection import TagColorSelection
from src.qt.modals.tag_search import TagSearchPanel
from src.qt.translations import Translations
from src.qt.widgets.panel import PanelModal, PanelWidget
from src.qt.widgets.tag import TagWidget
from src.qt.widgets.tag import (
TagWidget,
get_border_color,
get_highlight_color,
get_primary_color,
get_text_color,
)
from src.qt.widgets.tag_color_preview import TagColorPreview
logger = structlog.get_logger(__name__)
@@ -62,6 +71,7 @@ class BuildTagPanel(PanelWidget):
self.tag: Tag # NOTE: This gets set at the end of the init.
self.tag_color_namespace: str | None
self.tag_color_slug: str | None
self.disambiguation_id: int | None
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
@@ -129,6 +139,8 @@ class BuildTagPanel(PanelWidget):
self.parent_tags_layout.setContentsMargins(0, 0, 0, 0)
self.parent_tags_layout.setSpacing(0)
self.parent_tags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.disam_button_group = QButtonGroup(self)
self.disam_button_group.setExclusive(False)
self.parent_tags_title = QLabel()
Translations.translate_qobject(self.parent_tags_title, "tag.parent_tags")
@@ -314,22 +326,111 @@ class BuildTagPanel(PanelWidget):
while self.parent_tags_scroll_layout.itemAt(0):
self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater()
last: QWidget = self.aliases_table.cellWidget(self.aliases_table.rowCount() - 1, 1)
c = QWidget()
layout = QVBoxLayout(c)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(3)
for tag_id in self.parent_ids:
tag = self.lib.get_tag(tag_id)
tw = TagWidget(tag, has_edit=False, has_remove=True)
tw.on_remove.connect(lambda t=tag_id: self.remove_parent_tag_callback(t))
layout.addWidget(tw)
self.setTabOrder(last, tw.bg_button)
last = tw.bg_button
self.setTabOrder(last, self.name_field)
last_tab: QWidget = self.aliases_table.cellWidget(self.aliases_table.rowCount() - 1, 1)
next_tab: QWidget = last_tab
for parent_id in self.parent_ids:
tag = self.lib.get_tag(parent_id)
if not tag:
continue
is_disam = parent_id == self.disambiguation_id
last_tab, next_tab, container = self.__build_row_item_widget(tag, parent_id, is_disam)
layout.addWidget(container)
# TODO: Disam buttons after the first currently can't be added due to this error:
# QWidget::setTabOrder: 'first' and 'second' must be in the same window
self.setTabOrder(last_tab, next_tab)
self.setTabOrder(next_tab, self.name_field)
self.parent_tags_scroll_layout.addWidget(c)
def __build_row_item_widget(self, tag: Tag, parent_id: int, is_disambiguation: bool):
container = QWidget()
row = QHBoxLayout(container)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(3)
# Init Colors
primary_color = get_primary_color(tag)
border_color = (
get_border_color(primary_color)
if not (tag.color and tag.color.secondary)
else (QColor(tag.color.secondary))
)
highlight_color = get_highlight_color(
primary_color
if not (tag.color and tag.color.secondary)
else QColor(tag.color.secondary)
)
text_color: QColor
if tag.color and tag.color.secondary:
text_color = QColor(tag.color.secondary)
else:
text_color = get_text_color(primary_color, highlight_color)
# Add Disambiguation Tag Button
disam_button = QRadioButton()
disam_button.setObjectName(f"disambiguationButton.{parent_id}")
disam_button.setFixedSize(22, 22)
disam_button.setToolTip(Translations.translate_formatted("tag.disambiguation.tooltip"))
disam_button.setStyleSheet(
f"QRadioButton{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QRadioButton::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QRadioButton::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QRadioButton::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
)
self.disam_button_group.addButton(disam_button)
if is_disambiguation:
disam_button.setChecked(True)
disam_button.clicked.connect(lambda checked=False: self.toggle_disam_id(parent_id))
row.addWidget(disam_button)
# Add Tag Widget
tag_widget = TagWidget(
tag,
library=self.lib,
has_edit=False,
has_remove=True,
)
tag_widget.on_remove.connect(lambda t=parent_id: self.remove_parent_tag_callback(t))
row.addWidget(tag_widget)
return disam_button, tag_widget.bg_button, container
def toggle_disam_id(self, disambiguation_id: int | None):
if self.disambiguation_id == disambiguation_id:
self.disambiguation_id = None
else:
self.disambiguation_id = disambiguation_id
for button in self.disam_button_group.buttons():
if button.objectName() == f"disambiguationButton.{self.disambiguation_id}":
button.setChecked(True)
else:
button.setChecked(False)
def add_aliases(self):
names: set[str] = set()
for i in range(0, self.aliases_table.rowCount()):
@@ -406,6 +507,7 @@ class BuildTagPanel(PanelWidget):
self.alias_ids.append(alias_id)
self._set_aliases()
self.disambiguation_id = tag.disambiguation_id
for parent_id in tag.parent_ids:
self.parent_ids.add(parent_id)
self.set_parent_tags()
@@ -440,10 +542,10 @@ class BuildTagPanel(PanelWidget):
tag.name = self.name_field.text()
tag.shorthand = self.shorthand_field.text()
tag.is_category = self.cat_checkbox.isChecked()
tag.disambiguation_id = self.disambiguation_id
tag.color_namespace = self.tag_color_namespace
tag.color_slug = self.tag_color_slug
tag.is_category = self.cat_checkbox.isChecked()
logger.info("built tag", tag=tag)
return tag

View File

@@ -63,7 +63,9 @@ class TagDatabasePanel(TagSearchPanel):
message_box = QMessageBox()
Translations.translate_with_setter(message_box.setWindowTitle, "tag.remove")
Translations.translate_qobject(message_box, "tag.confirm_delete", tag_name=tag.name)
Translations.translate_qobject(
message_box, "tag.confirm_delete", tag_name=self.lib.tag_display_name(tag.id)
)
message_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore
message_box.setIcon(QMessageBox.Question) # type: ignore

View File

@@ -86,6 +86,7 @@ class TagSearchPanel(PanelWidget):
tag_widget = TagWidget(
tag,
library=self.lib,
has_edit=True,
has_remove=has_remove_button,
)
@@ -221,8 +222,9 @@ class TagSearchPanel(PanelWidget):
results_1.append(tag)
else:
results_2.append(tag)
results_1.sort(key=lambda tag: len(tag.name))
results_2.sort()
results_1.sort(key=lambda tag: self.lib.tag_display_name(tag.id))
results_1.sort(key=lambda tag: len(self.lib.tag_display_name(tag.id)))
results_2.sort(key=lambda tag: self.lib.tag_display_name(tag.id))
self.first_tag_id = results_1[0].id if len(results_1) > 0 else tag_results[0].id
for tag in results_1 + results_2:
self.scroll_layout.addWidget(self.__build_row_item_widget(tag))
@@ -266,7 +268,7 @@ class TagSearchPanel(PanelWidget):
self.edit_modal = PanelModal(
build_tag_panel,
tag.name,
self.lib.tag_display_name(tag.id),
done_callback=(self.update_tags(self.search_field.text())),
has_save=True,
)

View File

@@ -3,6 +3,7 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import typing
from types import FunctionType
import structlog
@@ -22,6 +23,10 @@ from src.qt.translations import Translations
logger = structlog.get_logger(__name__)
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.core.library.alchemy import Library
class TagAliasWidget(QWidget):
on_remove = Signal()
@@ -102,12 +107,14 @@ class TagWidget(QWidget):
tag: Tag,
has_edit: bool,
has_remove: bool,
library: "Library | None" = None,
on_remove_callback: FunctionType = None,
on_click_callback: FunctionType = None,
on_edit_callback: FunctionType = None,
) -> None:
super().__init__()
self.tag = tag
self.lib: Library | None = library
self.has_edit = has_edit
self.has_remove = has_remove
@@ -119,7 +126,10 @@ class TagWidget(QWidget):
self.bg_button = QPushButton(self)
self.bg_button.setFlat(True)
self.bg_button.setText(tag.name)
if self.lib:
self.bg_button.setText(self.lib.tag_display_name(tag.id))
else:
self.bg_button.setText(tag.name)
if has_edit:
edit_action = QAction(self)
edit_action.setText(Translations.translate_formatted("generic.edit"))

View File

@@ -46,13 +46,13 @@ class TagBoxWidget(FieldWidget):
self.set_tags(self.tags)
def set_tags(self, tags: typing.Iterable[Tag]):
tags_ = sorted(list(tags), key=lambda tag: tag.name)
tags_ = sorted(list(tags), key=lambda tag: self.driver.lib.tag_display_name(tag.id))
logger.info("[TagBoxWidget] Tags:", tags=tags)
while self.base_layout.itemAt(0):
self.base_layout.takeAt(0).widget().deleteLater()
for tag in tags_:
tag_widget = TagWidget(tag, has_edit=True, has_remove=True)
tag_widget = TagWidget(tag, library=self.driver.lib, has_edit=True, has_remove=True)
tag_widget.on_click.connect(
lambda tag_id=tag.id: (
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
@@ -75,7 +75,7 @@ class TagBoxWidget(FieldWidget):
self.edit_modal = PanelModal(
build_tag_panel,
tag.name, # TODO - display name including parent tags
self.driver.lib.tag_display_name(tag.id),
"Edit Tag",
done_callback=lambda: self.driver.preview_panel.update_widgets(update_preview=False),
has_save=True,