Skip to content
Merged
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
9 changes: 9 additions & 0 deletions docs/library-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,12 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |

- Applies repairs to the `tag_parents` table created in [version 100](#version-100), removing rows that reference tags that have been deleted.

#### Version 103

| Used From | Format | Location |
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [#1139](https://github.com/TagStudioDev/TagStudio/pull/1139) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |

- Adds the `is_hidden` column to the `tags` table (default `0`). Used for excluding entries tagged with hidden tags from library searches.
- Sets the `is_hidden` field on the built-in Archived tag to `1`, to match the Archived tag now being hidden by default.
2 changes: 1 addition & 1 deletion src/tagstudio/core/library/alchemy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
DB_VERSION_LEGACY_KEY: str = "DB_VERSION"
DB_VERSION_CURRENT_KEY: str = "CURRENT"
DB_VERSION_INITIAL_KEY: str = "INITIAL"
DB_VERSION: int = 102
DB_VERSION: int = 103

TAG_CHILDREN_QUERY = text("""
WITH RECURSIVE ChildTags AS (
Expand Down
4 changes: 2 additions & 2 deletions src/tagstudio/core/library/alchemy/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ def make_tables(engine: Engine) -> None:
conn.execute(
text(
"INSERT INTO tags "
"(id, name, color_namespace, color_slug, is_category) VALUES "
f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false)"
"(id, name, color_namespace, color_slug, is_category, is_hidden) VALUES "
f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false, false)"
)
)
conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}"))
Expand Down
5 changes: 5 additions & 0 deletions src/tagstudio/core/library/alchemy/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ class BrowsingState:
ascending: bool = True
random_seed: float = 0

show_hidden_entries: bool = False

query: str | None = None

# Abstract Syntax Tree Of the current Search Query
Expand Down Expand Up @@ -147,6 +149,9 @@ def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
def with_search_query(self, search_query: str) -> "BrowsingState":
return replace(self, query=search_query)

def with_show_hidden_entries(self, show_hidden_entries: bool) -> "BrowsingState":
return replace(self, show_hidden_entries=show_hidden_entries)


class FieldTypeEnum(enum.Enum):
TEXT_LINE = "Text Line"
Expand Down
45 changes: 43 additions & 2 deletions src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def get_default_tags() -> tuple[Tag, ...]:
name="Archived",
aliases={TagAlias(name="Archive")},
parent_tags={meta_tag},
is_hidden=True,
color_slug="red",
color_namespace="tagstudio-standard",
)
Expand Down Expand Up @@ -540,6 +541,8 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:
self.__apply_db8_schema_changes(session)
if loaded_db_version < 9:
self.__apply_db9_schema_changes(session)
if loaded_db_version < 103:
self.__apply_db103_schema_changes(session)
if loaded_db_version == 6:
self.__apply_repairs_for_db6(session)

Expand All @@ -551,6 +554,8 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:
self.__apply_db100_parent_repairs(session)
if loaded_db_version < 102:
self.__apply_db102_repairs(session)
if loaded_db_version < 103:
self.__apply_db103_default_data(session)

# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
self.migrate_sql_to_ts_ignore(library_dir)
Expand Down Expand Up @@ -698,6 +703,36 @@ def __apply_db102_repairs(self, session: Session):
session.commit()
logger.info("[Library][Migration] Verified TagParent table data")

def __apply_db103_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 103."""
add_is_hidden_column = text(
"ALTER TABLE tags ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT 0"
)
try:
session.execute(add_is_hidden_column)
session.commit()
logger.info("[Library][Migration] Added is_hidden column to tags table")
except Exception as e:
logger.error(
"[Library][Migration] Could not create is_hidden column in tags table!",
error=e,
)
session.rollback()

def __apply_db103_default_data(self, session: Session):
"""Apply default data changes introduced in DB_VERSION 103."""
try:
session.query(Tag).filter(Tag.id == TAG_ARCHIVED).update({"is_hidden": True})
session.commit()
logger.info("[Library][Migration] Updated archived tag to be hidden")
session.commit()
except Exception as e:
logger.error(
"[Library][Migration] Could not update archived tag to be hidden!",
error=e,
)
session.rollback()

def migrate_sql_to_ts_ignore(self, library_dir: Path):
# Do not continue if existing '.ts_ignore' file is found
if Path(library_dir / TS_FOLDER_NAME / IGNORE_NAME).exists():
Expand Down Expand Up @@ -1003,13 +1038,19 @@ def search_library(
else:
statement = select(Entry.id)

if search.ast:
ast = search.ast

if not search.show_hidden_entries:
statement = statement.where(~Entry.tags.any(Tag.is_hidden))

if ast:
start_time = time.time()
statement = statement.where(SQLBoolExpressionBuilder(self).visit(search.ast))
statement = statement.where(SQLBoolExpressionBuilder(self).visit(ast))
end_time = time.time()
logger.info(
f"SQL Expression Builder finished ({format_timespan(end_time - start_time)})"
)

statement = statement.distinct(Entry.id)

sort_on: ColumnExpressionArgument = Entry.id
Expand Down
3 changes: 3 additions & 0 deletions src/tagstudio/core/library/alchemy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class Tag(Base):
color_slug: Mapped[str | None] = mapped_column()
color: Mapped[TagColorGroup | None] = relationship(lazy="joined")
is_category: Mapped[bool]
is_hidden: Mapped[bool]
icon: Mapped[str | None]
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
parent_tags: Mapped[set["Tag"]] = relationship(
Expand Down Expand Up @@ -138,6 +139,7 @@ def __init__(
color_slug: str | None = None,
disambiguation_id: int | None = None,
is_category: bool = False,
is_hidden: bool = False,
):
self.name = name
self.aliases = aliases or set()
Expand All @@ -148,6 +150,7 @@ def __init__(
self.shorthand = shorthand
self.disambiguation_id = disambiguation_id
self.is_category = is_category
self.is_hidden = is_hidden
self.id = id # pyright: ignore[reportAttributeAccessIssue]
super().__init__()

Expand Down
2 changes: 2 additions & 0 deletions src/tagstudio/core/library/alchemy/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ def __separate_tags(
continue
case ConstraintType.FileType:
pass
case ConstraintType.MediaType:
pass
case ConstraintType.Path:
pass
case ConstraintType.Special:
Expand Down
43 changes: 43 additions & 0 deletions src/tagstudio/qt/mixed/build_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,46 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None:
self.cat_layout.addWidget(self.cat_checkbox)
self.cat_layout.addWidget(self.cat_title)

# Hidden ---------------------------------------------------------------
self.hidden_widget = QWidget()
self.hidden_layout = QHBoxLayout(self.hidden_widget)
self.hidden_layout.setStretch(1, 1)
self.hidden_layout.setContentsMargins(0, 0, 0, 0)
self.hidden_layout.setSpacing(6)
self.hidden_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.hidden_title = QLabel(Translations["tag.is_hidden"])
self.hidden_checkbox = QCheckBox()
self.hidden_checkbox.setFixedSize(22, 22)

self.hidden_checkbox.setStyleSheet(
f"QCheckBox{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QCheckBox::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QCheckBox::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QCheckBox::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
f"QCheckBox::focus{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"outline:none;"
f"}}"
)
self.hidden_layout.addWidget(self.hidden_checkbox)
self.hidden_layout.addWidget(self.hidden_title)

# Add Widgets to Layout ================================================
self.root_layout.addWidget(self.name_widget)
self.root_layout.addWidget(self.shorthand_widget)
Expand All @@ -256,6 +296,7 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None:
self.root_layout.addWidget(self.color_widget)
self.root_layout.addWidget(QLabel("<h3>Properties</h3>"))
self.root_layout.addWidget(self.cat_widget)
self.root_layout.addWidget(self.hidden_widget)

self.parent_ids: set[int] = set()
self.alias_ids: list[int] = []
Expand Down Expand Up @@ -544,6 +585,7 @@ def set_tag(self, tag: Tag):
self.color_button.set_tag_color_group(None)

self.cat_checkbox.setChecked(tag.is_category)
self.hidden_checkbox.setChecked(tag.is_hidden)

def on_name_changed(self):
is_empty = not self.name_field.text().strip()
Expand All @@ -567,6 +609,7 @@ def build_tag(self) -> Tag:
tag.color_namespace = self.tag_color_namespace
tag.color_slug = self.tag_color_slug
tag.is_category = self.cat_checkbox.isChecked()
tag.is_hidden = self.hidden_checkbox.isChecked()

logger.info("built tag", tag=tag)
return tag
Expand Down
15 changes: 15 additions & 0 deletions src/tagstudio/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ def _update_browsing_state():
BrowsingState.from_search_query(self.main_window.search_field.text())
.with_sorting_mode(self.main_window.sorting_mode)
.with_sorting_direction(self.main_window.sorting_direction)
.with_show_hidden_entries(self.main_window.show_hidden_entries)
)
except ParsingError as e:
self.main_window.status_bar.showMessage(
Expand Down Expand Up @@ -659,6 +660,12 @@ def _update_browsing_state():
lambda: self.thumb_size_callback(self.main_window.thumb_size_combobox.currentIndex())
)

# Exclude hidden entries checkbox
self.main_window.show_hidden_entries_checkbox.setChecked(False) # Default: No
self.main_window.show_hidden_entries_checkbox.stateChanged.connect(
self.show_hidden_entries_callback
)

self.main_window.back_button.clicked.connect(lambda: self.navigation_callback(-1))
self.main_window.forward_button.clicked.connect(lambda: self.navigation_callback(1))

Expand Down Expand Up @@ -1174,6 +1181,14 @@ def thumb_size_callback(self, size: int):
min(self.main_window.thumb_size // spacing_divisor, min_spacing)
)

def show_hidden_entries_callback(self):
logger.info("Show Hidden Entries Changed", exclude=self.main_window.show_hidden_entries)
self.update_browsing_state(
self.browsing_history.current.with_show_hidden_entries(
self.main_window.show_hidden_entries
)
)

def mouse_navigation(self, event: QMouseEvent):
# print(event.button())
if event.button() == Qt.MouseButton.ForwardButton:
Expand Down
65 changes: 62 additions & 3 deletions src/tagstudio/qt/views/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
from PIL import Image, ImageQt
from PySide6 import QtCore
from PySide6.QtCore import QMetaObject, QSize, QStringListModel, Qt
from PySide6.QtGui import QAction, QPixmap
from PySide6.QtGui import QAction, QColor, QPixmap
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QCompleter,
QFrame,
QGridLayout,
QHBoxLayout,
QLabel,
QLayout,
QLineEdit,
QMainWindow,
Expand All @@ -34,12 +36,14 @@
)

from tagstudio.core.enums import ShowFilepathOption
from tagstudio.core.library.alchemy.enums import SortingModeEnum
from tagstudio.core.library.alchemy.enums import SortingModeEnum, TagColorEnum
from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
from tagstudio.qt.mixed.landing import LandingWidget
from tagstudio.qt.mixed.pagination import Pagination
from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color
from tagstudio.qt.mnemonics import assign_mnemonics
from tagstudio.qt.models.palette import ColorType, get_tag_color
from tagstudio.qt.platform_strings import trash_term
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.thumb_grid_layout import ThumbGridLayout
Expand Down Expand Up @@ -578,7 +582,57 @@ def setup_extra_input_bar(self):
self.extra_input_layout = QHBoxLayout()
self.extra_input_layout.setObjectName("extra_input_layout")

## left side spacer
primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
border_color = get_border_color(primary_color)
highlight_color = get_highlight_color(primary_color)
text_color: QColor = get_text_color(primary_color, highlight_color)

## Show hidden entries checkbox
self.show_hidden_entries_widget = QWidget()
self.show_hidden_entries_layout = QHBoxLayout(self.show_hidden_entries_widget)
self.show_hidden_entries_layout.setStretch(1, 1)
self.show_hidden_entries_layout.setContentsMargins(0, 0, 0, 0)
self.show_hidden_entries_layout.setSpacing(6)
self.show_hidden_entries_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.show_hidden_entries_title = QLabel(Translations["home.show_hidden_entries"])
self.show_hidden_entries_checkbox = QCheckBox()
self.show_hidden_entries_checkbox.setFixedSize(22, 22)

self.show_hidden_entries_checkbox.setStyleSheet(
f"QCheckBox{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QCheckBox::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QCheckBox::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QCheckBox::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
f"QCheckBox::focus{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"outline:none;"
f"}}"
)

self.show_hidden_entries_checkbox.setChecked(False) # Default: No

self.show_hidden_entries_layout.addWidget(self.show_hidden_entries_checkbox)
self.show_hidden_entries_layout.addWidget(self.show_hidden_entries_title)

self.extra_input_layout.addWidget(self.show_hidden_entries_widget)

## Spacer
self.extra_input_layout.addItem(
QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
)
Expand Down Expand Up @@ -712,3 +766,8 @@ def sorting_direction(self) -> bool:
@property
def thumb_size(self) -> int:
return self.thumb_size_combobox.currentData()

@property
def show_hidden_entries(self) -> bool:
"""Whether to show entries tagged with hidden tags."""
return self.show_hidden_entries_checkbox.isChecked()
2 changes: 2 additions & 0 deletions src/tagstudio/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
"home.thumbnail_size.mini": "Mini Thumbnails",
"home.thumbnail_size.small": "Small Thumbnails",
"home.thumbnail_size": "Thumbnail Size",
"home.show_hidden_entries": "Show Hidden Entries",
"ignore.open_file": "Show \"{ts_ignore}\" File on Disk",
"json_migration.checking_for_parity": "Checking for Parity...",
"json_migration.creating_database_tables": "Creating SQL Database Tables...",
Expand Down Expand Up @@ -326,6 +327,7 @@
"tag.disambiguation.tooltip": "Use this tag for disambiguation",
"tag.edit": "Edit Tag",
"tag.is_category": "Is Category",
"tag.is_hidden": "Is Hidden",
"tag.name": "Name",
"tag.new": "New Tag",
"tag.parent_tags.add": "Add Parent Tag(s)",
Expand Down
Binary file modified tests/fixtures/search_library/.TagStudio/ts_library.sqlite
Binary file not shown.