diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index ee7f3ca0..980ddd2d 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -4,7 +4,7 @@ from pathlib import Path import structlog -from tagstudio.core.query_lang.ast import AST, Constraint, ConstraintType +from tagstudio.core.query_lang.ast import AST from tagstudio.core.query_lang.parser import Parser MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140 @@ -72,59 +72,57 @@ class SortingModeEnum(enum.Enum): @dataclass -class FilterState: +class BrowsingState: """Represent a state of the Library grid view.""" - # these should remain - page_size: int page_index: int = 0 sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED ascending: bool = True - # these should be erased on update + query: str | None = None + # Abstract Syntax Tree Of the current Search Query - ast: AST | None = None - @property - def limit(self): - return self.page_size - - @property - def offset(self): - return self.page_size * self.page_index + def ast(self) -> AST | None: + if self.query is None: + return None + return Parser(self.query).parse() @classmethod - def show_all(cls, page_size: int) -> "FilterState": - return FilterState(page_size=page_size) + def show_all(cls) -> "BrowsingState": + return BrowsingState() @classmethod - def from_search_query(cls, search_query: str, page_size: int) -> "FilterState": - return cls(ast=Parser(search_query).parse(), page_size=page_size) + def from_search_query(cls, search_query: str) -> "BrowsingState": + return cls(query=search_query) @classmethod - def from_tag_id(cls, tag_id: int | str, page_size: int) -> "FilterState": - return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []), page_size=page_size) + def from_tag_id(cls, tag_id: int | str) -> "BrowsingState": + return cls(query=f"tag_id:{str(tag_id)}") @classmethod - def from_path(cls, path: Path | str, page_size: int) -> "FilterState": - return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []), page_size=page_size) + def from_path(cls, path: Path | str) -> "BrowsingState": + return cls(query=f'path:"{str(path).strip()}"') @classmethod - def from_mediatype(cls, mediatype: str, page_size: int) -> "FilterState": - return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []), page_size=page_size) + def from_mediatype(cls, mediatype: str) -> "BrowsingState": + return cls(query=f"mediatype:{mediatype}") @classmethod - def from_filetype(cls, filetype: str, page_size: int) -> "FilterState": - return cls(ast=Constraint(ConstraintType.FileType, filetype, []), page_size=page_size) + def from_filetype(cls, filetype: str) -> "BrowsingState": + return cls(query=f"filetype:{filetype}") @classmethod - def from_tag_name(cls, tag_name: str, page_size: int) -> "FilterState": - return cls(ast=Constraint(ConstraintType.Tag, tag_name, []), page_size=page_size) + def from_tag_name(cls, tag_name: str) -> "BrowsingState": + return cls(query=f'tag:"{tag_name}"') - def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState": + def with_page_index(self, index: int) -> "BrowsingState": + return replace(self, page_index=index) + + def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState": return replace(self, sorting_mode=mode) - def with_sorting_direction(self, ascending: bool) -> "FilterState": + def with_sorting_direction(self, ascending: bool) -> "BrowsingState": return replace(self, ascending=ascending) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 81e259d4..c01c20b7 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -61,8 +61,8 @@ from tagstudio.core.library.alchemy import default_color_groups from tagstudio.core.library.alchemy.db import make_tables from tagstudio.core.library.alchemy.enums import ( MAX_SQL_VARIABLES, + BrowsingState, FieldTypeEnum, - FilterState, SortingModeEnum, ) from tagstudio.core.library.alchemy.fields import ( @@ -857,13 +857,14 @@ class Library: def search_library( self, - search: FilterState, + search: BrowsingState, + page_size: int, ) -> SearchResult: """Filter library by search query. :return: number of entries matching the query and one page of results. """ - assert isinstance(search, FilterState) + assert isinstance(search, BrowsingState) assert self.engine with Session(self.engine, expire_on_commit=False) as session: @@ -902,7 +903,7 @@ class Library: sort_on = func.lower(Entry.path) statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on)) - statement = statement.limit(search.limit).offset(search.offset) + statement = statement.limit(page_size).offset(search.page_index * page_size) logger.info( "searching library", diff --git a/src/tagstudio/core/utils/dupe_files.py b/src/tagstudio/core/utils/dupe_files.py index 4ffc08d2..eb5d6a4b 100644 --- a/src/tagstudio/core/utils/dupe_files.py +++ b/src/tagstudio/core/utils/dupe_files.py @@ -4,7 +4,7 @@ from pathlib import Path import structlog -from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry @@ -52,7 +52,7 @@ class DupeRegistry: continue results = self.library.search_library( - FilterState.from_path(path_relative, page_size=500), + BrowsingState.from_path(path_relative), 500 ) if not results: diff --git a/src/tagstudio/qt/modals/fix_unlinked.py b/src/tagstudio/qt/modals/fix_unlinked.py index 23b7c4d2..89eced5f 100644 --- a/src/tagstudio/qt/modals/fix_unlinked.py +++ b/src/tagstudio/qt/modals/fix_unlinked.py @@ -63,7 +63,7 @@ class FixUnlinkedEntriesModal(QWidget): self.relink_class.done.connect( # refresh the grid lambda: ( - self.driver.filter_items(), + self.driver.update_browsing_state(), self.refresh_missing_files(), ) ) @@ -78,7 +78,7 @@ class FixUnlinkedEntriesModal(QWidget): lambda: ( self.set_missing_count(), # refresh the grid - self.driver.filter_items(), + self.driver.update_browsing_state(), ) ) self.delete_button.clicked.connect(self.delete_modal.show) diff --git a/src/tagstudio/qt/modals/tag_search.py b/src/tagstudio/qt/modals/tag_search.py index 721b55ce..6b2ed4d7 100644 --- a/src/tagstudio/qt/modals/tag_search.py +++ b/src/tagstudio/qt/modals/tag_search.py @@ -24,7 +24,7 @@ from PySide6.QtWidgets import ( ) from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START -from tagstudio.core.library.alchemy.enums import FilterState, TagColorEnum +from tagstudio.core.library.alchemy.enums import BrowsingState, TagColorEnum from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Tag from tagstudio.core.palette import ColorType, get_tag_color @@ -292,9 +292,7 @@ class TagSearchPanel(PanelWidget): tag_widget.search_for_tag_action.triggered.connect( lambda checked=False, tag_id=tag.id: ( self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"), - self.driver.filter_items( - FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size) - ), + self.driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)), ) ) tag_widget.search_for_tag_action.setEnabled(True) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index a45c2eac..4ed07a33 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -10,7 +10,6 @@ import contextlib import ctypes -import dataclasses import math import os import platform @@ -21,6 +20,7 @@ from argparse import Namespace from pathlib import Path from queue import Queue from shutil import which +from typing import Generic, TypeVar from warnings import catch_warnings import structlog @@ -60,8 +60,8 @@ from tagstudio.core.driver import DriverMixin from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme from tagstudio.core.library.alchemy.enums import ( + BrowsingState, FieldTypeEnum, - FilterState, ItemType, SortingModeEnum, ) @@ -121,6 +121,10 @@ else: logger = structlog.get_logger(__name__) +def clamp(value, lower_bound, upper_bound): + return max(lower_bound, min(upper_bound, value)) + + class Consumer(QThread): MARKER_QUIT = "MARKER_QUIT" @@ -139,6 +143,38 @@ class Consumer(QThread): pass +T = TypeVar("T") + + +# Ex. User visits | A ->[B] | +# | A B ->[C]| +# | A [B]<- C | +# |[A]<- B C | Previous routes still exist +# | A ->[D] | Stack is cut from [:A] on new route +class History(Generic[T]): + __history: list[T] + __index: int = 0 + + def __init__(self, initial_value: T): + self.__history = [initial_value] + super().__init__() + + def erase_future(self) -> None: + self.__history = self.__history[: self.__index + 1] + + def push(self, value: T) -> None: + self.erase_future() + self.__history.append(value) + self.__index = len(self.__history) - 1 + + def move(self, delta: int): + self.__index = clamp(self.__index + delta, 0, len(self.__history) - 1) + + @property + def current(self) -> T: + return self.__history[self.__index] + + class QtDriver(DriverMixin, QObject): """A Qt GUI frontend driver for TagStudio.""" @@ -154,10 +190,13 @@ class QtDriver(DriverMixin, QObject): about_modal: AboutModal unlinked_modal: FixUnlinkedEntriesModal dupe_modal: FixDupeFilesModal + applied_theme: Theme lib: Library + browsing_history: History[BrowsingState] + def __init__(self, args: Namespace): super().__init__() # prevent recursive badges update when multiple items selected @@ -167,7 +206,6 @@ class QtDriver(DriverMixin, QObject): self.args = args self.frame_content: list[int] = [] # List of Entry IDs on the current page self.pages_count = 0 - self.applied_theme = None self.scrollbar_pos = 0 self.thumb_size = 128 @@ -195,7 +233,9 @@ class QtDriver(DriverMixin, QObject): "[Settings] Global Settings File does not exist creating", path=self.global_settings_path, ) - self.filter = FilterState.show_all(page_size=self.settings.page_size) + self.applied_theme = self.settings.theme + + self.__reset_navigation() if self.args.cache_file: path = Path(self.args.cache_file) @@ -237,6 +277,9 @@ class QtDriver(DriverMixin, QObject): self.add_tag_to_selected_action: QAction | None = None + def __reset_navigation(self) -> None: + self.browsing_history = History(BrowsingState.show_all()) + def init_workers(self): """Init workers for rendering thumbnails.""" if not self.thumb_threads: @@ -281,7 +324,6 @@ class QtDriver(DriverMixin, QObject): self.app.styleHints().setColorScheme( Qt.ColorScheme.Dark if self.settings.theme == Theme.DARK else Qt.ColorScheme.Light ) - self.applied_theme = self.settings.theme if ( platform.system() == "Darwin" or platform.system() == "Windows" @@ -700,7 +742,6 @@ class QtDriver(DriverMixin, QObject): ] self.item_thumbs: list[ItemThumb] = [] self.thumb_renderers: list[ThumbRenderer] = [] - self.filter = FilterState.show_all(page_size=self.settings.page_size) self.init_library_window() self.migration_modal: JsonMigrationModal = None @@ -744,12 +785,10 @@ class QtDriver(DriverMixin, QObject): # in a global dict for methods to access for different DPIs. # adj_font_size = math.floor(12 * self.main_window.devicePixelRatio()) - def _filter_items(): + def _update_browsing_state(): try: - self.filter_items( - FilterState.from_search_query( - self.main_window.searchField.text(), page_size=self.settings.page_size - ) + self.update_browsing_state( + BrowsingState.from_search_query(self.main_window.searchField.text()) .with_sorting_mode(self.sorting_mode) .with_sorting_direction(self.sorting_direction) ) @@ -758,21 +797,21 @@ class QtDriver(DriverMixin, QObject): f"{Translations['status.results.invalid_syntax']} " f'"{self.main_window.searchField.text()}"' ) - logger.error("[QtDriver] Could not filter items", error=e) + logger.error("[QtDriver] Could not update BrowsingState", error=e) # Search Button search_button: QPushButton = self.main_window.searchButton - search_button.clicked.connect(_filter_items) + search_button.clicked.connect(_update_browsing_state) # Search Field search_field: QLineEdit = self.main_window.searchField - search_field.returnPressed.connect(_filter_items) + search_field.returnPressed.connect(_update_browsing_state) # Sorting Dropdowns sort_mode_dropdown: QComboBox = self.main_window.sorting_mode_combobox for sort_mode in SortingModeEnum: sort_mode_dropdown.addItem(Translations[sort_mode.value], sort_mode) sort_mode_dropdown.setCurrentIndex( - list(SortingModeEnum).index(self.filter.sorting_mode) - ) # set according to self.filter + list(SortingModeEnum).index(self.browsing_history.current.sorting_mode) + ) # set according to navigation state sort_mode_dropdown.currentIndexChanged.connect(self.sorting_mode_callback) sort_dir_dropdown: QComboBox = self.main_window.sorting_direction_combobox @@ -794,9 +833,9 @@ class QtDriver(DriverMixin, QObject): self._init_thumb_grid() back_button: QPushButton = self.main_window.backButton - back_button.clicked.connect(lambda: self.page_move(-1)) + back_button.clicked.connect(lambda: self.navigation_callback(-1)) forward_button: QPushButton = self.main_window.forwardButton - forward_button.clicked.connect(lambda: self.page_move(1)) + forward_button.clicked.connect(lambda: self.navigation_callback(1)) # NOTE: Putting this early will result in a white non-responsive # window until everything is loaded. Consider adding a splash screen @@ -805,7 +844,7 @@ class QtDriver(DriverMixin, QObject): self.main_window.activateWindow() self.main_window.toggle_landing_page(enabled=True) - self.main_window.pagination.index.connect(lambda i: self.page_move(page_id=i)) + self.main_window.pagination.index.connect(lambda i: self.page_move(i, absolute=True)) self.splash.finish(self.main_window) @@ -825,7 +864,9 @@ class QtDriver(DriverMixin, QObject): ) self.file_extension_panel.setTitle(Translations["ignore_list.title"]) self.file_extension_panel.setWindowTitle(Translations["ignore_list.title"]) - self.file_extension_panel.saved.connect(lambda: (panel.save(), self.filter_items())) + self.file_extension_panel.saved.connect( + lambda: (panel.save(), self.update_browsing_state()) + ) self.manage_file_ext_action.triggered.connect(self.file_extension_panel.show) def show_grid_filenames(self, value: bool): @@ -871,7 +912,7 @@ class QtDriver(DriverMixin, QObject): self.main_window.searchField.setText("") scrollbar: QScrollArea = self.main_window.scrollArea scrollbar.verticalScrollBar().setValue(0) - self.filter = FilterState.show_all(page_size=self.settings.page_size) + self.__reset_navigation() self.lib.close() @@ -1059,7 +1100,7 @@ class QtDriver(DriverMixin, QObject): self.selected.clear() if deleted_count > 0: - self.filter_items() + self.update_browsing_state() self.preview_panel.update_widgets() if len(self.selected) <= 1 and deleted_count == 0: @@ -1211,7 +1252,7 @@ class QtDriver(DriverMixin, QObject): pw.hide(), pw.deleteLater(), # refresh the library only when new items are added - files_count and self.filter_items(), # type: ignore + files_count and self.update_browsing_state(), # type: ignore ) ) QThreadPool.globalInstance().start(r) @@ -1282,7 +1323,9 @@ class QtDriver(DriverMixin, QObject): def sorting_direction_callback(self): logger.info("Sorting Direction Changed", ascending=self.sorting_direction) - self.filter_items() + self.update_browsing_state( + self.browsing_history.current.with_sorting_direction(self.sorting_direction) + ) @property def sorting_mode(self) -> SortingModeEnum: @@ -1291,7 +1334,9 @@ class QtDriver(DriverMixin, QObject): def sorting_mode_callback(self): logger.info("Sorting Mode Changed", mode=self.sorting_mode) - self.filter_items() + self.update_browsing_state( + self.browsing_history.current.with_sorting_mode(self.sorting_mode) + ) def thumb_size_callback(self, index: int): """Perform actions needed when the thumbnail size selection is changed. @@ -1324,41 +1369,42 @@ class QtDriver(DriverMixin, QObject): def mouse_navigation(self, event: QMouseEvent): # print(event.button()) if event.button() == Qt.MouseButton.ForwardButton: - self.page_move(1) + self.navigation_callback(1) elif event.button() == Qt.MouseButton.BackButton: - self.page_move(-1) + self.navigation_callback(-1) - def page_move(self, delta: int = None, page_id: int = None) -> None: - """Navigate a step further into the navigation stack.""" - logger.info( - "page_move", - delta=delta, - page_id=page_id, + def page_move(self, value: int, absolute=False) -> None: + logger.info("page_move", value=value, absolute=absolute) + + if not absolute: + value += self.browsing_history.current.page_index + + self.browsing_history.push( + self.browsing_history.current.with_page_index(clamp(value, 0, self.pages_count - 1)) ) - # Ex. User visits | A ->[B] | - # | A B ->[C]| - # | A [B]<- C | - # |[A]<- B C | Previous routes still exist - # | A ->[D] | Stack is cut from [:A] on new route - - # sb: QScrollArea = self.main_window.scrollArea - # sb_pos = sb.verticalScrollBar().value() - - page_index = page_id if page_id is not None else self.filter.page_index + delta - page_index = max(0, min(page_index, self.pages_count - 1)) - - self.filter.page_index = page_index # TODO: Re-allow selecting entries across multiple pages at once. # This works fine with additive selection but becomes a nightmare with bridging. - self.filter_items() + + self.update_browsing_state() + + def navigation_callback(self, delta: int) -> None: + """Callback for the Forwads and Backwards Navigation Buttons next to the search bar.""" + logger.info( + "navigation_callback", + delta=delta, + ) + + self.browsing_history.move(delta) + + self.update_browsing_state() def remove_grid_item(self, grid_idx: int): self.frame_content[grid_idx] = None self.item_thumbs[grid_idx].hide() def _update_thumb_count(self): - missing_count = max(0, self.filter.page_size - len(self.item_thumbs)) + missing_count = max(0, self.settings.page_size - len(self.item_thumbs)) layout = self.flow_container.layout() for _ in range(missing_count): item_thumb = ItemThumb( @@ -1742,17 +1788,17 @@ class QtDriver(DriverMixin, QObject): pending_entries.get(badge_type, []), BADGE_TAGS[badge_type] ) - def filter_items(self, filter: FilterState | None = None) -> None: + def update_browsing_state(self, state: BrowsingState | None = None) -> None: + """Navigates to a new BrowsingState when state is given, otherwise updates the results.""" if not self.lib.library_dir: logger.info("Library not loaded") return assert self.lib.engine - if filter: - self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter)) - else: - self.filter.sorting_mode = self.sorting_mode - self.filter.ascending = self.sorting_direction + if state: + self.browsing_history.push(state) + + self.main_window.searchField.setText(self.browsing_history.current.query or "") # inform user about running search self.main_window.statusbar.showMessage(Translations["status.library_search_query"]) @@ -1760,7 +1806,7 @@ class QtDriver(DriverMixin, QObject): # search the library start_time = time.time() - results = self.lib.search_library(self.filter) + results = self.lib.search_library(self.browsing_history.current, self.settings.page_size) logger.info("items to render", count=len(results)) end_time = time.time() @@ -1778,9 +1824,9 @@ class QtDriver(DriverMixin, QObject): self.update_thumbs() # update pagination - self.pages_count = math.ceil(results.total_count / self.filter.page_size) + self.pages_count = math.ceil(results.total_count / self.settings.page_size) self.main_window.pagination.update_buttons( - self.pages_count, self.filter.page_index, emit=False + self.pages_count, self.browsing_history.current.page_index, emit=False ) def remove_recent_library(self, item_key: str): @@ -1915,14 +1961,14 @@ class QtDriver(DriverMixin, QObject): if open_status.json_migration_req: self.migration_modal = JsonMigrationModal(path) self.migration_modal.migration_finished.connect( - lambda: self.init_library(path, self.lib.open_library(path)) + lambda: self._init_library(path, self.lib.open_library(path)) ) self.main_window.landing_widget.set_status_label("") self.migration_modal.paged_panel.show() else: - self.init_library(path, open_status) + self._init_library(path, open_status) - def init_library(self, path: Path, open_status: LibraryStatus): + def _init_library(self, path: Path, open_status: LibraryStatus): if not open_status.success: self.show_error_message( error_name=open_status.message @@ -1933,7 +1979,7 @@ class QtDriver(DriverMixin, QObject): self.init_workers() - self.filter.page_size = self.settings.page_size + self.__reset_navigation() # TODO - make this call optional if self.lib.entries_count < 10000: @@ -1973,7 +2019,7 @@ class QtDriver(DriverMixin, QObject): self.preview_panel.update_widgets() # page (re)rendering, extract eventually - self.filter_items() + self.update_browsing_state() self.main_window.toggle_landing_page(enabled=False) return open_status diff --git a/src/tagstudio/qt/widgets/tag_box.py b/src/tagstudio/qt/widgets/tag_box.py index 68ac0fc2..dbeea052 100644 --- a/src/tagstudio/qt/widgets/tag_box.py +++ b/src/tagstudio/qt/widgets/tag_box.py @@ -8,7 +8,7 @@ import typing import structlog from PySide6.QtCore import Signal -from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.models import Tag from tagstudio.qt.flowlayout import FlowLayout from tagstudio.qt.modals.build_tag import BuildTagPanel @@ -67,9 +67,7 @@ class TagBoxWidget(FieldWidget): tag_widget.search_for_tag_action.triggered.connect( lambda checked=False, tag_id=tag.id: ( self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"), - self.driver.filter_items( - FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size) - ), + self.driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)), ) ) diff --git a/tests/macros/test_missing_files.py b/tests/macros/test_missing_files.py index bfbe0667..ba795db1 100644 --- a/tests/macros/test_missing_files.py +++ b/tests/macros/test_missing_files.py @@ -3,7 +3,7 @@ from tempfile import TemporaryDirectory import pytest -from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.library import Library from tagstudio.core.utils.missing_files import MissingRegistry @@ -28,5 +28,5 @@ def test_refresh_missing_files(library: Library): assert list(registry.fix_unlinked_entries()) == [0, 1] # `bar.md` should be relinked to new correct path - results = library.search_library(FilterState.from_path("bar.md", page_size=500)) + results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500) assert results[0].path == Path("bar.md") diff --git a/tests/qt/test_file_path_options.py b/tests/qt/test_file_path_options.py index be78224f..c903b7f6 100644 --- a/tests/qt/test_file_path_options.py +++ b/tests/qt/test_file_path_options.py @@ -135,7 +135,7 @@ def test_title_update( qt_driver.folders_to_tags_action = QAction(menu_bar) # Trigger the update - qt_driver.init_library(library_dir, open_status) + qt_driver._init_library(library_dir, open_status) # Assert the title is updated correctly qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(library_dir, base_title)) diff --git a/tests/qt/test_qt_driver.py b/tests/qt/test_qt_driver.py index 6081450b..2dcc363a 100644 --- a/tests/qt/test_qt_driver.py +++ b/tests/qt/test_qt_driver.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.json.library import ItemType from tagstudio.qt.widgets.item_thumb import ItemThumb @@ -66,7 +66,7 @@ if TYPE_CHECKING: # assert qt_driver.selected == [0, 1, 2] -def test_library_state_update(qt_driver: "QtDriver"): +def test_browsing_state_update(qt_driver: "QtDriver"): # Given for entry in qt_driver.lib.get_entries(with_joins=True): thumb = ItemThumb(ItemType.ENTRY, qt_driver.lib, qt_driver, (100, 100)) @@ -74,27 +74,25 @@ def test_library_state_update(qt_driver: "QtDriver"): qt_driver.frame_content.append(entry) # no filter, both items are returned - qt_driver.filter_items() + qt_driver.update_browsing_state() assert len(qt_driver.frame_content) == 2 # filter by tag - state = FilterState.from_tag_name("foo", page_size=10) - qt_driver.filter_items(state) - assert qt_driver.filter.page_size == 10 + state = BrowsingState.from_tag_name("foo") + qt_driver.update_browsing_state(state) assert len(qt_driver.frame_content) == 1 entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0]) assert list(entry.tags)[0].name == "foo" # When state is not changed, previous one is still applied - qt_driver.filter_items() - assert qt_driver.filter.page_size == 10 + qt_driver.update_browsing_state() assert len(qt_driver.frame_content) == 1 entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0]) assert list(entry.tags)[0].name == "foo" # When state property is changed, previous one is overwritten - state = FilterState.from_path("*bar.md", page_size=qt_driver.settings.page_size) - qt_driver.filter_items(state) + state = BrowsingState.from_path("*bar.md") + qt_driver.update_browsing_state(state) assert len(qt_driver.frame_content) == 1 entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0]) assert list(entry.tags)[0].name == "bar" diff --git a/tests/test_library.py b/tests/test_library.py index 498db3d0..f609a4ec 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -4,7 +4,7 @@ from tempfile import TemporaryDirectory import pytest from tagstudio.core.enums import DefaultEnum, LibraryPrefs -from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.fields import TextField, _FieldID from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry, Tag @@ -123,7 +123,8 @@ def test_library_search(library: Library, generate_tag, entry_full): tag = list(entry_full.tags)[0] results = library.search_library( - FilterState.from_tag_name(tag.name, page_size=500), + BrowsingState.from_tag_name(tag.name), + page_size=500, ) assert results.total_count == 1 @@ -152,7 +153,7 @@ def test_entries_count(library: Library): new_ids = library.add_entries(entries) assert len(new_ids) == 10 - results = library.search_library(FilterState.show_all(page_size=5)) + results = library.search_library(BrowsingState.show_all(), page_size=5) assert results.total_count == 12 assert len(results) == 5 @@ -199,9 +200,7 @@ def test_search_filter_extensions(library: Library, is_exclude: bool): library.set_prefs(LibraryPrefs.EXTENSION_LIST, ["md"]) # When - results = library.search_library( - FilterState.show_all(page_size=500), - ) + results = library.search_library(BrowsingState.show_all(), page_size=500) # Then assert results.total_count == 1 @@ -221,7 +220,8 @@ def test_search_library_case_insensitive(library: Library): # When results = library.search_library( - FilterState.from_tag_name(tag.name.upper(), page_size=500), + BrowsingState.from_tag_name(tag.name.upper()), + page_size=500, ) # Then @@ -443,100 +443,102 @@ def test_library_prefs_multiple_identical_vals(): def test_path_search_ilike(library: Library): - results = library.search_library(FilterState.from_path("bar.md", page_size=500)) + results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_like(library: Library): - results = library.search_library(FilterState.from_path("BAR.MD", page_size=500)) + results = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500) assert results.total_count == 0 assert len(results.items) == 0 def test_path_search_default_with_sep(library: Library): - results = library.search_library(FilterState.from_path("one/two", page_size=500)) + results = library.search_library(BrowsingState.from_path("one/two"), page_size=500) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_glob_after(library: Library): - results = library.search_library(FilterState.from_path("foo*", page_size=500)) + results = library.search_library(BrowsingState.from_path("foo*"), page_size=500) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_glob_in_front(library: Library): - results = library.search_library(FilterState.from_path("*bar.md", page_size=500)) + results = library.search_library(BrowsingState.from_path("*bar.md"), page_size=500) assert results.total_count == 1 assert len(results.items) == 1 def test_path_search_glob_both_sides(library: Library): - results = library.search_library(FilterState.from_path("*one/two*", page_size=500)) + results = library.search_library(BrowsingState.from_path("*one/two*"), page_size=500) assert results.total_count == 1 assert len(results.items) == 1 +# TODO: deduplicate this code with pytest parametrisation or a for loop def test_path_search_ilike_glob_equality(library: Library): - results_ilike = library.search_library(FilterState.from_path("one/two", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*one/two*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("one/two"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*one/two*"), page_size=500) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*bar*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("bar"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*bar*"), page_size=500) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None +# TODO: isn't this the exact same as the one before? def test_path_search_like_glob_equality(library: Library): - results_ilike = library.search_library(FilterState.from_path("ONE/two", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*ONE/two*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("ONE/two"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*ONE/two*"), page_size=500) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*BAR.MD*"), page_size=500) assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500) assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items] results_ilike, results_glob = None, None - results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500)) - results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500)) + results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500) + results_glob = library.search_library(BrowsingState.from_path("*BAR.MD*"), page_size=500) assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items] results_ilike, results_glob = None, None @pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)]) def test_filetype_search(library: Library, filetype, num_of_filetype): - results = library.search_library(FilterState.from_filetype(filetype, page_size=500)) + results = library.search_library(BrowsingState.from_filetype(filetype), page_size=500) assert len(results.items) == num_of_filetype @pytest.mark.parametrize(["filetype", "num_of_filetype"], [("png", 2), ("apng", 1), ("ng", 0)]) def test_filetype_return_one_filetype(file_mediatypes_library: Library, filetype, num_of_filetype): results = file_mediatypes_library.search_library( - FilterState.from_filetype(filetype, page_size=500) + BrowsingState.from_filetype(filetype), page_size=500 ) assert len(results.items) == num_of_filetype @pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)]) def test_mediatype_search(library: Library, mediatype, num_of_mediatype): - results = library.search_library(FilterState.from_mediatype(mediatype, page_size=500)) + results = library.search_library(BrowsingState.from_mediatype(mediatype), page_size=500) assert len(results.items) == num_of_mediatype diff --git a/tests/test_search.py b/tests/test_search.py index f8828819..bdd94834 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,12 +1,12 @@ import pytest -from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.library import Library from tagstudio.core.query_lang.util import ParsingError def verify_count(lib: Library, query: str, count: int): - results = lib.search_library(FilterState.from_search_query(query, page_size=500)) + results = lib.search_library(BrowsingState.from_search_query(query), page_size=500) assert results.total_count == count assert len(results.items) == count @@ -136,4 +136,4 @@ def test_parent_tags(search_library: Library, query: str, count: int): ) def test_syntax(search_library: Library, invalid_query: str): with pytest.raises(ParsingError) as e_info: # noqa: F841 - search_library.search_library(FilterState.from_search_query(invalid_query, page_size=500)) + search_library.search_library(BrowsingState.from_search_query(invalid_query), page_size=500)