Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions tagstudio/src/qt/modals/tag_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


import math
from typing import Optional

import structlog
from PySide6.QtCore import QSize, Qt, Signal
Expand All @@ -20,7 +21,7 @@
from src.core.library.alchemy.enums import FilterState
from src.core.palette import ColorType, get_tag_color
from src.qt.widgets.panel import PanelWidget
from src.qt.widgets.tag import TagWidget
from src.qt.widgets.tag import Tag, TagWidget

logger = structlog.get_logger(__name__)

Expand All @@ -31,6 +32,7 @@ class TagSearchPanel(PanelWidget):
def __init__(self, library: Library):
super().__init__()
self.lib = library
self.first_tag: Optional[Tag] = None
self.first_tag_id = None
self.tag_limit = 100
self.setMinimumSize(300, 400)
Expand Down Expand Up @@ -63,9 +65,9 @@ def __init__(self, library: Library):
self.update_tags()

def on_return(self, text: str):
if text and self.first_tag_id is not None:
# callback(self.first_tag_id)
self.tag_chosen.emit(self.first_tag_id)
if text and self.first_tag is not None:
# callback(self.first_tag)
self.tag_chosen.emit(self.first_tag.id)
self.search_field.setText("")
self.update_tags()
else:
Expand All @@ -78,11 +80,13 @@ def update_tags(self, name: str | None = None):

found_tags = self.lib.search_tags(
FilterState(
path=name,
tag=name,
page_size=self.tag_limit,
)
)

self.first_tag = found_tags[0] if found_tags else None

for tag in found_tags:
c = QWidget()
layout = QHBoxLayout(c)
Expand Down
65 changes: 54 additions & 11 deletions tagstudio/src/qt/widgets/tag_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import typing

import structlog
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QPushButton
from PySide6.QtCore import QObject, QStringListModel, Qt, Signal
from PySide6.QtWidgets import QCompleter, QLineEdit, QPushButton
from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from src.core.library import Entry, Tag
from src.core.library import Entry, Library, Tag
from src.core.library.alchemy.enums import FilterState
from src.core.library.alchemy.fields import TagBoxField
from src.qt.flowlayout import FlowLayout
Expand All @@ -26,6 +26,19 @@
logger = structlog.get_logger(__name__)


class TagCompleter(QCompleter):
def __init__(self, parent: QObject, lib: Library):
super().__init__(parent)
self.lib = lib
self.update(set())

def update(self, exclude: set[str]):
tags = {tag.name for tag in self.lib.tags}
tags -= exclude
model = QStringListModel(list(tags), self)
self.setModel(model)


class TagBoxWidget(FieldWidget):
updated = Signal()
error_occurred = Signal(Exception)
Expand All @@ -50,6 +63,13 @@ def __init__(
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.base_layout)

self.tag_entry = QLineEdit()
self.base_layout.addWidget(self.tag_entry)

self.tag_completer = TagCompleter(self.tag_entry, self.driver.lib)
self.tag_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.tag_completer.setWidget(self.tag_entry)

self.add_button = QPushButton()
self.add_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.add_button.setMinimumSize(23, 23)
Expand All @@ -73,13 +93,33 @@ def __init__(
f"background: #555555;"
f"}}"
)

tsp = TagSearchPanel(self.driver.lib)
tsp.tag_chosen.connect(lambda x: self.add_tag_callback(x))
tsp.tag_chosen.connect(
lambda x: (
self.add_tag_callback(x),
self.tag_entry.clear(),
)
)
self.add_modal = PanelModal(tsp, title, "Add Tags")
self.add_button.clicked.connect(
lambda: (
tsp.update_tags(),
self.add_modal.show(),

self.add_button.clicked.connect(self.add_modal.show)
self.tag_entry.textChanged.connect(
lambda text: (
tsp.search_field.setText(text),
tsp.update_tags(text),
self.tag_completer.setCompletionPrefix(text),
self.tag_completer.complete(),
)
)
self.tag_entry.returnPressed.connect(
lambda: self.tag_completer.activated.emit(self.tag_entry.text())
)
self.tag_completer.activated.connect(
lambda selected: (
tsp.update_tags(selected),
tsp.on_return(selected),
self.tag_entry.clear(),
)
)

Expand All @@ -89,8 +129,9 @@ def set_field(self, field: TagBoxField):
self.field = field

def set_tags(self, tags: typing.Iterable[Tag]):
self.tag_completer.update({tag.name for tag in tags})
is_recycled = False
while self.base_layout.itemAt(0) and self.base_layout.itemAt(1):
while self.base_layout.itemAt(0) and self.base_layout.itemAt(2):
self.base_layout.takeAt(0).widget().deleteLater()
is_recycled = True

Expand All @@ -112,15 +153,17 @@ def set_tags(self, tags: typing.Iterable[Tag]):
tag_widget.on_edit.connect(lambda t=tag: self.edit_tag(t))
self.base_layout.addWidget(tag_widget)

# Move or add the '+' button.
# Move or add the tag entry and '+' button.
if is_recycled:
self.base_layout.addWidget(self.base_layout.takeAt(0).widget())
self.base_layout.addWidget(self.base_layout.takeAt(0).widget())
else:
self.base_layout.addWidget(self.tag_entry)
self.base_layout.addWidget(self.add_button)

# Handles an edge case where there are no more tags and the '+' button
# doesn't move all the way to the left.
if self.base_layout.itemAt(0) and not self.base_layout.itemAt(1):
if self.base_layout.itemAt(0) and not self.base_layout.itemAt(2):
self.base_layout.update()

def edit_tag(self, tag: Tag):
Expand Down
24 changes: 24 additions & 0 deletions tagstudio/tests/qt/test_tag_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,27 @@ def test_tag_widget_edit(qtbot, qt_driver, library, entry_full):
assert isinstance(panel, BuildTagPanel)
assert panel.tag.name == tag.name
assert panel.name_field.text() == tag.name


def test_tag_widget_autocomplete(qtbot, qt_driver, library):
# Given
entry = next(library.get_entries(with_joins=True))
field = entry.tag_box_fields[0]

tag_widget = TagBoxWidget(field, "title", qt_driver)
tag_widget.driver.selected = [0]

qtbot.add_widget(tag_widget)

# Test autocomplete
tag_widget.tag_entry.setText("arch")
tag_widget.tag_entry.returnPressed.emit()

entry = next(library.get_entries(with_joins=True)) # Update entry
assert len(entry.tags) == 2

# Test unmatched autocomplete
tag_widget.tag_completer.activated.emit("missing")

entry = next(library.get_entries(with_joins=True)) # Update entry
assert len(entry.tags) == 2