diff --git a/tagstudio/src/qt/modals/build_tag.py b/tagstudio/src/qt/modals/build_tag.py index e6befed95..f4e43a0fb 100644 --- a/tagstudio/src/qt/modals/build_tag.py +++ b/tagstudio/src/qt/modals/build_tag.py @@ -3,31 +3,29 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -import math -from typing import cast +import sys 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, + QTableWidgetItem, QVBoxLayout, QWidget, ) from src.core.library import Library, Tag from src.core.library.alchemy.enums import TagColor from src.core.palette import ColorType, get_tag_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__) @@ -38,8 +36,6 @@ class BuildTagPanel(PanelWidget): 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) @@ -83,43 +79,16 @@ def __init__(self, library: Library, tag: Tag | None = None): 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) # Subtags ------------------------------------------------------------ @@ -134,42 +103,28 @@ def __init__(self, library: Library, tag: Tag | None = None): 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) + tsp = TagSearchPanel(self.lib) + 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) exclude_ids: list[int] = list() if tag is not None: @@ -179,11 +134,6 @@ def __init__(self, library: Library, tag: Tag | None = None): 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() @@ -201,7 +151,6 @@ def __init__(self, library: Library, tag: Tag | None = None): self.color_field.setStyleSheet("combobox-popup:0;") for color in TagColor: self.color_field.addItem(color.name, userData=color.value) - # self.color_field.setProperty("appearance", "flat") self.color_field.currentIndexChanged.connect( lambda c: ( self.color_field.setStyleSheet( @@ -215,59 +164,41 @@ def __init__(self, library: Library, tag: Tag | None = None): ) ) 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.new_item_id = sys.maxsize self.set_tag(tag or Tag(name="New Tag")) 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): + if isinstance(focused_widget, QTableWidget): self.add_alias_callback() - def remove_selected_alias(self): - count = self.aliases_flow_layout.count() - if count <= 0: - return - - focused_widget = QApplication.focusWidget() - - if focused_widget is None: - return - - if isinstance(focused_widget.parent(), TagAliasWidget): - cast(TagAliasWidget, focused_widget.parent()).on_remove.emit() + if event.key() == Qt.Key_Backspace: # type: ignore + focused_widget = QApplication.focusWidget() + row = self.aliases_table.rowCount() - 1 + is_table = isinstance(focused_widget, QTableWidget) + is_empty = self.aliases_table.item(row, 1).text().strip() == "" + if is_table and is_empty: + button = self.aliases_table.cellWidget(row, 0) - 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() + if button and isinstance(button, QPushButton): + button.click() + self.aliases_table.setCurrentCell(row - 1, 0) def add_subtag_callback(self, tag_id: int): logger.info("add_subtag_callback", tag_id=tag_id) @@ -281,21 +212,15 @@ def remove_subtag_callback(self, tag_id: int): 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.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() def remove_alias_callback(self, alias_name: str, alias_id: int | None = None): logger.info("remove_alias_callback") @@ -303,73 +228,70 @@ def remove_alias_callback(self, alias_name: str, alias_id: int | None = None): 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.item(i, 1) - if not isinstance(widget, TagAliasWidget): - return + names.add(widget.text()) - field: TagAliasWidget = cast(TagAliasWidget, widget) - fields.add(field) - - remove: set[str] = self.alias_names - set([a.text_field.text() for a in fields]) + remove: set[str] = self.alias_names - names 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 != "": + self.alias_names.add(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.item(i, 1) + self.new_alias_names[widget.data(Qt.UserRole)] = 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() - for alias_id in self.alias_ids: + for alias_id in list(self.alias_ids)[::-1]: alias = self.lib.get_alias(self.tag.id, alias_id) 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) - self.aliases_flow_layout.addWidget(self.alias_add_button) + 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 = QTableWidgetItem(alias_name) + new_item.setData(Qt.UserRole, alias_id) # type: ignore + + self.aliases_table.insertRow(row) + self.aliases_table.setItem(row, 1, new_item) + self.aliases_table.setCellWidget(row, 0, remove_btn) def set_tag(self, tag: Tag): self.tag = tag @@ -386,14 +308,6 @@ def set_tag(self, tag: Tag): 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) diff --git a/tagstudio/tests/qt/test_build_tag_panel.py b/tagstudio/tests/qt/test_build_tag_panel.py index 9026b4756..0d6631336 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 @@ def test_build_tag_panel_remove_subtag_callback(library, generate_tag): 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.item(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.item(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