Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete tags in the preview panel #495

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions tagstudio/src/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,8 @@ def search_tags(
if search.tag:
query = query.where(
or_(
Tag.name.ilike(search.tag),
Tag.shorthand.ilike(search.tag),
Tag.name.icontains(search.tag),
Tag.shorthand.icontains(search.tag),
)
)

Expand Down
16 changes: 10 additions & 6 deletions tagstudio/src/qt/modals/tag_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
QScrollArea,
QFrame,
)
from typing import Optional

from src.core.library import Library
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 TagWidget, Tag

logger = structlog.get_logger(__name__)

Expand All @@ -33,7 +34,7 @@ def __init__(self, library: Library):
super().__init__()
self.lib = library
# self.callback = callback
self.first_tag_id = None
self.first_tag: Optional[Tag] = None
self.tag_limit = 100
# self.selected_tag: int = 0
self.setMinimumSize(300, 400)
Expand Down Expand Up @@ -88,9 +89,9 @@ def __init__(self, library: Library):
# self.search_field.setFocus()

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 @@ -103,11 +104,14 @@ def update_tags(self, name: str | None = None):

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

if len(found_tags) > 0:
Tyrannicodin marked this conversation as resolved.
Show resolved Hide resolved
self.first_tag = found_tags[0]

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

import structlog
from PySide6.QtCore import Signal, Qt
from PySide6.QtWidgets import QPushButton
from PySide6.QtCore import Signal, Qt, QStringListModel
from PySide6.QtWidgets import QPushButton, QLineEdit, QCompleter

from src.core.constants import TAG_FAVORITE, TAG_ARCHIVED
from src.core.library import Entry, Tag
Expand All @@ -27,6 +27,19 @@
logger = structlog.get_logger(__name__)


class TagCompleter(QCompleter):
def __init__(self, parent, lib):
Tyrannicodin marked this conversation as resolved.
Show resolved Hide resolved
super().__init__(parent)
self.lib = lib
self.update([])

def update(self, exclude:typing.Iterable[str]):
tags = [tag.name for tag in self.lib.tags]
for value in exclude:
tags.remove(value)
Tyrannicodin marked this conversation as resolved.
Show resolved Hide resolved
model = QStringListModel(tags, self)
self.setModel(model)

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

self.tag_entry = QLineEdit()
Tyrannicodin marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -72,13 +92,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),
Tyrannicodin marked this conversation as resolved.
Show resolved Hide resolved
self.tag_completer.setCompletionPrefix(text),
self.tag_completer.complete(),
)
)
self.tag_entry.returnPressed.connect(
lambda: self.tag_completer.activated.emit(self.tag_entry.text())
Tyrannicodin marked this conversation as resolved.
Show resolved Hide resolved
)
self.tag_completer.activated.connect(
lambda selected: (
tsp.update_tags(selected),
tsp.on_return(selected),
self.tag_entry.clear(),
)
)

Expand All @@ -88,8 +128,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 @@ -111,15 +152,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
Loading