diff --git a/.gitignore b/.gitignore index 595a3290..e5f2d946 100644 --- a/.gitignore +++ b/.gitignore @@ -262,6 +262,7 @@ compile_commands.json tagstudio/tests/fixtures/library/* tagstudio/tests/fixtures/json_library/.TagStudio/*.sqlite TagStudio.ini +*.sqlite-journal .envrc .direnv diff --git a/docs/updates/roadmap.md b/docs/updates/roadmap.md index e204bc3b..7a8e2557 100644 --- a/docs/updates/roadmap.md +++ b/docs/updates/roadmap.md @@ -86,13 +86,13 @@ Features are broken up into the following priority levels, with nested prioritie - [ ] GPS Location [LOW] - [ ] Custom field names [HIGH] [#18](https://github.com/TagStudioDev/TagStudio/issues/18) - [ ] Search engine [HIGH] [#325](https://github.com/TagStudioDev/TagStudio/issues/325) - - [ ] Boolean operators [HIGH] [#225](https://github.com/TagStudioDev/TagStudio/issues/225), [#314](https://github.com/TagStudioDev/TagStudio/issues/314) + - [x] Boolean operators [HIGH] [#225](https://github.com/TagStudioDev/TagStudio/issues/225), [#314](https://github.com/TagStudioDev/TagStudio/issues/314) - [ ] Tag objects + autocomplete [HIGH] [#476 (Autocomplete)](https://github.com/TagStudioDev/TagStudio/issues/476) - - [ ] Filename search [HIGH] - - [ ] Filetype search [HIGH] - - [ ] Search by extension (e.g. ".jpg", ".png") [HIGH] - - [ ] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW] - - [ ] Search by media type (e.g. "image", "video", "document") [MEDIUM] + - [x] Filename search [HIGH] + - [x] Filetype search [HIGH] + - [x] Search by extension (e.g. ".jpg", ".png") [HIGH] + - [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW] + - [x] Search by media type (e.g. "image", "video", "document") [MEDIUM] - [ ] Field content search [HIGH] [#272](https://github.com/TagStudioDev/TagStudio/issues/272) - [ ] HAS operator for composition tags [HIGH] - [ ] OCR search [LOW] @@ -172,12 +172,12 @@ These version milestones are rough estimations for when the previous core featur - [ ] [Tag Categories](../library/tag_categories.md) [HIGH] - [ ] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH] - [ ] Search engine [HIGH] - - [ ] Boolean operators [HIGH] + - [x] Boolean operators [HIGH] - [ ] Tag objects + autocomplete [HIGH] - [x] Filename search [HIGH] - [x] Filetype search [HIGH] - [x] Search by extension (e.g. ".jpg", ".png") [HIGH] - - [ ] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW] + - [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW] - [x] Search by media type (e.g. "image", "video", "document") [MEDIUM] - [ ] Field content search [HIGH] - [ ] Sortable results [HIGH] diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json index 7d437814..024d1cd8 100644 --- a/tagstudio/resources/translations/en.json +++ b/tagstudio/resources/translations/en.json @@ -163,7 +163,6 @@ "tag.parent_tags": "Parent Tags", "tag.search_for_tag": "Search for Tag", "tag.shorthand": "Shorthand", - "tag.remove_alias": "Remove selected alias", "tag.tag_name_required": "Tag Name (Required)", "view.size.0": "Mini", "view.size.1": "Small", @@ -181,5 +180,5 @@ "drop_import.progress.label.plural": "Importing New Files...\n{count} Files Imported.{suffix}", "drop_import.progress.window_title": "Import Files", "drop_import.duplicates_choice.singular": "The following file has a filename that already exists in the library.", - "drop_import.duplicates_choice.plural": "The following {count} files have filenames that already exist in the library.", + "drop_import.duplicates_choice.plural": "The following {count} files have filenames that already exist in the library." } \ No newline at end of file diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index 043d9262..186cd797 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -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) diff --git a/tagstudio/src/core/library/alchemy/visitors.py b/tagstudio/src/core/library/alchemy/visitors.py index 6f73a45a..1756bb08 100644 --- a/tagstudio/src/core/library/alchemy/visitors.py +++ b/tagstudio/src/core/library/alchemy/visitors.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from sqlalchemy import and_, distinct, func, or_, select from sqlalchemy.orm import Session from sqlalchemy.sql.expression import BinaryExpression, ColumnExpressionArgument -from src.core.media_types import MediaCategories +from src.core.media_types import FILETYPE_EQUIVALENTS, MediaCategories from src.core.query_lang import BaseVisitor from src.core.query_lang.ast import AST, ANDList, Constraint, ConstraintType, Not, ORList, Property @@ -17,6 +17,13 @@ else: Library = None # don't import .library because of circular imports +def get_filetype_equivalency_list(item: str) -> list[str] | set[str]: + for s in FILETYPE_EQUIVALENTS: + if item in s: + return s + return [item] + + class SQLBoolExpressionBuilder(BaseVisitor[ColumnExpressionArgument]): def __init__(self, lib: Library) -> None: super().__init__() @@ -73,7 +80,9 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnExpressionArgument]): break return Entry.suffix.in_(map(lambda x: x.replace(".", ""), extensions)) elif node.type == ConstraintType.FileType: - return Entry.suffix.ilike(node.value) + return or_( + *[Entry.suffix.ilike(ft) for ft in get_filetype_equivalency_list(node.value)] + ) elif node.type == ConstraintType.Special: # noqa: SIM102 unnecessary once there is a second special constraint if node.value.lower() == "untagged": return ~Entry.id.in_( diff --git a/tagstudio/src/core/media_types.py b/tagstudio/src/core/media_types.py index 78755aac..be9ebdbd 100644 --- a/tagstudio/src/core/media_types.py +++ b/tagstudio/src/core/media_types.py @@ -10,6 +10,8 @@ from pathlib import Path logging.basicConfig(format="%(message)s", level=logging.INFO) +FILETYPE_EQUIVALENTS = [set(["jpg", "jpeg"])] + class MediaType(str, Enum): """Names of media types.""" diff --git a/tagstudio/src/qt/modals/build_tag.py b/tagstudio/src/qt/modals/build_tag.py index bfdc136c..73687e38 100644 --- a/tagstudio/src/qt/modals/build_tag.py +++ b/tagstudio/src/qt/modals/build_tag.py @@ -3,45 +3,60 @@ # 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 from ..translations import Translations 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) @@ -90,43 +105,16 @@ class BuildTagPanel(PanelWidget): Translations.translate_qobject(self.aliases_title, "tag.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 ------------------------------------------------------------ @@ -141,42 +129,25 @@ class BuildTagPanel(PanelWidget): Translations.translate_qobject(self.subtags_title, "tag.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: @@ -188,11 +159,6 @@ class BuildTagPanel(PanelWidget): Translations.translate_with_setter(self.add_tag_modal.setTitle, "tag.parent_tags.add") Translations.translate_with_setter(self.add_tag_modal.setWindowTitle, "tag.parent_tags.add") 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() @@ -224,66 +190,59 @@ class BuildTagPanel(PanelWidget): ) ) self.color_layout.addWidget(self.color_field) - remove_selected_alias_action = QAction(self) - Translations.translate_qobject(remove_selected_alias_action, "tag.remove_alias") - 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=Translations["tag.new"])) 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): @@ -293,76 +252,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() @@ -371,43 +325,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() diff --git a/tagstudio/src/qt/modals/tag_database.py b/tagstudio/src/qt/modals/tag_database.py index 8a308cd8..3e24a6c9 100644 --- a/tagstudio/src/qt/modals/tag_database.py +++ b/tagstudio/src/qt/modals/tag_database.py @@ -167,7 +167,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 diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 62fa51fc..948608d2 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -673,7 +673,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(), ) diff --git a/tagstudio/tests/qt/test_build_tag_panel.py b/tagstudio/tests/qt/test_build_tag_panel.py index 9026b475..87971bcc 100644 --- a/tagstudio/tests/qt/test_build_tag_panel.py +++ b/tagstudio/tests/qt/test_build_tag_panel.py @@ -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