From 30e0a31f77d3e2e679cdb20b303370cda10d5b23 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Fri, 12 Dec 2025 14:47:09 -0500 Subject: [PATCH 1/4] Add syntax cheatsheet modal and markdown widget --- src/tagstudio/qt/ts_qt.py | 11 ++++ src/tagstudio/qt/views/main_window.py | 4 ++ .../qt/views/markdown_widget_view.py | 32 ++++++++++ src/tagstudio/qt/views/search_syntax_modal.py | 50 +++++++++++++++ .../markdown/en/search_syntax_cheatsheet.md | 62 +++++++++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 src/tagstudio/qt/views/markdown_widget_view.py create mode 100644 src/tagstudio/qt/views/search_syntax_modal.py create mode 100644 src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 34dec16fa..fe1e05253 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -99,6 +99,7 @@ from tagstudio.qt.utils.function_iterator import FunctionIterator from tagstudio.qt.views.main_window import MainWindow from tagstudio.qt.views.panel_modal import PanelModal +from tagstudio.qt.views.search_syntax_modal import SearchSyntaxModal from tagstudio.qt.views.splash import SplashScreen BADGE_TAGS = { @@ -186,6 +187,7 @@ class QtDriver(DriverMixin, QObject): add_tag_modal: PanelModal | None = None folders_modal: FoldersToTagsModal about_modal: AboutModal + search_syntax_modal: SearchSyntaxModal unlinked_modal: FixUnlinkedEntriesModal ignored_modal: FixIgnoredEntriesModal dupe_modal: FixDupeFilesModal @@ -557,6 +559,15 @@ def create_about_modal(): self.main_window.menu_bar.about_action.triggered.connect(create_about_modal) + def create_search_syntax_cheatsheet_modal(): + if not hasattr(self, "search_syntax_modal"): + self.search_syntax_modal = SearchSyntaxModal(self) + self.search_syntax_modal.show() + + self.main_window.menu_bar.search_syntax_action.triggered.connect( + create_search_syntax_cheatsheet_modal + ) + # endregion # endregion diff --git a/src/tagstudio/qt/views/main_window.py b/src/tagstudio/qt/views/main_window.py index a4c0485a5..238571d59 100644 --- a/src/tagstudio/qt/views/main_window.py +++ b/src/tagstudio/qt/views/main_window.py @@ -94,6 +94,7 @@ class MainMenuBar(QMenuBar): help_menu: QMenu about_action: QAction + search_syntax_action: QAction def __init__(self, parent: QWidget | None = None): super().__init__(parent) @@ -398,6 +399,9 @@ def setup_help_menu(self): self.about_action = QAction(Translations["menu.help.about"], self) self.help_menu.addAction(self.about_action) + self.search_syntax_action = QAction(Translations["menu.help.search_syntax"], self) + self.help_menu.addAction(self.search_syntax_action) + assign_mnemonics(self.help_menu) self.addMenu(self.help_menu) diff --git a/src/tagstudio/qt/views/markdown_widget_view.py b/src/tagstudio/qt/views/markdown_widget_view.py new file mode 100644 index 000000000..e6eddbcbf --- /dev/null +++ b/src/tagstudio/qt/views/markdown_widget_view.py @@ -0,0 +1,32 @@ +from pathlib import Path +from typing import TYPE_CHECKING + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QLabel + +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + + +MARKDOWN_RESOURCE_PATH = Path(Path(__file__).parents[2] / "resources" / "markdown") +FALLBACK_LANGUAGE: str = "en" + + +class MarkdownWidgetView(QLabel): + def __init__(self, driver: "QtDriver"): + super().__init__() + self.__driver = driver + + self.setTextFormat(Qt.TextFormat.MarkdownText) + + def set_file(self, file_path: Path): + current_language: str = self.__driver.settings.language + + try: + with open(Path(MARKDOWN_RESOURCE_PATH / current_language / file_path)) as markdown_file: + self.setText(markdown_file.read()) + except FileNotFoundError: + with open( + Path(MARKDOWN_RESOURCE_PATH / FALLBACK_LANGUAGE / file_path) + ) as markdown_file: + self.setText(markdown_file.read()) diff --git a/src/tagstudio/qt/views/search_syntax_modal.py b/src/tagstudio/qt/views/search_syntax_modal.py new file mode 100644 index 000000000..f9d35243a --- /dev/null +++ b/src/tagstudio/qt/views/search_syntax_modal.py @@ -0,0 +1,50 @@ +from pathlib import Path +from typing import TYPE_CHECKING + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QFrame, QHBoxLayout, QPushButton, QScrollArea, QVBoxLayout, QWidget + +from tagstudio.qt.translations import Translations +from tagstudio.qt.views.markdown_widget_view import MarkdownWidgetView + +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + + +class SearchSyntaxModal(QWidget): + def __init__(self, driver: "QtDriver"): + super().__init__() + + # Modal + self.setWindowTitle(Translations["search_syntax.title"]) + + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.setMinimumSize(360, 540) + self.__root_layout = QVBoxLayout(self) + self.__root_layout.setSpacing(0) + self.__root_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignCenter) + + # Scroll + self.__scroll_area = QScrollArea() + self.__scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.__scroll_area.setWidgetResizable(True) + self.__scroll_area.setFrameShadow(QFrame.Shadow.Plain) + self.__scroll_area.setFrameShape(QFrame.Shape.NoFrame) + self.__root_layout.addWidget(self.__scroll_area) + + # Content + self.__content: MarkdownWidgetView = MarkdownWidgetView(driver) + self.__content.set_file(Path("search_syntax_cheatsheet.md")) + self.__content.setWordWrap(True) + self.__scroll_area.setWidget(self.__content) + + # Close button + self.__buttons_row = QWidget() + self.__buttons_row_layout = QHBoxLayout(self.__buttons_row) + self.__buttons_row_layout.setContentsMargins(12, 12, 12, 12) + self.__buttons_row_layout.addStretch(1) + self.__root_layout.addWidget(self.__buttons_row) + + self.__close_button = QPushButton(Translations["generic.close"]) + self.__close_button.clicked.connect(lambda: self.close()) + self.__buttons_row_layout.addWidget(self.__close_button) diff --git a/src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md b/src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md new file mode 100644 index 000000000..e687a6a34 --- /dev/null +++ b/src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md @@ -0,0 +1,62 @@ +# Search Syntax +For a more detailed explanation of search syntax, check [the documentation](https://docs.tagstud.io/search/). + +## Boolean Operators + +Search for entries that have Tag1 **AND** Tag2 +- Tag1 Tag2 +- Tag1 `AND` Tag2 + +Search for entries that have Tag1 **OR** Tag2 +- Tag1 `OR` Tag2 + +Search for entries that **don't** have Tag1 +- `NOT` Tag1 + +Searches can be grouped and nested by using parentheses to surround parts of your search query. +- (Tag1 `OR` Tag2) `AND` Tag3 + +## Escaping Characters + +To escape most search terms, surround the section of your search in plain quotes or replace spaces in tag names with underscores. +- "Tag Name With Spaces" +- Tag_Name_With_Spaces + +## Tags + +Search for entries that have the tag Tag1 +- Tag1 +- tag: Tag1 + +Search for entries that have the tag with ID 1 +- tag_id: 1 + +## Path + +Search for a file named `file.jpg` +- path: file.jpg + +Search for any `.jpg` file +- path: *.jpg + +Search for any file that ends in a number +- path: *2.* + +Search for a file located at `folder/file.jpg` +- path: folder/file.jpg + +Search for any file inside `folder/` +- path: folder/* + +## File/Media Type + +Search for videos +- mediatype: video + +Search for `.mp4` files +- filetype: mp4 + +## Special Searches + +Search for entries that don't contain any tags +- special: untagged \ No newline at end of file From 28c8b595657c2a5c8281f665a312b4e3821a9c7e Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Fri, 12 Dec 2025 16:40:22 -0500 Subject: [PATCH 2/4] Tweak modal size --- src/tagstudio/qt/views/search_syntax_modal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagstudio/qt/views/search_syntax_modal.py b/src/tagstudio/qt/views/search_syntax_modal.py index f9d35243a..c9aac03b2 100644 --- a/src/tagstudio/qt/views/search_syntax_modal.py +++ b/src/tagstudio/qt/views/search_syntax_modal.py @@ -19,7 +19,7 @@ def __init__(self, driver: "QtDriver"): self.setWindowTitle(Translations["search_syntax.title"]) self.setWindowModality(Qt.WindowModality.ApplicationModal) - self.setMinimumSize(360, 540) + self.setMinimumSize(320, 720) self.__root_layout = QVBoxLayout(self) self.__root_layout.setSpacing(0) self.__root_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignCenter) From 9da2091850f19da77fce321c0eead500afe7ec78 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Fri, 12 Dec 2025 16:44:20 -0500 Subject: [PATCH 3/4] Add translations --- src/tagstudio/resources/translations/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 8b84af737..0e5a8d3c4 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -228,6 +228,7 @@ "menu.file.save_library": "Save Library", "menu.file": "&File", "menu.help.about": "About", + "menu.help.search_syntax": "Search Syntax Cheatsheet", "menu.help": "&Help", "menu.macros.folders_to_tags": "Folders to Tags", "menu.macros": "&Macros", @@ -251,6 +252,7 @@ "preview.multiple_selection": "{count} Items Selected", "preview.no_selection": "No Items Selected", "preview.unlinked": "Unlinked", + "search_syntax.title": "Search Syntax Cheatsheet", "select.add_tag_to_selected": "Add Tag to Selected", "select.all": "Select All", "select.clear": "Clear Selection", From e8fe92a98478627210b9a0b204355b31ac4d2908 Mon Sep 17 00:00:00 2001 From: TrigamDev Date: Fri, 12 Dec 2025 17:31:40 -0500 Subject: [PATCH 4/4] Escape asterisks --- src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md b/src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md index e687a6a34..3fb4ab419 100644 --- a/src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md +++ b/src/tagstudio/resources/markdown/en/search_syntax_cheatsheet.md @@ -40,7 +40,7 @@ Search for any `.jpg` file - path: *.jpg Search for any file that ends in a number -- path: *2.* +- path: \*2.\* Search for a file located at `folder/file.jpg` - path: folder/file.jpg