Skip to content
Closed
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
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 @@ -9,7 +9,7 @@

DB_VERSION_CURRENT_KEY: str = "CURRENT"
DB_VERSION_INITIAL_KEY: str = "INITIAL"
DB_VERSION: int = 300
DB_VERSION: int = 301

TAG_CHILDREN_QUERY = text("""
WITH RECURSIVE ChildTags AS (
Expand Down
10 changes: 8 additions & 2 deletions src/tagstudio/core/library/alchemy/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ def make_tables(engine: Engine) -> None:
conn.execute(
text(
"INSERT INTO tags "
"(id, name, color_namespace, color_slug, is_category, is_hidden) VALUES "
f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false, false)"
"(id,"
"name,"
"color_namespace,"
"color_slug,"
"is_category,"
"is_hidden,"
"is_pinned) VALUES "
f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false, false, false)"
)
)
conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}"))
Expand Down
8 changes: 8 additions & 0 deletions src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ def open_sqlite_library(
(self.__apply_db201_migration, 201, 200), # changes: field tables
(self.__apply_db202_migration, 202, None), # changes: tag_parents
(self.__apply_db300_migration, 300, None), # changes: deletes folders
(self.__apply_db301_migration, 301, None), # changes: tags
]
for migration, v, iv in migrations:
if loaded_db_version < v and (iv is None or initial_db_version < iv):
Expand Down Expand Up @@ -914,6 +915,13 @@ def __apply_db300_migration(self, session: Session, library_dir: Path):
session.execute(text("DROP TABLE folders"))
session.flush()

def __apply_db301_migration(self, session: Session, library_dir: Path):
"""Migrate DB to DB_VERSION 301."""
# Add the new pinned column for tags
session.execute(text("ALTER TABLE tags ADD COLUMN is_pinned BOOLEAN NOT NULL DEFAULT 0"))
session.flush()
logger.info("[Library][Migration][103] Added is_pinned column to tags table")

@property
def field_templates(self) -> Sequence[BaseFieldTemplate]:
with Session(self.engine) as session:
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 @@ -95,6 +95,7 @@ class Tag(Base):
color: Mapped[TagColorGroup | None] = relationship(lazy="joined")
is_category: Mapped[bool]
is_hidden: Mapped[bool]
is_pinned: Mapped[bool]
icon: Mapped[str | None]
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
parent_tags: Mapped[set["Tag"]] = relationship(
Expand Down Expand Up @@ -137,6 +138,7 @@ def __init__(
disambiguation_id: int | None = None,
is_category: bool = False,
is_hidden: bool = False,
is_pinned: bool = False,
):
self.name = name
self.aliases = aliases or set()
Expand All @@ -148,6 +150,7 @@ def __init__(
self.disambiguation_id = disambiguation_id
self.is_category = is_category
self.is_hidden = is_hidden
self.is_pinned = is_pinned
self.id = id # pyright: ignore[reportAttributeAccessIssue]
super().__init__()

Expand Down
5 changes: 3 additions & 2 deletions src/tagstudio/qt/controllers/search_panel_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,12 @@ def update_items(self, query: str | None = None) -> None:
search_results: tuple[list[T], list[T]] = self.search_items(query_lower)

# Sort and prioritize the results
# not item.is_pinned is used beacuse False sorts before True.
direct_results = list({item for item in search_results[0] if not self._is_excluded(item)})
direct_results.sort(key=lambda item: _item_name(item).lower())
direct_results.sort(key=lambda item: (not item.is_pinned, _item_name(item).lower())) # pyright: ignore[reportAttributeAccessIssue]

ancestor_results = list({item for item in search_results[1] if not self._is_excluded(item)})
ancestor_results.sort(key=lambda item: _item_name(item).lower())
ancestor_results.sort(key=lambda item: (not item.is_pinned, _item_name(item).lower())) # pyright: ignore[reportAttributeAccessIssue]

raw_results = list(direct_results + ancestor_results)
priority_results: set[T] = set()
Expand Down
17 changes: 17 additions & 0 deletions src/tagstudio/qt/mixed/build_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,20 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None:
self.hidden_layout.addWidget(self.hidden_checkbox)
self.hidden_layout.addWidget(self.hidden_title)

# Pinned ---------------------------------------------------------------
self.pinned_widget = QWidget()
self.pinned_layout = QHBoxLayout(self.pinned_widget)
self.pinned_layout.setStretch(1, 1)
self.pinned_layout.setContentsMargins(0, 0, 0, 0)
self.pinned_layout.setSpacing(6)
self.pinned_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.pinned_title = QLabel(Translations["tag.is_pinned"])
self.pinned_checkbox = QCheckBox()
self.pinned_checkbox.setFixedSize(22, 22)
self.pinned_checkbox.setStyleSheet(checkbox_style())
self.pinned_layout.addWidget(self.pinned_checkbox)
self.pinned_layout.addWidget(self.pinned_title)

# Add Widgets to Layout ================================================
self.root_layout.addWidget(self.name_widget)
self.root_layout.addWidget(self.shorthand_widget)
Expand All @@ -249,6 +263,7 @@ def __init__(self, library: Library, tag: Tag | None = None) -> None:
self.root_layout.addWidget(QLabel(header(Translations["tag.properties"], 3)))
self.root_layout.addWidget(self.cat_widget)
self.root_layout.addWidget(self.hidden_widget)
self.root_layout.addWidget(self.pinned_widget)

self.set_tag(tag or Tag(name=Translations["tag.new"]))

Expand Down Expand Up @@ -488,6 +503,7 @@ def set_tag(self, tag: Tag):

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

def _on_name_change(self):
is_empty = not self.name_field.text().strip()
Expand All @@ -505,6 +521,7 @@ def build_tag(self) -> Tag:
tag.color_slug = self.tag_color_slug
tag.is_category = self.cat_checkbox.isChecked()
tag.is_hidden = self.hidden_checkbox.isChecked()
tag.is_pinned = self.pinned_checkbox.isChecked()

logger.info("[BuildTag] Build Tag", tag_id=tag.id, tag_name=tag.name)
return tag
Expand Down
1 change: 1 addition & 0 deletions src/tagstudio/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@
"tag.edit": "Edit Tag",
"tag.is_category": "Is Category",
"tag.is_hidden": "Is Hidden",
"tag.is_pinned": "Is Pinned",
"tag.name": "Name",
"tag.new": "New Tag",
"tag.parent_tags": "Parent Tags",
Expand Down
Loading