diff --git a/tagstudio/src/qt/modals/tag_database.py b/tagstudio/src/qt/modals/tag_database.py index d567e899..60f2a1d6 100644 --- a/tagstudio/src/qt/modals/tag_database.py +++ b/tagstudio/src/qt/modals/tag_database.py @@ -3,72 +3,32 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio import structlog -from PySide6.QtCore import QSize, Qt, Signal -from PySide6.QtGui import QShowEvent from PySide6.QtWidgets import ( - QFrame, - QHBoxLayout, - QLineEdit, QMessageBox, QPushButton, - QScrollArea, - QVBoxLayout, - QWidget, ) from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START from src.core.library import Library, Tag from src.qt.modals.build_tag import BuildTagPanel +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.panel import PanelModal logger = structlog.get_logger(__name__) -# TODO: This class shares the majority of its code with tag_search.py. -# It should either be made DRY, or be replaced with the intended and more robust -# Tag Management tab/pane outlined on the Feature Roadmap. +# TODO: Once this class is removed, the `is_tag_chooser` option of `TagSearchPanel` +# will most likely be enabled in every case +# and the possibilty of disabling it can therefore be removed -class TagDatabasePanel(PanelWidget): - tag_chosen = Signal(int) - +class TagDatabasePanel(TagSearchPanel): def __init__(self, library: Library): - super().__init__() - self.lib: Library = library - self.is_initialized: bool = False - self.first_tag_id = -1 - - self.setMinimumSize(300, 400) - self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(6, 0, 6, 0) - - self.search_field = QLineEdit() - self.search_field.setObjectName("searchField") - self.search_field.setMinimumSize(QSize(0, 32)) - Translations.translate_with_setter(self.search_field.setPlaceholderText, "home.search_tags") - self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text())) - self.search_field.returnPressed.connect( - lambda checked=False: self.on_return(self.search_field.text()) - ) - - self.scroll_contents = QWidget() - self.scroll_layout = QVBoxLayout(self.scroll_contents) - self.scroll_layout.setContentsMargins(6, 0, 6, 0) - self.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) + super().__init__(library, is_tag_chooser=False) self.create_tag_button = QPushButton() Translations.translate_qobject(self.create_tag_button, "tag.create") self.create_tag_button.clicked.connect(lambda: self.build_tag(self.search_field.text())) - self.root_layout.addWidget(self.search_field) - self.root_layout.addWidget(self.scroll_area) self.root_layout.addWidget(self.create_tag_button) self.update_tags() @@ -97,41 +57,6 @@ class TagDatabasePanel(PanelWidget): ) self.modal.show() - def on_return(self, text: str): - if text and self.first_tag_id >= 0: - # callback(self.first_tag_id) - self.search_field.setText("") - self.update_tags() - else: - self.search_field.setFocus() - self.parentWidget().hide() - - def update_tags(self, query: str | None = None): - # TODO: Look at recycling rather than deleting and re-initializing - logger.info("[Tag Manager Modal] Updating Tags") - while self.scroll_layout.itemAt(0): - self.scroll_layout.takeAt(0).widget().deleteLater() - - tags_results = self.lib.search_tags(name=query) - - for tag in tags_results: - container = QWidget() - row = QHBoxLayout(container) - row.setContentsMargins(0, 0, 0, 0) - row.setSpacing(3) - - if tag.id in range(RESERVED_TAG_START, RESERVED_TAG_END): - tag_widget = TagWidget(tag, has_edit=True, has_remove=False) - else: - tag_widget = TagWidget(tag, has_edit=True, has_remove=True) - - tag_widget.on_edit.connect(lambda checked=False, t=tag: self.edit_tag(t)) - tag_widget.on_remove.connect(lambda t=tag: self.remove_tag(t)) - row.addWidget(tag_widget) - self.scroll_layout.addWidget(container) - - self.search_field.setFocus() - def remove_tag(self, tag: Tag): if tag.id in range(RESERVED_TAG_START, RESERVED_TAG_END): return @@ -149,29 +74,3 @@ class TagDatabasePanel(PanelWidget): self.lib.remove_tag(tag) self.update_tags() - - def edit_tag(self, tag: Tag): - build_tag_panel = BuildTagPanel(self.lib, tag=tag) - - self.edit_modal = PanelModal( - build_tag_panel, - tag.name, - done_callback=(self.update_tags(self.search_field.text())), - has_save=True, - ) - Translations.translate_with_setter(self.edit_modal.setWindowTitle, "tag.edit") - # TODO Check Warning: Expected type 'BuildTagPanel', got 'PanelWidget' instead - self.edit_modal.saved.connect(lambda: self.edit_tag_callback(build_tag_panel)) - self.edit_modal.show() - - def edit_tag_callback(self, btp: BuildTagPanel): - self.lib.update_tag( - btp.build_tag(), set(btp.parent_ids), set(btp.alias_names), set(btp.alias_ids) - ) - self.update_tags(self.search_field.text()) - - def showEvent(self, event: QShowEvent) -> None: # noqa N802 - if not self.is_initialized: - self.update_tags() - self.is_initialized = True - return super().showEvent(event) diff --git a/tagstudio/src/qt/modals/tag_search.py b/tagstudio/src/qt/modals/tag_search.py index b95326ac..b0cf9398 100644 --- a/tagstudio/src/qt/modals/tag_search.py +++ b/tagstudio/src/qt/modals/tag_search.py @@ -17,10 +17,11 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.library import Library +from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START +from src.core.library import Library, Tag from src.core.palette import ColorType, get_tag_color from src.qt.translations import Translations -from src.qt.widgets.panel import PanelWidget +from src.qt.widgets.panel import PanelModal, PanelWidget from src.qt.widgets.tag import TagWidget logger = structlog.get_logger(__name__) @@ -28,13 +29,19 @@ logger = structlog.get_logger(__name__) class TagSearchPanel(PanelWidget): tag_chosen = Signal(int) + lib: Library + is_initialized: bool = False + first_tag_id: int = None + is_tag_chooser: bool + exclude: list[int] - def __init__(self, library: Library, exclude: list[int] | None = None): + def __init__(self, library: Library, exclude: list[int] = None, is_tag_chooser: bool = True): super().__init__() self.lib = library - self.exclude = exclude - self.is_initialized: bool = False - self.first_tag_id: int = None + self.exclude = exclude or [] + + self.is_tag_chooser = is_tag_chooser + self.setMinimumSize(300, 400) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(6, 0, 6, 0) @@ -44,9 +51,7 @@ class TagSearchPanel(PanelWidget): self.search_field.setMinimumSize(QSize(0, 32)) Translations.translate_with_setter(self.search_field.setPlaceholderText, "home.search_tags") self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text())) - self.search_field.returnPressed.connect( - lambda checked=False: self.on_return(self.search_field.text()) - ) + self.search_field.returnPressed.connect(lambda: self.on_return(self.search_field.text())) self.scroll_contents = QWidget() self.scroll_layout = QVBoxLayout(self.scroll_contents) @@ -63,40 +68,32 @@ class TagSearchPanel(PanelWidget): self.root_layout.addWidget(self.search_field) self.root_layout.addWidget(self.scroll_area) - def on_return(self, text: str): - if text and self.first_tag_id is not None: - self.tag_chosen.emit(self.first_tag_id) - self.search_field.setText("") - self.update_tags() - else: - self.search_field.setFocus() - self.parentWidget().hide() + def __build_row_item_widget(self, tag: Tag): + container = QWidget() + row = QHBoxLayout(container) + row.setContentsMargins(0, 0, 0, 0) + row.setSpacing(3) - def update_tags(self, query: str | None = None): - logger.info("[Tag Search Modal] Updating Tags") - while self.scroll_layout.count(): - self.scroll_layout.takeAt(0).widget().deleteLater() + has_remove_button = False + if not self.is_tag_chooser: + has_remove_button = tag.id not in range(RESERVED_TAG_START, RESERVED_TAG_END) - tag_results = self.lib.search_tags(name=query) - if len(tag_results) > 0: - self.first_tag_id = tag_results[0].id - else: - self.first_tag_id = None + tag_widget = TagWidget( + tag, + has_edit=True, + has_remove=has_remove_button, + ) - for tag in tag_results: - if self.exclude is not None and tag.id in self.exclude: - continue + tag_widget.on_edit.connect(lambda t=tag: self.edit_tag(t)) + tag_widget.on_remove.connect(lambda t=tag: self.remove_tag(t)) + row.addWidget(tag_widget) - c = QWidget() - layout = QHBoxLayout(c) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - tw = TagWidget(tag, has_edit=False, has_remove=False) - ab = QPushButton() - ab.setMinimumSize(23, 23) - ab.setMaximumSize(23, 23) - ab.setText("+") - ab.setStyleSheet( + if self.is_tag_chooser: + add_button = QPushButton() + add_button.setMinimumSize(23, 23) + add_button.setMaximumSize(23, 23) + add_button.setText("+") + add_button.setStyleSheet( f"QPushButton{{" f"background: {get_tag_color(ColorType.PRIMARY, tag.color)};" f"color: {get_tag_color(ColorType.TEXT, tag.color)};" @@ -115,17 +112,67 @@ class TagSearchPanel(PanelWidget): f"background: {get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};" f"}}" ) + add_button.clicked.connect(lambda x=tag.id: self.tag_chosen.emit(x)) + row.addWidget(add_button) + return container - ab.clicked.connect(lambda checked=False, x=tag.id: self.tag_chosen.emit(x)) + def update_tags(self, query: str | None = None): + logger.info("[Tag Search Super Class] Updating Tags") - layout.addWidget(tw) - layout.addWidget(ab) - self.scroll_layout.addWidget(c) + # TODO: Look at recycling rather than deleting and re-initializing + while self.scroll_layout.count(): + self.scroll_layout.takeAt(0).widget().deleteLater() + + tag_results = self.lib.search_tags(name=query) + if len(tag_results) > 0: + self.first_tag_id = tag_results[0].id + else: + self.first_tag_id = None + + for tag in tag_results: + if tag.id not in self.exclude: + self.scroll_layout.addWidget(self.__build_row_item_widget(tag)) self.search_field.setFocus() + def on_return(self, text: str): + if text and self.first_tag_id is not None: + if self.is_tag_chooser: + self.tag_chosen.emit(self.first_tag_id) + self.search_field.setText("") + self.update_tags() + else: + self.search_field.setFocus() + self.parentWidget().hide() + def showEvent(self, event: QShowEvent) -> None: # noqa N802 if not self.is_initialized: self.update_tags() self.is_initialized = True return super().showEvent(event) + + def remove_tag(self, tag: Tag): + pass + + def edit_tag(self, tag: Tag): + # only import here because of circular imports + from src.qt.modals.build_tag import BuildTagPanel + + def callback(btp: BuildTagPanel): + self.lib.update_tag( + btp.build_tag(), set(btp.parent_ids), set(btp.alias_names), set(btp.alias_ids) + ) + self.update_tags(self.search_field.text()) + + build_tag_panel = BuildTagPanel(self.lib, tag=tag) + + self.edit_modal = PanelModal( + build_tag_panel, + tag.name, + done_callback=(self.update_tags(self.search_field.text())), + has_save=True, + ) + Translations.translate_with_setter(self.edit_modal.setWindowTitle, "tag.edit") + + self.edit_modal.saved.connect(lambda: callback(build_tag_panel)) + self.edit_modal.show()