feat(ui): new tag alias UI (#534)

* updated parents and aliases to use the flowLaout in the build tag panel

added shortcuts for adding and removing aliases and updated the alias ui
to always show remove button and not cover alias

aliases use flowlayout

wrote test for buildTagPanel removeSelectedAlias

parent tags now use flowlayout in build tag panel

moved buttons for adding aliases and parents to be at the end of the
flowLayout

* added aliases and subtags to search results

* aliases now use a table, removed unnecessary keyboard shortcuts

* reverted subtags to regular list from flowlayout

* chor remove redundant lambda

* feat: added display names for tags

* fix: aliases table enter/return and backspace work as expected, display names work as expected, adding aliases outputs them in order

* format

* fix: add parent button on build tag panel

* fix: empty aliases where not being removed all the time

* fix: alias name changes would be discarded when a new alias was created or an alias was removed

* fix: removed display names, as they didn't display properly and should be added in a different PR

* fix: mypy

* fix: added missing session.expunge_all

session.expunge_all() on line 621 was removed, added it back.

* fix: parent_tags relationship in Tag class

parent_tags primaryJoin and secondaryJoin where in the wrong order. They have been switched back to the proper order.

* fix: pillow_jxl import was missing

* fix: ruff

* fix: type hint fixes

* fix: fix the type hint fixes

---------

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
This commit is contained in:
DandyDev01
2024-12-22 23:14:37 -07:00
committed by GitHub
parent dc2eed431b
commit 82c659c8a4
5 changed files with 161 additions and 225 deletions

View File

@@ -619,6 +619,7 @@ class Library:
)
session.expunge_all()
return res
def get_all_child_tag_ids(self, tag_id: int) -> list[int]:
@@ -901,9 +902,9 @@ class Library:
def add_tag(
self,
tag: Tag,
subtag_ids: set[int] | None = None,
alias_names: set[str] | None = None,
alias_ids: set[int] | None = None,
subtag_ids: list[int] | set[int] | None = None,
alias_names: list[str] | set[str] | None = None,
alias_ids: list[int] | set[int] | None = None,
) -> Tag | None:
with Session(self.engine, expire_on_commit=False) as session:
try:
@@ -1077,9 +1078,9 @@ class Library:
def update_tag(
self,
tag: Tag,
subtag_ids: set[int] | None = None,
alias_names: set[str] | None = None,
alias_ids: set[int] | None = None,
subtag_ids: list[int] | set[int] | None = None,
alias_names: list[str] | set[str] | None = None,
alias_ids: list[int] | set[int] | None = None,
) -> None:
"""Edit a Tag in the Library."""
self.add_tag(tag, subtag_ids, alias_names, alias_ids)

View File

@@ -3,43 +3,58 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import math
import sys
from typing import cast
import structlog
from PySide6 import QtCore
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import (
QAction,
)
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QFrame,
QLabel,
QLineEdit,
QPushButton,
QScrollArea,
QTableWidget,
QVBoxLayout,
QWidget,
)
from src.core.library import Library, Tag
from src.core.library.alchemy.enums import TagColor
from src.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
from src.qt.flowlayout import FlowLayout
from src.qt.modals.tag_search import TagSearchPanel
from src.qt.widgets.panel import PanelModal, PanelWidget
from src.qt.widgets.tag import TagAliasWidget, TagWidget
from src.qt.widgets.tag import TagWidget
logger = structlog.get_logger(__name__)
class CustomTableItem(QLineEdit):
def __init__(self, text, on_return, on_backspace, parent=None):
super().__init__(parent)
self.setText(text)
self.on_return = on_return
self.on_backspace = on_backspace
def set_id(self, id):
self.id = id
def keyPressEvent(self, event): # noqa: N802
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
self.on_return()
elif event.key() == Qt.Key.Key_Backspace and self.text().strip() == "":
self.on_backspace()
else:
super().keyPressEvent(event)
class BuildTagPanel(PanelWidget):
on_edit = Signal(Tag)
def __init__(self, library: Library, tag: Tag | None = None):
super().__init__()
self.lib = library
# self.callback = callback
# self.tag_id = tag_id
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
@@ -86,43 +101,16 @@ class BuildTagPanel(PanelWidget):
self.aliases_title.setText("Aliases")
self.aliases_layout.addWidget(self.aliases_title)
self.aliases_flow_widget = QWidget()
self.aliases_flow_layout = FlowLayout(self.aliases_flow_widget)
self.aliases_flow_layout.setContentsMargins(0, 0, 0, 0)
self.aliases_flow_layout.enable_grid_optimizations(value=False)
self.aliases_table = QTableWidget(0, 2)
self.aliases_table.horizontalHeader().setVisible(False)
self.aliases_table.verticalHeader().setVisible(False)
self.aliases_table.horizontalHeader().setStretchLastSection(True)
self.aliases_table.setColumnWidth(0, 35)
self.alias_add_button = QPushButton()
self.alias_add_button.setMinimumSize(23, 23)
self.alias_add_button.setMaximumSize(23, 23)
self.alias_add_button.setText("+")
self.alias_add_button.setToolTip("CTRL + A")
self.alias_add_button.setShortcut(
QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
QtCore.Qt.Key.Key_A,
)
)
self.alias_add_button.setStyleSheet(
f"QPushButton{{"
f"background: #1e1e1e;"
f"color: #FFFFFF;"
f"font-weight: bold;"
f"border-color: #333333;"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width:{math.ceil(self.devicePixelRatio())}px;"
f"padding-bottom: 5px;"
f"font-size: 20px;"
f"}}"
f"QPushButton::hover"
f"{{"
f"border-color: #CCCCCC;"
f"background: #555555;"
f"}}"
)
self.alias_add_button.clicked.connect(lambda: self.add_alias_callback())
self.aliases_flow_layout.addWidget(self.alias_add_button)
self.alias_add_button.clicked.connect(self.add_alias_callback)
# Subtags ------------------------------------------------------------
@@ -137,42 +125,25 @@ class BuildTagPanel(PanelWidget):
self.subtags_title.setText("Parent Tags")
self.subtags_layout.addWidget(self.subtags_title)
self.subtag_flow_widget = QWidget()
self.subtag_flow_layout = FlowLayout(self.subtag_flow_widget)
self.subtag_flow_layout.setContentsMargins(0, 0, 0, 0)
self.subtag_flow_layout.enable_grid_optimizations(value=False)
self.scroll_contents = QWidget()
self.subtags_scroll_layout = QVBoxLayout(self.scroll_contents)
self.subtags_scroll_layout.setContentsMargins(6, 0, 6, 0)
self.subtags_scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area = QScrollArea()
# self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_area.setWidget(self.scroll_contents)
# self.scroll_area.setMinimumHeight(60)
self.subtags_layout.addWidget(self.scroll_area)
self.subtags_add_button = QPushButton()
self.subtags_add_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.subtags_add_button.setText("+")
self.subtags_add_button.setToolTip("CTRL + P")
self.subtags_add_button.setMinimumSize(23, 23)
self.subtags_add_button.setMaximumSize(23, 23)
self.subtags_add_button.setShortcut(
QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
QtCore.Qt.Key.Key_P,
)
)
self.subtags_add_button.setStyleSheet(
f"QPushButton{{"
f"background: #1e1e1e;"
f"color: #FFFFFF;"
f"font-weight: bold;"
f"border-color: #333333;"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width:{math.ceil(self.devicePixelRatio())}px;"
f"padding-bottom: 5px;"
f"font-size: 20px;"
f"}}"
f"QPushButton::hover"
f"{{"
f"border-color: #CCCCCC;"
f"background: #555555;"
f"}}"
)
self.subtag_flow_layout.addWidget(self.subtags_add_button)
self.subtags_layout.addWidget(self.subtags_add_button)
exclude_ids: list[int] = list()
if tag is not None:
@@ -182,11 +153,6 @@ class BuildTagPanel(PanelWidget):
tsp.tag_chosen.connect(lambda x: self.add_subtag_callback(x))
self.add_tag_modal = PanelModal(tsp, "Add Parent Tags", "Add Parent Tags")
self.subtags_add_button.clicked.connect(self.add_tag_modal.show)
# self.subtags_layout.addWidget(self.subtags_add_button)
# self.subtags_field = TagBoxWidget()
# self.subtags_field.setMinimumHeight(60)
# self.subtags_layout.addWidget(self.subtags_field)
# Shorthand ------------------------------------------------------------
self.color_widget = QWidget()
@@ -218,65 +184,59 @@ class BuildTagPanel(PanelWidget):
)
)
self.color_layout.addWidget(self.color_field)
remove_selected_alias_action = QAction("remove selected alias", self)
remove_selected_alias_action.triggered.connect(self.remove_selected_alias)
remove_selected_alias_action.setShortcut(
QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
QtCore.Qt.Key.Key_D,
)
)
self.addAction(remove_selected_alias_action)
# Add Widgets to Layout ================================================
self.root_layout.addWidget(self.name_widget)
self.root_layout.addWidget(self.shorthand_widget)
self.root_layout.addWidget(self.aliases_widget)
self.root_layout.addWidget(self.aliases_flow_widget)
self.root_layout.addWidget(self.aliases_table)
self.root_layout.addWidget(self.alias_add_button)
self.root_layout.addWidget(self.subtags_widget)
self.root_layout.addWidget(self.subtag_flow_widget)
self.root_layout.addWidget(self.color_widget)
# self.parent().done.connect(self.update_tag)
self.subtag_ids: set[int] = set()
self.alias_ids: set[int] = set()
self.alias_names: set[str] = set()
self.new_alias_names: dict = dict()
self.subtag_ids: list[int] = []
self.alias_ids: list[int] = []
self.alias_names: list[str] = []
self.new_alias_names: dict = {}
self.new_item_id = sys.maxsize
self.set_tag(tag or Tag(name="New Tag"))
if tag is None:
self.name_field.selectAll()
def keyPressEvent(self, event): # noqa: N802
if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter: # type: ignore
focused_widget = QApplication.focusWidget()
if isinstance(focused_widget.parent(), TagAliasWidget):
self.add_alias_callback()
def remove_selected_alias(self):
count = self.aliases_flow_layout.count()
if count <= 0:
return
def backspace(self):
focused_widget = QApplication.focusWidget()
row = self.aliases_table.rowCount()
if focused_widget is None:
if isinstance(focused_widget, CustomTableItem) is False:
return
remove_row = 0
for i in range(0, row):
item = self.aliases_table.cellWidget(i, 1)
if (
isinstance(item, CustomTableItem)
and cast(CustomTableItem, item).id == cast(CustomTableItem, focused_widget).id
):
cast(QPushButton, self.aliases_table.cellWidget(i, 0)).click()
remove_row = i
break
if self.aliases_table.rowCount() <= 0:
return
if isinstance(focused_widget.parent(), TagAliasWidget):
cast(TagAliasWidget, focused_widget.parent()).on_remove.emit()
if remove_row == 0:
remove_row = 1
count = self.aliases_flow_layout.count()
if count > 1:
cast(
TagAliasWidget, self.aliases_flow_layout.itemAt(count - 2).widget()
).text_field.setFocus()
else:
self.alias_add_button.setFocus()
self.aliases_table.cellWidget(remove_row - 1, 1).setFocus()
def enter(self):
focused_widget = QApplication.focusWidget()
if isinstance(focused_widget, CustomTableItem):
self.add_alias_callback()
def add_subtag_callback(self, tag_id: int):
logger.info("add_subtag_callback", tag_id=tag_id)
self.subtag_ids.add(tag_id)
self.subtag_ids.append(tag_id)
self.set_subtags()
def remove_subtag_callback(self, tag_id: int):
@@ -286,76 +246,71 @@ class BuildTagPanel(PanelWidget):
def add_alias_callback(self):
logger.info("add_alias_callback")
# bug passing in the text for a here means when the text changes
# the remove callback uses what a whas initialy assigned
new_field = TagAliasWidget()
id = new_field.__hash__()
new_field.id = id
new_field.on_remove.connect(lambda a="": self.remove_alias_callback(a, id))
new_field.setMaximumHeight(25)
new_field.setMinimumHeight(25)
id = self.new_item_id
self.alias_ids.add(id)
self.alias_ids.append(id)
self.new_alias_names[id] = ""
self.aliases_flow_layout.addWidget(new_field)
new_field.text_field.setFocus()
self.aliases_flow_layout.addWidget(self.alias_add_button)
self.new_item_id -= 1
self._set_aliases()
row = self.aliases_table.rowCount() - 1
item = self.aliases_table.cellWidget(row, 1)
item.setFocus()
def remove_alias_callback(self, alias_name: str, alias_id: int | None = None):
logger.info("remove_alias_callback")
self.alias_ids.remove(alias_id)
self._set_aliases()
def set_subtags(self):
while self.subtag_flow_layout.itemAt(1):
self.subtag_flow_layout.takeAt(0).widget().deleteLater()
while self.subtags_scroll_layout.itemAt(0):
self.subtags_scroll_layout.takeAt(0).widget().deleteLater()
c = QWidget()
layout = QVBoxLayout(c)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(3)
for tag_id in self.subtag_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_subtag_callback(t))
self.subtag_flow_layout.addWidget(tw)
self.subtag_flow_layout.addWidget(self.subtags_add_button)
layout.addWidget(tw)
self.subtags_scroll_layout.addWidget(c)
def add_aliases(self):
fields: set[TagAliasWidget] = set()
for i in range(0, self.aliases_flow_layout.count() - 1):
widget = self.aliases_flow_layout.itemAt(i).widget()
names: set[str] = set()
for i in range(0, self.aliases_table.rowCount()):
widget = self.aliases_table.cellWidget(i, 1)
if not isinstance(widget, TagAliasWidget):
return
names.add(cast(CustomTableItem, widget).text())
field: TagAliasWidget = cast(TagAliasWidget, widget)
fields.add(field)
remove: set[str] = set(self.alias_names) - names
remove: set[str] = self.alias_names - set([a.text_field.text() for a in fields])
self.alias_names = list(set(self.alias_names) - remove)
self.alias_names = self.alias_names - remove
for field in fields:
for name in names:
# add new aliases
if field.text_field.text() != "":
self.alias_names.add(field.text_field.text())
if name.strip() != "" and name not in set(self.alias_names):
self.alias_names.append(name)
elif name.strip() == "" and name in set(self.alias_names):
self.alias_names.remove(name)
def _update_new_alias_name_dict(self):
for i in range(0, self.aliases_flow_layout.count() - 1):
widget = self.aliases_flow_layout.itemAt(i).widget()
if not isinstance(widget, TagAliasWidget):
return
field: TagAliasWidget = cast(TagAliasWidget, widget)
text_field_text = field.text_field.text()
self.new_alias_names[field.id] = text_field_text
row = self.aliases_table.rowCount()
logger.info(row)
for i in range(0, self.aliases_table.rowCount()):
widget = self.aliases_table.cellWidget(i, 1)
self.new_alias_names[widget.id] = widget.text() # type: ignore
def _set_aliases(self):
self._update_new_alias_name_dict()
while self.aliases_flow_layout.itemAt(1):
self.aliases_flow_layout.takeAt(0).widget().deleteLater()
while self.aliases_table.rowCount() > 0:
self.aliases_table.removeRow(0)
self.alias_names.clear()
@@ -364,43 +319,45 @@ class BuildTagPanel(PanelWidget):
alias_name = alias.name if alias else self.new_alias_names[alias_id]
new_field = TagAliasWidget(
alias_id,
alias_name,
lambda a=alias_name, id=alias_id: self.remove_alias_callback(a, id),
)
new_field.setMaximumHeight(25)
new_field.setMinimumHeight(25)
self.aliases_flow_layout.addWidget(new_field)
self.alias_names.add(alias_name)
# handel when an alias name changes
if alias_id in self.new_alias_names:
alias_name = self.new_alias_names[alias_id]
self.aliases_flow_layout.addWidget(self.alias_add_button)
self.alias_names.append(alias_name)
remove_btn = QPushButton("-")
remove_btn.clicked.connect(
lambda a=alias_name, id=alias_id: self.remove_alias_callback(a, id)
)
row = self.aliases_table.rowCount()
new_item = CustomTableItem(alias_name, self.enter, self.backspace)
new_item.set_id(alias_id)
new_item.editingFinished.connect(lambda item=new_item: self._alias_name_change(item))
self.aliases_table.insertRow(row)
self.aliases_table.setCellWidget(row, 1, new_item)
self.aliases_table.setCellWidget(row, 0, remove_btn)
def _alias_name_change(self, item: CustomTableItem):
self.new_alias_names[item.id] = item.text()
def set_tag(self, tag: Tag):
self.tag = tag
self.tag = tag
logger.info("setting tag", tag=tag)
self.name_field.setText(tag.name)
self.shorthand_field.setText(tag.shorthand or "")
for alias_id in tag.alias_ids:
self.alias_ids.add(alias_id)
self.alias_ids.append(alias_id)
self._set_aliases()
for subtag in tag.subtag_ids:
self.subtag_ids.add(subtag)
for alias_id in tag.alias_ids:
self.alias_ids.add(alias_id)
self._set_aliases()
for subtag in tag.subtag_ids:
self.subtag_ids.add(subtag)
self.subtag_ids.append(subtag)
self.set_subtags()

