Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49e320618c | |||
| 102cfdf4a2 | |||
| 47b123569e | |||
| 09a54d5dfc | |||
| 7782d2438f | |||
| 92e29635c7 | |||
| 6e288afd8c | |||
| 354c0a0afc | |||
| dd00f4d4a0 | |||
| 1bfea60d7a | |||
| c015b1d4f2 | |||
| 4b200a674e | |||
| 507823bfcd | |||
| 3def6c5239 | |||
| f04421fa93 |
@@ -45,6 +45,13 @@ SPDX-FileCopyrightText = "(c) 2026 Boxicons"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
|
||||
[[annotations]]
|
||||
path = ["src/tagstudio/resources/qt/images/dupe_file_stat.png"]
|
||||
path = [
|
||||
"src/tagstudio/resources/qt/images/dupe_file_stat.png",
|
||||
"src/tagstudio/resources/qt/images/hint_field_add.png",
|
||||
"src/tagstudio/resources/qt/images/hint_field_create.png",
|
||||
"src/tagstudio/resources/qt/images/hint_tag_add.png",
|
||||
"src/tagstudio/resources/qt/images/hint_tag_added.png",
|
||||
"src/tagstudio/resources/qt/images/hint_tag_create.png",
|
||||
]
|
||||
SPDX-FileCopyrightText = "(c) github:google/material-design-icons Contributors"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
@@ -26,7 +26,7 @@ TagStudio automatically scans for new or updated files when opening a library by
|
||||
|
||||
With one or more file entries selected, you can **search for** or **create a new tag** by clicking the "Add Tag" button at the bottom of the preview panel or by pressing <kbd>Ctrl</kbd>+<kbd>T</kbd>.
|
||||
|
||||
This will open the the **"Tag Bar"**, a combination search/creation bar that is designed to allow you to quickly create and apply tags that don't exist yet, find and apply existing tags in your library, and verify if certain tags already exist and/or are applied to your selection.
|
||||
This will open the the **"Tag Bar"**, a combination search/creation bar that is designed to allow you to quickly create and apply tags that don't exist yet, find and apply existing tags from your library, and verify if certain tags already exist and/or are applied to your selection.
|
||||
|
||||
Every step is keyboard-friendly, allowing for an efficient tagging "flow state" when it comes to creating and applying tags.
|
||||
|
||||
@@ -34,7 +34,11 @@ Every step is keyboard-friendly, allowing for an efficient tagging "flow state"
|
||||
{ width=80% }
|
||||
<br>
|
||||
{ width=80% }
|
||||
<figcaption>Clicking "Add Tag" or pressing <kbd>Ctrl</kbd>+<kbd>T</kbd> replaces the "Add" buttons with a tag search bar. Pressing <kbd>Esc</kbd> (or <kbd>Enter</kbd> with a blank search) will close the bar and return the "Add" buttons.</figcaption>
|
||||
<figcaption>
|
||||
Clicking "Add Tag" or pressing <kbd>Ctrl</kbd>+<kbd>T</kbd> replaces the "Add" buttons with a tag search bar.
|
||||
<br>
|
||||
You can close the search by pressing <kbd>Enter</kbd> or clicking away (with a blank search), or at any time by pressing <kbd>Esc</kbd>.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
@@ -43,7 +47,7 @@ Every step is keyboard-friendly, allowing for an efficient tagging "flow state"
|
||||
|
||||
### :material-tag-plus: Tag Bar: Creating Tags
|
||||
|
||||
Type to search for an existing tag, or use that search term to quickly create and apply a new tag to your selection. Pressing <kbd>Enter</kbd> with **no results** will **create** a tag from your search query and apply it to the selected entries.
|
||||
Type to search for an existing tag, or use that search term to quickly create and apply a new tag to your selection. Pressing <kbd>Enter</kbd> with **no results** will **create** :material-tag-plus-outline: a tag from your search query and apply it to the selected entries.
|
||||
|
||||
After applying a tag, the "Add Tag" button is now focused, allowing you to press <kbd>Enter</kbd> or <kbd>Space</kbd> to quickly reactivate the bar. Alternatively, you may edit your new tag by right-clicking the tag and selecting **"Edit"**.
|
||||
|
||||
@@ -62,9 +66,11 @@ Tags can be also viewed, created, edited, or deleted from the **Edit -> Manage T
|
||||
|
||||
### :material-tag-search: Tag Bar: Searching for Tags
|
||||
|
||||
Pressing <kbd>Enter</kbd> with **one or more results** will **apply the first tag** to your selection (assuming it's not already on your selection).
|
||||
Pressing <kbd>Enter</kbd> with **one or more results** will **apply** :material-tag: the underlined tag to your selection (assuming it's not already on your selection).
|
||||
|
||||
Tags that are already present on your entries will be greyed out and ignored when pressing <kbd>Enter</kbd>. This lets you visually confirm that a tag already exists and has been applied to the selection.
|
||||
Matching tags that are **already applied** :material-tag-check: to your entries will be greyed out placed at the end of the autofill results. This lets you visually confirm that a tag already exists and has been applied to the selection. Applying them again with <kbd>Enter</kbd> has no effect other than closing the search.
|
||||
|
||||
You can navigate the list of autofill suggestions using the scroll wheel, and change the selected tag by pressing <kbd>Tab</kbd> or <kbd>Shift</kbd>+<kbd>Tab</kbd> to navigate right and left, respectively.
|
||||
|
||||
<figure markdown="span">
|
||||
{ width=80% }
|
||||
@@ -75,13 +81,17 @@ Tags that are already present on your entries will be greyed out and ignored whe
|
||||
|
||||
#### :material-tag-arrow-up: Force Create Tag
|
||||
|
||||
Holding <kbd>Shift</kbd> while pressing <kbd>Enter</kbd> will **force create** a tag from the search bar text, regardless of the results. This is indicated by the first tag in the results becoming greyed out. This is especially useful when you need to create a new tag with a name that partially overlaps with an existing tag's name.
|
||||
Holding <kbd>Shift</kbd> while pressing <kbd>Enter</kbd> will **force create** :material-tag-plus-outline: a tag from the search bar text, regardless of the search results. This is especially useful when you need to create a new tag with a name that partially overlaps with an existing tag's name.
|
||||
|
||||
<figure markdown="span">
|
||||
{ width=80% }
|
||||
<figcaption>Pressing <kbd>Enter</kbd> will apply the underlined tag, "Cube".</figcaption>
|
||||
<figcaption markdown="span">Pressing <kbd>Enter</kbd> will **apply** :material-tag: the underlined tag, "Cube".</figcaption>
|
||||
{ width=80% }
|
||||
<figcaption>With <kbd>Shift</kbd> held, pressing <kbd>Enter</kbd> will <b>create a new tag</b> called "Cub".</figcaption>
|
||||
<figcaption markdown="span">
|
||||
<kbd>Shift</kbd>+<kbd>Enter</kbd> will **create** :material-tag-plus-outline: a new tag called "Cub".
|
||||
<br>
|
||||
Note the the greyed-out tag, showing it will not be applied.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
---
|
||||
@@ -90,7 +100,7 @@ Holding <kbd>Shift</kbd> while pressing <kbd>Enter</kbd> will **force create** a
|
||||
|
||||
[Fields](./fields.md) are extra pieces of information you can add to file entries, such as titles, comments, notes, specific dates or times. Unlike tags, fields are based on [templates](./fields.md#field-templates) that contain pre-filled information such as the field type and title, and that information is _copied_ to fields when adding them to entries. Editing field information on entries **does not** modify the template it was created from.
|
||||
|
||||
Creating and adding fields to entries is extremely similar to [how it works for tags](#tagging), with a few notable differences:
|
||||
Creating and adding fields to entries is extremely similar to how [tagging](#tagging) works, with a few notable differences:
|
||||
|
||||
- Click "Add Field" or press <kbd>Ctrl</kbd>+<kbd>L</kbd> to open the field bar.
|
||||
- The same template can by applied to an entry any number of times.
|
||||
|
||||
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
|
||||
[project]
|
||||
name = "TagStudio"
|
||||
description = "A User-Focused Photo & File Management System."
|
||||
version = "9.6.2"
|
||||
version = "9.6.3"
|
||||
license = "GPL-3.0-only"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.14"
|
||||
|
||||
@@ -221,6 +221,7 @@ class Library:
|
||||
if self.engine:
|
||||
self.engine.dispose()
|
||||
self.library_dir = None
|
||||
self.folder = None
|
||||
self.included_files = set()
|
||||
|
||||
self.dupe_entries_count = -1
|
||||
@@ -377,7 +378,8 @@ class Library:
|
||||
|
||||
return self.open_sqlite_library(library_dir, in_memory)
|
||||
|
||||
def _get_engine(self, library_dir: Path, in_memory: bool, sql_filename: str):
|
||||
@staticmethod
|
||||
def __get_engine(library_dir: Path, in_memory: bool, sql_filename: str):
|
||||
connection_string = URL.create(
|
||||
drivername="sqlite",
|
||||
database=(
|
||||
@@ -405,7 +407,7 @@ class Library:
|
||||
def create_sqlite_library(
|
||||
self, library_dir: Path, in_memory: bool, sql_filename: str = SQL_FILENAME
|
||||
) -> LibraryStatus:
|
||||
self.engine = self._get_engine(library_dir, in_memory, sql_filename)
|
||||
self.engine = self.__get_engine(library_dir, in_memory, sql_filename)
|
||||
|
||||
logger.info(
|
||||
"[Library] Opening SQLite Library",
|
||||
@@ -503,21 +505,21 @@ class Library:
|
||||
) -> LibraryStatus:
|
||||
logger.info("[Library] Opening SQLite Library", library_dir=library_dir)
|
||||
|
||||
self.engine = self._get_engine(library_dir, in_memory, sql_filename)
|
||||
self.library_dir = library_dir
|
||||
self.engine = self.__get_engine(library_dir, in_memory, sql_filename)
|
||||
|
||||
try:
|
||||
migrations = DBMigrations(self)
|
||||
migrations = DBMigrations(library_dir, self.engine)
|
||||
|
||||
# save backup if patches will be applied
|
||||
if migrations.required:
|
||||
self.save_library_backup_to_disk()
|
||||
Library.save_library_backup_to_disk(library_dir)
|
||||
|
||||
migrations.run()
|
||||
except MigrationError as e:
|
||||
self.library_dir = None
|
||||
return LibraryStatus(success=False, message=e.args[0])
|
||||
|
||||
# everything is fine, set the library path
|
||||
self.library_dir = library_dir
|
||||
return LibraryStatus(success=True, library_path=library_dir)
|
||||
|
||||
@property
|
||||
@@ -667,32 +669,37 @@ class Library:
|
||||
with Session(self.engine) as session:
|
||||
return unwrap(session.scalar(select(func.count(Entry.id))))
|
||||
|
||||
@staticmethod
|
||||
def _all_entries(session: Session, with_joins: bool = False) -> Iterator[Entry]:
|
||||
"""Load entries without joins."""
|
||||
stmt = select(Entry)
|
||||
if with_joins:
|
||||
# load Entry with all joins and all tags
|
||||
stmt = (
|
||||
stmt.outerjoin(Entry.text_fields)
|
||||
.outerjoin(Entry.datetime_fields)
|
||||
.outerjoin(Entry.tags)
|
||||
)
|
||||
stmt = stmt.options(
|
||||
contains_eager(Entry.text_fields),
|
||||
contains_eager(Entry.datetime_fields),
|
||||
contains_eager(Entry.tags),
|
||||
)
|
||||
|
||||
stmt = stmt.distinct()
|
||||
|
||||
entries = session.execute(stmt).scalars()
|
||||
if with_joins:
|
||||
entries = entries.unique()
|
||||
|
||||
for entry in entries:
|
||||
yield entry
|
||||
session.expunge(entry)
|
||||
|
||||
def all_entries(self, with_joins: bool = False) -> Iterator[Entry]:
|
||||
"""Load entries without joins."""
|
||||
with Session(self.engine) as session:
|
||||
stmt = select(Entry)
|
||||
if with_joins:
|
||||
# load Entry with all joins and all tags
|
||||
stmt = (
|
||||
stmt.outerjoin(Entry.text_fields)
|
||||
.outerjoin(Entry.datetime_fields)
|
||||
.outerjoin(Entry.tags)
|
||||
)
|
||||
stmt = stmt.options(
|
||||
contains_eager(Entry.text_fields),
|
||||
contains_eager(Entry.datetime_fields),
|
||||
contains_eager(Entry.tags),
|
||||
)
|
||||
|
||||
stmt = stmt.distinct()
|
||||
|
||||
entries = session.execute(stmt).scalars()
|
||||
if with_joins:
|
||||
entries = entries.unique()
|
||||
|
||||
for entry in entries:
|
||||
yield entry
|
||||
session.expunge(entry)
|
||||
return Library._all_entries(session, with_joins)
|
||||
|
||||
@property
|
||||
def tags(self) -> list[Tag]:
|
||||
@@ -1440,16 +1447,17 @@ class Library:
|
||||
session.rollback()
|
||||
return None
|
||||
|
||||
def save_library_backup_to_disk(self) -> Path:
|
||||
assert isinstance(self.library_dir, Path)
|
||||
makedirs(str(self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True)
|
||||
@staticmethod
|
||||
def save_library_backup_to_disk(library_dir: Path) -> Path:
|
||||
assert isinstance(library_dir, Path)
|
||||
makedirs(str(library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True)
|
||||
|
||||
filename = f"ts_library_backup_{datetime.now(UTC).strftime('%Y_%m_%d_%H%M%S')}.sqlite"
|
||||
|
||||
target_path = self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
|
||||
target_path = library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
|
||||
|
||||
shutil.copy2(
|
||||
self.library_dir / TS_FOLDER_NAME / SQL_FILENAME,
|
||||
library_dir / TS_FOLDER_NAME / SQL_FILENAME,
|
||||
target_path,
|
||||
)
|
||||
|
||||
@@ -1662,8 +1670,10 @@ class Library:
|
||||
)
|
||||
session.execute(update_tags_stmt)
|
||||
session.commit()
|
||||
else:
|
||||
self.add_color(new_color_group)
|
||||
return
|
||||
|
||||
# "if not existing_color", out of the session context
|
||||
self.add_color(new_color_group)
|
||||
|
||||
def update_aliases(self, tag: Tag, aliases: Iterable[TagAlias], session: Session) -> bool:
|
||||
"""Update TagAliases for a given Tag."""
|
||||
@@ -1741,8 +1751,12 @@ class Library:
|
||||
Args:
|
||||
key(str): The key for the name of the version type to set.
|
||||
"""
|
||||
with Session(self.engine) as session:
|
||||
engine = sqlalchemy.inspect(self.engine)
|
||||
return Library._get_version(self.engine, key)
|
||||
|
||||
@staticmethod
|
||||
def _get_version(engine, key: str) -> int:
|
||||
with Session(engine) as session:
|
||||
engine = sqlalchemy.inspect(engine)
|
||||
try:
|
||||
# "Version" table added in DB_VERSION 101
|
||||
if engine and engine.has_table("versions"):
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, override
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
import ujson
|
||||
from sqlalchemy import and_, delete, select, text, update
|
||||
from sqlalchemy import Engine, and_, delete, select, text, update
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from tagstudio.core.constants import IGNORE_NAME, TAG_ARCHIVED, TS_FOLDER_NAME
|
||||
@@ -21,14 +21,11 @@ from tagstudio.core.library.alchemy.constants import (
|
||||
)
|
||||
from tagstudio.core.library.alchemy.fields import LEGACY_FIELD_MAP, DatetimeField, TextField
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag, TagColorGroup, Version
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup, Version
|
||||
from tagstudio.core.library.ignore import migrate_ext_list
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@@ -41,19 +38,20 @@ class DBMigration:
|
||||
initial_version: int | None = None
|
||||
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log: Callable[[str], str]):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DBMigrations:
|
||||
def __init__(self, library: "Library") -> None:
|
||||
def __init__(self, library_dir: Path, engine: Engine) -> None:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
|
||||
self.lib = library
|
||||
self.engine = self.lib.engine
|
||||
self.library_dir = library_dir
|
||||
self.engine = engine
|
||||
|
||||
# Don't check DB version when creating new library
|
||||
self.loaded_db_version = self.lib.get_version(DB_VERSION_CURRENT_KEY)
|
||||
self.initial_db_version = self.lib.get_version(DB_VERSION_INITIAL_KEY)
|
||||
self.loaded_db_version = Library._get_version(engine, DB_VERSION_CURRENT_KEY)
|
||||
self.initial_db_version = Library._get_version(engine, DB_VERSION_INITIAL_KEY)
|
||||
|
||||
# ======================== Library Database Version Checking =======================
|
||||
# DB_VERSION 6 is the first supported SQLite DB version.
|
||||
@@ -74,8 +72,7 @@ class DBMigrations:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[Library][Migration] "
|
||||
f"Opening Library with DB Version {self.loaded_db_version}/{DB_VERSION}"
|
||||
f"[Library][Migration] Starting with library DB version: {self.loaded_db_version}"
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -101,9 +98,6 @@ class DBMigrations:
|
||||
MigrationTo300, # changes: deletes folders
|
||||
]
|
||||
with Session(self.engine) as session:
|
||||
if self.loaded_db_version > DB_VERSION:
|
||||
return
|
||||
|
||||
for migration in migrations:
|
||||
if self.loaded_db_version < migration.version and (
|
||||
migration.initial_version is None
|
||||
@@ -113,15 +107,12 @@ class DBMigrations:
|
||||
# any error causes transaction to rollback
|
||||
migration.run(
|
||||
session,
|
||||
self.lib,
|
||||
self.library_dir,
|
||||
lambda msg, v=migration.version: f"[Library][Migration][{v}] {msg}",
|
||||
)
|
||||
self.loaded_db_version = migration.version
|
||||
try:
|
||||
self._set_version(session, DB_VERSION_CURRENT_KEY, migration.version)
|
||||
logger.info(
|
||||
f"[Library][Migration][{migration.version}] Completed DB Migration"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
f"[Library][Migration][{migration.version}] "
|
||||
@@ -131,10 +122,12 @@ class DBMigrations:
|
||||
session.flush()
|
||||
else:
|
||||
session.commit()
|
||||
logger.info(f"[Library][Migration][{migration.version}] Completed DB Migration")
|
||||
|
||||
assert self.loaded_db_version >= DB_VERSION, (
|
||||
"Ran all migrations, but the DB is still not on the newest version"
|
||||
)
|
||||
logger.info(f"[Library][Migration] Library migrated to DB version {DB_VERSION}")
|
||||
|
||||
def _set_version(self, session: Session, key: str, value: int) -> None:
|
||||
"""Set a version value to the DB.
|
||||
@@ -153,7 +146,7 @@ class MigrationTo7(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB from DB_VERSION 6 to 7."""
|
||||
logger.info(fmt_log("Applying patches to DB_VERSION: 6 library..."))
|
||||
# Repair tags that may have a disambiguation_id pointing towards a deleted tag.
|
||||
@@ -173,7 +166,7 @@ class MigrationTo8(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB from DB_VERSION 7 to 8."""
|
||||
# Add the missing color_border column to the TagColorGroups table.
|
||||
session.execute(
|
||||
@@ -226,7 +219,7 @@ class MigrationTo9(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB from DB_VERSION 8 to 9."""
|
||||
# Apply database schema changes
|
||||
add_filename_column = text(
|
||||
@@ -237,8 +230,9 @@ class MigrationTo9(DBMigration):
|
||||
logger.info(fmt_log("Added filename column to entries table"))
|
||||
|
||||
# Populate the new filename column.
|
||||
entries = session.execute(select(Entry).distinct()).scalars()
|
||||
for entry in entries:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
|
||||
for entry in Library._all_entries(session):
|
||||
entry.filename = entry.path.name
|
||||
session.merge(entry)
|
||||
session.flush()
|
||||
@@ -250,7 +244,7 @@ class MigrationTo100(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB to DB_VERSION 100."""
|
||||
# Repair parent-child tag relationships that are the wrong way around.
|
||||
stmt = update(TagParent).values(
|
||||
@@ -267,7 +261,7 @@ class MigrationTo101(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB to DB_VERSION 101."""
|
||||
# Create versions table
|
||||
session.execute(
|
||||
@@ -290,7 +284,7 @@ class MigrationTo102(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB to DB_VERSION 102."""
|
||||
# delete TagParents with a dangling parent reference
|
||||
stmt = delete(TagParent).where(TagParent.parent_id.not_in(select(Tag.id).distinct()))
|
||||
@@ -304,7 +298,7 @@ class MigrationTo103(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB from DB_VERSION 102 to 103."""
|
||||
# add the new hidden column for tags
|
||||
session.execute(text("ALTER TABLE tags ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT 0"))
|
||||
@@ -322,17 +316,17 @@ class MigrationTo104(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB from DB_VERSION 103 to 104."""
|
||||
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
|
||||
cls.__migrate_sql_to_ts_ignore(session, library)
|
||||
cls.__migrate_sql_to_ts_ignore(session, library_dir)
|
||||
session.execute(text("DROP TABLE preferences"))
|
||||
session.flush()
|
||||
|
||||
@classmethod
|
||||
def __migrate_sql_to_ts_ignore(cls, session: Session, library: "Library"):
|
||||
def __migrate_sql_to_ts_ignore(cls, session: Session, library_dir: Path):
|
||||
# Do not continue if existing '.ts_ignore' file is found
|
||||
ts_ignore = unwrap(library.library_dir) / TS_FOLDER_NAME / IGNORE_NAME
|
||||
ts_ignore = library_dir / TS_FOLDER_NAME / IGNORE_NAME
|
||||
if Path(ts_ignore).exists():
|
||||
return
|
||||
|
||||
@@ -355,7 +349,7 @@ class MigrationTo200(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB to DB_VERSION 200."""
|
||||
# Drop unused 'boolean_fields' and 'value_type' tables
|
||||
logger.info(fmt_log("Dropping boolean_fields and value_type tables..."))
|
||||
@@ -469,7 +463,7 @@ class MigrationTo201(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB to DB_VERSION 201."""
|
||||
create_text_fields_table = text("""
|
||||
CREATE TABLE text_fields_new (
|
||||
@@ -525,7 +519,7 @@ class MigrationTo202(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
"""Migrate DB to DB_VERSION 202."""
|
||||
stmt = delete(TagParent).where(TagParent.child_id.not_in(select(Tag.id).distinct()))
|
||||
session.execute(stmt)
|
||||
@@ -538,7 +532,7 @@ class MigrationTo300(DBMigration):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]):
|
||||
def run(cls, session: Session, library_dir: Path, fmt_log):
|
||||
## remove folder_id column from entries table
|
||||
# create new table in the desired scheme (without folder_id column)
|
||||
session.execute(
|
||||
|
||||
@@ -5,9 +5,10 @@ from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtCore import QPoint, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QLineEdit,
|
||||
QMenu,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
@@ -22,7 +23,8 @@ logger = structlog.get_logger(__name__)
|
||||
class AutofillLineEdit(QLineEdit):
|
||||
return_pressed = Signal()
|
||||
shift_return_pressed = Signal()
|
||||
shift_holding = Signal(bool)
|
||||
holding_shift = Signal(bool)
|
||||
index_updated = Signal(int)
|
||||
|
||||
def __init__(self, popup: QWidget) -> None:
|
||||
super().__init__()
|
||||
@@ -39,23 +41,44 @@ class AutofillLineEdit(QLineEdit):
|
||||
return super().focusInEvent(arg__1)
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, arg__1: QtGui.QKeyEvent) -> None:
|
||||
if arg__1.key() == QtCore.Qt.Key.Key_Shift:
|
||||
self.shift_holding.emit(True) # noqa: FBT003
|
||||
def event(self, arg__1: QtCore.QEvent) -> bool:
|
||||
if arg__1.type() == QtCore.QEvent.Type.KeyPress:
|
||||
assert isinstance(arg__1, QtGui.QKeyEvent)
|
||||
|
||||
if arg__1.key() == QtCore.Qt.Key.Key_Escape:
|
||||
self.setText("")
|
||||
self.clearFocus()
|
||||
elif arg__1.key() == QtCore.Qt.Key.Key_Enter or arg__1.key() == QtCore.Qt.Key.Key_Return:
|
||||
if arg__1.modifiers() and QtCore.Qt.KeyboardModifier.ShiftModifier:
|
||||
self.shift_return_pressed.emit()
|
||||
else:
|
||||
self.return_pressed.emit()
|
||||
if arg__1.key() == QtCore.Qt.Key.Key_Tab:
|
||||
self.index_updated.emit(1)
|
||||
return True
|
||||
elif arg__1.key() == QtCore.Qt.Key.Key_Backtab:
|
||||
self.index_updated.emit(-1)
|
||||
return True
|
||||
|
||||
return super().keyPressEvent(arg__1)
|
||||
if arg__1.key() == QtCore.Qt.Key.Key_Shift:
|
||||
self.holding_shift.emit(True) # noqa: FBT003
|
||||
|
||||
if arg__1.key() == QtCore.Qt.Key.Key_Escape:
|
||||
self.setText("")
|
||||
self.clearFocus()
|
||||
elif (
|
||||
arg__1.key() == QtCore.Qt.Key.Key_Enter or arg__1.key() == QtCore.Qt.Key.Key_Return
|
||||
):
|
||||
if arg__1.modifiers() == QtCore.Qt.KeyboardModifier.ShiftModifier:
|
||||
self.shift_return_pressed.emit()
|
||||
else:
|
||||
self.return_pressed.emit()
|
||||
|
||||
return super().event(arg__1)
|
||||
|
||||
@override
|
||||
def keyReleaseEvent(self, arg__1: QtGui.QKeyEvent) -> None:
|
||||
if arg__1.key() == QtCore.Qt.Key.Key_Shift:
|
||||
self.shift_holding.emit(False) # noqa: FBT003
|
||||
self.holding_shift.emit(False) # noqa: FBT003
|
||||
return super().keyReleaseEvent(arg__1)
|
||||
|
||||
def show_action_menu(self, pos: QPoint) -> None:
|
||||
"""Show a context menu of actions."""
|
||||
menu = QMenu(self)
|
||||
for action in self.actions():
|
||||
# Filter out icon action(s)
|
||||
if action.text():
|
||||
menu.addAction(action)
|
||||
menu.exec(self.mapToGlobal(pos)) # pyright: ignore[reportArgumentType]
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtGui import QAction, Qt
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
@@ -30,16 +30,14 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
# Context Menu Actions
|
||||
edit_field_on_add_action = QAction(Translations["settings.edit_field_on_add"], self)
|
||||
edit_field_on_add_action.setCheckable(True)
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.addAction(edit_field_on_add_action)
|
||||
self.layout().search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.layout().search_field.addAction(edit_field_on_add_action)
|
||||
edit_field_on_add_action.setChecked(self._settings.edit_field_on_add)
|
||||
edit_field_on_add_action.triggered.connect(
|
||||
lambda checked: self.toggle_edit_on_field_add(checked)
|
||||
lambda checked: self._toggle_edit_on_field_add(checked)
|
||||
)
|
||||
|
||||
def toggle_edit_on_field_add(self, checked: bool) -> None:
|
||||
def _toggle_edit_on_field_add(self, checked: bool) -> None:
|
||||
"""Toggle the setting for opening the edit window after adding a field."""
|
||||
self._settings.edit_field_on_add = checked
|
||||
self._settings.save()
|
||||
@@ -77,7 +75,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
@override
|
||||
def _on_item_chosen(self, item: BaseFieldTemplate) -> None:
|
||||
self.item_chosen.emit(item)
|
||||
self.done.emit()
|
||||
self.done.emit("*")
|
||||
|
||||
@override
|
||||
def _search_items(self, query: str) -> tuple[list[BaseFieldTemplate], list[BaseFieldTemplate]]:
|
||||
@@ -86,6 +84,24 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
else:
|
||||
return ([], [])
|
||||
|
||||
@override
|
||||
def _on_shift_held(self, held: bool) -> None:
|
||||
if held:
|
||||
self.set_hint_icon(self._rm.hint_field_create)
|
||||
else:
|
||||
self._update_hint_icon()
|
||||
|
||||
return super()._on_shift_held(held)
|
||||
|
||||
@override
|
||||
def _update_hint_icon(self) -> None:
|
||||
if self.layout().search_field.text() and len(self._search_results) > 0:
|
||||
self.set_hint_icon(self._rm.hint_field_add)
|
||||
elif self.layout().search_field.text():
|
||||
self.set_hint_icon(self._rm.hint_field_create)
|
||||
else:
|
||||
self.set_hint_icon(None)
|
||||
|
||||
@override
|
||||
def _set_item_widget(self, item: BaseFieldTemplate | None, index: int) -> None:
|
||||
"""Set the field template of a field template widget at a specific index."""
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtWidgets import QScrollArea
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class HorizontalScrollArea(QScrollArea):
|
||||
"""A QScrollArea that translates vertical scrolling to horizontal movement."""
|
||||
|
||||
@override
|
||||
def wheelEvent(self, arg__1: QtGui.QWheelEvent) -> None:
|
||||
angle_y = arg__1.angleDelta().y()
|
||||
pixel_y = arg__1.pixelDelta().y()
|
||||
if angle_y != 0 or pixel_y != 0:
|
||||
translated_event = QtGui.QWheelEvent(
|
||||
arg__1.position(),
|
||||
arg__1.globalPosition(),
|
||||
QtCore.QPoint(pixel_y * -1, 0),
|
||||
QtCore.QPoint(angle_y * -1, 0),
|
||||
arg__1.buttons(),
|
||||
arg__1.modifiers(),
|
||||
arg__1.phase(),
|
||||
arg__1.inverted(),
|
||||
)
|
||||
arg__1.accept()
|
||||
return super().wheelEvent(translated_event)
|
||||
else:
|
||||
return super().wheelEvent(arg__1)
|
||||
@@ -49,6 +49,7 @@ class PreviewPanel(QWidget):
|
||||
super().__init__()
|
||||
self._driver = driver
|
||||
self._lib = self._driver.lib
|
||||
self._settings = self._driver.settings
|
||||
self._selected: list[int]
|
||||
self._current_stats: FileAttributeData | None = None
|
||||
|
||||
@@ -75,13 +76,13 @@ class PreviewPanel(QWidget):
|
||||
# Tag Search
|
||||
self.layout().add_tag_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.TAG))
|
||||
self._open_tag_search_action.activated.connect(self._open_tag_search_callback)
|
||||
self.layout().tag_search_box.done.connect(self._tag_added_callback)
|
||||
self.layout().tag_search_box.done.connect(partial(self._tag_added_callback))
|
||||
self.layout().containers.on_tags_update.connect(self._update_added_callback)
|
||||
|
||||
# Field Search
|
||||
self.layout().add_field_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.FIELD))
|
||||
self._open_field_search_action.activated.connect(self._open_field_search_callback)
|
||||
self.layout().field_search_box.done.connect(self._field_added_callback)
|
||||
self.layout().field_search_box.done.connect(partial(self._field_added_callback))
|
||||
|
||||
# Previews
|
||||
self.layout().preview_thumb.stats_updated.connect(self._thumb_stats_updated_callback)
|
||||
@@ -122,13 +123,20 @@ class PreviewPanel(QWidget):
|
||||
self.layout().add_field_button.setFocus()
|
||||
self.layout().add_field_button.click()
|
||||
|
||||
def _tag_added_callback(self):
|
||||
self._set_item_mode(None)
|
||||
self.layout().add_tag_button.setFocus()
|
||||
def _tag_added_callback(self, query: str):
|
||||
if not query or not self._settings.keep_suggest_boxes_open:
|
||||
self._set_item_mode(None)
|
||||
self.layout().add_tag_button.setFocus()
|
||||
else:
|
||||
self._update_added_callback()
|
||||
self.layout().tag_search_box.layout().search_field.setFocus()
|
||||
|
||||
def _field_added_callback(self):
|
||||
self._set_item_mode(None)
|
||||
self.layout().add_field_button.setFocus()
|
||||
def _field_added_callback(self, query: str):
|
||||
if not query or not self._settings.keep_suggest_boxes_open:
|
||||
self._set_item_mode(None)
|
||||
self.layout().add_field_button.setFocus()
|
||||
else:
|
||||
self.layout().field_search_box.layout().search_field.setFocus()
|
||||
|
||||
def _update_added_callback(self):
|
||||
self.layout().tag_search_box.added = self.layout().containers.tags
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from functools import partial
|
||||
from typing import Any, override
|
||||
|
||||
import structlog
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QShowEvent
|
||||
from PySide6.QtGui import QAction, QPixmap, QShowEvent, Qt
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
@@ -13,6 +15,9 @@ from tagstudio.qt.controllers.autofill_line_edit import QtCore, QtGui
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
from tagstudio.qt.helpers.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
autofill_line_edit_style,
|
||||
autofill_line_edit_top_style,
|
||||
@@ -42,7 +47,7 @@ def _item_name(item: object) -> str:
|
||||
|
||||
class SuggestBox[T](QWidget):
|
||||
item_chosen = Signal(object)
|
||||
done = Signal()
|
||||
done = Signal(str) # Query
|
||||
|
||||
def __init__(
|
||||
self, library: Library, settings: GlobalSettings, placeholder_text: str = ""
|
||||
@@ -50,15 +55,30 @@ class SuggestBox[T](QWidget):
|
||||
super().__init__()
|
||||
self._lib = library
|
||||
self._settings = settings
|
||||
self._limit = 5
|
||||
self._rm = ResourceManager()
|
||||
self._limit = 25
|
||||
self._is_shift_held = False
|
||||
self._search_results: list[T] = []
|
||||
self._selection_index = 0
|
||||
self.added: list[int] = []
|
||||
self.excluded: list[int] = []
|
||||
|
||||
self.setLayout(SuggestBoxView(placeholder_text))
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self._connect_callbacks()
|
||||
|
||||
self._keep_box_open_action = QAction(Translations["settings.keep_suggest_boxes_open"], self)
|
||||
self._keep_box_open_action.setCheckable(True)
|
||||
self.addAction(self._keep_box_open_action)
|
||||
self.layout().search_field.addAction(self._keep_box_open_action)
|
||||
self._keep_box_open_action.triggered.connect(
|
||||
lambda checked: self._toggle_keep_open(checked)
|
||||
)
|
||||
|
||||
def _toggle_keep_open(self, checked: bool) -> None:
|
||||
self._settings.keep_suggest_boxes_open = checked
|
||||
self._settings.save()
|
||||
|
||||
def set_placeholder_text(self, text: str) -> None:
|
||||
self.layout().search_field.setPlaceholderText(text)
|
||||
|
||||
@@ -79,24 +99,69 @@ class SuggestBox[T](QWidget):
|
||||
)
|
||||
)
|
||||
|
||||
self.layout().search_field.shift_holding.connect(lambda held: self._on_shift_held(held))
|
||||
self.layout().search_field.holding_shift.connect(partial(self._on_shift_held))
|
||||
self.layout().search_field.index_updated.connect(partial(self._on_index_updated))
|
||||
|
||||
def set_hint_icon(self, icon: Image.Image | None) -> None:
|
||||
if icon:
|
||||
pixmap = QPixmap.fromImage(ImageQt.ImageQt(auto_theme_overlay(icon)))
|
||||
self.layout().hint_icon_action.setIcon(pixmap)
|
||||
else:
|
||||
self.layout().hint_icon_action.setIcon(QPixmap())
|
||||
|
||||
def _on_shift_held(self, held: bool) -> None:
|
||||
if held:
|
||||
self._is_shift_held = True
|
||||
opacity_effect = QGraphicsOpacityEffect(self)
|
||||
opacity_effect.setOpacity(0.3)
|
||||
if self.layout().content_layout.count() > 0:
|
||||
underlined_widget = self.layout().content_layout.itemAt(0).widget()
|
||||
assert isinstance(underlined_widget, UnderlinedWidget)
|
||||
for i in range(0, self.layout().content_layout.count()):
|
||||
underlined_widget = self.layout().content_layout.itemAt(i).widget()
|
||||
assert isinstance(underlined_widget, UnderlinedWidget)
|
||||
|
||||
if held and i == self._selection_index:
|
||||
self._is_shift_held = True
|
||||
opacity_effect = QGraphicsOpacityEffect(self)
|
||||
opacity_effect.setOpacity(0.3)
|
||||
underlined_widget.widget.setGraphicsEffect(opacity_effect)
|
||||
else:
|
||||
self._is_shift_held = False
|
||||
if self.layout().content_layout.count() > 0:
|
||||
underlined_widget = self.layout().content_layout.itemAt(0).widget()
|
||||
assert isinstance(underlined_widget, UnderlinedWidget)
|
||||
elif (
|
||||
len(self._search_results) > i
|
||||
and _item_id(self._search_results[i]) not in self.added
|
||||
):
|
||||
self._is_shift_held = False
|
||||
underlined_widget.widget.setGraphicsEffect(None) # pyright: ignore[reportArgumentType]
|
||||
|
||||
def _on_index_updated(self, delta: int) -> None:
|
||||
# Initialize the widget count (non-hidden)
|
||||
widget_count = 0
|
||||
for i in range(0, self.layout().content_layout.count()):
|
||||
widget = self.layout().content_layout.itemAt(i).widget()
|
||||
if not widget.isHidden():
|
||||
widget_count += 1
|
||||
|
||||
# Update the index
|
||||
old_idx = self._selection_index
|
||||
max_idx = widget_count - 1
|
||||
if self._selection_index + delta < 0:
|
||||
# Can't move further left
|
||||
self._selection_index = 0
|
||||
elif self._selection_index + delta > max_idx:
|
||||
self._selection_index = max_idx
|
||||
else:
|
||||
self._selection_index = self._selection_index + delta
|
||||
|
||||
# Don't update the UI if there's no index change
|
||||
if old_idx == self._selection_index:
|
||||
return
|
||||
|
||||
# Draw the correct underline for the selected widget
|
||||
for i in range(0, widget_count):
|
||||
underlined_widget = self.layout().content_layout.itemAt(i).widget()
|
||||
assert isinstance(underlined_widget, UnderlinedWidget)
|
||||
if i == self._selection_index:
|
||||
underlined_widget.toggle_underline(is_hidden=False)
|
||||
self.layout().scroll_area.ensureWidgetVisible(
|
||||
underlined_widget, xmargin=24, ymargin=0
|
||||
)
|
||||
else:
|
||||
underlined_widget.toggle_underline(is_hidden=True)
|
||||
self._update_hint_icon()
|
||||
|
||||
def _clear_search_query(self) -> None:
|
||||
self.layout().search_field.setText("")
|
||||
|
||||
@@ -104,14 +169,13 @@ class SuggestBox[T](QWidget):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_search_query_changed(self, query: str) -> None:
|
||||
self._update_items(query)
|
||||
self._update_items(query.strip())
|
||||
self._update_hint_icon()
|
||||
|
||||
def _on_search_query_submitted(self, query: str, always_create: bool = False) -> None:
|
||||
# Focus search field if no query
|
||||
logger.info("Query submitted")
|
||||
if not query:
|
||||
self.done.emit()
|
||||
self.hide_and_reset()
|
||||
self.done.emit(query)
|
||||
return
|
||||
elif not self.isHidden():
|
||||
self.layout().search_field.setFocus()
|
||||
@@ -120,7 +184,7 @@ class SuggestBox[T](QWidget):
|
||||
if (len(self._search_results) <= 0) or always_create:
|
||||
self._on_item_create()
|
||||
else:
|
||||
self._on_item_chosen(self._search_results[0])
|
||||
self._on_item_chosen(self._search_results[self._selection_index])
|
||||
|
||||
self._clear_search_query()
|
||||
self._update_items()
|
||||
@@ -137,9 +201,23 @@ class SuggestBox[T](QWidget):
|
||||
def _is_excluded(self, item: T) -> bool:
|
||||
return _item_id(item) in self.excluded
|
||||
|
||||
def _is_selected_item_added(self) -> bool:
|
||||
return bool(
|
||||
len(self._search_results) > self._selection_index
|
||||
and _item_id(self._search_results[self._selection_index]) in self.added
|
||||
)
|
||||
|
||||
def _update_hint_icon(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _update_items(self, query: str | None = None) -> None:
|
||||
"""Update the item list given a search query."""
|
||||
logger.info("[SearchPanel] Updating items", limit=self._limit)
|
||||
self._selection_index = 0
|
||||
if self.layout().content_layout.count() > 0:
|
||||
self.layout().scroll_area.ensureWidgetVisible(
|
||||
self.layout().content_layout.itemAt(0).widget(), xmargin=24, ymargin=0
|
||||
)
|
||||
|
||||
# Get results for the search query
|
||||
query_lower = "" if not query else query.lower()
|
||||
@@ -187,7 +265,7 @@ class SuggestBox[T](QWidget):
|
||||
self.layout().search_field.setStyleSheet(autofill_line_edit_style())
|
||||
else:
|
||||
self.layout().scroll_area.setHidden(False)
|
||||
self.layout().content_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.layout().content_layout.setContentsMargins(4, 6, 4, 6)
|
||||
self.layout().search_field.setStyleSheet(autofill_line_edit_top_style())
|
||||
|
||||
def _search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter]
|
||||
@@ -197,9 +275,10 @@ class SuggestBox[T](QWidget):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _editing_finished_callback(self) -> None:
|
||||
if self.layout().search_field.text() == "":
|
||||
self.done.emit()
|
||||
self.hide_and_reset()
|
||||
# Only trigger when the search field is clicked off of and there's no query.
|
||||
# NOTE: The search field is already cleared by this point when pressing enter.
|
||||
if self.layout().search_field.text() == "" and not self.layout().search_field.hasFocus():
|
||||
self.done.emit("")
|
||||
|
||||
def _create_item_from_modal(self, edit_item_panel: ModalContent) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
@@ -213,6 +292,7 @@ class SuggestBox[T](QWidget):
|
||||
self._on_shift_held(held=False)
|
||||
self.layout().search_field.setDisabled(False)
|
||||
self._clear_search_query()
|
||||
self._keep_box_open_action.setChecked(self._settings.keep_suggest_boxes_open)
|
||||
return super().showEvent(event)
|
||||
|
||||
@override
|
||||
@@ -222,9 +302,7 @@ class SuggestBox[T](QWidget):
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
||||
# When Escape is pressed, focus back on the search box.
|
||||
if event.key() in {
|
||||
QtCore.Qt.Key.Key_Escape,
|
||||
QtCore.Qt.Key.Key_Enter,
|
||||
QtCore.Qt.Key.Key_Return,
|
||||
}:
|
||||
self.hide_and_reset()
|
||||
if event.key() in {QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return}:
|
||||
self.done.emit("*")
|
||||
elif event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
self.done.emit("")
|
||||
|
||||
@@ -7,7 +7,7 @@ from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QAction, Qt
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
@@ -33,18 +33,17 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
super().__init__(library, settings, placeholder_text)
|
||||
|
||||
# Context Menu Actions
|
||||
|
||||
edit_tag_on_create_action = QAction(Translations["settings.edit_tag_on_create"], self)
|
||||
edit_tag_on_create_action.setCheckable(True)
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.addAction(edit_tag_on_create_action)
|
||||
self.layout().search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.layout().search_field.addAction(edit_tag_on_create_action)
|
||||
edit_tag_on_create_action.setChecked(self._settings.edit_tag_on_create)
|
||||
edit_tag_on_create_action.triggered.connect(
|
||||
lambda checked: self.toggle_edit_on_tag_create(checked)
|
||||
lambda checked: self._toggle_edit_on_tag_create(checked)
|
||||
)
|
||||
|
||||
def toggle_edit_on_tag_create(self, checked: bool) -> None:
|
||||
def _toggle_edit_on_tag_create(self, checked: bool) -> None:
|
||||
"""Toggle the setting for opening the edit window after creating a tag."""
|
||||
self._settings.edit_tag_on_create = checked
|
||||
self._settings.save()
|
||||
@@ -89,7 +88,33 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
@override
|
||||
def _on_item_chosen(self, item: Tag) -> None:
|
||||
self.item_chosen.emit(item.id)
|
||||
self.done.emit()
|
||||
self._clear_search_query()
|
||||
self.done.emit("*") # The query does not matter
|
||||
|
||||
@override
|
||||
def _on_shift_held(self, held: bool) -> None:
|
||||
# Bypass normal _update_hint_icon() behavior first
|
||||
if not held and self._is_selected_item_added():
|
||||
self.set_hint_icon(self._rm.hint_tag_added)
|
||||
elif held:
|
||||
self.set_hint_icon(self._rm.hint_tag_create)
|
||||
else:
|
||||
self._update_hint_icon()
|
||||
|
||||
return super()._on_shift_held(held)
|
||||
|
||||
@override
|
||||
def _update_hint_icon(self) -> None:
|
||||
results = bool(len(self._search_results) > 0)
|
||||
|
||||
if results and self._is_selected_item_added():
|
||||
self.set_hint_icon(self._rm.hint_tag_added)
|
||||
elif results:
|
||||
self.set_hint_icon(self._rm.hint_tag_add)
|
||||
elif self.layout().search_field.text():
|
||||
self.set_hint_icon(self._rm.hint_tag_create)
|
||||
else:
|
||||
self.set_hint_icon(None)
|
||||
|
||||
@override
|
||||
def _search_items(self, query: str) -> tuple[list[Tag], list[Tag]]:
|
||||
@@ -117,7 +142,7 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
if item is None:
|
||||
return
|
||||
|
||||
# TODO: Add tabbing to different items, and use underline to indicate which will be added
|
||||
# Select first item
|
||||
underlined_widget.toggle_underline(index != 0)
|
||||
|
||||
# Disconnect previous callbacks
|
||||
@@ -144,7 +169,6 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases
|
||||
)
|
||||
self._on_item_chosen(tag)
|
||||
self._clear_search_query()
|
||||
|
||||
edit_item_panel.hide()
|
||||
self._on_search_query_changed(self.layout().search_field.text())
|
||||
|
||||
@@ -76,6 +76,7 @@ class GlobalSettings(BaseModel):
|
||||
tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT)
|
||||
edit_tag_on_create: bool = Field(default=False)
|
||||
edit_field_on_add: bool = Field(default=True)
|
||||
keep_suggest_boxes_open: bool = Field(default=True)
|
||||
theme: Theme = Field(default=Theme.SYSTEM)
|
||||
splash: Splash = Field(default=Splash.DEFAULT)
|
||||
windows_start_command: bool = Field(default=False)
|
||||
|
||||
@@ -26,6 +26,7 @@ from tagstudio.core.constants import (
|
||||
GITHUB_REPO_URL,
|
||||
VERSION,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.constants import DB_VERSION
|
||||
from tagstudio.core.ts_core import TagStudioCore
|
||||
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
|
||||
from tagstudio.core.utils.ripgrep_status import RipgrepStatus
|
||||
@@ -109,22 +110,28 @@ class AboutModal(QWidget):
|
||||
self.system_info_layout.setSpacing(4)
|
||||
self.system_info_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
# Version
|
||||
version_title = QLabel(Translations["about.version"])
|
||||
latest_version = unwrap(TagStudioCore.get_most_recent_release_version(), "0.0.0")
|
||||
version_content_style = form_content_style()
|
||||
if not is_version_outdated(VERSION, latest_version):
|
||||
version_content = QLabel(f"{VERSION}")
|
||||
# TagStudio Version
|
||||
ts_version_title = QLabel(Translations["about.version"])
|
||||
latest_ts_version = unwrap(TagStudioCore.get_most_recent_release_version(), "0.0.0")
|
||||
ts_version_content_style = form_content_style()
|
||||
if not is_version_outdated(VERSION, latest_ts_version):
|
||||
ts_version_content = QLabel(f"{VERSION}")
|
||||
else:
|
||||
version_content = QLabel(
|
||||
ts_version_content = QLabel(
|
||||
Translations.format(
|
||||
"about.version.latest", built_version=VERSION, latest_version=latest_version
|
||||
"about.version.latest", built_version=VERSION, latest_version=latest_ts_version
|
||||
)
|
||||
)
|
||||
version_content_style += f"color: {red};"
|
||||
version_content.setStyleSheet(version_content_style)
|
||||
self.system_info_layout.addRow(version_title, version_content)
|
||||
version_content.setMaximumWidth(version_content.sizeHint().width())
|
||||
ts_version_content_style += f"color: {red};"
|
||||
ts_version_content.setStyleSheet(ts_version_content_style)
|
||||
self.system_info_layout.addRow(ts_version_title, ts_version_content)
|
||||
ts_version_content.setMaximumWidth(ts_version_content.sizeHint().width())
|
||||
|
||||
# Library DB Version
|
||||
db_version_title = QLabel(Translations["about.library_version"])
|
||||
db_version_content = QLabel(f"{DB_VERSION}")
|
||||
db_version_content.setStyleSheet(form_content_style())
|
||||
self.system_info_layout.addRow(db_version_title, db_version_content)
|
||||
|
||||
# Config Path
|
||||
config_path_title = QLabel(f"{Translations['about.config_path']}")
|
||||
|
||||
@@ -231,6 +231,15 @@ class SettingsPanel(ModalContent):
|
||||
Translations["settings.edit_field_on_add"], self.edit_field_on_add_checkbox
|
||||
)
|
||||
|
||||
# Keep Tag/Field Suggest Boxes Open After Adding Items
|
||||
self.keep_suggest_boxes_open_checkbox = QCheckBox()
|
||||
self.keep_suggest_boxes_open_checkbox.setChecked(
|
||||
self.driver.settings.keep_suggest_boxes_open
|
||||
)
|
||||
form_layout.addRow(
|
||||
Translations["settings.keep_suggest_boxes_open"], self.keep_suggest_boxes_open_checkbox
|
||||
)
|
||||
|
||||
# TODO: Implement Library Settings
|
||||
def __build_library_settings(self): # pyright: ignore[reportUnusedFunction]
|
||||
form_layout = QFormLayout(self.library_settings_container)
|
||||
@@ -383,6 +392,7 @@ class SettingsPanel(ModalContent):
|
||||
"tag_click_action": self.tag_click_action_combobox.currentData(),
|
||||
"edit_tag_on_create": self.edit_tag_on_create_checkbox.isChecked(),
|
||||
"edit_field_on_add": self.edit_field_on_add_checkbox.isChecked(),
|
||||
"keep_suggest_boxes_open": self.keep_suggest_boxes_open_checkbox.isChecked(),
|
||||
"date_format": self.dateformat_combobox.currentData(),
|
||||
"hour_format": self.hourformat_checkbox.isChecked(),
|
||||
"zero_padding": self.zeropadding_checkbox.isChecked(),
|
||||
@@ -407,6 +417,7 @@ class SettingsPanel(ModalContent):
|
||||
driver.settings.tag_click_action = settings["tag_click_action"]
|
||||
driver.settings.edit_tag_on_create = settings["edit_tag_on_create"]
|
||||
driver.settings.edit_field_on_add = settings["edit_field_on_add"]
|
||||
driver.settings.keep_suggest_boxes_open = settings["keep_suggest_boxes_open"]
|
||||
driver.settings.date_format = settings["date_format"]
|
||||
driver.settings.hour_format = settings["hour_format"]
|
||||
driver.settings.zero_padding = settings["zero_padding"]
|
||||
|
||||
@@ -68,7 +68,7 @@ class TagColorManager(QWidget):
|
||||
self.scroll_layout = QVBoxLayout()
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.scroll_layout.setContentsMargins(3, 3, 3, 3)
|
||||
self.scroll_layout.setSpacing(0)
|
||||
self.scroll_layout.setSpacing(6)
|
||||
|
||||
scroll_container: QWidget = QWidget()
|
||||
scroll_container.setObjectName("entryScrollContainer")
|
||||
|
||||
@@ -36,6 +36,11 @@ class ResourceManager:
|
||||
edit: Image.Image
|
||||
file_generic: Image.Image
|
||||
font: Image.Image
|
||||
hint_field_add: Image.Image
|
||||
hint_field_create: Image.Image
|
||||
hint_tag_add: Image.Image
|
||||
hint_tag_added: Image.Image
|
||||
hint_tag_create: Image.Image
|
||||
icon: Image.Image
|
||||
ignored_stat: Image.Image
|
||||
ignored: Image.Image
|
||||
|
||||
@@ -71,6 +71,26 @@
|
||||
"mode": "pil",
|
||||
"path": "qt/images/file_icons/font.png"
|
||||
},
|
||||
"hint_field_add": {
|
||||
"mode": "pil",
|
||||
"path": "qt/images/hint_field_add.png"
|
||||
},
|
||||
"hint_field_create": {
|
||||
"mode": "pil",
|
||||
"path": "qt/images/hint_field_create.png"
|
||||
},
|
||||
"hint_tag_add": {
|
||||
"mode": "pil",
|
||||
"path": "qt/images/hint_tag_add.png"
|
||||
},
|
||||
"hint_tag_added": {
|
||||
"mode": "pil",
|
||||
"path": "qt/images/hint_tag_added.png"
|
||||
},
|
||||
"hint_tag_create": {
|
||||
"mode": "pil",
|
||||
"path": "qt/images/hint_tag_create.png"
|
||||
},
|
||||
"icon": {
|
||||
"mode": "pil",
|
||||
"path": "icon.png"
|
||||
|
||||
@@ -849,7 +849,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
logger.info("Backing Up Library...")
|
||||
self.main_window.status_bar.showMessage(Translations["status.library_backup_in_progress"])
|
||||
start_time = time.time()
|
||||
target_path = self.lib.save_library_backup_to_disk()
|
||||
target_path = Library.save_library_backup_to_disk(unwrap(self.lib.library_dir))
|
||||
end_time = time.time()
|
||||
self.main_window.status_bar.showMessage(
|
||||
Translations.format(
|
||||
@@ -1651,8 +1651,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
else:
|
||||
self._init_library(path, open_status)
|
||||
|
||||
def _init_library(self, path: Path, open_status: LibraryStatus, is_test: bool = False):
|
||||
# TODO: Don't have an is_test parameter, the frontend and backend tasks here can be split.
|
||||
def _init_library(self, path: Path, open_status: LibraryStatus):
|
||||
if not open_status.success:
|
||||
self.show_error_message(
|
||||
error_name=open_status.message
|
||||
@@ -1662,8 +1661,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
return open_status
|
||||
|
||||
assert self.lib.library_dir
|
||||
if not is_test:
|
||||
self.init_workers()
|
||||
self.init_workers()
|
||||
Ignore.get_patterns(self.lib.library_dir, include_global=True)
|
||||
self.__reset_navigation()
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QScrollArea, QSizePolicy, QVBoxLayout, QWidget
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.controllers.autofill_line_edit import AutofillLineEdit
|
||||
from tagstudio.qt.controllers.horizontal_scroll_area import HorizontalScrollArea
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
autofill_line_edit_style,
|
||||
autofill_scroll_top_style,
|
||||
@@ -18,18 +20,13 @@ logger = structlog.get_logger(__name__)
|
||||
class SuggestBoxView(QVBoxLayout):
|
||||
def __init__(self, placeholder_text: str = "") -> None:
|
||||
super().__init__()
|
||||
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.setSpacing(0)
|
||||
|
||||
# HACK: The transparent border allows for the focus border color to
|
||||
# still show above the tags at the edges... sort of (overlaps on left when h-scrolling)
|
||||
scroll_area_style = """
|
||||
QScrollArea{
|
||||
background: transparent;
|
||||
border: solid;
|
||||
border-color: transparent;
|
||||
border-width: 0px 2px;
|
||||
padding-left: -2px;
|
||||
}
|
||||
QScrollArea > QWidget > QWidget{
|
||||
background: transparent;
|
||||
@@ -48,8 +45,9 @@ class SuggestBoxView(QVBoxLayout):
|
||||
scroll_area_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
scroll_area_container_layout.setSpacing(0)
|
||||
scroll_area_container.setStyleSheet(autofill_scroll_top_style("container"))
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area = HorizontalScrollArea()
|
||||
self.scroll_area.setStyleSheet(scroll_area_style)
|
||||
self.scroll_area.setViewportMargins(2, 0, 2, 0)
|
||||
scroll_area_container_layout.addWidget(self.scroll_area)
|
||||
self.scroll_area.setWidget(contents)
|
||||
search_bar_height = 28
|
||||
@@ -68,9 +66,14 @@ class SuggestBoxView(QVBoxLayout):
|
||||
self.search_field = AutofillLineEdit(scroll_area_container)
|
||||
self.search_field.setStyleSheet(autofill_line_edit_style())
|
||||
self.search_field.setObjectName("search_field")
|
||||
self.search_field.setMinimumHeight(28)
|
||||
self.search_field.setMinimumHeight(search_bar_height)
|
||||
self.search_field.setPlaceholderText(placeholder_text)
|
||||
self.hint_icon_action = self.search_field.addAction(
|
||||
QPixmap(), AutofillLineEdit.ActionPosition.TrailingPosition
|
||||
)
|
||||
self.scroll_area.setFocusProxy(self.search_field)
|
||||
self.search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.search_field.customContextMenuRequested.connect(self.search_field.show_action_menu)
|
||||
|
||||
# Finalize Layout
|
||||
self.addWidget(scroll_area_container)
|
||||
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -9,6 +9,7 @@
|
||||
"about.version": "Version",
|
||||
"about.version.latest": "{built_version} (Latest Release: {latest_version})",
|
||||
"about.website": "Website",
|
||||
"about.library_version": "Library Format",
|
||||
"app.git": "Git Commit",
|
||||
"app.nightly": "Nightly",
|
||||
"app.pre_release": "Pre-Release",
|
||||
@@ -167,7 +168,7 @@
|
||||
"home.search_or_create_fields": "Search or Create Fields…",
|
||||
"home.search_or_create_tags": "Search or Create Tags…",
|
||||
"home.search_tags": "Search Tags…",
|
||||
"home.search.how_to_exit": "(Esc to Exit)",
|
||||
"home.search.how_to_exit": "(Enter to Exit)",
|
||||
"home.search.view_limit": "View Limit:",
|
||||
"home.show_hidden_entries": "Show Hidden Entries",
|
||||
"home.thumbnail_size": "Thumbnail Size",
|
||||
@@ -329,6 +330,7 @@
|
||||
"settings.global": "Global Settings",
|
||||
"settings.hourformat.label": "24-Hour Time",
|
||||
"settings.infinite_scroll": "Infinite Scrolling",
|
||||
"settings.keep_suggest_boxes_open": "Keep Search Boxes Open After Adding Items",
|
||||
"settings.language": "Language",
|
||||
"settings.library": "Library Settings",
|
||||
"settings.localization": "Localization",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"about.version.latest": "{built_version} (Dernière version : {latest_version})",
|
||||
"about.website": "Site Internet",
|
||||
"app.git": "Git Commit",
|
||||
"app.nightly": "Version de développement",
|
||||
"app.pre_release": "Version Préliminaire",
|
||||
"app.title": "{base_title} - Bibliothèque '{library_dir}'",
|
||||
"color.color_border": "Utiliser la couleur secondaire sur la bordure",
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"color.primary": "צבע ראשי",
|
||||
"color.secondary": "צבע משני",
|
||||
"color.title.no_color": "ללא צבע",
|
||||
"color_manager.title": "שנה צבע תגית"
|
||||
"color_manager.title": "נהל צבעיי תגיות",
|
||||
"window.title.open_create_library": "פתח/צור ספרייה"
|
||||
}
|
||||
|
||||
@@ -161,11 +161,14 @@
|
||||
"generic.skip_alt": "&Kihagyás",
|
||||
"generic.yes": "Igen",
|
||||
"home.search": "Keresés",
|
||||
"home.search.how_to_exit": "(Kilépés az Esc billentyűvel)",
|
||||
"home.search.view_limit": "Megtekintési korlát:",
|
||||
"home.search_entries": "Tételek keresése",
|
||||
"home.search_field_templates": "Keresés a mezőminták között",
|
||||
"home.search_field_templates": "Keresés a mezőminták között…",
|
||||
"home.search_library": "Keresés a könyvtárban",
|
||||
"home.search_tags": "Címkék keresése",
|
||||
"home.search_or_create_fields": "Mezők keresése/létrehozása…",
|
||||
"home.search_or_create_tags": "Címkék keresése/létrehozása…",
|
||||
"home.search_tags": "Címkék keresése…",
|
||||
"home.show_hidden_entries": "Rejtett elemel megjelenítése",
|
||||
"home.thumbnail_size": "Miniatűrök mérete",
|
||||
"home.thumbnail_size.extra_large": "Extra nagy miniatűrök",
|
||||
@@ -316,6 +319,8 @@
|
||||
"settings.dateformat.international": "Nemzetközi",
|
||||
"settings.dateformat.label": "Dátumformátum",
|
||||
"settings.dateformat.system": "Rendszer",
|
||||
"settings.edit_field_on_add": "Mező szerkesztése létrehozás után",
|
||||
"settings.edit_tag_on_create": "Címke szerkesztése létrehozás után",
|
||||
"settings.filepath.label": "&Elérési utak láthatósága",
|
||||
"settings.filepath.option.full": "Teljes elérési út megjelenítése",
|
||||
"settings.filepath.option.name": "Csak a fájlnév megjelenítése",
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"about.app_cache_path": "アプリ キャッシュのパス",
|
||||
"about.config_path": "設定ファイルのパス",
|
||||
"about.description": "TagStudio は、タグベースのシステムに基づく、写真とファイルの整理アプリです。独自のプログラムやフォーマットは使用せず、サイドカーファイルが大量に生成されることもありません。ファイルシステム全体に大きな変更を加えることなく、ユーザーに自由で柔軟な運用を提供します。",
|
||||
"about.documentation": "ドキュメント",
|
||||
"about.module.found": "インストール済み",
|
||||
"about.modules.title": "オプション モジュール",
|
||||
"about.title": "TagStudio について",
|
||||
"about.version": "バージョン",
|
||||
"about.version.latest": "{built_version} (最新リリース: {latest_version})",
|
||||
"about.website": "ウェブサイト",
|
||||
"app.git": "Git コミット",
|
||||
"app.nightly": "ナイトリー",
|
||||
"app.pre_release": "公開前",
|
||||
"app.title": "{base_title} - ライブラリ '{library_dir}'",
|
||||
"color.color_border": "境界線にアクセントカラーを使う",
|
||||
@@ -26,7 +31,7 @@
|
||||
"drop_import.description": "次のファイルは、ライブラリ内にすでに存在するファイル パスと一致しています",
|
||||
"drop_import.duplicates_choice.plural": "以下の {count} 件のファイルは、ライブラリ内にすでに存在するファイル パスと一致しています。",
|
||||
"drop_import.duplicates_choice.singular": "次のファイルは、ライブラリ内にすでに存在するファイル パスと一致しています。",
|
||||
"drop_import.progress.label.initial": "新しいファイルをインポートしています...",
|
||||
"drop_import.progress.label.initial": "新しいファイルをインポートしています…",
|
||||
"drop_import.progress.label.plural": "新しいファイルをインポートしています...\n{count} 件のファイルをインポートしました。{suffix}",
|
||||
"drop_import.progress.label.singular": "新しいファイルをインポートしています...\n1 件のファイルをインポートしました。{suffix}",
|
||||
"drop_import.progress.window_title": "ファイルをインポート",
|
||||
@@ -36,26 +41,26 @@
|
||||
"edit.paste_fields": "フィールドを貼り付け",
|
||||
"edit.tag_manager": "タグを管理",
|
||||
"entries.duplicate.merge": "重複エントリをマージ",
|
||||
"entries.duplicate.merge.label": "重複エントリを統合しています...",
|
||||
"entries.duplicate.merge.label": "重複エントリを統合しています…",
|
||||
"entries.duplicate.refresh": "重複エントリを最新の状態にする",
|
||||
"entries.duplicates.description": "重複エントリとは、ディスク上の同じファイルを指す複数のエントリを指します。これらをマージすると、すべての重複エントリのタグとメタデータが1つのまとまったエントリに統合されます。TagStudio の外部にあるファイル自体の複製である「重複ファイル」と混同しないようにご注意ください。",
|
||||
"entries.generic.refresh_alt": "最新の情報に更新(&R)",
|
||||
"entries.generic.remove.removing": "エントリの削除",
|
||||
"entries.generic.remove.removing_count": "{count} 個のエントリを削除しています...",
|
||||
"entries.generic.remove.removing_count": "{count} 個のエントリを削除しています…",
|
||||
"entries.ignored.description": "「無視」されたエントリとは、ユーザーの無視ルール(“.ts_ignore” ファイル)を更新して対象外にした後も、更新前にライブラリへ追加されていたため残っている項目を指します。無視ルールを更新した際の誤削除によるデータ損失を防ぐため、既定ではこれらのファイルはライブラリに保持されます。",
|
||||
"entries.ignored.ignored_count": "無視されたエントリ: {count}",
|
||||
"entries.ignored.remove": "無視されたエントリを削除",
|
||||
"entries.ignored.remove_alt": "無視されたエントリを削除(&V)",
|
||||
"entries.ignored.scanning": "無視されたエントリをライブラリ内でスキャンしています...",
|
||||
"entries.ignored.scanning": "無視されたエントリをライブラリ内でスキャンしています…",
|
||||
"entries.ignored.title": "無視されたエントリの修正",
|
||||
"entries.mirror": "ミラー(&M)",
|
||||
"entries.mirror.confirmation": "以下の {count} 件のエントリをミラーリングしてもよろしいですか?",
|
||||
"entries.mirror.label": "{total} 件中 {idx} 件のエントリをミラーリングしています...",
|
||||
"entries.mirror.label": "{total} 件中 {idx} 件のエントリをミラーリングしています…",
|
||||
"entries.mirror.title": "エントリをミラー",
|
||||
"entries.mirror.window_title": "エントリをミラー",
|
||||
"entries.remove.plural.confirm": "これら <b>{count}</b> 件のエントリをライブラリから削除しますか? ディスク上のファイルは削除されません。",
|
||||
"entries.remove.singular.confirm": "このエントリをライブラリから削除しますか? ディスク上のファイルは削除されません。",
|
||||
"entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています...",
|
||||
"entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています…",
|
||||
"entries.running.dialog.title": "新しいファイルエントリを追加",
|
||||
"entries.tags": "タグ",
|
||||
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルが TagStudio 以外で移動または削除された場合、そのエントリはリンク切れとして扱われます。<br><br>リンク切れのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
|
||||
@@ -64,18 +69,34 @@
|
||||
"entries.unlinked.relink.title": "エントリの再リンク",
|
||||
"entries.unlinked.remove": "リンク切れのエントリを削除",
|
||||
"entries.unlinked.remove_alt": "リンク切れのエントリを削除(&V)",
|
||||
"entries.unlinked.scanning": "リンク切れのエントリをライブラリ内でスキャンしています...",
|
||||
"entries.unlinked.scanning": "リンク切れのエントリをライブラリ内でスキャンしています…",
|
||||
"entries.unlinked.search_and_relink": "検索して再リンク(&S)",
|
||||
"entries.unlinked.title": "リンク切れのエントリを修正",
|
||||
"entries.unlinked.unlinked_count": "リンク切れのエントリ数: {count}",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.add": "フィールドの追加",
|
||||
"field.add.plural": "フィールドの追加",
|
||||
"field.confirm_remove": "「{name}」フィールドを削除してもよろしいですか?",
|
||||
"field.copy": "フィールドをコピー",
|
||||
"field.edit": "フィールドを編集",
|
||||
"field.field_name_required": "フィールドの名前 (必須)",
|
||||
"field.mixed_data": "混在データ",
|
||||
"field.name": "名前",
|
||||
"field.paste": "フィールドを貼り付け",
|
||||
"field.remove": "フィールドの削除",
|
||||
"field.text.is_multiline": "複数行",
|
||||
"field.type": "種類",
|
||||
"field_template.all_field_templates": "全てのフィールド テンプレート",
|
||||
"field_template.confirm_delete": "フィールド テンプレート「{field_template_name}」を削除してもよろしいですか?",
|
||||
"field_template.create": "フィールド テンプレートを作成",
|
||||
"field_template.create_add": "\"{query}\" を作成して追加",
|
||||
"field_template.delete": "フィールド テンプレートを削除",
|
||||
"field_template.edit": "フィールド テンプレートの編集",
|
||||
"field_template.new": "新しいフィールド テンプレート",
|
||||
"field_template_manager.title": "ライブラリのフィールド テンプレート",
|
||||
"field_type.datetime": "日時",
|
||||
"field_type.text": "テキスト",
|
||||
"field_type.unknown": "不明な種類",
|
||||
"file.date_added": "追加日時",
|
||||
"file.date_created": "作成日時",
|
||||
"file.date_modified": "更新日時",
|
||||
@@ -117,6 +138,7 @@
|
||||
"generic.delete_alt": "削除(&D)",
|
||||
"generic.done": "完了",
|
||||
"generic.done_alt": "完了(&D)",
|
||||
"generic.dont_remind": "次回から表示しない",
|
||||
"generic.edit": "編集",
|
||||
"generic.edit_alt": "編集(&E)",
|
||||
"generic.filename": "ファイル名",
|
||||
@@ -139,10 +161,14 @@
|
||||
"generic.skip_alt": "スキップ(&S)",
|
||||
"generic.yes": "はい",
|
||||
"home.search": "検索",
|
||||
"home.search.how_to_exit": "(Esc で終了)",
|
||||
"home.search.view_limit": "表示件数:",
|
||||
"home.search_entries": "エントリを検索",
|
||||
"home.search_field_templates": "フィールド テンプレートの検索…",
|
||||
"home.search_library": "ライブラリを検索",
|
||||
"home.search_tags": "タグを検索",
|
||||
"home.search_or_create_fields": "フィールドの検索または作成…",
|
||||
"home.search_or_create_tags": "タグの検索または作成…",
|
||||
"home.search_tags": "タグの検索…",
|
||||
"home.show_hidden_entries": "非表示のエントリを表示",
|
||||
"home.thumbnail_size": "サムネイルのサイズ",
|
||||
"home.thumbnail_size.extra_large": "特大サムネイル",
|
||||
@@ -151,8 +177,8 @@
|
||||
"home.thumbnail_size.mini": "極小サムネイル",
|
||||
"home.thumbnail_size.small": "小サムネイル",
|
||||
"ignore.open_file": "ディスク上の \"{ts_ignore}\" ファイルを表示",
|
||||
"json_migration.checking_for_parity": "パリティチェック中...",
|
||||
"json_migration.creating_database_tables": "SQLデータベース テーブルを作成しています...",
|
||||
"json_migration.checking_for_parity": "パリティチェック中…",
|
||||
"json_migration.creating_database_tables": "SQLデータベース テーブルを作成しています…",
|
||||
"json_migration.description": "<br>ライブラリの移行処理を開始し、結果をプレビューします。変換されたライブラリは、 「移行完了」をクリックしない限り<i>使用されません</i>。<br><br>ライブラリ データは、値が一致しているか、「一致」ラベルが表示されている必要があります。 値が一致しない場合は赤色で表示され、その横に「<b>(!)</b>」マークが表示されます。<br><center><i>大規模なライブラリの場合、この処理に数分かかることがあります。</i></center>",
|
||||
"json_migration.discrepancies_found": "ライブラリの差異が見つかりました",
|
||||
"json_migration.discrepancies_found.description": "元のライブラリ形式と変換後の形式との間に差異が見つかりました。内容を確認のうえ、確認して、移行を続行するかキャンセルするかを選択してください。",
|
||||
@@ -160,13 +186,14 @@
|
||||
"json_migration.heading.aliases": "エイリアス:",
|
||||
"json_migration.heading.colors": "色:",
|
||||
"json_migration.heading.differ": "差異",
|
||||
"json_migration.heading.extensions": "拡張子:",
|
||||
"json_migration.heading.match": "一致",
|
||||
"json_migration.heading.names": "名前:",
|
||||
"json_migration.heading.parent_tags": "親タグ:",
|
||||
"json_migration.heading.paths": "パス:",
|
||||
"json_migration.heading.shorthands": "略称:",
|
||||
"json_migration.info.description": "TagStudio バージョン<b>9.4 以前</b>で作成されたライブラリ保存ファイルは、新しいバージョン<b>9.5 以降</b>の形式に移行する必要があります。<br><h2>ご確認ください:</h2><ul><li>既存のライブラリ保存ファイルが<b><i>削除されることはありません</i></b></li><li>個人ファイルが<b><i>削除・移動・変更されることはありません</i></b></li><li>新しい v9.5 以降の保存形式は、旧バージョンの TagStudio では開くことができません</li></ul><h3>変更点:</h3><ul><li>「タグ フィールド」は「タグ カテゴリ」に置き換えられました。従来のようにタグを先にフィールドに追加するのではなく、タグを直接ファイル エントリに追加します。その後、タグ編集メニューで「カテゴリとして扱う」プロパティが有効になっている親タグに基づいて、タグが自動的にカテゴリとして整理されます。どのタグでもカテゴリとして指定でき、カテゴリに指定された親タグの下に子タグが自動で整理されます。「お気に入り」タグおよび「アーカイブ」タグは、新しく追加された「メタタグ」というデフォルトのカテゴリ タグの下に分類されます。</li><li>タグの色が調整・拡張されました。一部の色は名前が変更されたり統合されたりしましたが、すべてのタグの色は v9.5 において同一または類似の色に変換されます。</li></ul>",
|
||||
"json_migration.migrating_files_entries": "{entries} 件のファイル エントリを移行しています...",
|
||||
"json_migration.migrating_files_entries": "{entries} 件のファイル エントリを移行しています…",
|
||||
"json_migration.migration_complete": "移行が完了しました!",
|
||||
"json_migration.migration_complete_with_discrepancies": "移行が完了し、差異が見つかりました",
|
||||
"json_migration.start_and_preview": "開始とプレビュー",
|
||||
@@ -174,13 +201,41 @@
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ ライブラリ</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 ライブラリ</h2>",
|
||||
"landing.open_create_library": "ライブラリを開く/作成する {shortcut}",
|
||||
"language.am": "アムハラ語",
|
||||
"language.ceb": "セブアノ語",
|
||||
"language.cs": "チェコ語",
|
||||
"language.da": "デンマーク語",
|
||||
"language.de": "ドイツ語",
|
||||
"language.el": "ギリシャ語",
|
||||
"language.en": "英語",
|
||||
"language.es": "スペイン語",
|
||||
"language.fi": "フィンランド語",
|
||||
"language.fil": "フィリピノ語",
|
||||
"language.fr": "フランス語",
|
||||
"language.hu": "ハンガリー語",
|
||||
"language.is": "アイスランド語",
|
||||
"language.it": "イタリア語",
|
||||
"language.ja": "日本語",
|
||||
"language.nb_NO": "ノルウェー語 (ブークモール)",
|
||||
"language.nl": "オランダ語",
|
||||
"language.pl": "ポーランド語",
|
||||
"language.pt": "ポルトガル語",
|
||||
"language.pt_BR": "ポルトガル語 (ブラジル)",
|
||||
"language.qpv": "ヴィオッサ語",
|
||||
"language.ro": "ルーマニア語",
|
||||
"language.ru": "ロシア語",
|
||||
"language.sv": "スウェーデン語",
|
||||
"language.ta": "タミル語",
|
||||
"language.th": "タイ語",
|
||||
"language.tok": "トキポナ",
|
||||
"language.tr": "トルコ語",
|
||||
"language.zh_Hans": "中国語 (簡体字)",
|
||||
"language.zh_Hant": "中国語 (繁体字)",
|
||||
"library.missing": "ライブラリの場所が見つかりません",
|
||||
"library.name": "ライブラリ",
|
||||
"library.refresh.scanning.plural": "新しいファイルを検索中...\n{searched_count} 件を検索、{found_count} 件の新規ファイルを検出",
|
||||
"library.refresh.scanning.singular": "新しいファイルを検索中...\n{searched_count} 件を検索、{found_count} 件の新規ファイルを検出",
|
||||
"library.refresh.scanning_preparing": "新しいファイルを検索中...\n準備中...",
|
||||
"library.refresh.scanning_preparing": "新しいファイルを検索中...\n準備中…",
|
||||
"library.refresh.title": "ディレクトリを更新しています",
|
||||
"library.scan_library.title": "ライブラリをスキャンしています",
|
||||
"library_info.cleanup": "クリーンアップ",
|
||||
@@ -202,7 +257,7 @@
|
||||
"library_object.name_required": "名前 (必須)",
|
||||
"library_object.slug": "ID スラッグ",
|
||||
"library_object.slug_required": "ID スラッグ (必須)",
|
||||
"macros.running.dialog.new_entries": "新しいファイル エントリ {total} 件中 {count} 件に設定済みマクロを実行しています...",
|
||||
"macros.running.dialog.new_entries": "新しいファイル エントリ {total} 件中 {count} 件に設定済みマクロを実行しています…",
|
||||
"macros.running.dialog.title": "新しいエントリにマクロを実行しています",
|
||||
"media_player.autoplay": "自動再生",
|
||||
"media_player.loop": "繰り返し",
|
||||
@@ -211,6 +266,7 @@
|
||||
"menu.delete_selected_files_singular": "ファイルを {trash_term} に移動",
|
||||
"menu.edit": "編集",
|
||||
"menu.edit.ignore_files": "ファイルとフォルダを無視",
|
||||
"menu.edit.manage_field_templates": "フィールド テンプレートの管理",
|
||||
"menu.edit.manage_tags": "タグの管理",
|
||||
"menu.edit.new_tag": "新しいタグ(&T)",
|
||||
"menu.file": "ファイル(&F)",
|
||||
@@ -231,7 +287,7 @@
|
||||
"menu.macros": "マクロ(&M)",
|
||||
"menu.macros.folders_to_tags": "フォルダー構造からタグを生成",
|
||||
"menu.select": "選択",
|
||||
"menu.settings": "設定...",
|
||||
"menu.settings": "設定…",
|
||||
"menu.tools": "ツール(&T)",
|
||||
"menu.tools.fix_duplicate_files": "重複ファイルの修正(&D)",
|
||||
"menu.tools.fix_ignored_entries": "無視されたエントリの修正(&I)",
|
||||
@@ -247,6 +303,8 @@
|
||||
"namespace.new.button": "新しい名前空間",
|
||||
"namespace.new.prompt": "カスタムカラーを追加するには、新しい名前空間を作成してください!",
|
||||
"preview.ignored": "無視",
|
||||
"preview.missing_module.jxl": "JPEG XL のプレビューには {module} が必要です",
|
||||
"preview.missing_module.multimedia": "マルチメディアの再生には {module} が必要です",
|
||||
"preview.multiple_selection": "<b>{count}</b> 件選択済み",
|
||||
"preview.no_selection": "選択されていません",
|
||||
"preview.unlinked": "リンク切れ",
|
||||
@@ -254,11 +312,15 @@
|
||||
"select.all": "すべて選択",
|
||||
"select.clear": "選択を解除",
|
||||
"select.inverse": "選択を反転",
|
||||
"settings.appearance": "外観",
|
||||
"settings.cached_thumb_resolution.label": "サムネイル キャッシュの解像度",
|
||||
"settings.clear_thumb_cache.title": "サムネイルキャッシュをクリア",
|
||||
"settings.dateformat.english": "English",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.dateformat.label": "日付形式",
|
||||
"settings.dateformat.system": "システム",
|
||||
"settings.edit_field_on_add": "フィールド追加後に編集",
|
||||
"settings.edit_tag_on_create": "新しいタグ作成後に編集",
|
||||
"settings.filepath.label": "ファイルパスの表示形式",
|
||||
"settings.filepath.option.full": "フルパスを表示",
|
||||
"settings.filepath.option.name": "ファイル名のみ表示",
|
||||
@@ -269,12 +331,16 @@
|
||||
"settings.infinite_scroll": "無限スクロール",
|
||||
"settings.language": "言語",
|
||||
"settings.library": "ライブラリ設定",
|
||||
"settings.localization": "ローカライズ",
|
||||
"settings.media": "メディア",
|
||||
"settings.open_library_on_start": "起動時にライブラリを開く",
|
||||
"settings.page_size": "ページサイズ",
|
||||
"settings.restart_required": "変更を反映するには、TagStudio を再起動してください。",
|
||||
"settings.scan_files_on_open": "新しいファイルを自動的に読み込む",
|
||||
"settings.show_filenames_in_grid": "グリッドにファイル名を表示",
|
||||
"settings.show_recent_libraries": "最近使用したライブラリを表示",
|
||||
"settings.splash.label": "スプラッシュ スクリーン",
|
||||
"settings.splash.option.aurora": "オーロラ (9.6)",
|
||||
"settings.splash.option.classic": "クラシック (9.0)",
|
||||
"settings.splash.option.default": "既定",
|
||||
"settings.splash.option.goo_gears": "オープン ソース (9.4)",
|
||||
@@ -294,18 +360,18 @@
|
||||
"sorting.direction.ascending": "昇順",
|
||||
"sorting.direction.descending": "降順",
|
||||
"sorting.mode.random": "ランダム",
|
||||
"splash.opening_library": "ライブラリ \"{library_path}\" を開いています...",
|
||||
"splash.opening_library": "ライブラリ \"{library_path}\" を開いています…",
|
||||
"status.deleted_file_plural": "{count} 件のファイルを削除しました!",
|
||||
"status.deleted_file_singular": "1 件のファイルを削除しました!",
|
||||
"status.deleted_none": "ファイルは削除されませんでした。",
|
||||
"status.deleted_partial_warning": "{count} 件のファイルしか削除できませんでした。ファイルが存在しないか、使用中でないかを確認してください。",
|
||||
"status.deleting_file": "[{i}/{count}] 件目のファイルを削除ファイルを削除しています : \"{path}\"...",
|
||||
"status.library_backup_in_progress": "ライブラリを保存しています...",
|
||||
"status.deleting_file": "[{i}/{count}] 件目のファイルを削除ファイルを削除しています : \"{path}\"…",
|
||||
"status.library_backup_in_progress": "ライブラリを保存しています…",
|
||||
"status.library_backup_success": "ライブラリのバックアップを保存しました: \"{path}\" ({time_span})",
|
||||
"status.library_closed": "ライブラリを閉じました ({time_span})",
|
||||
"status.library_closing": "ライブラリを閉じています...",
|
||||
"status.library_closing": "ライブラリを閉じています…",
|
||||
"status.library_save_success": "ライブラリを保存して閉じました!",
|
||||
"status.library_search_query": "ライブラリを検索しています...",
|
||||
"status.library_search_query": "ライブラリを検索しています…",
|
||||
"status.library_version_expected": "想定されるバージョン:",
|
||||
"status.library_version_found": "検出されたバージョン:",
|
||||
"status.library_version_mismatch": "ライブラリのバージョンが一致しません!",
|
||||
@@ -331,6 +397,7 @@
|
||||
"tag.parent_tags": "親タグ",
|
||||
"tag.parent_tags.add": "親タグを追加",
|
||||
"tag.parent_tags.description": "このタグは、検索時にこれらの親タグの代わりとして扱うことができます。",
|
||||
"tag.properties": "プロパティ",
|
||||
"tag.remove": "タグの削除",
|
||||
"tag.search_for_tag": "このタグで検索",
|
||||
"tag.shorthand": "略称",
|
||||
@@ -339,8 +406,8 @@
|
||||
"trash.context.ambiguous": "ファイルを {trash_term} に移動",
|
||||
"trash.context.plural": "ファイルを {trash_term} に移動",
|
||||
"trash.context.singular": "ファイルを {trash_term} に移動",
|
||||
"trash.dialog.disambiguation_warning.plural": "これにより、TagStudio だけでなく<i>ファイル システムからも</i>削除されます!",
|
||||
"trash.dialog.disambiguation_warning.singular": "これにより、TagStudio だけでなく<i>ファイル システムからも</i>削除されます!",
|
||||
"trash.dialog.disambiguation_warning.plural": "これにより、TagStudio <i>と</i> ファイル システムからも削除されます!",
|
||||
"trash.dialog.disambiguation_warning.singular": "これにより、TagStudio <i>と</i> ファイル システムからも削除されます!",
|
||||
"trash.dialog.move.confirmation.plural": "{count} 件のファイルを{trash_term}に移動してもよろしいですか?",
|
||||
"trash.dialog.move.confirmation.singular": "このファイルを{trash_term}に移動してもよろしいですか?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>警告:</b> このファイルを{trash_term}に移動できない場合、<b>完全に削除されます!</b>",
|
||||
@@ -348,6 +415,7 @@
|
||||
"trash.dialog.title.singular": "ファイルの削除",
|
||||
"trash.name.generic": "ごみ箱",
|
||||
"trash.name.windows": "ごみ箱",
|
||||
"update.view_update": "更新を表示",
|
||||
"version_modal.description": "TagStudio の新しいバージョンが利用できます。<a href=\"{github_url}\">GitHub</a> から最新リリースをダウンロードできます。",
|
||||
"version_modal.status": "インストール済みのバージョン: {installed_version}<br>最新リリースのバージョン: {latest_release_version}",
|
||||
"version_modal.title": "TagStudio の更新があります",
|
||||
|
||||
@@ -144,7 +144,7 @@ def test_title_update(
|
||||
qt_driver.main_window.menu_bar.folders_to_tags_action = QAction(menu_bar)
|
||||
|
||||
# Trigger the update
|
||||
qt_driver._init_library(library_dir, open_status, is_test=True)
|
||||
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))
|
||||
|
||||