Skip to content

Commit bec513f

Browse files
authored
feat: add autocomplete for search engine (#586)
* feat: add autocomplete for mediatype, filetype, path, tag, and tag_id searches * fix: address issues brought up in review * fix: fix mypy issue * fix: fix mypy issues for real this time
1 parent 9078fee commit bec513f

File tree

3 files changed

+73
-2
lines changed

3 files changed

+73
-2
lines changed

tagstudio/src/core/library/alchemy/library.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ def has_path_entry(self, path: Path) -> bool:
395395
with Session(self.engine) as session:
396396
return session.query(exists().where(Entry.path == path)).scalar()
397397

398+
def get_paths(self, glob: str | None = None) -> list[str]:
399+
with Session(self.engine) as session:
400+
paths = session.scalars(select(Entry.path)).unique()
401+
402+
path_strings: list[str] = list(map(lambda x: x.as_posix(), paths))
403+
return path_strings
404+
398405
def search_library(
399406
self,
400407
search: FilterState,

tagstudio/src/qt/main_window.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515

1616
import logging
1717
import typing
18-
from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt)
18+
from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt, QStringListModel)
1919
from PySide6.QtGui import QFont
2020
from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout,
2121
QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow,
2222
QPushButton, QScrollArea, QSizePolicy,
2323
QStatusBar, QWidget, QSplitter, QCheckBox,
24-
QSpacerItem)
24+
QSpacerItem, QCompleter)
2525
from src.qt.pagination import Pagination
2626
from src.qt.widgets.landing import LandingWidget
2727

@@ -167,6 +167,11 @@ def setupUi(self, MainWindow):
167167
font2.setBold(False)
168168
self.searchField.setFont(font2)
169169

170+
self.searchFieldCompletionList = QStringListModel()
171+
self.searchFieldCompleter = QCompleter(self.searchFieldCompletionList, self.searchField)
172+
self.searchFieldCompleter.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
173+
self.searchField.setCompleter(self.searchFieldCompleter)
174+
170175
self.horizontalLayout_2.addWidget(self.searchField)
171176

172177
self.searchButton = QPushButton(self.centralwidget)

tagstudio/src/qt/ts_qt.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import dataclasses
1212
import math
1313
import os
14+
import re
1415
import sys
1516
import time
1617
import webbrowser
@@ -72,6 +73,7 @@
7273
)
7374
from src.core.library.alchemy.fields import _FieldID
7475
from src.core.library.alchemy.library import LibraryStatus
76+
from src.core.media_types import MediaCategories
7577
from src.core.ts_core import TagStudioCore
7678
from src.core.utils.refresh_dir import RefreshDirTracker
7779
from src.core.utils.web import strip_web_protocol
@@ -445,6 +447,8 @@ def create_folders_tags_modal():
445447
menu_bar.addMenu(window_menu)
446448
menu_bar.addMenu(help_menu)
447449

450+
self.main_window.searchField.textChanged.connect(self.update_completions_list)
451+
448452
self.preview_panel = PreviewPanel(self.lib, self)
449453
splitter = self.main_window.splitter
450454
splitter.addWidget(self.preview_panel)
@@ -949,6 +953,61 @@ def select_item(self, grid_index: int, append: bool, bridge: bool):
949953
def set_macro_menu_viability(self):
950954
self.autofill_action.setDisabled(not self.selected)
951955

956+
def update_completions_list(self, text: str) -> None:
957+
matches = re.search(r"(mediatype|filetype|path|tag):(\"?[A-Za-z0-9\ \t]+\"?)?", text)
958+
959+
completion_list: list[str] = []
960+
if len(text) < 3:
961+
completion_list = ["mediatype:", "filetype:", "path:", "tag:"]
962+
self.main_window.searchFieldCompletionList.setStringList(completion_list)
963+
964+
if not matches:
965+
return
966+
967+
query_type: str
968+
query_value: str | None
969+
query_type, query_value = matches.groups()
970+
971+
if not query_value:
972+
return
973+
974+
if query_type == "tag":
975+
completion_list = list(map(lambda x: "tag:" + x.name, self.lib.tags))
976+
elif query_type == "path":
977+
completion_list = list(map(lambda x: "path:" + x, self.lib.get_paths()))
978+
elif query_type == "mediatype":
979+
single_word_completions = map(
980+
lambda x: "mediatype:" + x.name,
981+
filter(lambda y: " " not in y.name, MediaCategories.ALL_CATEGORIES),
982+
)
983+
single_word_completions_quoted = map(
984+
lambda x: 'mediatype:"' + x.name + '"',
985+
filter(lambda y: " " not in y.name, MediaCategories.ALL_CATEGORIES),
986+
)
987+
multi_word_completions = map(
988+
lambda x: 'mediatype:"' + x.name + '"',
989+
filter(lambda y: " " in y.name, MediaCategories.ALL_CATEGORIES),
990+
)
991+
992+
all_completions = [
993+
single_word_completions,
994+
single_word_completions_quoted,
995+
multi_word_completions,
996+
]
997+
completion_list = [j for i in all_completions for j in i]
998+
elif query_type == "filetype":
999+
extensions_list: set[str] = set()
1000+
for media_cat in MediaCategories.ALL_CATEGORIES:
1001+
extensions_list = extensions_list | media_cat.extensions
1002+
completion_list = list(map(lambda x: "filetype:" + x.replace(".", ""), extensions_list))
1003+
1004+
update_completion_list: bool = (
1005+
completion_list != self.main_window.searchFieldCompletionList.stringList()
1006+
or self.main_window.searchFieldCompletionList == []
1007+
)
1008+
if update_completion_list:
1009+
self.main_window.searchFieldCompletionList.setStringList(completion_list)
1010+
9521011
def update_thumbs(self):
9531012
"""Update search thumbnails."""
9541013
# start_time = time.time()

0 commit comments

Comments
 (0)