View File

@@ -165,7 +165,9 @@ class TagDatabasePanel(PanelWidget):
self.edit_modal.show()
def edit_tag_callback(self, btp: BuildTagPanel):
self.lib.update_tag(btp.build_tag(), btp.subtag_ids, btp.alias_names, btp.alias_ids)
self.lib.update_tag(
btp.build_tag(), set(btp.subtag_ids), set(btp.alias_names), set(btp.alias_ids)
)
self.update_tags(self.search_field.text())
def showEvent(self, event: QShowEvent) -> None: # noqa N802

View File

@@ -635,7 +635,10 @@ class QtDriver(DriverMixin, QObject):
self.modal.saved.connect(
lambda: (
self.lib.add_tag(
panel.build_tag(), panel.subtag_ids, panel.alias_names, panel.alias_ids
panel.build_tag(),
set(panel.subtag_ids),
set(panel.alias_names),
set(panel.alias_ids),
),
self.modal.hide(),
)

View File

@@ -1,9 +1,5 @@
from typing import cast
from PySide6.QtWidgets import QApplication, QMainWindow
from src.core.library.alchemy.models import Tag
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.widgets.tag import TagAliasWidget
def test_build_tag_panel_add_sub_tag_callback(library, generate_tag):
@@ -43,29 +39,6 @@ import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"
def test_build_tag_panel_remove_selected_alias(library, generate_tag):
app = QApplication.instance() or QApplication([])
window = QMainWindow()
parent_tag = library.add_tag(generate_tag("xxx", id=123))
panel = BuildTagPanel(library, parent_tag)
panel.setParent(window)
panel.add_alias_callback()
window.show()
assert panel.aliases_flow_layout.count() == 2
alias_widget = panel.aliases_flow_layout.itemAt(0).widget()
alias_widget.text_field.setFocus()
app.processEvents()
panel.remove_selected_alias()
assert panel.aliases_flow_layout.count() == 1
def test_build_tag_panel_add_alias_callback(library, generate_tag):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
@@ -74,7 +47,7 @@ def test_build_tag_panel_add_alias_callback(library, generate_tag):
panel.add_alias_callback()
assert panel.aliases_flow_layout.count() == 2
assert panel.aliases_table.rowCount() == 1
def test_build_tag_panel_remove_alias_callback(library, generate_tag):
@@ -112,7 +85,7 @@ def test_build_tag_panel_set_subtags(library, generate_tag):
panel: BuildTagPanel = BuildTagPanel(library, child)
assert len(panel.subtag_ids) == 1
assert panel.subtag_flow_layout.count() == 2
assert panel.subtags_scroll_layout.count() == 1
def test_build_tag_panel_add_aliases(library, generate_tag):
@@ -128,19 +101,19 @@ def test_build_tag_panel_add_aliases(library, generate_tag):
panel: BuildTagPanel = BuildTagPanel(library, tag)
widget = panel.aliases_flow_layout.itemAt(0).widget()
widget = panel.aliases_table.cellWidget(0, 1)
alias_names: set[str] = set()
alias_names.add(cast(TagAliasWidget, widget).text_field.text())
alias_names.add(widget.text())
widget = panel.aliases_flow_layout.itemAt(1).widget()
alias_names.add(cast(TagAliasWidget, widget).text_field.text())
widget = panel.aliases_table.cellWidget(1, 1)
alias_names.add(widget.text())
assert "alias" in alias_names
assert "alias_2" in alias_names
old_text = cast(TagAliasWidget, widget).text_field.text()
cast(TagAliasWidget, widget).text_field.setText("alias_update")
old_text = widget.text()
widget.setText("alias_update")
panel.add_aliases()
@@ -161,7 +134,7 @@ def test_build_tag_panel_set_aliases(library, generate_tag):
panel: BuildTagPanel = BuildTagPanel(library, tag)
assert panel.aliases_flow_layout.count() == 2
assert panel.aliases_table.rowCount() == 1
assert len(panel.alias_names) == 1
assert len(panel.alias_ids) == 1