Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ packages = ["src/tagstudio"]
[tool.pytest.ini_options]
#addopts = "-m 'not qt'"
qt_api = "pyside6"
pythonpath = ["src"]

[tool.pyright]
ignore = [
Expand Down
66 changes: 55 additions & 11 deletions src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -1251,32 +1251,33 @@ def search_tags(self, name: str | None, limit: int = 100) -> tuple[list[Tag], li
if limit <= 0:
limit = sys.maxsize

name = name or ""
name = name.lower()
search_query: str = name.lower() if name else ""

def sort_key(text: str):
priority = text.startswith(name)
priority = text.startswith(search_query)
p_ordering = len(text) if priority else sys.maxsize
return (not priority, p_ordering, text)
return not priority, p_ordering, text

with Session(self.engine) as session:
query = select(Tag.id, Tag.name)

if limit > 0 and not name:
if limit > 0 and not search_query:
query = query.order_by(Tag.name).limit(limit)

if name:
if search_query:
query = query.where(
or_(
Tag.name.icontains(name),
Tag.shorthand.icontains(name),
Tag.name.icontains(search_query),
Tag.shorthand.icontains(search_query),
)
)

tags = list(session.execute(query))

if name:
query = select(TagAlias.tag_id, TagAlias.name).where(TagAlias.name.icontains(name))
if search_query:
query = select(TagAlias.tag_id, TagAlias.name).where(
TagAlias.name.icontains(search_query)
)
tags.extend(session.execute(query))

tags.sort(key=lambda t: sort_key(t[1]))
Expand All @@ -1286,7 +1287,7 @@ def sort_key(text: str):

logger.info(
"searching tags",
search=name,
search=search_query,
limit=limit,
statement=str(query),
results=len(tag_ids),
Expand All @@ -1312,6 +1313,49 @@ def sort_key(text: str):

return direct_tags, descendant_tags

def search_field_templates(self, name: str | None, limit: int = 100) -> list[BaseFieldTemplate]:
"""Return field template rows matching the query, detached from the session."""
if limit <= 0:
limit = sys.maxsize

search_query: str = name.lower() if name else ""

def sort_key(template: BaseFieldTemplate) -> tuple:
text = template.name.lower()
if not search_query:
return (text,)
priority = text.startswith(search_query)
p_ordering = len(text) if priority else sys.maxsize
return (not priority, p_ordering, text)

with Session(self.engine) as session:
text_stmt = select(TextFieldTemplate)
datetime_stmt = select(DatetimeFieldTemplate)
if search_query:
text_stmt = text_stmt.where(TextFieldTemplate.name.icontains(search_query))
datetime_stmt = datetime_stmt.where(
DatetimeFieldTemplate.name.icontains(search_query)
)

field_templates: list[BaseFieldTemplate] = [
*session.scalars(text_stmt),
*session.scalars(datetime_stmt),
]
field_templates.sort(key=sort_key)
field_templates = field_templates[:limit]

for ft in field_templates:
session.expunge(ft)
make_transient(ft)

logger.info(
"Searching field templates",
search=search_query,
limit=limit,
results=len(field_templates),
)
return field_templates

def update_entry_path(self, entry_id: int | Entry, path: Path) -> bool:
"""Set the path field of an entry.

Expand Down
123 changes: 123 additions & 0 deletions src/tagstudio/qt/controllers/field_template_search_panel_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only


from collections.abc import Sequence
from warnings import catch_warnings

import structlog
from PySide6.QtCore import Signal

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget

logger = structlog.get_logger(__name__)


class FieldTemplateSearchModal(PanelModal):
def __init__(
self,
library: Library,
exclude: Sequence[BaseFieldTemplate] | None = None,
is_field_template_chooser: bool = True,
done_callback=None,
save_callback=None,
has_save=False,
) -> None:
self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
library, exclude, is_field_template_chooser
)
super().__init__(
self.search_panel,
Translations["field.add.plural"],
done_callback=done_callback,
save_callback=save_callback,
has_save=has_save,
)


class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate], FieldTemplateSearchPanelView):
field_template_chosen = Signal(object)

def __init__(
self,
library: Library,
exclude: Sequence[BaseFieldTemplate] | None = None,
is_field_template_chooser: bool = True,
) -> None:
super().__init__([], is_field_template_chooser)
self.__lib = library
self._exclude_keys: frozenset[tuple[str, int]] = frozenset(
(t.__class__.__name__, t.id) for t in (exclude or ())
)

self._unlimited_limit_item_label = Translations["field_template.all_field_templates"]
self._create_and_add_button_label_key = "field_template.create_add"

def _get_max_limit(self) -> int:
return len(self.__lib.field_templates)

def _is_excluded(self, item: BaseFieldTemplate) -> bool: # type: ignore[override]
return (item.__class__.__name__, item.id) in self._exclude_keys

def _on_item_create(self) -> None:
# TODO: Allow creation of field templates
pass

def on_item_edit(self, item: BaseFieldTemplate) -> None:
# TODO: Allow creation of field templates
pass

def _on_item_remove(self, item: BaseFieldTemplate) -> None:
if self.is_chooser:
return

# TODO: Allow creation of field templates
pass

def _on_item_create_and_add(self) -> None:
# TODO: Allow creation of field templates
pass

def _on_item_chosen(self, item: BaseFieldTemplate) -> None:
self.field_template_chosen.emit(item)

def search_items(self, query: str) -> tuple[list[BaseFieldTemplate], list[BaseFieldTemplate]]:
return self.__lib.search_field_templates(name=query, limit=self._get_limit()[1]), []

def set_item_widget(self, item: BaseFieldTemplate | None, index: int) -> None:
"""Set the field template of a field template widget at a specific index."""
field_template_widget: FieldTemplateWidget = self.get_item_widget(index, self.__lib)
field_template_widget.set_field_template(item)
field_template_widget.setHidden(item is None)

if item is None:
return

# field_template_widget.has_remove = not self.is_chooser

# Disconnect previous callbacks
with catch_warnings(record=True):
# tag_widget.on_edit.disconnect()
# tag_widget.on_remove.disconnect()
field_template_widget.on_click.disconnect()

# Connect callbacks
# tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
# tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
field_template_widget.on_click.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)

def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
# TODO: Allow creation of field templates
pass

def edit_item(self, edit_item_panel: PanelWidget) -> None:
# TODO: Allow creation of field templates
pass
22 changes: 22 additions & 0 deletions src/tagstudio/qt/controllers/field_template_widget_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.views.field_template_widget_view import FieldTemplateWidgetView


class FieldTemplateWidget(FieldTemplateWidgetView):
def __init__(self) -> None:
super().__init__()

self.__field_template: BaseFieldTemplate | None = None

def set_field_template(self, field_template: BaseFieldTemplate | None) -> None:
self.__field_template = field_template

if field_template is None:
return

field_name_key: str = FIELD_TYPE_KEYS.get(field_template.class_name, "field_type.unknown")
self._bg_button.setText(f"{field_template.name} ({Translations[field_name_key]})")
33 changes: 17 additions & 16 deletions src/tagstudio/qt/controllers/preview_panel_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,48 @@
import typing
from warnings import catch_warnings

from PySide6.QtWidgets import QListWidgetItem

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.mixed.add_field import AddFieldModal
from tagstudio.qt.mixed.tag_search import TagSearchModal
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchModal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.views.preview_panel_view import PreviewPanelView

if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver


class PreviewPanel(PreviewPanelView):
def __init__(self, library: Library, driver: "QtDriver"):
def __init__(self, library: Library, driver: "QtDriver") -> None:
super().__init__(library, driver)

self.__add_field_modal = AddFieldModal(self.lib)
self.__add_field_modal = FieldTemplateSearchModal(self.lib, is_field_template_chooser=True)
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)

@typing.override
def _add_field_button_callback(self):
def _add_field_button_callback(self) -> None:
self.__add_field_modal.show()

@typing.override
def _add_tag_button_callback(self):
def _add_tag_button_callback(self) -> None:
self.__add_tag_modal.show()

@typing.override
def _set_selection_callback(self):
def _set_selection_callback(self) -> None:
with catch_warnings(record=True):
self.__add_field_modal.done.disconnect()
self.__add_tag_modal.tsp.tag_chosen.disconnect()
self.__add_field_modal.search_panel.field_template_chosen.disconnect()
self.__add_tag_modal.tsp.item_chosen.disconnect()

self.__add_field_modal.done.connect(self._add_field_to_selected)
self.__add_tag_modal.tsp.tag_chosen.connect(self._add_tag_to_selected)
self.__add_field_modal.search_panel.field_template_chosen.connect(
self._add_field_to_selected
)
self.__add_tag_modal.tsp.item_chosen.connect(self._add_tag_to_selected)

def _add_field_to_selected(self, field_list: list[QListWidgetItem]):
self._fields.add_field_to_selected(field_list)
def _add_field_to_selected(self, template: BaseFieldTemplate) -> None:
self._fields.add_field_to_selected(template)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])

def _add_tag_to_selected(self, tag_id: int):
def _add_tag_to_selected(self, tag_id: int) -> None:
self._fields.add_tags_to_selected(tag_id)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])
Loading
Loading