From 205a362bda7ea37063e1342c17411dffefa822de Mon Sep 17 00:00:00 2001 From: Brady LaTessa <40649522+blatessa@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:57:45 -0400 Subject: [PATCH] feat(tags): add tag merging (#322) * added merge tag context menu option, created modal, and created merge_tag logic * forgot to close out the modal * added ability to re-assign current tag * ruff/mypy checks --- tagstudio/src/core/library.py | 25 +++++++++++ tagstudio/src/qt/modals/merge_tag.py | 63 ++++++++++++++++++++++++++++ tagstudio/src/qt/widgets/tag.py | 8 ++++ 3 files changed, 96 insertions(+) create mode 100644 tagstudio/src/qt/modals/merge_tag.py diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 7eedb8b8..ec89bae9 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -1895,6 +1895,31 @@ class Library: for t in self.tags: self._map_tag_strings_to_tag_id(t) + def merge_tag(self, source_tag: Tag, target_tag: Tag) -> None: + source_tag_id: int = source_tag.id + target_tag_id: int = target_tag.id + + if source_tag.name not in target_tag.aliases: + target_tag.aliases.append(source_tag.name) + + for alias in source_tag.aliases: + if alias not in target_tag.aliases: + target_tag.aliases.append(alias) + + for subtag_id in source_tag.subtag_ids: + if subtag_id not in target_tag.subtag_ids: + target_tag.subtag_ids.append(subtag_id) + + for entry in self.entries: + for field in entry.fields: + if self.get_field_attr(field, "type") == "tag_box": + if source_tag_id in self.get_field_attr(field, "content"): + self.get_field_attr(field, "content").remove(source_tag_id) + if target_tag_id not in self.get_field_attr(field, "content"): + self.get_field_attr(field, "content").append(target_tag_id) + + self.remove_tag(source_tag_id) + def get_tag_ref_count(self, tag_id: int) -> tuple[int, int]: """Returns an int tuple (entry_ref_count, subtag_ref_count) of Tag reference counts.""" entry_ref_count: int = 0 diff --git a/tagstudio/src/qt/modals/merge_tag.py b/tagstudio/src/qt/modals/merge_tag.py new file mode 100644 index 00000000..dc69dc8a --- /dev/null +++ b/tagstudio/src/qt/modals/merge_tag.py @@ -0,0 +1,63 @@ +from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QComboBox, QPushButton +from PySide6.QtCore import Qt +import logging + + +class MergeTagModal(QDialog): + def __init__(self, library, current_tag): + super().__init__() + self.lib = library + self.current_tag = current_tag + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + + layout.addWidget(QLabel("Selected tag:")) + self.selected_tag_dropdown = QComboBox(self) + for tag in self.lib.tags: + self.selected_tag_dropdown.addItem(tag.display_name(self.lib), tag) + self.selected_tag_dropdown.setCurrentIndex(self.find_current_tag_index()) + self.selected_tag_dropdown.currentIndexChanged.connect(self.update_current_tag) + layout.addWidget(self.selected_tag_dropdown) + + arrow_label = QLabel("↓") + arrow_label.setAlignment(Qt.AlignCenter) + layout.addWidget(arrow_label) + + layout.addWidget(QLabel("Select tag to merge with:")) + self.tag2_dropdown = QComboBox(self) + self.update_tag2_dropdown() + layout.addWidget(self.tag2_dropdown) + + self.merge_button = QPushButton("Merge", self) + layout.addWidget(self.merge_button) + + self.merge_button.clicked.connect(self.merge_tags) + + self.setLayout(layout) + + def find_current_tag_index(self): + for index in range(self.selected_tag_dropdown.count()): + if self.selected_tag_dropdown.itemData(index) == self.current_tag: + return index + return 0 + + def update_current_tag(self): + self.current_tag = self.selected_tag_dropdown.currentData() + self.update_tag2_dropdown() + + def update_tag2_dropdown(self): + self.tag2_dropdown.clear() + for tag in self.lib.tags: + if tag.id != self.current_tag.id: + self.tag2_dropdown.addItem(tag.display_name(self.lib), tag) + + def merge_tags(self): + target_tag = self.tag2_dropdown.currentData() + if target_tag and self.current_tag != target_tag: + self.lib.merge_tag(self.current_tag, target_tag) + self.accept() + else: + logging.error("MergeTagModal: Invalid tag selection.") + self.reject() diff --git a/tagstudio/src/qt/widgets/tag.py b/tagstudio/src/qt/widgets/tag.py index 739369dc..3511bbca 100644 --- a/tagstudio/src/qt/widgets/tag.py +++ b/tagstudio/src/qt/widgets/tag.py @@ -15,6 +15,7 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton from src.core.library import Library, Tag from src.core.palette import ColorType, get_tag_color +from src.qt.modals.merge_tag import MergeTagModal ERROR = f"[ERROR]" @@ -77,6 +78,10 @@ class TagWidget(QWidget): add_to_search_action = QAction("Add to Search", self) self.bg_button.addAction(add_to_search_action) + merge_tag_action = QAction("Merge Tag", self) + merge_tag_action.triggered.connect(self.show_merge_tag_modal) + self.bg_button.addAction(merge_tag_action) + self.inner_layout = QHBoxLayout() self.inner_layout.setObjectName("innerLayout") self.inner_layout.setContentsMargins(2, 2, 2, 2) @@ -234,6 +239,9 @@ class TagWidget(QWidget): # pass # # self.bg.clicked.connect(lambda checked=False, filepath=filepath: open_file(filepath)) # # self.bg.clicked.connect(function) + def show_merge_tag_modal(self): + modal = MergeTagModal(self.lib, self.tag) + modal.exec_() def enterEvent(self, event: QEnterEvent) -> None: if self.has_remove: