mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-11 04:12:28 +00:00
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:
committed by
GitHub
parent
54b8397e92
commit
d1b006a897
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user