Skip to content

Commit 88d0b47

Browse files
TrigamDevCyanVoxel
andauthored
feat: add hidden tags (#1139)
* Add `is_hidden` field to the `tags` table * Add hidden checkbox to the edit tag panel * Fix formatting * Exclude hidden tags from search results * Fix formatting (I should probably actually check before committing? lmao?) * Bit of cleanup * Add toggle for excluding hidden entries below search bar * That might be important maybe * Update Save Format Changes page in docs (and include updated test database) * Simplify query and invert name+logic * chore: remove unused code, tweak strings --------- Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
1 parent c38cc9d commit 88d0b47

File tree

12 files changed

+187
-8
lines changed

12 files changed

+187
-8
lines changed

docs/library-changes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,12 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
123123
| [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
124124

125125
- Applies repairs to the `tag_parents` table created in [version 100](#version-100), removing rows that reference tags that have been deleted.
126+
127+
#### Version 103
128+
129+
| Used From | Format | Location |
130+
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
131+
| [#1139](https://github.com/TagStudioDev/TagStudio/pull/1139) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
132+
133+
- Adds the `is_hidden` column to the `tags` table (default `0`). Used for excluding entries tagged with hidden tags from library searches.
134+
- Sets the `is_hidden` field on the built-in Archived tag to `1`, to match the Archived tag now being hidden by default.

src/tagstudio/core/library/alchemy/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
DB_VERSION_LEGACY_KEY: str = "DB_VERSION"
1212
DB_VERSION_CURRENT_KEY: str = "CURRENT"
1313
DB_VERSION_INITIAL_KEY: str = "INITIAL"
14-
DB_VERSION: int = 102
14+
DB_VERSION: int = 103
1515

1616
TAG_CHILDREN_QUERY = text("""
1717
WITH RECURSIVE ChildTags AS (

src/tagstudio/core/library/alchemy/db.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ def make_tables(engine: Engine) -> None:
5757
conn.execute(
5858
text(
5959
"INSERT INTO tags "
60-
"(id, name, color_namespace, color_slug, is_category) VALUES "
61-
f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false)"
60+
"(id, name, color_namespace, color_slug, is_category, is_hidden) VALUES "
61+
f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false, false)"
6262
)
6363
)
6464
conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}"))

src/tagstudio/core/library/alchemy/enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class BrowsingState:
8282
ascending: bool = True
8383
random_seed: float = 0
8484

85+
show_hidden_entries: bool = False
86+
8587
query: str | None = None
8688

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

152+
def with_show_hidden_entries(self, show_hidden_entries: bool) -> "BrowsingState":
153+
return replace(self, show_hidden_entries=show_hidden_entries)
154+
150155

151156
class FieldTypeEnum(enum.Enum):
152157
TEXT_LINE = "Text Line"

src/tagstudio/core/library/alchemy/library.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def get_default_tags() -> tuple[Tag, ...]:
151151
name="Archived",
152152
aliases={TagAlias(name="Archive")},
153153
parent_tags={meta_tag},
154+
is_hidden=True,
154155
color_slug="red",
155156
color_namespace="tagstudio-standard",
156157
)
@@ -540,6 +541,8 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:
540541
self.__apply_db8_schema_changes(session)
541542
if loaded_db_version < 9:
542543
self.__apply_db9_schema_changes(session)
544+
if loaded_db_version < 103:
545+
self.__apply_db103_schema_changes(session)
543546
if loaded_db_version == 6:
544547
self.__apply_repairs_for_db6(session)
545548

@@ -551,6 +554,8 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:
551554
self.__apply_db100_parent_repairs(session)
552555
if loaded_db_version < 102:
553556
self.__apply_db102_repairs(session)
557+
if loaded_db_version < 103:
558+
self.__apply_db103_default_data(session)
554559

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

706+
def __apply_db103_schema_changes(self, session: Session):
707+
"""Apply database schema changes introduced in DB_VERSION 103."""
708+
add_is_hidden_column = text(
709+
"ALTER TABLE tags ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT 0"
710+
)
711+
try:
712+
session.execute(add_is_hidden_column)
713+
session.commit()
714+
logger.info("[Library][Migration] Added is_hidden column to tags table")
715+
except Exception as e:
716+
logger.error(
717+
"[Library][Migration] Could not create is_hidden column in tags table!",
718+
error=e,
719+
)
720+
session.rollback()
721+
722+
def __apply_db103_default_data(self, session: Session):
723+
"""Apply default data changes introduced in DB_VERSION 103."""
724+
try:
725+
session.query(Tag).filter(Tag.id == TAG_ARCHIVED).update({"is_hidden": True})
726+
session.commit()
727+
logger.info("[Library][Migration] Updated archived tag to be hidden")
728+
session.commit()
729+
except Exception as e:
730+
logger.error(
731+
"[Library][Migration] Could not update archived tag to be hidden!",
732+
error=e,
733+
)
734+
session.rollback()
735+
701736
def migrate_sql_to_ts_ignore(self, library_dir: Path):
702737
# Do not continue if existing '.ts_ignore' file is found
703738
if Path(library_dir / TS_FOLDER_NAME / IGNORE_NAME).exists():
@@ -1003,13 +1038,19 @@ def search_library(
10031038
else:
10041039
statement = select(Entry.id)
10051040

1006-
if search.ast:
1041+
ast = search.ast
1042+
1043+
if not search.show_hidden_entries:
1044+
statement = statement.where(~Entry.tags.any(Tag.is_hidden))
1045+
1046+
if ast:
10071047
start_time = time.time()
1008-
statement = statement.where(SQLBoolExpressionBuilder(self).visit(search.ast))
1048+
statement = statement.where(SQLBoolExpressionBuilder(self).visit(ast))
10091049
end_time = time.time()
10101050
logger.info(
10111051
f"SQL Expression Builder finished ({format_timespan(end_time - start_time)})"
10121052
)
1053+
10131054
statement = statement.distinct(Entry.id)
10141055

10151056
sort_on: ColumnExpressionArgument = Entry.id

src/tagstudio/core/library/alchemy/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class Tag(Base):
9797
color_slug: Mapped[str | None] = mapped_column()
9898
color: Mapped[TagColorGroup | None] = relationship(lazy="joined")
9999
is_category: Mapped[bool]
100+
is_hidden: Mapped[bool]
100101
icon: Mapped[str | None]
101102
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
102103
parent_tags: Mapped[set["Tag"]] = relationship(
@@ -138,6 +139,7 @@ def __init__(
138139
color_slug: str | None = None,
139140
disambiguation_id: int | None = None,
140141
is_category: bool = False,
142+
is_hidden: bool = False,
141143
):
142144
self.name = name
143145
self.aliases = aliases or set()
@@ -148,6 +150,7 @@ def __init__(
148150
self.shorthand = shorthand
149151
self.disambiguation_id = disambiguation_id
150152
self.is_category = is_category
153+
self.is_hidden = is_hidden
151154
self.id = id # pyright: ignore[reportAttributeAccessIssue]
152155
super().__init__()
153156

src/tagstudio/core/library/alchemy/visitors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ def __separate_tags(
171171
continue
172172
case ConstraintType.FileType:
173173
pass
174+
case ConstraintType.MediaType:
175+
pass
174176
case ConstraintType.Path:
175177
pass
176178
case ConstraintType.Special:

src/tagstudio/qt/mixed/build_tag.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,46 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None:
246246
self.cat_layout.addWidget(self.cat_checkbox)
247247
self.cat_layout.addWidget(self.cat_title)
248248

249+
# Hidden ---------------------------------------------------------------
250+
self.hidden_widget = QWidget()
251+
self.hidden_layout = QHBoxLayout(self.hidden_widget)
252+
self.hidden_layout.setStretch(1, 1)
253+
self.hidden_layout.setContentsMargins(0, 0, 0, 0)
254+
self.hidden_layout.setSpacing(6)
255+
self.hidden_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
256+
self.hidden_title = QLabel(Translations["tag.is_hidden"])
257+
self.hidden_checkbox = QCheckBox()
258+
self.hidden_checkbox.setFixedSize(22, 22)
259+
260+
self.hidden_checkbox.setStyleSheet(
261+
f"QCheckBox{{"
262+
f"background: rgba{primary_color.toTuple()};"
263+
f"color: rgba{text_color.toTuple()};"
264+
f"border-color: rgba{border_color.toTuple()};"
265+
f"border-radius: 6px;"
266+
f"border-style:solid;"
267+
f"border-width: 2px;"
268+
f"}}"
269+
f"QCheckBox::indicator{{"
270+
f"width: 10px;"
271+
f"height: 10px;"
272+
f"border-radius: 2px;"
273+
f"margin: 4px;"
274+
f"}}"
275+
f"QCheckBox::indicator:checked{{"
276+
f"background: rgba{text_color.toTuple()};"
277+
f"}}"
278+
f"QCheckBox::hover{{"
279+
f"border-color: rgba{highlight_color.toTuple()};"
280+
f"}}"
281+
f"QCheckBox::focus{{"
282+
f"border-color: rgba{highlight_color.toTuple()};"
283+
f"outline:none;"
284+
f"}}"
285+
)
286+
self.hidden_layout.addWidget(self.hidden_checkbox)
287+
self.hidden_layout.addWidget(self.hidden_title)
288+
249289
# Add Widgets to Layout ================================================
250290
self.root_layout.addWidget(self.name_widget)
251291
self.root_layout.addWidget(self.shorthand_widget)
@@ -256,6 +296,7 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None:
256296
self.root_layout.addWidget(self.color_widget)
257297
self.root_layout.addWidget(QLabel("<h3>Properties</h3>"))
258298
self.root_layout.addWidget(self.cat_widget)
299+
self.root_layout.addWidget(self.hidden_widget)
259300

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

546587
self.cat_checkbox.setChecked(tag.is_category)
588+
self.hidden_checkbox.setChecked(tag.is_hidden)
547589

548590
def on_name_changed(self):
549591
is_empty = not self.name_field.text().strip()
@@ -567,6 +609,7 @@ def build_tag(self) -> Tag:
567609
tag.color_namespace = self.tag_color_namespace
568610
tag.color_slug = self.tag_color_slug
569611
tag.is_category = self.cat_checkbox.isChecked()
612+
tag.is_hidden = self.hidden_checkbox.isChecked()
570613

571614
logger.info("built tag", tag=tag)
572615
return tag

src/tagstudio/qt/ts_qt.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ def _update_browsing_state():
627627
BrowsingState.from_search_query(self.main_window.search_field.text())
628628
.with_sorting_mode(self.main_window.sorting_mode)
629629
.with_sorting_direction(self.main_window.sorting_direction)
630+
.with_show_hidden_entries(self.main_window.show_hidden_entries)
630631
)
631632
except ParsingError as e:
632633
self.main_window.status_bar.showMessage(
@@ -659,6 +660,12 @@ def _update_browsing_state():
659660
lambda: self.thumb_size_callback(self.main_window.thumb_size_combobox.currentIndex())
660661
)
661662

663+
# Exclude hidden entries checkbox
664+
self.main_window.show_hidden_entries_checkbox.setChecked(False) # Default: No
665+
self.main_window.show_hidden_entries_checkbox.stateChanged.connect(
666+
self.show_hidden_entries_callback
667+
)
668+
662669
self.main_window.back_button.clicked.connect(lambda: self.navigation_callback(-1))
663670
self.main_window.forward_button.clicked.connect(lambda: self.navigation_callback(1))
664671

@@ -1174,6 +1181,14 @@ def thumb_size_callback(self, size: int):
11741181
min(self.main_window.thumb_size // spacing_divisor, min_spacing)
11751182
)
11761183

1184+
def show_hidden_entries_callback(self):
1185+
logger.info("Show Hidden Entries Changed", exclude=self.main_window.show_hidden_entries)
1186+
self.update_browsing_state(
1187+
self.browsing_history.current.with_show_hidden_entries(
1188+
self.main_window.show_hidden_entries
1189+
)
1190+
)
1191+
11771192
def mouse_navigation(self, event: QMouseEvent):
11781193
# print(event.button())
11791194
if event.button() == Qt.MouseButton.ForwardButton:

src/tagstudio/qt/views/main_window.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
from PIL import Image, ImageQt
1212
from PySide6 import QtCore
1313
from PySide6.QtCore import QMetaObject, QSize, QStringListModel, Qt
14-
from PySide6.QtGui import QAction, QPixmap
14+
from PySide6.QtGui import QAction, QColor, QPixmap
1515
from PySide6.QtWidgets import (
16+
QCheckBox,
1617
QComboBox,
1718
QCompleter,
1819
QFrame,
1920
QGridLayout,
2021
QHBoxLayout,
22+
QLabel,
2123
QLayout,
2224
QLineEdit,
2325
QMainWindow,
@@ -34,12 +36,14 @@
3436
)
3537

3638
from tagstudio.core.enums import ShowFilepathOption
37-
from tagstudio.core.library.alchemy.enums import SortingModeEnum
39+
from tagstudio.core.library.alchemy.enums import SortingModeEnum, TagColorEnum
3840
from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel
3941
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
4042
from tagstudio.qt.mixed.landing import LandingWidget
4143
from tagstudio.qt.mixed.pagination import Pagination
44+
from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color
4245
from tagstudio.qt.mnemonics import assign_mnemonics
46+
from tagstudio.qt.models.palette import ColorType, get_tag_color
4347
from tagstudio.qt.platform_strings import trash_term
4448
from tagstudio.qt.resource_manager import ResourceManager
4549
from tagstudio.qt.thumb_grid_layout import ThumbGridLayout
@@ -578,7 +582,57 @@ def setup_extra_input_bar(self):
578582
self.extra_input_layout = QHBoxLayout()
579583
self.extra_input_layout.setObjectName("extra_input_layout")
580584

581-
## left side spacer
585+
primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
586+
border_color = get_border_color(primary_color)
587+
highlight_color = get_highlight_color(primary_color)
588+
text_color: QColor = get_text_color(primary_color, highlight_color)
589+
590+
## Show hidden entries checkbox
591+
self.show_hidden_entries_widget = QWidget()
592+
self.show_hidden_entries_layout = QHBoxLayout(self.show_hidden_entries_widget)
593+
self.show_hidden_entries_layout.setStretch(1, 1)
594+
self.show_hidden_entries_layout.setContentsMargins(0, 0, 0, 0)
595+
self.show_hidden_entries_layout.setSpacing(6)
596+
self.show_hidden_entries_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
597+
self.show_hidden_entries_title = QLabel(Translations["home.show_hidden_entries"])
598+
self.show_hidden_entries_checkbox = QCheckBox()
599+
self.show_hidden_entries_checkbox.setFixedSize(22, 22)
600+
601+
self.show_hidden_entries_checkbox.setStyleSheet(
602+
f"QCheckBox{{"
603+
f"background: rgba{primary_color.toTuple()};"
604+
f"color: rgba{text_color.toTuple()};"
605+
f"border-color: rgba{border_color.toTuple()};"
606+
f"border-radius: 6px;"
607+
f"border-style:solid;"
608+
f"border-width: 2px;"
609+
f"}}"
610+
f"QCheckBox::indicator{{"
611+
f"width: 10px;"
612+
f"height: 10px;"
613+
f"border-radius: 2px;"
614+
f"margin: 4px;"
615+
f"}}"
616+
f"QCheckBox::indicator:checked{{"
617+
f"background: rgba{text_color.toTuple()};"
618+
f"}}"
619+
f"QCheckBox::hover{{"
620+
f"border-color: rgba{highlight_color.toTuple()};"
621+
f"}}"
622+
f"QCheckBox::focus{{"
623+
f"border-color: rgba{highlight_color.toTuple()};"
624+
f"outline:none;"
625+
f"}}"
626+
)
627+
628+
self.show_hidden_entries_checkbox.setChecked(False) # Default: No
629+
630+
self.show_hidden_entries_layout.addWidget(self.show_hidden_entries_checkbox)
631+
self.show_hidden_entries_layout.addWidget(self.show_hidden_entries_title)
632+
633+
self.extra_input_layout.addWidget(self.show_hidden_entries_widget)
634+
635+
## Spacer
582636
self.extra_input_layout.addItem(
583637
QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
584638
)
@@ -712,3 +766,8 @@ def sorting_direction(self) -> bool:
712766
@property
713767
def thumb_size(self) -> int:
714768
return self.thumb_size_combobox.currentData()
769+
770+
@property
771+
def show_hidden_entries(self) -> bool:
772+
"""Whether to show entries tagged with hidden tags."""
773+
return self.show_hidden_entries_checkbox.isChecked()

0 commit comments

Comments
 (0)