Skip to content

Commit

Permalink
feat: add autocomplete for search engine (#586)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
python357-1 authored Nov 18, 2024
1 parent 9078fee commit bec513f
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 2 deletions.
7 changes: 7 additions & 0 deletions tagstudio/src/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ def has_path_entry(self, path: Path) -> bool:
with Session(self.engine) as session:
return session.query(exists().where(Entry.path == path)).scalar()

def get_paths(self, glob: str | None = None) -> list[str]:
with Session(self.engine) as session:
paths = session.scalars(select(Entry.path)).unique()

path_strings: list[str] = list(map(lambda x: x.as_posix(), paths))
return path_strings

def search_library(
self,
search: FilterState,
Expand Down
9 changes: 7 additions & 2 deletions tagstudio/src/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@

import logging
import typing
from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt)
from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt, QStringListModel)
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout,
QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow,
QPushButton, QScrollArea, QSizePolicy,
QStatusBar, QWidget, QSplitter, QCheckBox,
QSpacerItem)
QSpacerItem, QCompleter)
from src.qt.pagination import Pagination
from src.qt.widgets.landing import LandingWidget

Expand Down Expand Up @@ -167,6 +167,11 @@ def setupUi(self, MainWindow):
font2.setBold(False)
self.searchField.setFont(font2)

self.searchFieldCompletionList = QStringListModel()
self.searchFieldCompleter = QCompleter(self.searchFieldCompletionList, self.searchField)
self.searchFieldCompleter.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.searchField.setCompleter(self.searchFieldCompleter)

self.horizontalLayout_2.addWidget(self.searchField)

self.searchButton = QPushButton(self.centralwidget)
Expand Down
59 changes: 59 additions & 0 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import dataclasses
import math
import os
import re
import sys
import time
import webbrowser
Expand Down Expand Up @@ -72,6 +73,7 @@
)
from src.core.library.alchemy.fields import _FieldID
from src.core.library.alchemy.library import LibraryStatus
from src.core.media_types import MediaCategories
from src.core.ts_core import TagStudioCore
from src.core.utils.refresh_dir import RefreshDirTracker
from src.core.utils.web import strip_web_protocol
Expand Down Expand Up @@ -445,6 +447,8 @@ def create_folders_tags_modal():
menu_bar.addMenu(window_menu)
menu_bar.addMenu(help_menu)

self.main_window.searchField.textChanged.connect(self.update_completions_list)

self.preview_panel = PreviewPanel(self.lib, self)
splitter = self.main_window.splitter
splitter.addWidget(self.preview_panel)
Expand Down Expand Up @@ -949,6 +953,61 @@ def select_item(self, grid_index: int, append: bool, bridge: bool):
def set_macro_menu_viability(self):
self.autofill_action.setDisabled(not self.selected)

def update_completions_list(self, text: str) -> None:
matches = re.search(r"(mediatype|filetype|path|tag):(\"?[A-Za-z0-9\ \t]+\"?)?", text)

completion_list: list[str] = []
if len(text) < 3:
completion_list = ["mediatype:", "filetype:", "path:", "tag:"]
self.main_window.searchFieldCompletionList.setStringList(completion_list)

if not matches:
return

query_type: str
query_value: str | None
query_type, query_value = matches.groups()

if not query_value:
return

if query_type == "tag":
completion_list = list(map(lambda x: "tag:" + x.name, self.lib.tags))
elif query_type == "path":
completion_list = list(map(lambda x: "path:" + x, self.lib.get_paths()))
elif query_type == "mediatype":
single_word_completions = map(
lambda x: "mediatype:" + x.name,
filter(lambda y: " " not in y.name, MediaCategories.ALL_CATEGORIES),
)
single_word_completions_quoted = map(
lambda x: 'mediatype:"' + x.name + '"',
filter(lambda y: " " not in y.name, MediaCategories.ALL_CATEGORIES),
)
multi_word_completions = map(
lambda x: 'mediatype:"' + x.name + '"',
filter(lambda y: " " in y.name, MediaCategories.ALL_CATEGORIES),
)

all_completions = [
single_word_completions,
single_word_completions_quoted,
multi_word_completions,
]
completion_list = [j for i in all_completions for j in i]
elif query_type == "filetype":
extensions_list: set[str] = set()
for media_cat in MediaCategories.ALL_CATEGORIES:
extensions_list = extensions_list | media_cat.extensions
completion_list = list(map(lambda x: "filetype:" + x.replace(".", ""), extensions_list))

update_completion_list: bool = (
completion_list != self.main_window.searchFieldCompletionList.stringList()
or self.main_window.searchFieldCompletionList == []
)
if update_completion_list:
self.main_window.searchFieldCompletionList.setStringList(completion_list)

def update_thumbs(self):
"""Update search thumbnails."""
# start_time = time.time()
Expand Down

0 comments on commit bec513f

Please sign in to comment.