From b6e216760557c5507b12f210e1e48c531f49ffa3 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Fri, 13 Sep 2024 00:28:00 -0700 Subject: [PATCH] ci(ruff)!: update ruff linter config, refactor to comply (#499) * ci: update ruff linter config - Set line length to 100 - Enforce Google-style docstrings - Lint docstrings and imports * ci(ruff): exclude missing docstring warnings * ci(ruff): exclude docstring checks from tests dir * fix(ruff): change top level linter setting Fix Ruff warning: `warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `pyproject.toml`: - 'per-file-ignores' -> 'lint.per-file-ignores'`. * chore: format with ruff * add `.git-blame-ignore-revs` * ci(ruff): add E402 and F541 checks * ci(ruff): add FBT003 check (#500) * ci(ruff): add T20 check * ci(ruff)!: add N check, refactor method names * ci(ruff): add E501 check, refactor strings Much commented-out code is removed in this commit. * update `.git-blame-ignore.revs` --------- Co-authored-by: yed --- .git-blame-ignore-revs | 27 +++ pyproject.toml | 31 ++- .../src/core/library/alchemy/__init__.py | 5 +- tagstudio/src/core/library/alchemy/enums.py | 10 +- tagstudio/src/core/library/alchemy/fields.py | 50 ++--- tagstudio/src/core/library/alchemy/joins.py | 4 +- tagstudio/src/core/library/alchemy/library.py | 106 ++++------ tagstudio/src/core/library/alchemy/models.py | 31 ++- tagstudio/src/core/library/json/library.py | 154 ++++----------- tagstudio/src/core/palette.py | 1 - tagstudio/src/core/ts_core.py | 30 +-- tagstudio/src/core/utils/dupe_files.py | 16 +- tagstudio/src/core/utils/encoding.py | 7 +- tagstudio/src/core/utils/missing_files.py | 10 +- tagstudio/src/core/utils/refresh_dir.py | 5 +- tagstudio/src/core/utils/web.py | 2 +- tagstudio/src/qt/flowlayout.py | 74 ++----- tagstudio/src/qt/helpers/color_overlay.py | 12 +- tagstudio/src/qt/helpers/custom_runnable.py | 2 +- tagstudio/src/qt/helpers/file_opener.py | 23 ++- tagstudio/src/qt/helpers/function_iterator.py | 2 +- tagstudio/src/qt/helpers/gradient.py | 12 +- tagstudio/src/qt/helpers/qbutton_wrapper.py | 7 +- tagstudio/src/qt/modals/add_field.py | 9 +- tagstudio/src/qt/modals/build_tag.py | 32 +-- tagstudio/src/qt/modals/delete_unlinked.py | 11 +- tagstudio/src/qt/modals/file_extension.py | 36 ++-- tagstudio/src/qt/modals/fix_dupes.py | 58 ++---- tagstudio/src/qt/modals/fix_unlinked.py | 13 +- tagstudio/src/qt/modals/folders_to_tags.py | 43 ++-- tagstudio/src/qt/modals/merge_dupe_entries.py | 9 +- tagstudio/src/qt/modals/mirror_entities.py | 19 +- tagstudio/src/qt/modals/relink_unlinked.py | 5 +- tagstudio/src/qt/modals/tag_database.py | 51 +---- tagstudio/src/qt/modals/tag_search.py | 50 +---- tagstudio/src/qt/pagination.py | 187 +----------------- tagstudio/src/qt/resource_manager.py | 5 +- tagstudio/src/qt/ts_qt.py | 166 ++++++++-------- tagstudio/src/qt/widgets/clickable_label.py | 2 +- tagstudio/src/qt/widgets/collage_icon.py | 20 +- tagstudio/src/qt/widgets/fields.py | 45 +---- tagstudio/src/qt/widgets/item_thumb.py | 129 ++++-------- tagstudio/src/qt/widgets/landing.py | 39 ++-- tagstudio/src/qt/widgets/panel.py | 16 +- tagstudio/src/qt/widgets/preview_panel.py | 145 +++++--------- tagstudio/src/qt/widgets/progress.py | 6 +- tagstudio/src/qt/widgets/tag.py | 63 +----- tagstudio/src/qt/widgets/tag_box.py | 21 +- tagstudio/src/qt/widgets/text.py | 4 +- tagstudio/src/qt/widgets/text_box_edit.py | 3 +- tagstudio/src/qt/widgets/text_line_edit.py | 3 +- tagstudio/src/qt/widgets/thumb_button.py | 20 +- tagstudio/src/qt/widgets/thumb_renderer.py | 50 ++--- tagstudio/src/qt/widgets/video_player.py | 168 ++++++++-------- tagstudio/tag_studio.py | 9 +- tagstudio/tests/conftest.py | 8 +- tagstudio/tests/macros/test_folders_tags.py | 4 +- tagstudio/tests/macros/test_missing_files.py | 1 - tagstudio/tests/macros/test_refresh_dir.py | 1 - tagstudio/tests/macros/test_sidecar.py | 1 - tagstudio/tests/qt/test_driver.py | 11 +- tagstudio/tests/qt/test_flow_widget.py | 7 +- tagstudio/tests/qt/test_item_thumb.py | 3 +- tagstudio/tests/qt/test_preview_panel.py | 3 +- tagstudio/tests/qt/test_tag_widget.py | 15 +- tagstudio/tests/test_filter_state.py | 1 - tagstudio/tests/test_library.py | 19 +- 67 files changed, 727 insertions(+), 1405 deletions(-) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..55d0cb3c --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,27 @@ +# Date: Thu, 12 Sep 2024 19:38:58 -0700 +# chore: format with ruff +308be42bd21c3483098088ceb2ebc89208f5f26c + +# Date: Thu, 12 Sep 2024 21:43:01 -0700 +# ci(ruff): add E402 and F541 checks +f52c1fc541fef0044edf5463a0818e17601e5fab + +# Date: Fri, 13 Sep 2024 11:43:08 +0700 +# ci(ruff): add FBT003 check +16dd6346e4d7e86212d4df1ac91c19d9cb11c572 + +# Date: Fri, 13 Sep 2024 11:43:08 +0700 +# Merge #500 into #499 +3baa07bd33decad5f048ff9ad7a36188bb2956b7 + +# Date: Thu, 12 Sep 2024 21:50:00 -0700 +# ci(ruff): add T20 check +162bcf5a6c7b2e4a39ec7e571375998c9859a42a + +# Date: Thu, 12 Sep 2024 22:28:24 -0700 +# ci(ruff)!: add N check, refactor method names +5bc815cfba5b92bca8fc646b92cb4a12222f3a0e + +# Date: Thu, 12 Sep 2024 23:40:24 -0700 +# ci(ruff): add E501 check, refactor strings +836bb0155d0eae3448a752e3f2690061803707b4 diff --git a/pyproject.toml b/pyproject.toml index 3fe8ffd0..023ee6f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,36 @@ [tool.ruff] exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"] +line-length = 100 + +[tool.ruff.lint.per-file-ignores] +"tagstudio/tests/**" = ["D", "E402"] + +[tool.ruff.lint.pydocstyle] +convention = "google" [tool.ruff.lint] -select = ["E", "F", "UP", "B", 'SIM'] -ignore = ["E402", "E501", "F541"] +select = [ + "B", + "D", + "E", + "F", + "FBT003", + "I", + "N", + "SIM", + "T20", + "UP", +] +ignore = [ + "D100", + "D101", + "D102", + "D103", + "D104", + "D105", + "D106", + "D107", +] [tool.mypy] strict_optional = false diff --git a/tagstudio/src/core/library/alchemy/__init__.py b/tagstudio/src/core/library/alchemy/__init__.py index 993e1aa0..2cfed3b1 100644 --- a/tagstudio/src/core/library/alchemy/__init__.py +++ b/tagstudio/src/core/library/alchemy/__init__.py @@ -1,6 +1,5 @@ -from .models import Entry -from .library import Library -from .models import Tag from .enums import ItemType +from .library import Library +from .models import Entry, Tag __all__ = ["Entry", "Library", "Tag", "ItemType"] diff --git a/tagstudio/src/core/library/alchemy/enums.py b/tagstudio/src/core/library/alchemy/enums.py index f7595e62..aaf9d32a 100644 --- a/tagstudio/src/core/library/alchemy/enums.py +++ b/tagstudio/src/core/library/alchemy/enums.py @@ -104,9 +104,7 @@ class FilterState: else: self.tag = self.tag and self.tag.strip() - self.tag_id = ( - int(self.tag_id) if str(self.tag_id).isnumeric() else self.tag_id - ) + self.tag_id = int(self.tag_id) if str(self.tag_id).isnumeric() else self.tag_id self.path = self.path and str(self.path).strip() self.name = self.name and self.name.strip() self.id = int(self.id) if str(self.id).isnumeric() else self.id @@ -118,10 +116,8 @@ class FilterState: @property def summary(self): - """Show query summary""" - return ( - self.query or self.tag or self.name or self.tag_id or self.path or self.id - ) + """Show query summary.""" + return self.query or self.tag or self.name or self.tag_id or self.path or self.id @property def limit(self): diff --git a/tagstudio/src/core/library/alchemy/fields.py b/tagstudio/src/core/library/alchemy/fields.py index 6b079b0b..1b77c1e6 100644 --- a/tagstudio/src/core/library/alchemy/fields.py +++ b/tagstudio/src/core/library/alchemy/fields.py @@ -2,10 +2,10 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any from sqlalchemy import ForeignKey -from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr +from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship from .db import Base from .enums import FieldTypeEnum @@ -18,27 +18,27 @@ class BaseField(Base): __abstract__ = True @declared_attr - def id(cls) -> Mapped[int]: + def id(cls) -> Mapped[int]: # noqa: N805 return mapped_column(primary_key=True, autoincrement=True) @declared_attr - def type_key(cls) -> Mapped[str]: + def type_key(cls) -> Mapped[str]: # noqa: N805 return mapped_column(ForeignKey("value_type.key")) @declared_attr - def type(cls) -> Mapped[ValueType]: + def type(cls) -> Mapped[ValueType]: # noqa: N805 return relationship(foreign_keys=[cls.type_key], lazy=False) # type: ignore @declared_attr - def entry_id(cls) -> Mapped[int]: + def entry_id(cls) -> Mapped[int]: # noqa: N805 return mapped_column(ForeignKey("entries.id")) @declared_attr - def entry(cls) -> Mapped[Entry]: + def entry(cls) -> Mapped[Entry]: # noqa: N805 return relationship(foreign_keys=[cls.entry_id]) # type: ignore @declared_attr - def position(cls) -> Mapped[int]: + def position(cls) -> Mapped[int]: # noqa: N805 return mapped_column(default=0) def __hash__(self): @@ -125,33 +125,23 @@ class DefaultField: class _FieldID(Enum): - """Only for bootstrapping content of DB table""" + """Only for bootstrapping content of DB table.""" - TITLE = DefaultField( - id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True - ) + TITLE = DefaultField(id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True) AUTHOR = DefaultField(id=1, name="Author", type=FieldTypeEnum.TEXT_LINE) ARTIST = DefaultField(id=2, name="Artist", type=FieldTypeEnum.TEXT_LINE) URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE) DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_LINE) NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX) TAGS = DefaultField(id=6, name="Tags", type=FieldTypeEnum.TAGS) - TAGS_CONTENT = DefaultField( - id=7, name="Content Tags", type=FieldTypeEnum.TAGS, is_default=True - ) - TAGS_META = DefaultField( - id=8, name="Meta Tags", type=FieldTypeEnum.TAGS, is_default=True - ) + TAGS_CONTENT = DefaultField(id=7, name="Content Tags", type=FieldTypeEnum.TAGS, is_default=True) + TAGS_META = DefaultField(id=8, name="Meta Tags", type=FieldTypeEnum.TAGS, is_default=True) COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE) DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME) DATE_CREATED = DefaultField(id=11, name="Date Created", type=FieldTypeEnum.DATETIME) - DATE_MODIFIED = DefaultField( - id=12, name="Date Modified", type=FieldTypeEnum.DATETIME - ) + DATE_MODIFIED = DefaultField(id=12, name="Date Modified", type=FieldTypeEnum.DATETIME) DATE_TAKEN = DefaultField(id=13, name="Date Taken", type=FieldTypeEnum.DATETIME) - DATE_PUBLISHED = DefaultField( - id=14, name="Date Published", type=FieldTypeEnum.DATETIME - ) + DATE_PUBLISHED = DefaultField(id=14, name="Date Published", type=FieldTypeEnum.DATETIME) # ARCHIVED = DefaultField(id=15, name="Archived", type=CheckboxField.checkbox) # FAVORITE = DefaultField(id=16, name="Favorite", type=CheckboxField.checkbox) BOOK = DefaultField(id=17, name="Book", type=FieldTypeEnum.TEXT_LINE) @@ -159,18 +149,12 @@ class _FieldID(Enum): SERIES = DefaultField(id=19, name="Series", type=FieldTypeEnum.TEXT_LINE) MANGA = DefaultField(id=20, name="Manga", type=FieldTypeEnum.TEXT_LINE) SOURCE = DefaultField(id=21, name="Source", type=FieldTypeEnum.TEXT_LINE) - DATE_UPLOADED = DefaultField( - id=22, name="Date Uploaded", type=FieldTypeEnum.DATETIME - ) - DATE_RELEASED = DefaultField( - id=23, name="Date Released", type=FieldTypeEnum.DATETIME - ) + DATE_UPLOADED = DefaultField(id=22, name="Date Uploaded", type=FieldTypeEnum.DATETIME) + DATE_RELEASED = DefaultField(id=23, name="Date Released", type=FieldTypeEnum.DATETIME) VOLUME = DefaultField(id=24, name="Volume", type=FieldTypeEnum.TEXT_LINE) ANTHOLOGY = DefaultField(id=25, name="Anthology", type=FieldTypeEnum.TEXT_LINE) MAGAZINE = DefaultField(id=26, name="Magazine", type=FieldTypeEnum.TEXT_LINE) PUBLISHER = DefaultField(id=27, name="Publisher", type=FieldTypeEnum.TEXT_LINE) - GUEST_ARTIST = DefaultField( - id=28, name="Guest Artist", type=FieldTypeEnum.TEXT_LINE - ) + GUEST_ARTIST = DefaultField(id=28, name="Guest Artist", type=FieldTypeEnum.TEXT_LINE) COMPOSER = DefaultField(id=29, name="Composer", type=FieldTypeEnum.TEXT_LINE) COMMENTS = DefaultField(id=30, name="Comments", type=FieldTypeEnum.TEXT_LINE) diff --git a/tagstudio/src/core/library/alchemy/joins.py b/tagstudio/src/core/library/alchemy/joins.py index b3f4b711..71dddb81 100644 --- a/tagstudio/src/core/library/alchemy/joins.py +++ b/tagstudio/src/core/library/alchemy/joins.py @@ -14,7 +14,5 @@ class TagSubtag(Base): class TagField(Base): __tablename__ = "tag_fields" - field_id: Mapped[int] = mapped_column( - ForeignKey("tag_box_fields.id"), primary_key=True - ) + field_id: Mapped[int] = mapped_column(ForeignKey("tag_box_fields.id"), primary_key=True) tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True) diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index fc8efbac..125c88c8 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -1,58 +1,57 @@ -from dataclasses import dataclass -from datetime import datetime, UTC +import re import shutil +import unicodedata +from dataclasses import dataclass +from datetime import UTC, datetime from os import makedirs from pathlib import Path -from typing import Iterator, Any, Type +from typing import Any, Iterator, Type from uuid import uuid4 import structlog from sqlalchemy import ( + URL, + Engine, and_, + create_engine, + delete, + exists, + func, or_, select, - create_engine, - Engine, - func, update, - URL, - exists, - delete, ) from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import ( Session, contains_eager, - selectinload, make_transient, + selectinload, ) +from ...constants import ( + BACKUP_FOLDER_NAME, + TAG_ARCHIVED, + TAG_FAVORITE, + TS_FOLDER_NAME, + LibraryPrefs, +) from .db import make_tables -from .enums import TagColor, FilterState, FieldTypeEnum +from .enums import FieldTypeEnum, FilterState, TagColor from .fields import ( + BaseField, DatetimeField, TagBoxField, TextField, _FieldID, - BaseField, -) -from .joins import TagSubtag, TagField -from .models import Entry, Preferences, Tag, TagAlias, ValueType, Folder -from ...constants import ( - LibraryPrefs, - TS_FOLDER_NAME, - TAG_ARCHIVED, - TAG_FAVORITE, - BACKUP_FOLDER_NAME, ) +from .joins import TagField, TagSubtag +from .models import Entry, Folder, Preferences, Tag, TagAlias, ValueType LIBRARY_FILENAME: str = "ts_library.sqlite" logger = structlog.get_logger(__name__) -import re -import unicodedata - def slugify(input_string: str) -> str: # Convert to lowercase and normalize unicode characters @@ -92,8 +91,9 @@ def get_default_tags() -> tuple[Tag, ...]: class SearchResult: """Wrapper for search results. - :param total_count: total number of items for given query, might be different than len(items) - :param items: items for current page (size matches filter.page_size) + Attributes: + total_count(int): total number of items for given query, might be different than len(items). + items(list[Entry]): for current page (size matches filter.page_size). """ total_count: int @@ -130,9 +130,7 @@ class Library: self.storage_path = None self.folder = None - def open_library( - self, library_dir: Path | str, storage_path: str | None = None - ) -> None: + def open_library(self, library_dir: Path | str, storage_path: str | None = None) -> None: if isinstance(library_dir, str): library_dir = Path(library_dir) @@ -186,9 +184,7 @@ class Library: session.rollback() # check if folder matching current path exists already - self.folder = session.scalar( - select(Folder).where(Folder.path == self.library_dir) - ) + self.folder = session.scalar(select(Folder).where(Folder.path == self.library_dir)) if not self.folder: folder = Folder( path=self.library_dir, @@ -400,13 +396,9 @@ class Library: if not search.id: # if `id` is set, we don't need to filter by extensions if extensions and is_exclude_list: - statement = statement.where( - Entry.path.notilike(f"%.{','.join(extensions)}") - ) + statement = statement.where(Entry.path.notilike(f"%.{','.join(extensions)}")) elif extensions: - statement = statement.where( - Entry.path.ilike(f"%.{','.join(extensions)}") - ) + statement = statement.where(Entry.path.ilike(f"%.{','.join(extensions)}")) statement = statement.options( selectinload(Entry.text_fields), @@ -424,9 +416,7 @@ class Library: logger.info( "searching library", filter=search, - query_full=str( - statement.compile(compile_kwargs={"literal_binds": True}) - ), + query_full=str(statement.compile(compile_kwargs={"literal_binds": True})), ) res = SearchResult( @@ -443,7 +433,6 @@ class Library: search: FilterState, ) -> list[Tag]: """Return a list of Tag records matching the query.""" - with Session(self.engine) as session: query = select(Tag) query = query.options( @@ -475,7 +464,6 @@ class Library: def get_all_child_tag_ids(self, tag_id: int) -> list[int]: """Recursively traverse a Tag's subtags and return a list of all children tags.""" - all_subtags: set[int] = {tag_id} with Session(self.engine) as session: @@ -512,9 +500,7 @@ class Library: def remove_tag_from_field(self, tag: Tag, field: TagBoxField) -> None: with Session(self.engine) as session: - field_ = session.scalars( - select(TagBoxField).where(TagBoxField.id == field.id) - ).one() + field_ = session.scalars(select(TagBoxField).where(TagBoxField.id == field.id)).one() tag = session.scalars(select(Tag).where(Tag.id == tag.id)).one() @@ -559,7 +545,7 @@ class Library: field: BaseField, entry_ids: list[int], ) -> None: - FieldClass = type(field) + FieldClass = type(field) # noqa: N806 logger.info( "remove_entry_field", @@ -596,7 +582,7 @@ class Library: if isinstance(entry_ids, int): entry_ids = [entry_ids] - FieldClass = type(field) + FieldClass = type(field) # noqa: N806 with Session(self.engine) as session: update_stmt = ( @@ -766,9 +752,7 @@ class Library: session.add(tag_field) session.commit() - logger.info( - "tag added to field", tag=tag, field=field, entry_id=entry.id - ) + logger.info("tag added to field", tag=tag, field=field, entry_id=entry.id) return True except IntegrityError as e: @@ -779,13 +763,9 @@ class Library: 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 - ) + makedirs(str(self.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' - ) + 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 @@ -825,9 +805,7 @@ class Library: return False def update_tag(self, tag: Tag, subtag_ids: list[int]) -> None: - """ - Edit a Tag in the Library. - """ + """Edit a Tag in the Library.""" # TODO - maybe merge this with add_tag? if tag.shorthand: @@ -873,17 +851,13 @@ class Library: def prefs(self, key: LibraryPrefs) -> Any: # load given item from Preferences table with Session(self.engine) as session: - return session.scalar( - select(Preferences).where(Preferences.key == key.name) - ).value + return session.scalar(select(Preferences).where(Preferences.key == key.name)).value def set_prefs(self, key: LibraryPrefs, value: Any) -> None: # set given item in Preferences table with Session(self.engine) as session: # load existing preference and update value - pref = session.scalar( - select(Preferences).where(Preferences.key == key.name) - ) + pref = session.scalar(select(Preferences).where(Preferences.key == key.name)) pref.value = value session.add(pref) session.commit() diff --git a/tagstudio/src/core/library/alchemy/models.py b/tagstudio/src/core/library/alchemy/models.py index 3c4e0402..303e9049 100644 --- a/tagstudio/src/core/library/alchemy/models.py +++ b/tagstudio/src/core/library/alchemy/models.py @@ -4,19 +4,19 @@ from typing import Optional from sqlalchemy import JSON, ForeignKey, Integer, event from sqlalchemy.orm import Mapped, mapped_column, relationship +from ...constants import TAG_ARCHIVED, TAG_FAVORITE from .db import Base, PathType from .enums import TagColor from .fields import ( - DatetimeField, - TagBoxField, - TextField, - FieldTypeEnum, - _FieldID, BaseField, BooleanField, + DatetimeField, + FieldTypeEnum, + TagBoxField, + TextField, + _FieldID, ) from .joins import TagSubtag -from ...constants import TAG_FAVORITE, TAG_ARCHIVED class TagAlias(Base): @@ -191,8 +191,9 @@ class Entry(Base): return tag in self.tags def remove_tag(self, tag: Tag, field: TagBoxField | None = None) -> None: - """ - Removes a Tag from the Entry. If given a field index, the given Tag will + """Removes a Tag from the Entry. + + If given a field index, the given Tag will only be removed from that index. If left blank, all instances of that Tag will be removed from the Entry. """ @@ -225,22 +226,16 @@ class ValueType(Base): position: Mapped[int] # add relations to other tables - text_fields: Mapped[list[TextField]] = relationship( - "TextField", back_populates="type" - ) + text_fields: Mapped[list[TextField]] = relationship("TextField", back_populates="type") datetime_fields: Mapped[list[DatetimeField]] = relationship( "DatetimeField", back_populates="type" ) - tag_box_fields: Mapped[list[TagBoxField]] = relationship( - "TagBoxField", back_populates="type" - ) - boolean_fields: Mapped[list[BooleanField]] = relationship( - "BooleanField", back_populates="type" - ) + tag_box_fields: Mapped[list[TagBoxField]] = relationship("TagBoxField", back_populates="type") + boolean_fields: Mapped[list[BooleanField]] = relationship("BooleanField", back_populates="type") @property def as_field(self) -> BaseField: - FieldClass = { + FieldClass = { # noqa: N806 FieldTypeEnum.TEXT_LINE: TextField, FieldTypeEnum.TEXT_BOX: TextField, FieldTypeEnum.TAGS: TagBoxField, diff --git a/tagstudio/src/core/library/json/library.py b/tagstudio/src/core/library/json/library.py index c95a2c79..4322525c 100644 --- a/tagstudio/src/core/library/json/library.py +++ b/tagstudio/src/core/library/json/library.py @@ -47,9 +47,7 @@ logger = structlog.get_logger(__name__) class Entry: """A Library Entry Object. Referenced by ID.""" - def __init__( - self, id: int, filename: str | Path, path: str | Path, fields: list[dict] - ) -> None: + def __init__(self, id: int, filename: str | Path, path: str | Path, fields: list[dict]) -> None: # Required Fields ====================================================== self.id = int(id) self.filename = Path(filename) @@ -138,9 +136,7 @@ class Entry: while tag_id in t: t.remove(tag_id) - def add_tag( - self, library: "Library", tag_id: int, field_id: int, field_index: int = -1 - ): + def add_tag(self, library: "Library", tag_id: int, field_id: int, field_index: int = -1): # if self.fields: # if field_index != -1: # logger.info(f'[LIBRARY] ADD TAG to E:{self.id}, F-DI:{field_id}, F-INDEX:{field_index}') @@ -212,9 +208,7 @@ class Tag: """Returns a formatted tag name intended for displaying.""" if self.subtag_ids: if library.get_tag(self.subtag_ids[0]).shorthand: - return ( - f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).shorthand})" - ) + return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).shorthand})" else: return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).name})" else: @@ -473,9 +467,7 @@ class Library: "ignored_extensions", self.default_ext_exclude_list ) else: - self.ext_list = json_dump.get( - "ext_list", self.default_ext_exclude_list - ) + self.ext_list = json_dump.get("ext_list", self.default_ext_exclude_list) # Sanitizes older lists (v9.2.1) that don't use leading periods. # Without this, existing lists (including default lists) @@ -535,9 +527,7 @@ class Library: self._map_tag_id_to_index(t, -1) self._map_tag_strings_to_tag_id(t) else: - logger.info( - f"[LIBRARY]Skipping Tag with duplicate ID: {tag}" - ) + logger.info(f"[LIBRARY]Skipping Tag with duplicate ID: {tag}") # Step 3: Map each Tag's subtags together now that all Tag objects in it. for t in self.tags: @@ -586,19 +576,14 @@ class Library: matched = False collation_id = -1 for c in self.collations: - if ( - c.title - == self.get_field_attr(f, "content")[ - "name" - ] - ): + if c.title == self.get_field_attr(f, "content")["name"]: c.e_ids_and_pages.append( ( id, int( - self.get_field_attr( - f, "content" - )["page"] + self.get_field_attr(f, "content")[ + "page" + ] ), ) ) @@ -607,9 +592,7 @@ class Library: if not matched: c = Collation( id=self._next_collation_id, - title=self.get_field_attr(f, "content")[ - "name" - ], + title=self.get_field_attr(f, "content")["name"], e_ids_and_pages=[], sort_order="", ) @@ -618,11 +601,7 @@ class Library: c.e_ids_and_pages.append( ( id, - int( - self.get_field_attr( - f, "content" - )["page"] - ), + int(self.get_field_attr(f, "content")["page"]), ) ) self.collations.append(c) @@ -645,9 +624,7 @@ class Library: self._map_entry_id_to_index(e, -1) end_time = time.time() - logger.info( - f"[LIBRARY] Entries loaded", load_time=end_time - start_time - ) + logger.info(f"[LIBRARY] Entries loaded", load_time=end_time - start_time) # Parse Collations ----------------------------------------- if "collations" in json_dump.keys(): @@ -761,9 +738,7 @@ class Library: self.verify_ts_folders() - with open( - self.library_dir / TS_FOLDER_NAME / filename, "w", encoding="utf-8" - ) as outfile: + with open(self.library_dir / TS_FOLDER_NAME / filename, "w", encoding="utf-8") as outfile: outfile.flush() ujson.dump( self.to_json(), @@ -773,9 +748,7 @@ class Library: ) # , indent=4 <-- How to prettyprint dump end_time = time.time() - logger.info( - f"[LIBRARY] Library saved to disk in {(end_time - start_time):.3f} seconds" - ) + logger.info(f"[LIBRARY] Library saved to disk in {(end_time - start_time):.3f} seconds") def save_library_backup_to_disk(self) -> str: """ @@ -1053,9 +1026,7 @@ class Library: file_1.resolve in self.filename_to_entry_id_map.keys() and file_2 in self.filename_to_entry_id_map.keys() ): - self.dupe_files.append( - (files[match[0]], files[match[1]], match[2]) - ) + self.dupe_files.append((files[match[0]], files[match[1]], match[2])) print("") for dupe in self.dupe_files: @@ -1287,9 +1258,7 @@ class Library: """Returns an Entry ID given the full filepath it points to.""" try: if self.entries: - return self.filename_to_entry_id_map[ - Path(filename).relative_to(self.library_dir) - ] + return self.filename_to_entry_id_map[Path(filename).relative_to(self.library_dir)] except KeyError: return -1 @@ -1334,8 +1303,7 @@ class Library: for j, term in enumerate(query_words): if ( query_words[i : j + 1] - and " ".join(query_words[i : j + 1]) - in self._tag_strings_to_id_map + and " ".join(query_words[i : j + 1]) in self._tag_strings_to_id_map ): all_tag_terms.append(" ".join(query_words[i : j + 1])) # print(all_tag_terms) @@ -1404,9 +1372,7 @@ class Library: if allow_adv: if [q for q in query_words if (q in str(entry.path).lower())]: results.append((ItemType.ENTRY, entry.id)) - elif [ - q for q in query_words if (q in str(entry.filename).lower()) - ]: + elif [q for q in query_words if (q in str(entry.filename).lower())]: results.append((ItemType.ENTRY, entry.id)) elif tag_only: if entry.has_tag(self, int(query_words[0])): @@ -1423,19 +1389,14 @@ class Library: added = False for f in entry.fields: if self.get_field_attr(f, "type") == "collation": - if ( - self.get_field_attr(f, "content") - not in collations_added - ): + if self.get_field_attr(f, "content") not in collations_added: results.append( ( ItemType.COLLATION, self.get_field_attr(f, "content"), ) ) - collations_added.append( - self.get_field_attr(f, "content") - ) + collations_added.append(self.get_field_attr(f, "content")) added = True if not added: @@ -1453,9 +1414,7 @@ class Library: # (You're 99.9999999% likely to just get 1 item) for id in self._tag_strings_to_id_map[term]: cluster.add(id) - cluster = cluster.union( - set(self.get_tag_cluster(id)) - ) + cluster = cluster.union(set(self.get_tag_cluster(id))) # print(f'Full Cluster: {cluster}') # For each of the Tag IDs in the term's ID cluster: for t in cluster: @@ -1516,19 +1475,14 @@ class Library: if allowed_ext == self.is_exclude_list: for f in entry.fields: if self.get_field_attr(f, "type") == "collation": - if ( - self.get_field_attr(f, "content") - not in collations_added - ): + if self.get_field_attr(f, "content") not in collations_added: results.append( ( ItemType.COLLATION, self.get_field_attr(f, "content"), ) ) - collations_added.append( - self.get_field_attr(f, "content") - ) + collations_added.append(self.get_field_attr(f, "content")) added = True if not added: @@ -1603,9 +1557,7 @@ class Library: if query == string: exact_match = True elif string.startswith(query): - if len(query) >= ( - len(string) // (len(string) if threshold == 1 else threshold) - ): + if len(query) >= (len(string) // (len(string) if threshold == 1 else threshold)): partial_match = True if exact_match or partial_match: @@ -1640,9 +1592,7 @@ class Library: id_weights.append((id, 0)) # Contextual Weighing - if context and ( - (len(id_weights) > 1 and len(priority_ids) > 1) or (len(priority_ids) > 1) - ): + if context and ((len(id_weights) > 1 and len(priority_ids) > 1) or (len(priority_ids) > 1)): context_strings: list[str] = [ s.replace(" ", "") .replace("_", "") @@ -1691,11 +1641,7 @@ class Library: split += ts.split(" ") tag_strings += split tag_strings = [ - s.replace(" ", "") - .replace("_", "") - .replace("-", "") - .replace("'", "") - .lower() + s.replace(" ", "").replace("_", "").replace("-", "").replace("'", "").lower() for s in tag_strings ] while "" in tag_strings: @@ -1921,38 +1867,26 @@ class Library: if data: # Add a Title Field if the data doesn't already exist. if data.get("title"): - if not self.does_field_content_exist( - entry_id, FieldID.TITLE, data["title"] - ): + if not self.does_field_content_exist(entry_id, FieldID.TITLE, data["title"]): self.add_field_to_entry(entry_id, FieldID.TITLE) self.update_entry_field(entry_id, -1, data["title"], "replace") # Add an Author Field if the data doesn't already exist. if data.get("author"): - if not self.does_field_content_exist( - entry_id, FieldID.AUTHOR, data["author"] - ): + if not self.does_field_content_exist(entry_id, FieldID.AUTHOR, data["author"]): self.add_field_to_entry(entry_id, FieldID.AUTHOR) self.update_entry_field(entry_id, -1, data["author"], "replace") # Add an Artist Field if the data doesn't already exist. if data.get("artist"): - if not self.does_field_content_exist( - entry_id, FieldID.ARTIST, data["artist"] - ): + if not self.does_field_content_exist(entry_id, FieldID.ARTIST, data["artist"]): self.add_field_to_entry(entry_id, FieldID.ARTIST) self.update_entry_field(entry_id, -1, data["artist"], "replace") # Add a Date Published Field if the data doesn't already exist. if data.get("date_published"): - date = str( - datetime.datetime.strptime( - data["date_published"], "%Y-%m-%d %H:%M:%S" - ) - ) - if not self.does_field_content_exist( - entry_id, FieldID.DATE_PUBLISHED, date - ): + date = str(datetime.datetime.strptime(data["date_published"], "%Y-%m-%d %H:%M:%S")) + if not self.does_field_content_exist(entry_id, FieldID.DATE_PUBLISHED, date): self.add_field_to_entry(entry_id, FieldID.DATE_PUBLISHED) # entry = self.entries[entry_id] self.update_entry_field(entry_id, -1, date, "replace") @@ -2025,9 +1959,7 @@ class Library: ) else: self.add_field_to_entry(entry_id, FieldID.CONTENT_TAGS) - self.update_entry_field( - entry_id, -1, [matching[0]], "append" - ) + self.update_entry_field(entry_id, -1, [matching[0]], "append") # Add all original string tags as a note. str_tags = f"Original Tags: {tags}" @@ -2041,9 +1973,7 @@ class Library: entry_id, FieldID.DESCRIPTION, data["description"] ): self.add_field_to_entry(entry_id, FieldID.DESCRIPTION) - self.update_entry_field( - entry_id, -1, data["description"], "replace" - ) + self.update_entry_field(entry_id, -1, data["description"], "replace") if data.get("content"): if not self.does_field_content_exist( entry_id, FieldID.DESCRIPTION, data["content"] @@ -2054,9 +1984,7 @@ class Library: for source in data["source"].split(" "): if source and source != " ": source = strip_web_protocol(string=source) - if not self.does_field_content_exist( - entry_id, FieldID.SOURCE, source - ): + if not self.does_field_content_exist(entry_id, FieldID.SOURCE, source): self.add_field_to_entry(entry_id, FieldID.SOURCE) self.update_entry_field(entry_id, -1, source, "replace") @@ -2140,9 +2068,7 @@ class Library: elif attribute.lower() == "content": return entry_field[self.get_field_attr(entry_field, "id")] else: - return self.get_field_obj(self.get_field_attr(entry_field, "id"))[ - attribute.lower() - ] + return self.get_field_obj(self.get_field_attr(entry_field, "id"))[attribute.lower()] def get_field_obj(self, field_id: int) -> dict: """ @@ -2215,11 +2141,7 @@ class Library: if subtag.subtag_ids: self._map_tag_id_to_cluster( tag, - [ - self.get_tag(sub_id) - for sub_id in subtag.subtag_ids - if sub_id != tag.id - ], + [self.get_tag(sub_id) for sub_id in subtag.subtag_ids if sub_id != tag.id], ) def _map_tag_id_to_index(self, tag: Tag, index: int) -> None: @@ -2285,6 +2207,4 @@ class Library: def sort_fields(self, entry_id: int, order: list[int]) -> None: """Sorts an Entry's Fields given an ordered list of Field IDs.""" entry = self.get_entry(entry_id) - entry.fields = sorted( - entry.fields, key=lambda x: order.index(self.get_field_attr(x, "id")) - ) + entry.fields = sorted(entry.fields, key=lambda x: order.index(self.get_field_attr(x, "id"))) diff --git a/tagstudio/src/core/palette.py b/tagstudio/src/core/palette.py index 74a30fc0..f104b692 100644 --- a/tagstudio/src/core/palette.py +++ b/tagstudio/src/core/palette.py @@ -6,7 +6,6 @@ from enum import IntEnum from typing import Any import structlog - from src.core.library.alchemy.enums import TagColor logger = structlog.get_logger(__name__) diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index aeed6409..9611397e 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -7,8 +7,8 @@ import json from pathlib import Path -from src.core.library import Entry, Library from src.core.constants import TS_FOLDER_NAME +from src.core.library import Entry, Library from src.core.library.alchemy.fields import _FieldID from src.core.utils.missing_files import logger @@ -19,8 +19,7 @@ class TagStudioCore: @classmethod def get_gdl_sidecar(cls, filepath: Path, source: str = "") -> dict: - """ - Attempt to open and dump a Gallery-DL Sidecar file for the filepath. + """Attempt to open and dump a Gallery-DL Sidecar file for the filepath. Return a formatted object with notable values or an empty object if none is found. """ @@ -34,9 +33,7 @@ class TagStudioCore: newstem = _filepath.stem[:-16] + "1" + _filepath.stem[-15:] _filepath = _filepath.parent / (newstem + ".json") - logger.info( - "get_gdl_sidecar", filepath=filepath, source=source, sidecar=_filepath - ) + logger.info("get_gdl_sidecar", filepath=filepath, source=source, sidecar=_filepath) try: with open(_filepath, encoding="utf8") as f: @@ -66,9 +63,6 @@ class TagStudioCore: info[_FieldID.ARTIST] = json_dump["user"].strip() info[_FieldID.DESCRIPTION] = json_dump["description"].strip() info[_FieldID.SOURCE] = json_dump["post_url"].strip() - # else: - # print( - # f'[INFO]: TagStudio does not currently support sidecar files for "{source}"') except Exception: logger.exception("Error handling sidecar file.", path=_filepath) @@ -101,7 +95,6 @@ class TagStudioCore: @classmethod def match_conditions(cls, lib: Library, entry_id: int) -> bool: """Match defined conditions against a file to add Entry data.""" - # TODO - what even is this file format? # TODO: Make this stored somewhere better instead of temporarily in this JSON file. cond_file = lib.library_dir / TS_FOLDER_NAME / "conditions.json" @@ -127,17 +120,13 @@ class TagStudioCore: return False fields = c["fields"] - entry_field_types = { - field.type_key: field for field in entry.fields - } + entry_field_types = {field.type_key: field for field in entry.fields} for field in fields: is_new = field["id"] not in entry_field_types field_key = field["id"] if is_new: - lib.add_entry_field_type( - entry.id, field_key, field["value"] - ) + lib.add_entry_field_type(entry.id, field_key, field["value"]) else: lib.update_entry_field(entry.id, field_key, field["value"]) @@ -149,7 +138,6 @@ class TagStudioCore: @classmethod def build_url(cls, entry: Entry, source: str): """Try to rebuild a source URL given a specific filename structure.""" - source = source.lower().replace("-", " ").replace("_", " ") if "twitter" in source: return cls._build_twitter_url(entry) @@ -158,8 +146,8 @@ class TagStudioCore: @classmethod def _build_twitter_url(cls, entry: Entry): - """ - Build a Twitter URL given a specific filename structure. + """Build a Twitter URL given a specific filename structure. + Method expects filename to be formatted as 'USERNAME_TWEET-ID_INDEX_YEAR-MM-DD' """ try: @@ -172,8 +160,8 @@ class TagStudioCore: @classmethod def _build_instagram_url(cls, entry: Entry): - """ - Build an Instagram URL given a specific filename structure. + """Build an Instagram URL given a specific filename structure. + Method expects filename to be formatted as 'USERNAME_POST-ID_INDEX_YEAR-MM-DD' """ try: diff --git a/tagstudio/src/core/utils/dupe_files.py b/tagstudio/src/core/utils/dupe_files.py index 425785ad..2d0a074b 100644 --- a/tagstudio/src/core/utils/dupe_files.py +++ b/tagstudio/src/core/utils/dupe_files.py @@ -1,10 +1,9 @@ +import xml.etree.ElementTree as ET from dataclasses import dataclass, field from pathlib import Path -import xml.etree.ElementTree as ET import structlog - -from src.core.library import Library, Entry +from src.core.library import Entry, Library from src.core.library.alchemy.enums import FilterState logger = structlog.get_logger() @@ -22,8 +21,8 @@ class DupeRegistry: return len(self.groups) def refresh_dupe_files(self, results_filepath: str | Path): - """ - Refresh the list of duplicate files. + """Refresh the list of duplicate files. + A duplicate file is defined as an identical or near-identical file as determined by a DupeGuru results file. """ @@ -67,9 +66,10 @@ class DupeRegistry: self.groups.append(files) def merge_dupe_entries(self): - """ - Merge the duplicate Entry items. - A duplicate Entry is defined as an Entry pointing to a file that one or more other Entries are also pointing to + """Merge the duplicate Entry items. + + A duplicate Entry is defined as an Entry pointing to a file + that one or more other Entries are also pointing to """ logger.info( "Consolidating Entries... (This may take a while for larger libraries)", diff --git a/tagstudio/src/core/utils/encoding.py b/tagstudio/src/core/utils/encoding.py index 9485acba..f5a4a0d9 100644 --- a/tagstudio/src/core/utils/encoding.py +++ b/tagstudio/src/core/utils/encoding.py @@ -2,13 +2,13 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from chardet.universaldetector import UniversalDetector from pathlib import Path +from chardet.universaldetector import UniversalDetector + def detect_char_encoding(filepath: Path) -> str | None: - """ - Attempts to detect the character encoding of a text file. + """Attempts to detect the character encoding of a text file. Args: filepath (Path): The path of the text file to analyze. @@ -16,7 +16,6 @@ def detect_char_encoding(filepath: Path) -> str | None: Returns: str | None: The detected character encoding, if any. """ - detector = UniversalDetector() with open(filepath, "rb") as text_file: for line in text_file.readlines(): diff --git a/tagstudio/src/core/utils/missing_files.py b/tagstudio/src/core/utils/missing_files.py index 8ecf9a4c..7c54c136 100644 --- a/tagstudio/src/core/utils/missing_files.py +++ b/tagstudio/src/core/utils/missing_files.py @@ -1,10 +1,9 @@ from collections.abc import Iterator -from dataclasses import field, dataclass +from dataclasses import dataclass, field from pathlib import Path import structlog - -from src.core.library import Library, Entry +from src.core.library import Entry, Library IGNORE_ITEMS = [ "$recycle.bin", @@ -36,11 +35,10 @@ class MissingRegistry: yield i def match_missing_file(self, match_item: Entry) -> list[Path]: - """ - Try to find missing entry files within the library directory. + """Try to find missing entry files within the library directory. + Works if files were just moved to different subfolders and don't have duplicate names. """ - matches = [] for item in self.library.library_dir.glob(f"**/{match_item.path.name}"): if item.name == match_item.path.name: # TODO - implement IGNORE_ITEMS diff --git a/tagstudio/src/core/utils/refresh_dir.py b/tagstudio/src/core/utils/refresh_dir.py index e93d35bc..87b734ea 100644 --- a/tagstudio/src/core/utils/refresh_dir.py +++ b/tagstudio/src/core/utils/refresh_dir.py @@ -1,12 +1,11 @@ -from time import time from collections.abc import Iterator from dataclasses import dataclass, field from pathlib import Path +from time import time import structlog - from src.core.constants import TS_FOLDER_NAME -from src.core.library import Library, Entry +from src.core.library import Entry, Library logger = structlog.get_logger(__name__) diff --git a/tagstudio/src/core/utils/web.py b/tagstudio/src/core/utils/web.py index ec4f483f..67d4ce72 100644 --- a/tagstudio/src/core/utils/web.py +++ b/tagstudio/src/core/utils/web.py @@ -4,7 +4,7 @@ def strip_web_protocol(string: str) -> str: - """Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string.""" + r"""Strips a leading web protocol (ex. \"https://\") as well as \"www.\" from a string.""" prefixes = ["https://", "http://", "www.", "www2."] for prefix in prefixes: string = string.removeprefix(prefix) diff --git a/tagstudio/src/qt/flowlayout.py b/tagstudio/src/qt/flowlayout.py index 05bcc1b0..6334cf95 100644 --- a/tagstudio/src/qt/flowlayout.py +++ b/tagstudio/src/qt/flowlayout.py @@ -2,26 +2,12 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x""" +"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x.""" -from PySide6.QtCore import Qt, QMargins, QPoint, QRect, QSize +from PySide6.QtCore import QMargins, QPoint, QRect, QSize, Qt from PySide6.QtWidgets import QLayout, QSizePolicy, QWidget -# class Window(QWidget): -# def __init__(self): -# super().__init__() - -# flow_layout = FlowLayout(self) -# flow_layout.addWidget(QPushButton("Short")) -# flow_layout.addWidget(QPushButton("Longer")) -# flow_layout.addWidget(QPushButton("Different text")) -# flow_layout.addWidget(QPushButton("More text")) -# flow_layout.addWidget(QPushButton("Even longer button text")) - -# self.setWindowTitle("Flow Layout") - - class FlowWidget(QWidget): def __init__(self, parent=None) -> None: super().__init__(parent) @@ -43,46 +29,46 @@ class FlowLayout(QLayout): while item: item = self.takeAt(0) - def addItem(self, item): + def addItem(self, item): # noqa: N802 self._item_list.append(item) def count(self): return len(self._item_list) - def itemAt(self, index): + def itemAt(self, index): # noqa: N802 if 0 <= index < len(self._item_list): return self._item_list[index] return None - def takeAt(self, index): + def takeAt(self, index): # noqa: N802 if 0 <= index < len(self._item_list): return self._item_list.pop(index) return None - def expandingDirections(self): + def expandingDirections(self): # noqa: N802 return Qt.Orientation(0) - def hasHeightForWidth(self): + def hasHeightForWidth(self): # noqa: N802 return True - def heightForWidth(self, width): - height = self._do_layout(QRect(0, 0, width, 0), True) + def heightForWidth(self, width): # noqa: N802 + height = self._do_layout(QRect(0, 0, width, 0), test_only=True) return height - def setGeometry(self, rect): + def setGeometry(self, rect): # noqa: N802 super().setGeometry(rect) - self._do_layout(rect, False) + self._do_layout(rect, test_only=False) - def setGridEfficiency(self, value: bool): + def enable_grid_optimizations(self, value: bool): """Enable or Disable efficiencies when all objects are equally sized.""" self.grid_efficiency = value - def sizeHint(self): + def sizeHint(self): # noqa: N802 return self.minimumSize() - def minimumSize(self): + def minimumSize(self): # noqa: N802 if self.grid_efficiency: if self._item_list: return self._item_list[0].minimumSize() @@ -94,9 +80,7 @@ class FlowLayout(QLayout): for item in self._item_list: size = size.expandedTo(item.minimumSize()) - size += QSize( - 2 * self.contentsMargins().top(), 2 * self.contentsMargins().top() - ) + size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top()) return size def _do_layout(self, rect: QRect, test_only: bool) -> float: @@ -122,20 +106,13 @@ class FlowLayout(QLayout): ) for item in self._item_list: - # print(issubclass(type(item.widget()), FlowWidget)) - # print(item.widget().ignore_size) skip_count = 0 - if ( - issubclass(type(item.widget()), FlowWidget) - and item.widget().ignore_size - ): + if issubclass(type(item.widget()), FlowWidget) and item.widget().ignore_size: skip_count += 1 - if ( - issubclass(type(item.widget()), FlowWidget) - and not item.widget().ignore_size - ) or (not issubclass(type(item.widget()), FlowWidget)): - # print(f'Item {i}') + if (issubclass(type(item.widget()), FlowWidget) and not item.widget().ignore_size) or ( + not issubclass(type(item.widget()), FlowWidget) + ): if not self.grid_efficiency: style = item.widget().style() layout_spacing_x = style.layoutSpacing( @@ -163,15 +140,4 @@ class FlowLayout(QLayout): x = next_x line_height = max(line_height, item.sizeHint().height()) - # print(y + line_height - rect.y() * ((len(self._item_list) - skip_count) / len(self._item_list))) - # print(y + line_height - rect.y()) * ((len(self._item_list) - skip_count) / len(self._item_list)) - return ( - y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list)) - ) - - -# if __name__ == "__main__": -# app = QApplication(sys.argv) -# main_win = Window() -# main_win.show() -# sys.exit(app.exec()) + return y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list)) diff --git a/tagstudio/src/qt/helpers/color_overlay.py b/tagstudio/src/qt/helpers/color_overlay.py index c19ba73e..3bdb1fa5 100644 --- a/tagstudio/src/qt/helpers/color_overlay.py +++ b/tagstudio/src/qt/helpers/color_overlay.py @@ -15,13 +15,11 @@ _THEME_LIGHT_FG: str = "#000000DD" def theme_fg_overlay(image: Image.Image) -> Image.Image: - """ - Overlay the foreground theme color onto an image. + """Overlay the foreground theme color onto an image. Args: image (Image): The PIL Image object to apply an overlay to. """ - overlay_color = ( _THEME_DARK_FG if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark @@ -32,23 +30,19 @@ def theme_fg_overlay(image: Image.Image) -> Image.Image: def gradient_overlay(image: Image.Image, gradient=list[str]) -> Image.Image: - """ - Overlay a color gradient onto an image. + """Overlay a color gradient onto an image. Args: image (Image): The PIL Image object to apply an overlay to. gradient (list[str): A list of string hex color codes for use as the colors of the gradient. """ - im: Image.Image = _apply_overlay(image, linear_gradient(image.size, gradient)) return im def _apply_overlay(image: Image.Image, overlay: Image.Image) -> Image.Image: - """ - Internal method to apply an overlay on top of an image, using - the image's alpha channel as a mask. + """Apply an overlay on top of an image, using the image's alpha channel as a mask. Args: image (Image): The PIL Image object to apply an overlay to. diff --git a/tagstudio/src/qt/helpers/custom_runnable.py b/tagstudio/src/qt/helpers/custom_runnable.py index 0e8adb4d..6520c16f 100644 --- a/tagstudio/src/qt/helpers/custom_runnable.py +++ b/tagstudio/src/qt/helpers/custom_runnable.py @@ -3,7 +3,7 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtCore import Signal, QRunnable, QObject +from PySide6.QtCore import QObject, QRunnable, Signal class CustomRunnable(QRunnable, QObject): diff --git a/tagstudio/src/qt/helpers/file_opener.py b/tagstudio/src/qt/helpers/file_opener.py index d9afcd73..0f4a7e0a 100644 --- a/tagstudio/src/qt/helpers/file_opener.py +++ b/tagstudio/src/qt/helpers/file_opener.py @@ -2,16 +2,15 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -import subprocess import shutil +import subprocess import sys import traceback from pathlib import Path import structlog -from PySide6.QtWidgets import QLabel from PySide6.QtCore import Qt - +from PySide6.QtWidgets import QLabel logger = structlog.get_logger(__name__) @@ -21,8 +20,8 @@ def open_file(path: str | Path, file_manager: bool = False): Args: path (str): The path to the file to open. - file_manager (bool, optional): Whether to open the file in the file manager (e.g. Finder on macOS). - Defaults to False. + file_manager (bool, optional): Whether to open the file in the file manager + (e.g. Finder on macOS). Defaults to False. """ path = Path(path) logger.info("Opening file", path=path) @@ -36,7 +35,8 @@ def open_file(path: str | Path, file_manager: bool = False): if file_manager: command_name = "explorer" command_args = '/select,"' + normpath + '"' - # For some reason, if the args are passed in a list, this will error when the path has spaces, even while surrounded in double quotes + # For some reason, if the args are passed in a list, this will error when the + # path has spaces, even while surrounded in double quotes. subprocess.Popen( command_name + command_args, shell=True, @@ -82,9 +82,7 @@ def open_file(path: str | Path, file_manager: bool = False): if command is not None: subprocess.Popen([command] + command_args, close_fds=True) else: - logger.info( - "Could not find command on system PATH", command=command_name - ) + logger.info("Could not find command on system PATH", command=command_name) except Exception: traceback.print_exc() @@ -125,7 +123,7 @@ class FileOpenerLabel(QLabel): """ super().__init__(text, parent) - def setFilePath(self, filepath): + def set_file_path(self, filepath): """Set the filepath to open. Args: @@ -133,10 +131,11 @@ class FileOpenerLabel(QLabel): """ self.filepath = filepath - def mousePressEvent(self, event): + def mousePressEvent(self, event): # noqa: N802 """Handle mouse press events. - On a left click, open the file in the default file explorer. On a right click, show a context menu. + On a left click, open the file in the default file explorer. + On a right click, show a context menu. Args: event (QMouseEvent): The mouse press event. diff --git a/tagstudio/src/qt/helpers/function_iterator.py b/tagstudio/src/qt/helpers/function_iterator.py index 770e2846..a07f16da 100644 --- a/tagstudio/src/qt/helpers/function_iterator.py +++ b/tagstudio/src/qt/helpers/function_iterator.py @@ -3,7 +3,7 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio from collections.abc import Callable -from PySide6.QtCore import Signal, QObject +from PySide6.QtCore import QObject, Signal class FunctionIterator(QObject): diff --git a/tagstudio/src/qt/helpers/gradient.py b/tagstudio/src/qt/helpers/gradient.py index dabe7639..a5f3b9fa 100644 --- a/tagstudio/src/qt/helpers/gradient.py +++ b/tagstudio/src/qt/helpers/gradient.py @@ -2,12 +2,10 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PIL import Image, ImageEnhance, ImageChops +from PIL import Image, ImageChops, ImageEnhance -def four_corner_gradient_background( - image: Image.Image, adj_size, mask, hl -) -> Image.Image: +def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl) -> Image.Image: if image.size != (adj_size, adj_size): # Old 1 color method. # bg_col = image.copy().resize((1, 1)).getpixel((0,0)) @@ -16,7 +14,11 @@ def four_corner_gradient_background( # bg = bg.resize((adj_size,adj_size), resample=Image.Resampling.NEAREST) # Small gradient background. Looks decent, and is only a one-liner. - # bg = image.copy().resize((2, 2), resample=Image.Resampling.BILINEAR).resize((adj_size,adj_size),resample=Image.Resampling.BILINEAR) + # bg = ( + # image.copy() + # .resize((2, 2), resample=Image.Resampling.BILINEAR) + # .resize((adj_size, adj_size), resample=Image.Resampling.BILINEAR) + # ) # Four-Corner Gradient Background. # Not exactly a one-liner, but it's (subjectively) really cool. diff --git a/tagstudio/src/qt/helpers/qbutton_wrapper.py b/tagstudio/src/qt/helpers/qbutton_wrapper.py index ea554446..31d2ad81 100644 --- a/tagstudio/src/qt/helpers/qbutton_wrapper.py +++ b/tagstudio/src/qt/helpers/qbutton_wrapper.py @@ -6,9 +6,10 @@ from PySide6.QtWidgets import QPushButton class QPushButtonWrapper(QPushButton): - """ - This is a customized implementation of the PySide6 QPushButton that allows to suppress the warning that is triggered - by disconnecting a signal that is not currently connected. + """Custom QPushButton wrapper. + + This is a customized implementation of the PySide6 QPushButton that allows to suppress + the warning that is triggered by disconnecting a signal that is not currently connected. """ def __init__(self, *args, **kwargs): diff --git a/tagstudio/src/qt/modals/add_field.py b/tagstudio/src/qt/modals/add_field.py index 9e1ef5c7..fd0e0c99 100644 --- a/tagstudio/src/qt/modals/add_field.py +++ b/tagstudio/src/qt/modals/add_field.py @@ -3,17 +3,16 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtCore import Signal, Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QListWidget, QListWidgetItem, + QPushButton, + QVBoxLayout, + QWidget, ) - from src.core.library import Library diff --git a/tagstudio/src/qt/modals/build_tag.py b/tagstudio/src/qt/modals/build_tag.py index 5507fb09..dc45563c 100644 --- a/tagstudio/src/qt/modals/build_tag.py +++ b/tagstudio/src/qt/modals/build_tag.py @@ -4,26 +4,24 @@ import structlog -from PySide6.QtCore import Signal, Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QLabel, - QPushButton, - QLineEdit, - QScrollArea, - QFrame, - QTextEdit, QComboBox, + QFrame, + QLabel, + QLineEdit, + QPushButton, + QScrollArea, + QTextEdit, + QVBoxLayout, + QWidget, ) - -from src.core.library import Tag, Library +from src.core.library import Library, Tag from src.core.library.alchemy.enums import TagColor from src.core.palette import ColorType, get_tag_color - -from src.qt.widgets.panel import PanelWidget, PanelModal -from src.qt.widgets.tag import TagWidget from src.qt.modals.tag_search import TagSearchPanel +from src.qt.widgets.panel import PanelModal, PanelWidget +from src.qt.widgets.tag import TagWidget logger = structlog.get_logger(__name__) @@ -145,7 +143,9 @@ class BuildTagPanel(PanelWidget): "combobox-popup:0;" "font-weight:600;" f"color:{get_tag_color(ColorType.TEXT, self.color_field.currentData())};" - f"background-color:{get_tag_color(ColorType.PRIMARY, self.color_field.currentData())};" + f"background-color:{get_tag_color( + ColorType.PRIMARY, + self.color_field.currentData())};" ) ) ) @@ -183,7 +183,7 @@ class BuildTagPanel(PanelWidget): layout.setSpacing(3) for tag_id in self.subtags: tag = self.lib.get_tag(tag_id) - tw = TagWidget(tag, False, True) + tw = TagWidget(tag, has_edit=False, has_remove=True) tw.on_remove.connect(lambda t=tag_id: self.remove_subtag_callback(t)) layout.addWidget(tw) self.scroll_layout.addWidget(c) diff --git a/tagstudio/src/qt/modals/delete_unlinked.py b/tagstudio/src/qt/modals/delete_unlinked.py index 19a3af08..04b7f8f5 100644 --- a/tagstudio/src/qt/modals/delete_unlinked.py +++ b/tagstudio/src/qt/modals/delete_unlinked.py @@ -4,17 +4,16 @@ import typing -from PySide6.QtCore import Signal, Qt, QThreadPool -from PySide6.QtGui import QStandardItemModel, QStandardItem +from PySide6.QtCore import Qt, QThreadPool, Signal +from PySide6.QtGui import QStandardItem, QStandardItemModel from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QListView, + QPushButton, + QVBoxLayout, + QWidget, ) - from src.core.utils.missing_files import MissingRegistry from src.qt.helpers.custom_runnable import CustomRunnable from src.qt.helpers.function_iterator import FunctionIterator diff --git a/tagstudio/src/qt/modals/file_extension.py b/tagstudio/src/qt/modals/file_extension.py index c631cd50..15403a88 100644 --- a/tagstudio/src/qt/modals/file_extension.py +++ b/tagstudio/src/qt/modals/file_extension.py @@ -3,32 +3,27 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtCore import Signal, Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( - QVBoxLayout, + QComboBox, QHBoxLayout, - QWidget, + QLabel, + QLineEdit, QPushButton, + QStyledItemDelegate, QTableWidget, QTableWidgetItem, - QStyledItemDelegate, - QLineEdit, - QComboBox, - QLabel, + QVBoxLayout, + QWidget, ) - +from src.core.constants import LibraryPrefs from src.core.library import Library from src.qt.widgets.panel import PanelWidget -from src.core.constants import LibraryPrefs class FileExtensionItemDelegate(QStyledItemDelegate): - def setModelData(self, editor, model, index): - if ( - isinstance(editor, QLineEdit) - and editor.text() - and not editor.text().startswith(".") - ): + def setModelData(self, editor, model, index): # noqa: N802 + if isinstance(editor, QLineEdit) and editor.text() and not editor.text().startswith("."): editor.setText(f".{editor.text()}") super().setModelData(editor, model, index) @@ -75,9 +70,7 @@ class FileExtensionModal(PanelWidget): is_exclude_list = int(bool(self.lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST))) self.mode_combobox.setCurrentIndex(is_exclude_list) - self.mode_combobox.currentIndexChanged.connect( - lambda i: self.update_list_mode(i) - ) + self.mode_combobox.currentIndexChanged.connect(lambda i: self.update_list_mode(i)) self.mode_layout.addWidget(self.mode_label) self.mode_layout.addWidget(self.mode_combobox) self.mode_layout.setStretch(1, 1) @@ -85,16 +78,13 @@ class FileExtensionModal(PanelWidget): # Add Widgets To Layout ------------------------------------------------ self.root_layout.addWidget(self.mode_widget) self.root_layout.addWidget(self.table) - self.root_layout.addWidget( - self.add_button, alignment=Qt.AlignmentFlag.AlignCenter - ) + self.root_layout.addWidget(self.add_button, alignment=Qt.AlignmentFlag.AlignCenter) # Finalize Modal ------------------------------------------------------- self.refresh_list() def update_list_mode(self, mode: int): - """ - Update the mode of the extension list: "Exclude" or "Include". + """Update the mode of the extension list: "Exclude" or "Include". Args: mode (int): The list mode, given by the index of the mode inside diff --git a/tagstudio/src/qt/modals/fix_dupes.py b/tagstudio/src/qt/modals/fix_dupes.py index e323c20e..fccdb5bc 100644 --- a/tagstudio/src/qt/modals/fix_dupes.py +++ b/tagstudio/src/qt/modals/fix_dupes.py @@ -7,14 +7,13 @@ import typing from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, + QFileDialog, QHBoxLayout, QLabel, QPushButton, - QFileDialog, + QVBoxLayout, + QWidget, ) - from src.core.library import Library from src.core.utils.dupe_files import DupeRegistry from src.qt.modals.mirror_entities import MirrorEntriesModal @@ -25,7 +24,6 @@ if typing.TYPE_CHECKING: class FixDupeFilesModal(QWidget): - # done = Signal(int) def __init__(self, library: "Library", driver: "QtDriver"): super().__init__() self.lib = library @@ -43,41 +41,19 @@ class FixDupeFilesModal(QWidget): self.desc_widget = QLabel() self.desc_widget.setObjectName("descriptionLabel") self.desc_widget.setWordWrap(True) - self.desc_widget.setStyleSheet( - # 'background:blue;' - "text-align:left;" - # 'font-weight:bold;' - # 'font-size:14px;' - # 'padding-top: 6px' - "" - ) + self.desc_widget.setStyleSheet("text-align:left;") self.desc_widget.setText( - """TagStudio supports importing DupeGuru results to manage duplicate files.""" + "TagStudio supports importing DupeGuru results to manage duplicate files." ) self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.dupe_count = QLabel() self.dupe_count.setObjectName("dupeCountLabel") - self.dupe_count.setStyleSheet( - # 'background:blue;' - # 'text-align:center;' - "font-weight:bold;" - "font-size:14px;" - # 'padding-top: 6px' - "" - ) + self.dupe_count.setStyleSheet("font-weight:bold;" "font-size:14px;" "") self.dupe_count.setAlignment(Qt.AlignmentFlag.AlignCenter) self.file_label = QLabel() self.file_label.setObjectName("fileLabel") - # self.file_label.setStyleSheet( - # # 'background:blue;' - # # 'text-align:center;' - # 'font-weight:bold;' - # 'font-size:14px;' - # # 'padding-top: 6px' - # '') - # self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.file_label.setText("No DupeGuru File Selected") self.open_button = QPushButton() @@ -93,17 +69,19 @@ class FixDupeFilesModal(QWidget): self.mirror_desc = QLabel() self.mirror_desc.setWordWrap(True) self.mirror_desc.setText( - """Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.""" + "Mirror the Entry data across each duplicate match set, combining all data while not " + "removing or duplicating fields. This operation will not delete any files or data." ) - # self.mirror_delete_button = QPushButton() - # self.mirror_delete_button.setText('Mirror && Delete') - self.advice_label = QLabel() self.advice_label.setWordWrap(True) + # fmt: off self.advice_label.setText( - """After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's "Fix Unlinked Entries" feature in the Tools menu in order to delete the unlinked Entries.""" + "After mirroring, you're free to use DupeGuru to delete the unwanted files. " + "Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the " + "Tools menu in order to delete the unlinked Entries." ) + # fmt: on self.button_container = QWidget() self.button_layout = QHBoxLayout(self.button_container) @@ -112,28 +90,18 @@ class FixDupeFilesModal(QWidget): self.done_button = QPushButton() self.done_button.setText("&Done") - # self.save_button.setAutoDefault(True) self.done_button.setDefault(True) self.done_button.clicked.connect(self.hide) - # self.done_button.clicked.connect(lambda: self.done.emit(self.combo_box.currentIndex())) - # self.save_button.clicked.connect(lambda: save_callback(widget.get_content())) self.button_layout.addWidget(self.done_button) - # self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex())) - - # self.done.connect(lambda x: callback(x)) - self.root_layout.addWidget(self.desc_widget) self.root_layout.addWidget(self.dupe_count) self.root_layout.addWidget(self.file_label) self.root_layout.addWidget(self.open_button) - # self.mirror_delete_button.setHidden(True) self.root_layout.addWidget(self.mirror_button) self.root_layout.addWidget(self.mirror_desc) - # self.root_layout.addWidget(self.mirror_delete_button) self.root_layout.addWidget(self.advice_label) - # self.root_layout.setStretch(1,2) self.root_layout.addStretch(1) self.root_layout.addWidget(self.button_container) diff --git a/tagstudio/src/qt/modals/fix_unlinked.py b/tagstudio/src/qt/modals/fix_unlinked.py index b933aa6d..0e51144f 100644 --- a/tagstudio/src/qt/modals/fix_unlinked.py +++ b/tagstudio/src/qt/modals/fix_unlinked.py @@ -6,15 +6,14 @@ import typing from PySide6.QtCore import Qt, QThreadPool -from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton - +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from src.core.library import Library from src.core.utils.missing_files import MissingRegistry -from src.qt.helpers.function_iterator import FunctionIterator from src.qt.helpers.custom_runnable import CustomRunnable +from src.qt.helpers.function_iterator import FunctionIterator from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal -from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries from src.qt.modals.merge_dupe_entries import MergeDuplicateEntries +from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. @@ -43,7 +42,11 @@ class FixUnlinkedEntriesModal(QWidget): self.unlinked_desc_widget.setWordWrap(True) self.unlinked_desc_widget.setStyleSheet("text-align:left;") self.unlinked_desc_widget.setText( - """Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked. Unlinked entries may be automatically relinked via searching your directories, manually relinked by the user, or deleted if desired.""" + "Each library entry is linked to a file in one of your directories. " + "If a file linked to an entry is moved or deleted outside of TagStudio, " + "it is then considered unlinked.\n\n" + "Unlinked entries may be automatically relinked via searching your directories, " + "manually relinked by the user, or deleted if desired." ) self.missing_count_label = QLabel() diff --git a/tagstudio/src/qt/modals/folders_to_tags.py b/tagstudio/src/qt/modals/folders_to_tags.py index ae6dacb3..f9962a0e 100644 --- a/tagstudio/src/qt/modals/folders_to_tags.py +++ b/tagstudio/src/qt/modals/folders_to_tags.py @@ -10,17 +10,16 @@ from dataclasses import dataclass, field import structlog from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, + QFrame, QHBoxLayout, QLabel, QPushButton, QScrollArea, - QFrame, + QVBoxLayout, + QWidget, ) - -from src.core.constants import TAG_FAVORITE, TAG_ARCHIVED -from src.core.library import Tag, Library +from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE +from src.core.library import Library, Tag from src.core.library.alchemy.fields import _FieldID from src.core.palette import ColorType, get_tag_color from src.qt.flowlayout import FlowLayout @@ -38,9 +37,7 @@ class BranchData: tag: Tag | None = None -def add_folders_to_tree( - library: Library, tree: BranchData, items: tuple[str, ...] -) -> BranchData: +def add_folders_to_tree(library: Library, tree: BranchData, items: tuple[str, ...]) -> BranchData: branch = tree for folder in items: if folder not in branch.dirs: @@ -160,7 +157,6 @@ def generate_preview_data(library: Library) -> BranchData: class FoldersToTagsModal(QWidget): - # done = Signal(int) def __init__(self, library: "Library", driver: "QtDriver"): super().__init__() self.library = library @@ -177,9 +173,7 @@ class FoldersToTagsModal(QWidget): self.title_widget = QLabel() self.title_widget.setObjectName("title") self.title_widget.setWordWrap(True) - self.title_widget.setStyleSheet( - "font-weight:bold;" "font-size:14px;" "padding-top: 6px" - ) + self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px") self.title_widget.setText("Create Tags From Folders") self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -187,7 +181,8 @@ class FoldersToTagsModal(QWidget): self.desc_widget.setObjectName("descriptionLabel") self.desc_widget.setWordWrap(True) self.desc_widget.setText( - """Creates tags based on your folder structure and applies them to your entries.\n The structure below shows all the tags that will be created and what entries they will be applied to.""" + """Creates tags based on your folder structure and applies them to your entries. + This tree shows all tags to be created and which entries they will be applied to.""" ) self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -210,9 +205,7 @@ class FoldersToTagsModal(QWidget): self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.scroll_area = QScrollArea() - self.scroll_area.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOn - ) + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) self.scroll_area.setWidgetResizable(True) self.scroll_area.setFrameShadow(QFrame.Shadow.Plain) self.scroll_area.setFrameShape(QFrame.Shape.NoFrame) @@ -229,9 +222,7 @@ class FoldersToTagsModal(QWidget): self.root_layout.addWidget(self.desc_widget) self.root_layout.addWidget(self.open_close_button_w) self.root_layout.addWidget(self.scroll_area) - self.root_layout.addWidget( - self.apply_button, alignment=Qt.AlignmentFlag.AlignCenter - ) + self.root_layout.addWidget(self.apply_button, alignment=Qt.AlignmentFlag.AlignCenter) def on_apply(self, event): folders_to_tags(self.library) @@ -310,10 +301,10 @@ class TreeItem(QWidget): self.label.setText(">" if self.children_widget.isHidden() else "v") -class ModifiedTagWidget( - QWidget -): # Needed to be modified because the original searched the display name in the library where it wasn't added yet - def __init__(self, tag: Tag, parentTag: Tag) -> None: +class ModifiedTagWidget(QWidget): + """Modified TagWidget that does not search for the Tag's display name in the Library.""" + + def __init__(self, tag: Tag, parent_tag: Tag) -> None: super().__init__() self.tag = tag @@ -324,8 +315,8 @@ class ModifiedTagWidget( self.bg_button = QPushButton(self) self.bg_button.setFlat(True) - if parentTag is not None: - text = f"{tag.name} ({parentTag.name})".replace("&", "&&") + if parent_tag is not None: + text = f"{tag.name} ({parent_tag.name})".replace("&", "&&") else: text = tag.name.replace("&", "&&") self.bg_button.setText(text) diff --git a/tagstudio/src/qt/modals/merge_dupe_entries.py b/tagstudio/src/qt/modals/merge_dupe_entries.py index 47051233..cc81949d 100644 --- a/tagstudio/src/qt/modals/merge_dupe_entries.py +++ b/tagstudio/src/qt/modals/merge_dupe_entries.py @@ -4,12 +4,11 @@ import typing -from PySide6.QtCore import QObject, Signal, QThreadPool - +from PySide6.QtCore import QObject, QThreadPool, Signal from src.core.library import Library from src.core.utils.dupe_files import DupeRegistry -from src.qt.helpers.function_iterator import FunctionIterator from src.qt.helpers.custom_runnable import CustomRunnable +from src.qt.helpers.function_iterator import FunctionIterator from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. @@ -39,9 +38,7 @@ class MergeDuplicateEntries(QObject): pw.show() iterator.value.connect(lambda x: pw.update_progress(x)) - iterator.value.connect( - lambda: (pw.update_label("Merging Duplicate Entries...")) - ) + iterator.value.connect(lambda: (pw.update_label("Merging Duplicate Entries..."))) r = CustomRunnable(iterator.run) r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit())) diff --git a/tagstudio/src/qt/modals/mirror_entities.py b/tagstudio/src/qt/modals/mirror_entities.py index 97a11357..d7178ec4 100644 --- a/tagstudio/src/qt/modals/mirror_entities.py +++ b/tagstudio/src/qt/modals/mirror_entities.py @@ -3,23 +3,22 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from time import sleep import typing +from time import sleep -from PySide6.QtCore import Signal, Qt, QThreadPool -from PySide6.QtGui import QStandardItemModel, QStandardItem +from PySide6.QtCore import Qt, QThreadPool, Signal +from PySide6.QtGui import QStandardItem, QStandardItemModel from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QListView, + QPushButton, + QVBoxLayout, + QWidget, ) - from src.core.utils.dupe_files import DupeRegistry -from src.qt.helpers.function_iterator import FunctionIterator from src.qt.helpers.custom_runnable import CustomRunnable +from src.qt.helpers.function_iterator import FunctionIterator from src.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. @@ -95,9 +94,7 @@ class MirrorEntriesModal(QWidget): pw.show() iterator.value.connect(lambda x: pw.update_progress(x + 1)) iterator.value.connect( - lambda x: pw.update_label( - f"Mirroring {x + 1}/{self.tracker.groups_count} Entries..." - ) + lambda x: pw.update_label(f"Mirroring {x + 1}/{self.tracker.groups_count} Entries...") ) r = CustomRunnable(iterator.run) QThreadPool.globalInstance().start(r) diff --git a/tagstudio/src/qt/modals/relink_unlinked.py b/tagstudio/src/qt/modals/relink_unlinked.py index 36e8da82..e567f8b5 100644 --- a/tagstudio/src/qt/modals/relink_unlinked.py +++ b/tagstudio/src/qt/modals/relink_unlinked.py @@ -3,11 +3,10 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtCore import QObject, Signal, QThreadPool - +from PySide6.QtCore import QObject, QThreadPool, Signal from src.core.utils.missing_files import MissingRegistry -from src.qt.helpers.function_iterator import FunctionIterator from src.qt.helpers.custom_runnable import CustomRunnable +from src.qt.helpers.function_iterator import FunctionIterator from src.qt.widgets.progress import ProgressWidget diff --git a/tagstudio/src/qt/modals/tag_database.py b/tagstudio/src/qt/modals/tag_database.py index 9b9f11e4..f1aebad6 100644 --- a/tagstudio/src/qt/modals/tag_database.py +++ b/tagstudio/src/qt/modals/tag_database.py @@ -2,21 +2,20 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtCore import Signal, Qt, QSize +from PySide6.QtCore import QSize, Qt, Signal from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, + QFrame, QHBoxLayout, QLineEdit, QScrollArea, - QFrame, + QVBoxLayout, + QWidget, ) - from src.core.library import Library, Tag from src.core.library.alchemy.enums import FilterState -from src.qt.widgets.panel import PanelWidget, PanelModal -from src.qt.widgets.tag import TagWidget from src.qt.modals.build_tag import BuildTagPanel +from src.qt.widgets.panel import PanelModal, PanelWidget +from src.qt.widgets.tag import TagWidget class TagDatabasePanel(PanelWidget): @@ -25,10 +24,8 @@ class TagDatabasePanel(PanelWidget): def __init__(self, library: Library): super().__init__() self.lib: Library = library - # self.callback = callback self.first_tag_id = -1 self.tag_limit = 30 - # self.selected_tag: int = 0 self.setMinimumSize(300, 400) self.root_layout = QVBoxLayout(self) @@ -38,49 +35,27 @@ class TagDatabasePanel(PanelWidget): self.search_field.setObjectName("searchField") self.search_field.setMinimumSize(QSize(0, 32)) self.search_field.setPlaceholderText("Search Tags") - self.search_field.textEdited.connect( - lambda: self.update_tags(self.search_field.text()) - ) + self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text())) self.search_field.returnPressed.connect( lambda checked=False: self.on_return(self.search_field.text()) ) - # self.content_container = QWidget() - # self.content_layout = QHBoxLayout(self.content_container) - self.scroll_contents = QWidget() self.scroll_layout = QVBoxLayout(self.scroll_contents) self.scroll_layout.setContentsMargins(6, 0, 6, 0) self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.scroll_area = QScrollArea() - # self.scroll_area.setStyleSheet('background: #000000;') - self.scroll_area.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOn - ) - # self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) self.scroll_area.setWidgetResizable(True) self.scroll_area.setFrameShadow(QFrame.Shadow.Plain) self.scroll_area.setFrameShape(QFrame.Shape.NoFrame) - # sa.setMaximumWidth(self.preview_size[0]) self.scroll_area.setWidget(self.scroll_contents) - # self.add_button = QPushButton() - # self.root_layout.addWidget(self.add_button) - # self.add_button.setText('Add Tag') - # # self.done_button.clicked.connect(lambda checked=False, x=1101: (callback(x), self.hide())) - # self.add_button.clicked.connect(lambda checked=False, x=1101: callback(x)) - # # self.setLayout(self.root_layout) - self.root_layout.addWidget(self.search_field) self.root_layout.addWidget(self.scroll_area) self.update_tags() - # def reset(self): - # self.search_field.setText('') - # self.update_tags('') - # self.search_field.setFocus() - def on_return(self, text: str): if text and self.first_tag_id >= 0: # callback(self.first_tag_id) @@ -91,7 +66,7 @@ class TagDatabasePanel(PanelWidget): self.parentWidget().hide() def update_tags(self, query: str | None = None): - # TODO: Look at recycling rather than deleting and reinitializing + # TODO: Look at recycling rather than deleting and re-initializing while self.scroll_layout.itemAt(0): self.scroll_layout.takeAt(0).widget().deleteLater() @@ -102,7 +77,7 @@ class TagDatabasePanel(PanelWidget): row = QHBoxLayout(container) row.setContentsMargins(0, 0, 0, 0) row.setSpacing(3) - tag_widget = TagWidget(tag, True, False) + tag_widget = TagWidget(tag, has_edit=True, has_remove=False) tag_widget.on_edit.connect(lambda checked=False, t=tag: self.edit_tag(t)) row.addWidget(tag_widget) self.scroll_layout.addWidget(container) @@ -119,7 +94,6 @@ class TagDatabasePanel(PanelWidget): done_callback=(self.update_tags(self.search_field.text())), has_save=True, ) - # self.edit_modal.widget.update_display_name.connect(lambda t: self.edit_modal.title_widget.setText(t)) # TODO Check Warning: Expected type 'BuildTagPanel', got 'PanelWidget' instead self.edit_modal.saved.connect(lambda: self.edit_tag_callback(build_tag_panel)) self.edit_modal.show() @@ -127,8 +101,3 @@ class TagDatabasePanel(PanelWidget): def edit_tag_callback(self, btp: BuildTagPanel): self.lib.add_tag(btp.build_tag()) self.update_tags(self.search_field.text()) - - # def enterEvent(self, event: QEnterEvent) -> None: - # self.search_field.setFocus() - # return super().enterEvent(event) - # self.focusOutEvent diff --git a/tagstudio/src/qt/modals/tag_search.py b/tagstudio/src/qt/modals/tag_search.py index 8459f11a..c44278fd 100644 --- a/tagstudio/src/qt/modals/tag_search.py +++ b/tagstudio/src/qt/modals/tag_search.py @@ -6,17 +6,16 @@ import math import structlog -from PySide6.QtCore import Signal, Qt, QSize +from PySide6.QtCore import QSize, Qt, Signal from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QHBoxLayout, - QPushButton, - QLineEdit, - QScrollArea, QFrame, + QHBoxLayout, + QLineEdit, + QPushButton, + QScrollArea, + QVBoxLayout, + QWidget, ) - from src.core.library import Library from src.core.library.alchemy.enums import FilterState from src.core.palette import ColorType, get_tag_color @@ -32,10 +31,8 @@ class TagSearchPanel(PanelWidget): def __init__(self, library: Library): super().__init__() self.lib = library - # self.callback = callback self.first_tag_id = None self.tag_limit = 100 - # self.selected_tag: int = 0 self.setMinimumSize(300, 400) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(6, 0, 6, 0) @@ -44,49 +41,27 @@ class TagSearchPanel(PanelWidget): self.search_field.setObjectName("searchField") self.search_field.setMinimumSize(QSize(0, 32)) self.search_field.setPlaceholderText("Search Tags") - self.search_field.textEdited.connect( - lambda: self.update_tags(self.search_field.text()) - ) + self.search_field.textEdited.connect(lambda: self.update_tags(self.search_field.text())) self.search_field.returnPressed.connect( lambda checked=False: self.on_return(self.search_field.text()) ) - # self.content_container = QWidget() - # self.content_layout = QHBoxLayout(self.content_container) - self.scroll_contents = QWidget() self.scroll_layout = QVBoxLayout(self.scroll_contents) self.scroll_layout.setContentsMargins(6, 0, 6, 0) self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.scroll_area = QScrollArea() - # self.scroll_area.setStyleSheet('background: #000000;') - self.scroll_area.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOn - ) - # self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) self.scroll_area.setWidgetResizable(True) self.scroll_area.setFrameShadow(QFrame.Shadow.Plain) self.scroll_area.setFrameShape(QFrame.Shape.NoFrame) - # sa.setMaximumWidth(self.preview_size[0]) self.scroll_area.setWidget(self.scroll_contents) - # self.add_button = QPushButton() - # self.root_layout.addWidget(self.add_button) - # self.add_button.setText('Add Tag') - # # self.done_button.clicked.connect(lambda checked=False, x=1101: (callback(x), self.hide())) - # self.add_button.clicked.connect(lambda checked=False, x=1101: callback(x)) - # # self.setLayout(self.root_layout) - self.root_layout.addWidget(self.search_field) self.root_layout.addWidget(self.scroll_area) self.update_tags() - # def reset(self): - # self.search_field.setText('') - # self.update_tags('') - # self.search_field.setFocus() - def on_return(self, text: str): if text and self.first_tag_id is not None: # callback(self.first_tag_id) @@ -113,7 +88,7 @@ class TagSearchPanel(PanelWidget): layout = QHBoxLayout(c) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(3) - tw = TagWidget(tag, False, False) + tw = TagWidget(tag, has_edit=False, has_remove=False) ab = QPushButton() ab.setMinimumSize(23, 23) ab.setMaximumSize(23, 23) @@ -145,8 +120,3 @@ class TagSearchPanel(PanelWidget): self.scroll_layout.addWidget(c) self.search_field.setFocus() - - # def enterEvent(self, event: QEnterEvent) -> None: - # self.search_field.setFocus() - # return super().enterEvent(event) - # self.focusOutEvent diff --git a/tagstudio/src/qt/pagination.py b/tagstudio/src/qt/pagination.py index 174bc845..3acdc6f2 100644 --- a/tagstudio/src/qt/pagination.py +++ b/tagstudio/src/qt/pagination.py @@ -3,24 +3,18 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio """A pagination widget created for TagStudio.""" -# I never want to see this code again. -from PySide6.QtCore import QObject, Signal, QSize +from PySide6.QtCore import QObject, QSize, Signal from PySide6.QtGui import QIntValidator from PySide6.QtWidgets import ( - QWidget, QHBoxLayout, QLabel, QLineEdit, QSizePolicy, + QWidget, ) from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper -# class NumberEdit(QLineEdit): -# def __init__(self, parent=None) -> None: -# super().__init__(parent) -# self.textChanged - class Pagination(QWidget, QObject): """Widget containing controls for navigating between pages of items.""" @@ -46,7 +40,6 @@ class Pagination(QWidget, QObject): self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred) self.root_layout.setContentsMargins(0, 6, 0, 0) self.root_layout.setSpacing(3) - # self.setMinimumHeight(32) # [<] ---------------------------------- self.prev_button = QPushButtonWrapper() @@ -58,14 +51,11 @@ class Pagination(QWidget, QObject): self.start_button = QPushButtonWrapper() self.start_button.setMinimumSize(self.button_size) self.start_button.setMaximumSize(self.button_size) - # self.start_button.setStyleSheet('background:cyan;') - # self.start_button.setMaximumHeight(self.button_size.height()) # ------ ... --------------------------- self.start_ellipses = QLabel() self.start_ellipses.setMinimumSize(self.button_size) self.start_ellipses.setMaximumSize(self.button_size) - # self.start_ellipses.setMaximumHeight(self.button_size.height()) self.start_ellipses.setText(". . .") # --------- [3][4] --------------------- @@ -73,7 +63,6 @@ class Pagination(QWidget, QObject): self.start_buffer_layout = QHBoxLayout(self.start_buffer_container) self.start_buffer_layout.setContentsMargins(0, 0, 0, 0) self.start_buffer_layout.setSpacing(3) - # self.start_buffer_container.setStyleSheet('background:blue;') # ---------------- [5] ----------------- self.current_page_field = QLineEdit() @@ -84,30 +73,23 @@ class Pagination(QWidget, QObject): self.current_page_field.returnPressed.connect( lambda: self._goto_page(int(self.current_page_field.text()) - 1) ) - # self.current_page_field.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred) - # self.current_page_field.setMaximumHeight(self.button_size.height()) - # self.current_page_field.setMaximumWidth(self.button_size.width()) # -------------------- [6][7] ---------- self.end_buffer_container = QWidget() self.end_buffer_layout = QHBoxLayout(self.end_buffer_container) self.end_buffer_layout.setContentsMargins(0, 0, 0, 0) self.end_buffer_layout.setSpacing(3) - # self.end_buffer_container.setStyleSheet('background:orange;') # -------------------------- ... ------- self.end_ellipses = QLabel() self.end_ellipses.setMinimumSize(self.button_size) self.end_ellipses.setMaximumSize(self.button_size) - # self.end_ellipses.setMaximumHeight(self.button_size.height()) self.end_ellipses.setText(". . .") # ----------------------------- [42] --- self.end_button = QPushButtonWrapper() self.end_button.setMinimumSize(self.button_size) self.end_button.setMaximumSize(self.button_size) - # self.end_button.setMaximumHeight(self.button_size.height()) - # self.end_button.setStyleSheet('background:red;') # ---------------------------------- [>] self.next_button = QPushButtonWrapper() @@ -129,14 +111,12 @@ class Pagination(QWidget, QObject): self.root_layout.addStretch(1) self._populate_buffer_buttons() - # self.update_buttons(page_count=9, index=0) def update_buttons(self, page_count: int, index: int, emit: bool = True): # Guard if index < 0: raise ValueError("Negative index detected") - # Screw it for i in range(0, 10): if self.start_buffer_layout.itemAt(i): self.start_buffer_layout.itemAt(i).widget().setHidden(True) @@ -148,46 +128,22 @@ class Pagination(QWidget, QObject): # Hide everything if there are only one or less pages. # [-------------- HIDDEN --------------] self.setHidden(True) - # elif page_count > 1 and page_count < 7: - # # Only show Next/Prev, current index field, and both start and end - # # buffers (the end may be odd). - # # [<] [1][2][3][4][5][6] [>] - # self.start_button.setHidden(True) - # self.start_ellipses.setHidden(True) - # self.end_ellipses.setHidden(True) - # self.end_button.setHidden(True) - # elif page_count > 1: - # self.start_button.setHidden(False) - # self.start_ellipses.setHidden(False) - # self.end_ellipses.setHidden(False) - # self.end_button.setHidden(False) - - # self.start_button.setText('1') - # self.assign_click(self.start_button, 0) - # self.end_button.setText(str(page_count)) - # self.assign_click(self.end_button, page_count-1) elif page_count > 1: # Enable/Disable Next+Prev Buttons if index == 0: self.prev_button.setDisabled(True) - # self.start_buffer_layout.setContentsMargins(0,0,0,0) else: - # self.start_buffer_layout.setContentsMargins(3,0,3,0) self._assign_click(self.prev_button, index - 1) self.prev_button.setDisabled(False) if index == end_page: self.next_button.setDisabled(True) - # self.end_buffer_layout.setContentsMargins(0,0,0,0) else: - # self.end_buffer_layout.setContentsMargins(3,0,3,0) self._assign_click(self.next_button, index + 1) self.next_button.setDisabled(False) # Set Ellipses Sizes - # I do not know where these magic values were derived from, but - # this is better than the chain elif's that were here before if 8 <= page_count <= 11: end_scale = max(1, page_count - index - 6) srt_scale = max(1, index - 5) @@ -204,42 +160,29 @@ class Pagination(QWidget, QObject): self.start_ellipses.setMaximumWidth(srt_size) # Enable/Disable Ellipses - # if index <= max(self.buffer_page_count, 5)+1: if index <= self.buffer_page_count + 1: self.start_ellipses.setHidden(True) - # self.start_button.setHidden(True) else: self.start_ellipses.setHidden(False) - # self.start_button.setHidden(False) - # self.start_button.setText('1') self._assign_click(self.start_button, 0) - # if index >=(page_count-max(self.buffer_page_count, 5)-2): if index >= (page_count - self.buffer_page_count - 2): self.end_ellipses.setHidden(True) - # self.end_button.setHidden(True) else: self.end_ellipses.setHidden(False) - # self.end_button.setHidden(False) - # self.end_button.setText(str(page_count)) - # self.assign_click(self.end_button, page_count-1) # Hide/Unhide Start+End Buttons if index != 0: self.start_button.setText("1") self._assign_click(self.start_button, 0) self.start_button.setHidden(False) - # self.start_buffer_layout.setContentsMargins(3,0,0,0) else: self.start_button.setHidden(True) - # self.start_buffer_layout.setContentsMargins(0,0,0,0) if index != page_count - 1: self.end_button.setText(str(page_count)) self._assign_click(self.end_button, page_count - 1) self.end_button.setHidden(False) - # self.end_buffer_layout.setContentsMargins(0,0,3,0) else: self.end_button.setHidden(True) - # self.end_buffer_layout.setContentsMargins(0,0,0,0) if index == 0 or index == 1: self.start_buffer_container.setHidden(True) @@ -251,47 +194,26 @@ class Pagination(QWidget, QObject): else: self.end_buffer_container.setHidden(False) - # for i in range(0, self.buffer_page_count): - # self.start_buffer_layout.itemAt(i).widget().setHidden(True) - # Current Field and Buffer Pages sbc = 0 - # for i in range(0, max(self.buffer_page_count*2, 11)): for i in range(0, page_count): - # for j in range(0, self.buffer_page_count+1): - # self.start_buffer_layout.itemAt(j).widget().setHidden(True) - # if i == 1: - # self.start_buffer_layout.itemAt(i).widget().setHidden(True) - # elif i == page_count-2: - # self.end_buffer_layout.itemAt(i).widget().setHidden(True) - # Set Field if i == index: - # print(f'Current Index: {i}') if self.start_buffer_layout.itemAt(i): self.start_buffer_layout.itemAt(i).widget().setHidden(True) if self.end_buffer_layout.itemAt(i): self.end_buffer_layout.itemAt(i).widget().setHidden(True) sbc += 1 self.current_page_field.setText(str(i + 1)) - # elif index == page_count-1: - # self.start_button.setText(str(page_count)) start_offset = max(0, (index - 4) - 4) end_offset = min(page_count - 1, (index + 4) - 4) if i < index: - # if i != 0 and ((i-self.buffer_page_count) >= 0 or i <= self.buffer_page_count): if (i != 0) and i >= index - 4: - # print(f' Start i: {i}') - # print(f'Start Offset: {start_offset}') - # print(f' Requested i: {i-start_offset}') - # print(f'Setting Text "{str(i+1)}" for Local Start i:{i-start_offset}, Global i:{i}') - self.start_buffer_layout.itemAt( - i - start_offset - ).widget().setHidden(False) - self.start_buffer_layout.itemAt( - i - start_offset - ).widget().setText(str(i + 1)) # type: ignore + self.start_buffer_layout.itemAt(i - start_offset).widget().setHidden(False) + self.start_buffer_layout.itemAt(i - start_offset).widget().setText( # type: ignore + str(i + 1) + ) self._assign_click( self.start_buffer_layout.itemAt(i - start_offset).widget(), # type: ignore i, @@ -299,25 +221,12 @@ class Pagination(QWidget, QObject): sbc += 1 else: if self.start_buffer_layout.itemAt(i): - # print(f'Removing S-Start {i}') self.start_buffer_layout.itemAt(i).widget().setHidden(True) if self.end_buffer_layout.itemAt(i): - # print(f'Removing S-End {i}') self.end_buffer_layout.itemAt(i).widget().setHidden(True) elif i > index: - # if i != page_count-1: if i != page_count - 1 and i <= index + 4: - # print(f'End Buffer: {i}') - # print(f' End i: {i}') - # print(f' End Offset: {end_offset}') - # print(f'Requested i: {i-end_offset}') - # print(f'Requested i: {end_offset-sbc-i}') - # if self.start_buffer_layout.itemAt(i): - # self.start_buffer_layout.itemAt(i).widget().setHidden(True) - # print(f'Setting Text "{str(i+1)}" for Local End i:{i-end_offset}, Global i:{i}') - self.end_buffer_layout.itemAt( - i - end_offset - ).widget().setHidden(False) + self.end_buffer_layout.itemAt(i - end_offset).widget().setHidden(False) self.end_buffer_layout.itemAt(i - end_offset).widget().setText( # type: ignore str(i + 1) ) @@ -326,104 +235,26 @@ class Pagination(QWidget, QObject): i, ) else: - # if self.start_buffer_layout.itemAt(i-1): - # print(f'Removing E-Start {i-1}') - # self.start_buffer_layout.itemAt(i-1).widget().setHidden(True) - # if self.start_buffer_layout.itemAt(i-start_offset): - # print(f'Removing E-Start Offset {i-end_offset}') - # self.start_buffer_layout.itemAt(i-end_offset).widget().setHidden(True) - if self.end_buffer_layout.itemAt(i): - # print(f'Removing E-End {i}') self.end_buffer_layout.itemAt(i).widget().setHidden(True) for j in range(0, self.buffer_page_count): if self.end_buffer_layout.itemAt(i - end_offset + j): - # print(f'Removing E-End-Offset {i-end_offset+j}') self.end_buffer_layout.itemAt( i - end_offset + j ).widget().setHidden(True) - # if self.end_buffer_layout.itemAt(i+1): - # print(f'Removing T-End {i+1}') - # self.end_buffer_layout.itemAt(i+1).widget().setHidden(True) - if self.start_buffer_layout.itemAt(i - 1): - # print(f'Removing T-Start {i-1}') self.start_buffer_layout.itemAt(i - 1).widget().setHidden(True) - # if index == 0 or index == 1: - # print(f'Removing Start i: {i}') - # if self.start_buffer_layout.itemAt(i): - # self.start_buffer_layout.itemAt(i).widget().setHidden(True) - - # elif index == page_count-1 or index == page_count-2 or index == page_count-3 or index == page_count-4: - # print(f' Removing End i: {i}') - # if self.end_buffer_layout.itemAt(i): - # self.end_buffer_layout.itemAt(i).widget().setHidden(True) - - # else: - # print(f'Truncate: {i}') - # if self.start_buffer_layout.itemAt(i): - # self.start_buffer_layout.itemAt(i).widget().setHidden(True) - # if self.end_buffer_layout.itemAt(i): - # self.end_buffer_layout.itemAt(i).widget().setHidden(True) - - # if i < self.buffer_page_count: - # print(f'start {i}') - # if i == 0: - # self.start_buffer_layout.itemAt(i).widget().setHidden(True) - # self.current_page_field.setText((str(i+1))) - # else: - # self.start_buffer_layout.itemAt(i).widget().setHidden(False) - # self.start_buffer_layout.itemAt(i).widget().setText(str(i+1)) - # elif i >= self.buffer_page_count and i < count: - # print(f'end {i}') - # self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setHidden(False) - # self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setText(str(i+1)) - # else: - # self.end_buffer_layout.itemAt(i-self.buffer_page_count).widget().setHidden(True) - self.setHidden(False) - # elif page_count >= 7: - # # Show everything, except truncate the buffers as needed. - # # [<] [1]...[3] [4] [5]...[7] [>] - # self.start_button.setHidden(False) - # self.start_ellipses.setHidden(False) - # self.end_ellipses.setHidden(False) - # self.end_button.setHidden(False) - - # if index == 0: - # self.prev_button.setDisabled(True) - # self.start_buffer_layout.setContentsMargins(0,0,3,0) - # else: - # self.start_buffer_layout.setContentsMargins(3,0,3,0) - # self.assign_click(self.prev_button, index-1) - # self.prev_button.setDisabled(False) - - # if index == page_count-1: - # self.next_button.setDisabled(True) - # self.end_buffer_layout.setContentsMargins(3,0,0,0) - # else: - # self.end_buffer_layout.setContentsMargins(3,0,3,0) - # self.assign_click(self.next_button, index+1) - # self.next_button.setDisabled(False) - - # self.start_button.setText('1') - # self.assign_click(self.start_button, 0) - # self.end_button.setText(str(page_count)) - # self.assign_click(self.end_button, page_count-1) - - # self.setHidden(False) self.validator.setTop(page_count) - # if self.current_page_index != index: if emit: self.index.emit(index) self.current_page_index = index self.page_count = page_count def _goto_page(self, index: int): - # print(f'GOTO PAGE: {index}') self.update_buttons(self.page_count, index) def _assign_click(self, button: QPushButtonWrapper, index): @@ -438,14 +269,12 @@ class Pagination(QWidget, QObject): button.setMinimumSize(self.button_size) button.setMaximumSize(self.button_size) button.setHidden(True) - # button.setMaximumHeight(self.button_size.height()) self.start_buffer_layout.addWidget(button) end_button = QPushButtonWrapper() end_button.setMinimumSize(self.button_size) end_button.setMaximumSize(self.button_size) end_button.setHidden(True) - # button.setMaximumHeight(self.button_size.height()) self.end_buffer_layout.addWidget(end_button) @@ -454,7 +283,5 @@ class Validator(QIntValidator): super().__init__(bottom, top, parent) def fixup(self, input: str) -> str: - # print(input) input = input.strip("0") - print(input) return super().fixup(str(self.top()) if input else "1") diff --git a/tagstudio/src/qt/resource_manager.py b/tagstudio/src/qt/resource_manager.py index 137363bb..a9826e72 100644 --- a/tagstudio/src/qt/resource_manager.py +++ b/tagstudio/src/qt/resource_manager.py @@ -23,13 +23,12 @@ class ResourceManager: if not ResourceManager._initialized: with open(Path(__file__).parent / "resources.json", encoding="utf-8") as f: ResourceManager._map = ujson.load(f) - logger.info( - "resources registered", count=len(ResourceManager._map.items()) - ) + logger.info("resources registered", count=len(ResourceManager._map.items())) ResourceManager._initialized = True def get(self, id: str) -> Any: """Get a resource from the ResourceManager. + This can include resources inside and outside of QResources, and will return theme-respecting variations of resources if available. diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 92d5318a..8ecd4e7a 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -19,88 +19,85 @@ from itertools import zip_longest from pathlib import Path from queue import Queue +# this import has side-effect of import PySide resources +import src.qt.resources_rc # noqa: F401 import structlog +from humanfriendly import format_timespan from PySide6 import QtCore from PySide6.QtCore import ( QObject, - QThread, - Signal, + QSettings, Qt, + QThread, QThreadPool, QTimer, - QSettings, + Signal, ) from PySide6.QtGui import ( - QGuiApplication, - QPixmap, - QMouseEvent, - QColor, QAction, + QColor, QFontDatabase, + QGuiApplication, QIcon, + QMouseEvent, + QPixmap, ) from PySide6.QtUiTools import QUiLoader from PySide6.QtWidgets import ( QApplication, - QWidget, - QPushButton, - QLineEdit, - QScrollArea, + QComboBox, QFileDialog, - QSplashScreen, + QLineEdit, QMenu, QMenuBar, - QComboBox, + QPushButton, + QScrollArea, + QSplashScreen, + QWidget, ) -from humanfriendly import format_timespan - -from src.core.enums import SettingItems, MacroID - from src.core.constants import ( - TS_FOLDER_NAME, - VERSION_BRANCH, - VERSION, - LibraryPrefs, TAG_ARCHIVED, TAG_FAVORITE, + TS_FOLDER_NAME, + VERSION, + VERSION_BRANCH, + LibraryPrefs, ) +from src.core.enums import MacroID, SettingItems from src.core.library.alchemy.enums import ( - SearchMode, + FieldTypeEnum, FilterState, ItemType, - FieldTypeEnum, + SearchMode, ) from src.core.library.alchemy.fields import _FieldID from src.core.ts_core import TagStudioCore from src.core.utils.refresh_dir import RefreshDirTracker from src.core.utils.web import strip_web_protocol from src.qt.flowlayout import FlowLayout -from src.qt.main_window import Ui_MainWindow -from src.qt.helpers.function_iterator import FunctionIterator from src.qt.helpers.custom_runnable import CustomRunnable -from src.qt.resource_manager import ResourceManager -from src.qt.widgets.panel import PanelModal -from src.qt.widgets.thumb_renderer import ThumbRenderer -from src.qt.widgets.progress import ProgressWidget -from src.qt.widgets.preview_panel import PreviewPanel -from src.qt.widgets.item_thumb import ItemThumb, BadgeType +from src.qt.helpers.function_iterator import FunctionIterator +from src.qt.main_window import Ui_MainWindow from src.qt.modals.build_tag import BuildTagPanel -from src.qt.modals.tag_database import TagDatabasePanel from src.qt.modals.file_extension import FileExtensionModal -from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal from src.qt.modals.fix_dupes import FixDupeFilesModal +from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal from src.qt.modals.folders_to_tags import FoldersToTagsModal - -# this import has side-effect of import PySide resources -import src.qt.resources_rc # noqa: F401 +from src.qt.modals.tag_database import TagDatabasePanel +from src.qt.resource_manager import ResourceManager +from src.qt.widgets.item_thumb import BadgeType, ItemThumb +from src.qt.widgets.panel import PanelModal +from src.qt.widgets.preview_panel import PreviewPanel +from src.qt.widgets.progress import ProgressWidget +from src.qt.widgets.thumb_renderer import ThumbRenderer # SIGQUIT is not defined on Windows if sys.platform == "win32": - from signal import signal, SIGINT, SIGTERM + from signal import SIGINT, SIGTERM, signal SIGQUIT = SIGTERM else: - from signal import signal, SIGINT, SIGTERM, SIGQUIT + from signal import SIGINT, SIGQUIT, SIGTERM, signal logger = structlog.get_logger(__name__) @@ -156,7 +153,7 @@ class QtDriver(QObject): # grid indexes of selected items self.selected: list[int] = [] - self.SIGTERM.connect(self.handleSIGTERM) + self.SIGTERM.connect(self.handle_sigterm) if self.args.config_file: path = Path(self.args.config_file) @@ -178,7 +175,9 @@ class QtDriver(QObject): max_threads = os.cpu_count() for i in range(max_threads): - # thread = threading.Thread(target=self.consumer, name=f'ThumbRenderer_{i}',args=(), daemon=True) + # thread = threading.Thread( + # target=self.consumer, name=f"ThumbRenderer_{i}", args=(), daemon=True + # ) # thread.start() thread = Consumer(self.thumb_job_queue) thread.setObjectName(f"ThumbRenderer_{i}") @@ -206,7 +205,6 @@ class QtDriver(QObject): def start(self) -> None: """Launch the main Qt window.""" - _ = QUiLoader() if os.name == "nt": sys.argv += ["-platform", "windows:darkmode=2"] @@ -370,12 +368,10 @@ class QtDriver(QObject): check_action = QAction("Open library on start", self) check_action.setCheckable(True) check_action.setChecked( - bool(self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool)) + bool(self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool)) ) check_action.triggered.connect( - lambda checked: self.settings.setValue( - SettingItems.START_LOAD_LAST, checked - ) + lambda checked: self.settings.setValue(SettingItems.START_LOAD_LAST, checked) ) window_menu.addAction(check_action) @@ -415,7 +411,7 @@ class QtDriver(QObject): show_libs_list_action = QAction("Show Recent Libraries", menu_bar) show_libs_list_action.setCheckable(True) show_libs_list_action.setChecked( - bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, True, type=bool)) + bool(self.settings.value(SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool)) ) show_libs_list_action.triggered.connect( lambda checked: ( @@ -467,14 +463,12 @@ class QtDriver(QObject): lib: str | None = None if self.args.open: lib = self.args.open - elif self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool): + elif self.settings.value(SettingItems.START_LOAD_LAST, defaultValue=True, type=bool): lib = str(self.settings.value(SettingItems.LAST_LIBRARY)) # TODO: Remove this check if the library is no longer saved with files if lib and not (Path(lib) / TS_FOLDER_NAME).exists(): - logger.error( - f"[QT DRIVER] {TS_FOLDER_NAME} folder in {lib} does not exist." - ) + logger.error(f"[QT DRIVER] {TS_FOLDER_NAME} folder in {lib} does not exist.") self.settings.setValue(SettingItems.LAST_LIBRARY, "") lib = None @@ -501,22 +495,16 @@ class QtDriver(QObject): search_button: QPushButton = self.main_window.searchButton search_button.clicked.connect( - lambda: self.filter_items( - FilterState(query=self.main_window.searchField.text()) - ) + lambda: self.filter_items(FilterState(query=self.main_window.searchField.text())) ) search_field: QLineEdit = self.main_window.searchField search_field.returnPressed.connect( # TODO - parse search field for filters - lambda: self.filter_items( - FilterState(query=self.main_window.searchField.text()) - ) + lambda: self.filter_items(FilterState(query=self.main_window.searchField.text())) ) search_type_selector: QComboBox = self.main_window.comboBox_2 search_type_selector.currentIndexChanged.connect( - lambda: self.set_search_type( - SearchMode(search_type_selector.currentIndex()) - ) + lambda: self.set_search_type(SearchMode(search_type_selector.currentIndex())) ) back_button: QPushButton = self.main_window.backButton @@ -529,7 +517,7 @@ class QtDriver(QObject): # or implementing some clever loading tricks. self.main_window.show() self.main_window.activateWindow() - self.main_window.toggle_landing_page(True) + self.main_window.toggle_landing_page(enabled=True) self.main_window.pagination.index.connect(lambda i: self.page_move(page_id=i)) @@ -544,15 +532,15 @@ class QtDriver(QObject): self.preview_panel.update() def callback_library_needed_check(self, func): - """Check if loaded library has valid path before executing the button function""" + """Check if loaded library has valid path before executing the button function.""" if self.lib.library_dir: func() - def handleSIGTERM(self): + def handle_sigterm(self): self.shutdown() def shutdown(self): - """Save Library on Application Exit""" + """Save Library on Application Exit.""" self.close_library(is_shutdown=True) logger.info("[SHUTDOWN] Ending Thumbnail Threads...") for _ in self.thumb_threads: @@ -590,7 +578,7 @@ class QtDriver(QObject): [x.set_mode(None) for x in self.item_thumbs] self.preview_panel.update_widgets() - self.main_window.toggle_landing_page(True) + self.main_window.toggle_landing_page(enabled=True) self.main_window.pagination.setHidden(True) @@ -663,7 +651,6 @@ class QtDriver(QObject): def add_new_files_callback(self): """Run when user initiates adding new files to the Library.""" - tracker = RefreshDirTracker(self.lib) pw = ProgressWidget( @@ -680,7 +667,9 @@ class QtDriver(QObject): lambda x: ( pw.update_progress(x + 1), pw.update_label( - f'Scanning Directories for New Files...\n{x + 1} File{"s" if x + 1 != 1 else ""} Searched, {tracker.files_count} New Files Found' + f"Scanning Directories for New Files...\n{x + 1}" + f" File{"s" if x + 1 != 1 else ""} Searched," + f" {tracker.files_count} New Files Found" ), ) ) @@ -695,11 +684,13 @@ class QtDriver(QObject): QThreadPool.globalInstance().start(r) def add_new_files_runnable(self, tracker: RefreshDirTracker): + """Adds any known new files to the library and run default macros on them. + + Threaded method. """ - Threaded method that adds any known new files to the library and - initiates running default macros on them. - """ - # pb = QProgressDialog(f'Running Configured Macros on 1/{len(new_ids)} New Entries', None, 0,len(new_ids)) + # pb = QProgressDialog( + # f"Running Configured Macros on 1/{len(new_ids)} New Entries", None, 0, len(new_ids) + # ) # pb.setFixedSize(432, 112) # pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint) # pb.setWindowTitle('Running Macros') @@ -711,13 +702,14 @@ class QtDriver(QObject): # r.run() # # QThreadPool.globalInstance().start(r) - # # self.main_window.statusbar.showMessage(f'Running configured Macros on {len(new_ids)} new Entries...', 3) + # # self.main_window.statusbar.showMessage( + # # f"Running configured Macros on {len(new_ids)} new Entries...", 3 + # # ) # # pb.hide() files_count = tracker.files_count - # iterator = FunctionIterator(lambda: self.new_file_macros_runnable(tracker.files_not_in_library)) iterator = FunctionIterator(tracker.save_new_files) pw = ProgressWidget( window_title="Running Macros on New Entries", @@ -730,9 +722,7 @@ class QtDriver(QObject): iterator.value.connect( lambda x: ( pw.update_progress(x + 1), - pw.update_label( - f"Running Configured Macros on {x + 1}/{files_count} New Entries" - ), + pw.update_label(f"Running Configured Macros on {x + 1}/{files_count} New Entries"), ) ) r = CustomRunnable(iterator.run) @@ -846,7 +836,7 @@ class QtDriver(QObject): def _init_thumb_grid(self): layout = FlowLayout() - layout.setGridEfficiency(True) + layout.enable_grid_optimizations(value=True) layout.setSpacing(min(self.thumb_size // 10, 12)) layout.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -868,9 +858,7 @@ class QtDriver(QObject): def select_item(self, grid_index: int, append: bool, bridge: bool): """Select one or more items in the Thumbnail Grid.""" - logger.info( - "selecting item", grid_index=grid_index, append=append, bridge=bridge - ) + logger.info("selecting item", grid_index=grid_index, append=append, bridge=bridge) if append: if grid_index not in self.selected: self.selected.append(grid_index) @@ -967,8 +955,7 @@ class QtDriver(QObject): == Qt.KeyboardModifier.ControlModifier ), bridge=( - QGuiApplication.keyboardModifiers() - == Qt.KeyboardModifier.ShiftModifier + QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier ), ) ) @@ -1003,9 +990,7 @@ class QtDriver(QObject): if filter: self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter)) - self.main_window.statusbar.showMessage( - f'Searching Library: "{self.filter.summary}"' - ) + self.main_window.statusbar.showMessage(f'Searching Library: "{self.filter.summary}"') self.main_window.statusbar.repaint() start_time = time.time() @@ -1015,9 +1000,12 @@ class QtDriver(QObject): end_time = time.time() if self.filter.summary: + # fmt: off self.main_window.statusbar.showMessage( - f'{results.total_count} Results Found for "{self.filter.summary}" ({format_timespan(end_time - start_time)})' + f"{results.total_count} Results Found for \"{self.filter.summary}\"" + f" ({format_timespan(end_time - start_time)})" ) + # fmt: on else: self.main_window.statusbar.showMessage( f"{results.total_count} Results ({format_timespan(end_time - start_time)})" @@ -1048,8 +1036,8 @@ class QtDriver(QObject): self.settings.sync() def update_libs_list(self, path: Path | str): - """add library to list in SettingItems.LIBS_LIST""" - ITEMS_LIMIT = 5 + """Add library to list in SettingItems.LIBS_LIST.""" + item_limit: int = 5 path = Path(path) self.settings.beginGroup(SettingItems.LIBS_LIST) @@ -1067,7 +1055,7 @@ class QtDriver(QObject): # remove previously saved items self.settings.clear() - for item_key, item_value in all_libs_list[:ITEMS_LIMIT]: + for item_key, item_value in all_libs_list[:item_limit]: self.settings.setValue(item_key, item_value) self.settings.endGroup() @@ -1097,4 +1085,4 @@ class QtDriver(QObject): # page (re)rendering, extract eventually self.filter_items() - self.main_window.toggle_landing_page(False) + self.main_window.toggle_landing_page(enabled=False) diff --git a/tagstudio/src/qt/widgets/clickable_label.py b/tagstudio/src/qt/widgets/clickable_label.py index ca812f95..d4e57445 100644 --- a/tagstudio/src/qt/widgets/clickable_label.py +++ b/tagstudio/src/qt/widgets/clickable_label.py @@ -14,5 +14,5 @@ class ClickableLabel(QLabel): def __init__(self): super().__init__() - def mousePressEvent(self, event): + def mousePressEvent(self, event): # noqa: N802 self.clicked.emit() diff --git a/tagstudio/src/qt/widgets/collage_icon.py b/tagstudio/src/qt/widgets/collage_icon.py index b8382a49..f3e1b364 100644 --- a/tagstudio/src/qt/widgets/collage_icon.py +++ b/tagstudio/src/qt/widgets/collage_icon.py @@ -12,8 +12,7 @@ from PySide6.QtCore import ( QObject, Signal, ) - -from src.core.constants import DOC_TYPES, VIDEO_TYPES, IMAGE_TYPES +from src.core.constants import DOC_TYPES, IMAGE_TYPES, VIDEO_TYPES from src.core.library import Library from src.core.library.alchemy.fields import _FieldID @@ -84,15 +83,11 @@ class CollageIconRenderer(QObject): pic = pic.resize(size) if data_tint_mode and color: pic = pic.convert(mode="RGB") - pic = ImageChops.hard_light( - pic, Image.new("RGB", size, color) - ) + pic = ImageChops.hard_light(pic, Image.new("RGB", size, color)) # collage.paste(pic, (y*thumb_size, x*thumb_size)) self.rendered.emit(pic) except DecompressionBombError: - logger.exception( - "One of the images was too big", entry=entry.path - ) + logger.exception("One of the images was too big", entry=entry.path) elif filepath.suffix.lower() in VIDEO_TYPES: video = cv2.VideoCapture(str(filepath)) video.set( @@ -113,18 +108,13 @@ class CollageIconRenderer(QObject): else: pic = pic.resize(size) if data_tint_mode and color: - pic = ImageChops.hard_light( - pic, Image.new("RGB", size, color) - ) + pic = ImageChops.hard_light(pic, Image.new("RGB", size, color)) # collage.paste(pic, (y*thumb_size, x*thumb_size)) self.rendered.emit(pic) except (UnidentifiedImageError, FileNotFoundError): logger.error("Couldn't read entry", entry=entry.path) with Image.open( - str( - Path(__file__).parents[2] - / "resources/qt/images/thumb_broken_512.png" - ) + str(Path(__file__).parents[2] / "resources/qt/images/thumb_broken_512.png") ) as pic: pic.thumbnail(size) if data_tint_mode and color: diff --git a/tagstudio/src/qt/widgets/fields.py b/tagstudio/src/qt/widgets/fields.py index fc7a1fd8..2fb8d7fb 100644 --- a/tagstudio/src/qt/widgets/fields.py +++ b/tagstudio/src/qt/widgets/fields.py @@ -4,14 +4,14 @@ import math -from types import MethodType from pathlib import Path -from typing import Optional, Callable +from types import MethodType +from typing import Callable, Optional from PIL import Image, ImageQt -from PySide6.QtCore import Qt, QEvent -from PySide6.QtGui import QPixmap, QEnterEvent -from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel +from PySide6.QtCore import QEvent, Qt +from PySide6.QtGui import QEnterEvent, QPixmap +from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper @@ -34,22 +34,17 @@ class FieldContainer(QWidget): def __init__(self, title: str = "Field", inline: bool = True) -> None: super().__init__() - # self.mode:str = mode self.setObjectName("fieldContainer") - # self.item = item self.title: str = title self.inline: bool = inline - # self.editable:bool = editable self.copy_callback: Callable = None self.edit_callback: Callable = None self.remove_callback: Callable = None button_size = 24 - # self.setStyleSheet('border-style:solid;border-color:#1e1a33;border-radius:8px;border-width:2px;') self.root_layout = QVBoxLayout(self) self.root_layout.setObjectName("baseLayout") self.root_layout.setContentsMargins(0, 0, 0, 0) - # self.setStyleSheet('background-color:red;') self.inner_layout = QVBoxLayout() self.inner_layout.setObjectName("innerLayout") @@ -61,7 +56,6 @@ class FieldContainer(QWidget): self.root_layout.addWidget(self.inner_container) self.title_container = QWidget() - # self.title_container.setStyleSheet('background:black;') self.title_layout = QHBoxLayout(self.title_container) self.title_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.title_layout.setObjectName("fieldLayout") @@ -74,20 +68,15 @@ class FieldContainer(QWidget): self.title_widget.setObjectName("fieldTitle") self.title_widget.setWordWrap(True) self.title_widget.setStyleSheet("font-weight: bold; font-size: 14px;") - # self.title_widget.setStyleSheet('background-color:orange;') self.title_widget.setText(title) - # self.inner_layout.addWidget(self.title_widget) self.title_layout.addWidget(self.title_widget) - self.title_layout.addStretch(2) self.copy_button = QPushButtonWrapper() self.copy_button.setMinimumSize(button_size, button_size) self.copy_button.setMaximumSize(button_size, button_size) self.copy_button.setFlat(True) - self.copy_button.setIcon( - QPixmap.fromImage(ImageQt.ImageQt(self.clipboard_icon_128)) - ) + self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.clipboard_icon_128))) self.copy_button.setCursor(Qt.CursorShape.PointingHandCursor) self.title_layout.addWidget(self.copy_button) self.copy_button.setHidden(True) @@ -105,9 +94,7 @@ class FieldContainer(QWidget): self.remove_button.setMinimumSize(button_size, button_size) self.remove_button.setMaximumSize(button_size, button_size) self.remove_button.setFlat(True) - self.remove_button.setIcon( - QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon_128)) - ) + self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon_128))) self.remove_button.setCursor(Qt.CursorShape.PointingHandCursor) self.title_layout.addWidget(self.remove_button) self.remove_button.setHidden(True) @@ -118,11 +105,8 @@ class FieldContainer(QWidget): self.field_layout.setObjectName("fieldLayout") self.field_layout.setContentsMargins(0, 0, 0, 0) self.field_container.setLayout(self.field_layout) - # self.field_container.setStyleSheet('background-color:#666600;') self.inner_layout.addWidget(self.field_container) - # self.set_inner_widget(mode) - def set_copy_callback(self, callback: Optional[MethodType]): if self.copy_button.is_connected: self.copy_button.clicked.disconnect() @@ -150,12 +134,7 @@ class FieldContainer(QWidget): self.remove_button.is_connected = True def set_inner_widget(self, widget: "FieldWidget"): - # widget.setStyleSheet('background-color:green;') - # self.inner_container.dumpObjectTree() - # logging.info('') if self.field_layout.itemAt(0): - # logging.info(f'Removing {self.field_layout.itemAt(0)}') - # self.field_layout.removeItem(self.field_layout.itemAt(0)) self.field_layout.itemAt(0).widget().deleteLater() self.field_layout.addWidget(widget) @@ -171,12 +150,7 @@ class FieldContainer(QWidget): def set_inline(self, inline: bool): self.inline = inline - # def set_editable(self, editable:bool): - # self.editable = editable - - def enterEvent(self, event: QEnterEvent) -> None: - # if self.field_layout.itemAt(1): - # self.field_layout.itemAt(1). + def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802 # NOTE: You could pass the hover event to the FieldWidget if needed. if self.copy_callback: self.copy_button.setHidden(False) @@ -186,7 +160,7 @@ class FieldContainer(QWidget): self.remove_button.setHidden(False) return super().enterEvent(event) - def leaveEvent(self, event: QEvent) -> None: + def leaveEvent(self, event: QEvent) -> None: # noqa: N802 if self.copy_callback: self.copy_button.setHidden(True) if self.edit_callback: @@ -199,5 +173,4 @@ class FieldContainer(QWidget): class FieldWidget(QWidget): def __init__(self, title) -> None: super().__init__() - # self.item = item self.title = title diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index 04f61d77..005cf1b0 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -10,32 +10,30 @@ from typing import TYPE_CHECKING import structlog from PIL import Image, ImageQt -from PySide6.QtCore import Qt, QSize, QEvent -from PySide6.QtGui import QPixmap, QEnterEvent, QAction +from PySide6.QtCore import QEvent, QSize, Qt +from PySide6.QtGui import QAction, QEnterEvent, QPixmap from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QHBoxLayout, - QLabel, QBoxLayout, QCheckBox, + QHBoxLayout, + QLabel, + QVBoxLayout, + QWidget, ) - from src.core.constants import ( AUDIO_TYPES, - VIDEO_TYPES, IMAGE_TYPES, - TAG_FAVORITE, TAG_ARCHIVED, + TAG_FAVORITE, + VIDEO_TYPES, ) -from src.core.library import ItemType, Entry, Library +from src.core.library import Entry, ItemType, Library from src.core.library.alchemy.enums import FilterState from src.core.library.alchemy.fields import _FieldID - from src.qt.flowlayout import FlowWidget from src.qt.helpers.file_opener import FileOpenerHelper -from src.qt.widgets.thumb_renderer import ThumbRenderer from src.qt.widgets.thumb_button import ThumbButton +from src.qt.widgets.thumb_renderer import ThumbRenderer if TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -74,9 +72,7 @@ def badge_update_lock(func): class ItemThumb(FlowWidget): - """ - The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.). - """ + """The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.).""" update_cutoff: float = time.time() @@ -91,27 +87,27 @@ class ItemThumb(FlowWidget): tag_group_icon_128.load() small_text_style = ( - f"background-color:rgba(0, 0, 0, 192);" - f"font-family:Oxanium;" - f"font-weight:bold;" - f"font-size:12px;" - f"border-radius:3px;" - f"padding-top: 4px;" - f"padding-right: 1px;" - f"padding-bottom: 1px;" - f"padding-left: 1px;" + "background-color:rgba(0, 0, 0, 192);" + "font-family:Oxanium;" + "font-weight:bold;" + "font-size:12px;" + "border-radius:3px;" + "padding-top: 4px;" + "padding-right: 1px;" + "padding-bottom: 1px;" + "padding-left: 1px;" ) med_text_style = ( - f"background-color:rgba(0, 0, 0, 192);" - f"font-family:Oxanium;" - f"font-weight:bold;" - f"font-size:18px;" - f"border-radius:3px;" - f"padding-top: 4px;" - f"padding-right: 1px;" - f"padding-bottom: 1px;" - f"padding-left: 1px;" + "background-color:rgba(0, 0, 0, 192);" + "font-family:Oxanium;" + "font-weight:bold;" + "font-size:18px;" + "border-radius:3px;" + "padding-top: 4px;" + "padding-right: 1px;" + "padding-bottom: 1px;" + "padding-left: 1px;" ) def __init__( @@ -132,7 +128,6 @@ class ItemThumb(FlowWidget): self.setMinimumSize(*thumb_size) self.setMaximumSize(*thumb_size) check_size = 24 - # self.setStyleSheet('background-color:red;') # +----------+ # | ARC FAV| Top Right: Favorite & Archived Badges @@ -151,7 +146,6 @@ class ItemThumb(FlowWidget): # +----------+ self.base_layout = QVBoxLayout(self) self.base_layout.setObjectName("baseLayout") - # self.base_layout.setRowStretch(1, 2) self.base_layout.setContentsMargins(0, 0, 0, 0) # +----------+ @@ -162,8 +156,6 @@ class ItemThumb(FlowWidget): # +----------+ self.top_layout = QHBoxLayout() self.top_layout.setObjectName("topLayout") - # self.top_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - # self.top_layout.setColumnStretch(1, 2) self.top_layout.setContentsMargins(6, 6, 6, 6) self.top_container = QWidget() self.top_container.setLayout(self.top_layout) @@ -185,20 +177,11 @@ class ItemThumb(FlowWidget): # +----------+ self.bottom_layout = QHBoxLayout() self.bottom_layout.setObjectName("bottomLayout") - # self.bottom_container.setAlignment(Qt.AlignmentFlag.AlignBottom) - # self.bottom_layout.setColumnStretch(1, 2) self.bottom_layout.setContentsMargins(6, 6, 6, 6) self.bottom_container = QWidget() self.bottom_container.setLayout(self.bottom_layout) self.base_layout.addWidget(self.bottom_container) - # self.root_layout = QGridLayout(self) - # self.root_layout.setObjectName('rootLayout') - # self.root_layout.setColumnStretch(1, 2) - # self.root_layout.setRowStretch(1, 2) - # self.root_layout.setContentsMargins(6,6,6,6) - # # root_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter) - self.thumb_button = ThumbButton(self, thumb_size) self.renderer = ThumbRenderer() self.renderer.updated.connect( @@ -209,14 +192,9 @@ class ItemThumb(FlowWidget): ) ) self.thumb_button.setFlat(True) - - # self.bg_button.setStyleSheet('background-color:blue;') - # self.bg_button.setLayout(self.root_layout) self.thumb_button.setLayout(self.base_layout) - # self.bg_button.setMinimumSize(*thumb_size) - # self.bg_button.setMaximumSize(*thumb_size) - self.thumb_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + self.opener = FileOpenerHelper("") open_file_action = QAction("Open file", self) open_file_action.triggered.connect(self.opener.open_file) @@ -243,50 +221,32 @@ class ItemThumb(FlowWidget): ) self.item_type_badge.setMinimumSize(check_size, check_size) self.item_type_badge.setMaximumSize(check_size, check_size) - # self.root_layout.addWidget(self.item_type_badge, 2, 0) self.bottom_layout.addWidget(self.item_type_badge) # File Extension Badge ------------------------------------------------- # Mutually exclusive with the File Extension Badge. self.ext_badge = QLabel() self.ext_badge.setObjectName("extBadge") - # self.ext_badge.setText('MP4') - # self.ext_badge.setAlignment(Qt.AlignmentFlag.AlignVCenter) self.ext_badge.setStyleSheet(ItemThumb.small_text_style) - # self.type_badge.setAlignment(Qt.AlignmentFlag.AlignRight) - # self.root_layout.addWidget(self.ext_badge, 2, 0) self.bottom_layout.addWidget(self.ext_badge) - # self.type_badge.setHidden(True) - # bl_layout.addWidget(self.type_badge) - self.bottom_layout.addStretch(2) # Count Badge ---------------------------------------------------------- # Used for Tag Group + Collation counts, video length, word count, etc. self.count_badge = QLabel() self.count_badge.setObjectName("countBadge") - # self.count_badge.setMaximumHeight(17) self.count_badge.setText("-:--") - # self.count_badge.setAlignment(Qt.AlignmentFlag.AlignVCenter) self.count_badge.setStyleSheet(ItemThumb.small_text_style) - # self.count_badge.setAlignment(Qt.AlignmentFlag.AlignBottom) - # self.root_layout.addWidget(self.count_badge, 2, 2) - self.bottom_layout.addWidget( - self.count_badge, alignment=Qt.AlignmentFlag.AlignBottom - ) - + self.bottom_layout.addWidget(self.count_badge, alignment=Qt.AlignmentFlag.AlignBottom) self.top_layout.addStretch(2) # Intractable Badges =================================================== self.cb_container = QWidget() - # check_badges.setStyleSheet('background-color:cyan;') self.cb_layout = QHBoxLayout() self.cb_layout.setDirection(QBoxLayout.Direction.RightToLeft) self.cb_layout.setContentsMargins(0, 0, 0, 0) self.cb_layout.setSpacing(6) self.cb_container.setLayout(self.cb_layout) - # self.cb_container.setHidden(True) - # self.root_layout.addWidget(self.check_badges, 0, 2) self.top_layout.addWidget(self.cb_container) self.badge_active: dict[BadgeType, bool] = { @@ -336,14 +296,11 @@ class ItemThumb(FlowWidget): def set_mode(self, mode: ItemType | None) -> None: if mode is None: - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=True) self.unsetCursor() self.thumb_button.setHidden(True) - # self.check_badges.setHidden(True) - # self.ext_badge.setHidden(True) - # self.item_type_badge.setHidden(True) elif mode == ItemType.ENTRY and self.mode != ItemType.ENTRY: - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False) self.setCursor(Qt.CursorShape.PointingHandCursor) self.thumb_button.setHidden(False) self.cb_container.setHidden(False) @@ -353,7 +310,7 @@ class ItemThumb(FlowWidget): self.count_badge.setHidden(True) self.ext_badge.setHidden(True) elif mode == ItemType.COLLATION and self.mode != ItemType.COLLATION: - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False) self.setCursor(Qt.CursorShape.PointingHandCursor) self.thumb_button.setHidden(False) self.cb_container.setHidden(True) @@ -362,15 +319,13 @@ class ItemThumb(FlowWidget): self.count_badge.setHidden(False) self.item_type_badge.setHidden(False) elif mode == ItemType.TAG_GROUP and self.mode != ItemType.TAG_GROUP: - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False) self.setCursor(Qt.CursorShape.PointingHandCursor) self.thumb_button.setHidden(False) - # self.cb_container.setHidden(True) self.ext_badge.setHidden(True) self.count_badge.setHidden(False) self.item_type_badge.setHidden(False) self.mode = mode - # logging.info(f'Set Mode To: {self.mode}') def set_extension(self, ext: str) -> None: if ext and ext.startswith(".") is False: @@ -396,14 +351,11 @@ class ItemThumb(FlowWidget): def update_thumb(self, timestamp: float, image: QPixmap | None = None): """Update attributes of a thumbnail element.""" - # logging.info(f'[GUI] Updating Thumbnail for element {id(element)}: {id(image) if image else None}') if timestamp > ItemThumb.update_cutoff: self.thumb_button.setIcon(image if image else QPixmap()) - # element.repaint() def update_size(self, timestamp: float, size: QSize): """Updates attributes of a thumbnail element.""" - # logging.info(f'[GUI] Updating size for element {id(element)}: {size.__str__()}') if timestamp > ItemThumb.update_cutoff and self.thumb_button.iconSize != size: self.thumb_button.setIconSize(size) self.thumb_button.setMinimumSize(size) @@ -411,7 +363,6 @@ class ItemThumb(FlowWidget): def update_clickable(self, clickable: typing.Callable): """Updates attributes of a thumbnail element.""" - # logging.info(f'[GUI] Updating Click Event for element {id(element)}: {id(clickable) if clickable else None}') if self.thumb_button.is_connected: self.thumb_button.clicked.disconnect() if clickable: @@ -455,12 +406,12 @@ class ItemThumb(FlowWidget): is_hidden = not (show or self.badge_active[badge_type]) badge.setHidden(is_hidden) - def enterEvent(self, event: QEnterEvent) -> None: - self.show_check_badges(True) + def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802 + self.show_check_badges(show=True) return super().enterEvent(event) - def leaveEvent(self, event: QEvent) -> None: - self.show_check_badges(False) + def leaveEvent(self, event: QEvent) -> None: # noqa: N802 + self.show_check_badges(show=False) return super().leaveEvent(event) @badge_update_lock @@ -482,7 +433,7 @@ class ItemThumb(FlowWidget): for idx in update_items: entry = self.driver.frame_content[idx] self.toggle_item_tag( - entry, toggle_value, tag_id, _FieldID.TAGS_META.name, True + entry, toggle_value, tag_id, _FieldID.TAGS_META.name, create_field=True ) # update the entry self.driver.frame_content[idx] = self.lib.search_library( diff --git a/tagstudio/src/qt/widgets/landing.py b/tagstudio/src/qt/widgets/landing.py index e5df9752..c1a5a7d8 100644 --- a/tagstudio/src/qt/widgets/landing.py +++ b/tagstudio/src/qt/widgets/landing.py @@ -7,12 +7,13 @@ import logging import sys import typing from pathlib import Path + from PIL import Image, ImageQt -from PySide6.QtCore import Qt, QPropertyAnimation, QPoint, QEasingCurve +from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt from PySide6.QtGui import QPixmap -from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton -from src.qt.widgets.clickable_label import ClickableLabel +from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget from src.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay +from src.qt.widgets.clickable_label import ClickableLabel # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: @@ -39,8 +40,7 @@ class LandingWidget(QWidget): # Create landing logo -------------------------------------------------- # self.landing_logo_pixmap = QPixmap(":/images/tagstudio_logo_text_mono.png") self.logo_raw: Image.Image = Image.open( - Path(__file__).parents[3] - / "resources/qt/images/tagstudio_logo_text_mono.png" + Path(__file__).parents[3] / "resources/qt/images/tagstudio_logo_text_mono.png" ) self.landing_pixmap: QPixmap = QPixmap() self.update_logo_color() @@ -78,21 +78,15 @@ class LandingWidget(QWidget): # Add widgets to layout ------------------------------------------------ self.landing_layout.addWidget(self.logo_label) - self.landing_layout.addWidget( - self.open_button, alignment=Qt.AlignmentFlag.AlignCenter - ) - self.landing_layout.addWidget( - self.status_label, alignment=Qt.AlignmentFlag.AlignCenter - ) + self.landing_layout.addWidget(self.open_button, alignment=Qt.AlignmentFlag.AlignCenter) + self.landing_layout.addWidget(self.status_label, alignment=Qt.AlignmentFlag.AlignCenter) def update_logo_color(self, style: str = "mono"): - """ - Update the color of the TagStudio logo. + """Update the color of the TagStudio logo. Args: style (str): = The style of the logo. Either "mono" or "gradient". """ - logo_im: Image.Image = None if style == "mono": logo_im = theme_fg_overlay(self.logo_raw) @@ -100,9 +94,7 @@ class LandingWidget(QWidget): gradient_colors: list[str] = ["#d27bf4", "#7992f5", "#63c6e3", "#63f5cf"] logo_im = gradient_overlay(self.logo_raw, gradient_colors) - logo_final: Image.Image = Image.new( - mode="RGBA", size=self.logo_raw.size, color="#00000000" - ) + logo_final: Image.Image = Image.new(mode="RGBA", size=self.logo_raw.size, color="#00000000") logo_final.paste(logo_im, (0, 0), mask=self.logo_raw) @@ -118,9 +110,9 @@ class LandingWidget(QWidget): self.logo_label.setPixmap(self.landing_pixmap) def _update_special_click(self): - """ - Increment the click count for the logo easter egg if it has not - been triggered. If it reaches the click threshold, this triggers it + """Increment the click count for the logo easter egg if it has not been triggered. + + If it reaches the click threshold, this triggers it and prevents it from triggering again. """ if self._special_click_count >= 0: @@ -137,9 +129,7 @@ class LandingWidget(QWidget): # the cause of this is, so I've just done this workaround to disable # the animation if the y position is too incorrect. if self.logo_label.y() > 50: - self.logo_pos_anim.setStartValue( - QPoint(self.logo_label.x(), self.logo_label.y() - 100) - ) + self.logo_pos_anim.setStartValue(QPoint(self.logo_label.x(), self.logo_label.y() - 100)) self.logo_pos_anim.setEndValue(self.logo_label.pos()) self.logo_pos_anim.start() @@ -169,8 +159,7 @@ class LandingWidget(QWidget): # self.status_pos_anim.start() def set_status_label(self, text=str): - """ - Set the text of the status label. + """Set the text of the status label. Args: text (str): Text of the status to set. diff --git a/tagstudio/src/qt/widgets/panel.py b/tagstudio/src/qt/widgets/panel.py index 1448d73e..8c403a66 100644 --- a/tagstudio/src/qt/widgets/panel.py +++ b/tagstudio/src/qt/widgets/panel.py @@ -4,8 +4,8 @@ import logging from typing import Callable -from PySide6.QtCore import Signal, Qt -from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton +from PySide6.QtCore import Qt, Signal +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget class PanelModal(QWidget): @@ -35,9 +35,7 @@ class PanelModal(QWidget): self.title_widget = QLabel() self.title_widget.setObjectName("fieldTitle") self.title_widget.setWordWrap(True) - self.title_widget.setStyleSheet( - "font-weight:bold;" "font-size:14px;" "padding-top: 6px" - ) + self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px") self.title_widget.setText(title) self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -76,9 +74,7 @@ class PanelModal(QWidget): self.save_button.clicked.connect(done_callback) if save_callback: - self.save_button.clicked.connect( - lambda: save_callback(widget.get_content()) - ) + self.save_button.clicked.connect(lambda: save_callback(widget.get_content())) self.button_layout.addWidget(self.save_button) @@ -94,9 +90,7 @@ class PanelModal(QWidget): class PanelWidget(QWidget): - """ - Used for widgets that go in a modal panel, ex. for editing or searching. - """ + """Used for widgets that go in a modal panel, ex. for editing or searching.""" done = Signal() diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 2b31afc9..f595644f 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -2,56 +2,55 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio import sys -from collections.abc import Callable -from pathlib import Path import time import typing +from collections.abc import Callable from datetime import datetime as dt +from pathlib import Path import cv2 import rawpy import structlog +from humanfriendly import format_size from PIL import Image, UnidentifiedImageError from PIL.Image import DecompressionBombError -from PySide6.QtCore import Signal, Qt, QSize -from PySide6.QtGui import QResizeEvent, QAction +from PySide6.QtCore import QSize, Qt, Signal +from PySide6.QtGui import QAction, QResizeEvent from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, + QFrame, QHBoxLayout, QLabel, + QMessageBox, QPushButton, QScrollArea, - QFrame, - QSplitter, QSizePolicy, - QMessageBox, + QSplitter, + QVBoxLayout, + QWidget, ) -from humanfriendly import format_size - +from src.core.constants import IMAGE_TYPES, RAW_IMAGE_TYPES, TS_FOLDER_NAME, VIDEO_TYPES from src.core.enums import SettingItems, Theme -from src.core.constants import VIDEO_TYPES, IMAGE_TYPES, RAW_IMAGE_TYPES, TS_FOLDER_NAME from src.core.library.alchemy.enums import FilterState from src.core.library.alchemy.fields import ( - TagBoxField, + BaseField, DatetimeField, FieldTypeEnum, - _FieldID, + TagBoxField, TextField, - BaseField, + _FieldID, ) -from src.qt.helpers.file_opener import FileOpenerLabel, FileOpenerHelper, open_file +from src.core.library.alchemy.library import Library +from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel, open_file +from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper from src.qt.modals.add_field import AddFieldModal -from src.qt.widgets.thumb_renderer import ThumbRenderer from src.qt.widgets.fields import FieldContainer +from src.qt.widgets.panel import PanelModal from src.qt.widgets.tag_box import TagBoxWidget from src.qt.widgets.text import TextWidget -from src.qt.widgets.panel import PanelModal from src.qt.widgets.text_box_edit import EditTextBox from src.qt.widgets.text_line_edit import EditTextLine -from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper +from src.qt.widgets.thumb_renderer import ThumbRenderer from src.qt.widgets.video_player import VideoPlayer -from src.core.library.alchemy.library import Library if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -64,9 +63,7 @@ def update_selected_entry(driver: "QtDriver"): entry = driver.frame_content[grid_idx] # reload entry results = driver.lib.search_library(FilterState(id=entry.id)) - logger.info( - "found item", entries=len(results), grid_idx=grid_idx, lookup_id=entry.id - ) + logger.info("found item", entries=len(results), grid_idx=grid_idx, lookup_id=entry.id) assert results, f"Entry not found: {entry.id}" driver.frame_content[grid_idx] = next(results) @@ -111,9 +108,7 @@ class PreviewPanel(QWidget): self.preview_vid = VideoPlayer(driver) self.preview_vid.hide() self.thumb_renderer = ThumbRenderer() - self.thumb_renderer.updated.connect( - lambda ts, i, s: (self.preview_img.setIcon(i)) - ) + self.thumb_renderer.updated.connect(lambda ts, i, s: (self.preview_img.setIcon(i))) self.thumb_renderer.updated_ratio.connect( lambda ratio: ( self.set_image_ratio(ratio), @@ -134,9 +129,7 @@ class PreviewPanel(QWidget): self.image_container.setMinimumSize(*self.img_button_size) self.file_label = FileOpenerLabel("Filename") self.file_label.setWordWrap(True) - self.file_label.setTextInteractionFlags( - Qt.TextInteractionFlag.TextSelectableByMouse - ) + self.file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self.file_label.setStyleSheet("font-weight: bold; font-size: 12px") self.dimensions_label = QLabel("Dimensions") @@ -173,9 +166,7 @@ class PreviewPanel(QWidget): scroll_area = QScrollArea() scroll_area.setObjectName("entryScrollArea") - scroll_area.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding - ) + scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll_area.setWidgetResizable(True) scroll_area.setFrameShadow(QFrame.Shadow.Plain) @@ -211,7 +202,7 @@ class PreviewPanel(QWidget): # set initial visibility based on settings if not self.driver.settings.value( - SettingItems.WINDOW_SHOW_LIBS, True, type=bool + SettingItems.WINDOW_SHOW_LIBS, defaultValue=True, type=bool ): self.libs_flow_container.hide() @@ -279,9 +270,7 @@ class PreviewPanel(QWidget): self.render_libs = new_keys self._fill_libs_widget(libs_sorted, layout) - def _fill_libs_widget( - self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout - ): + def _fill_libs_widget(self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout): def clear_layout(layout_item: QVBoxLayout): for i in reversed(range(layout_item.count())): child = layout_item.itemAt(i) @@ -357,7 +346,7 @@ class PreviewPanel(QWidget): layout.addLayout(row_layout) - def resizeEvent(self, event: QResizeEvent) -> None: + def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802 self.update_image_size( (self.image_container.size().width(), self.image_container.size().height()) ) @@ -370,7 +359,6 @@ class PreviewPanel(QWidget): ) def set_image_ratio(self, ratio: float): - # logging.info(f'Updating Ratio to: {ratio} #####################################################') self.image_ratio = ratio def update_image_size(self, size: tuple[int, int], ratio: float = None): @@ -407,23 +395,20 @@ class PreviewPanel(QWidget): self.img_button_size = (int(adj_width), int(adj_height)) self.preview_img.setMaximumSize(adj_size) self.preview_img.setIconSize(adj_size) - self.preview_vid.resizeVideo(adj_size) + self.preview_vid.resize_video(adj_size) self.preview_vid.setMaximumSize(adj_size) self.preview_vid.setMinimumSize(adj_size) # self.preview_img.setMinimumSize(adj_size) def place_add_field_button(self): self.scroll_layout.addWidget(self.afb_container) - self.scroll_layout.setAlignment( - self.afb_container, Qt.AlignmentFlag.AlignHCenter - ) + self.scroll_layout.setAlignment(self.afb_container, Qt.AlignmentFlag.AlignHCenter) if self.add_field_modal.is_connected: self.add_field_modal.done.disconnect() if self.add_field_button.is_connected: self.add_field_button.clicked.disconnect() - # self.afm.done.connect(lambda f: (self.lib.add_field_to_entry(self.selected[0][1], f), self.update_widgets())) self.add_field_modal.done.connect( lambda items: ( self.add_field_to_selected(items), @@ -446,9 +431,7 @@ class PreviewPanel(QWidget): ) def update_widgets(self) -> bool: - """ - Render the panel widgets with the newest data from the Library. - """ + """Render the panel widgets with the newest data from the Library.""" logger.info("update_widgets", selected=self.driver.selected) self.is_open = True # self.tag_callback = tag_callback if tag_callback else None @@ -460,13 +443,11 @@ class PreviewPanel(QWidget): if not self.driver.selected: if self.selected or not self.initialized: self.file_label.setText("No Items Selected") - self.file_label.setFilePath("") + self.file_label.set_file_path("") self.file_label.setCursor(Qt.CursorShape.ArrowCursor) self.dimensions_label.setText("") - self.preview_img.setContextMenuPolicy( - Qt.ContextMenuPolicy.NoContextMenu - ) + self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) self.preview_img.setCursor(Qt.CursorShape.ArrowCursor) ratio = self.devicePixelRatio() @@ -475,7 +456,7 @@ class PreviewPanel(QWidget): "", (512, 512), ratio, - True, + is_loading=True, update_on_ratio_change=True, ) if self.preview_img.is_connected: @@ -520,7 +501,7 @@ class PreviewPanel(QWidget): # If a new selection is made, update the thumbnail and filepath. if not self.selected or self.selected != self.driver.selected: filepath = self.lib.library_dir / item.path - self.file_label.setFilePath(filepath) + self.file_label.set_file_path(filepath) ratio = self.devicePixelRatio() self.thumb_renderer.render( time.time(), @@ -532,9 +513,7 @@ class PreviewPanel(QWidget): self.file_label.setText("\u200b".join(str(filepath))) self.file_label.setCursor(Qt.CursorShape.PointingHandCursor) - self.preview_img.setContextMenuPolicy( - Qt.ContextMenuPolicy.ActionsContextMenu - ) + self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor) self.opener = FileOpenerHelper(filepath) @@ -550,9 +529,7 @@ class PreviewPanel(QWidget): try: with rawpy.imread(str(filepath)) as raw: rgb = raw.postprocess() - image = Image.new( - "L", (rgb.shape[1], rgb.shape[0]), color="black" - ) + image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black") except ( rawpy._rawpy.LibRawIOError, rawpy._rawpy.LibRawFileUnsupportedError, @@ -568,9 +545,7 @@ class PreviewPanel(QWidget): image = Image.fromarray(frame) if success: self.preview_img.hide() - self.preview_vid.play( - filepath, QSize(image.width, image.height) - ) + self.preview_vid.play(filepath, QSize(image.width, image.height)) self.resizeEvent( QResizeEvent( QSize(image.width, image.height), @@ -584,11 +559,14 @@ class PreviewPanel(QWidget): IMAGE_TYPES + VIDEO_TYPES + RAW_IMAGE_TYPES ): self.dimensions_label.setText( - f"{filepath.suffix.upper()[1:]} • {format_size(filepath.stat().st_size)}\n{image.width} x {image.height} px" + f"{filepath.suffix.upper()[1:]}" + f" • {format_size(filepath.stat().st_size)}\n{image.width} " + f"x {image.height} px" ) else: self.dimensions_label.setText( - f"{filepath.suffix.upper()[1:]} • {format_size(filepath.stat().st_size)}" + f"{filepath.suffix.upper()[1:]}" + f" • {format_size(filepath.stat().st_size)}" ) if not filepath.is_file(): @@ -596,9 +574,7 @@ class PreviewPanel(QWidget): except (FileNotFoundError, cv2.error) as e: self.dimensions_label.setText(f"{filepath.suffix.upper()}") - logger.error( - "Couldn't Render thumbnail", filepath=filepath, error=e - ) + logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) except ( UnidentifiedImageError, @@ -607,15 +583,11 @@ class PreviewPanel(QWidget): self.dimensions_label.setText( f"{filepath.suffix.upper()[1:]} • {format_size(filepath.stat().st_size)}" ) - logger.error( - "Couldn't Render thumbnail", filepath=filepath, error=e - ) + logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) if self.preview_img.is_connected: self.preview_img.clicked.disconnect() - self.preview_img.clicked.connect( - lambda checked=False, pth=filepath: open_file(pth) - ) + self.preview_img.clicked.connect(lambda checked=False, pth=filepath: open_file(pth)) self.preview_img.is_connected = True self.selected = self.driver.selected @@ -643,12 +615,10 @@ class PreviewPanel(QWidget): if self.selected != self.driver.selected: self.file_label.setText(f"{len(self.driver.selected)} Items Selected") self.file_label.setCursor(Qt.CursorShape.ArrowCursor) - self.file_label.setFilePath("") + self.file_label.set_file_path("") self.dimensions_label.setText("") - self.preview_img.setContextMenuPolicy( - Qt.ContextMenuPolicy.NoContextMenu - ) + self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) self.preview_img.setCursor(Qt.CursorShape.ArrowCursor) ratio = self.devicePixelRatio() @@ -657,7 +627,7 @@ class PreviewPanel(QWidget): "", (512, 512), ratio, - True, + is_loading=True, update_on_ratio_change=True, ) if self.preview_img.is_connected: @@ -712,9 +682,7 @@ class PreviewPanel(QWidget): return True def set_tags_updated_slot(self, slot: object): - """ - Replacement for tag_callback. - """ + """Replacement for tag_callback.""" if self.is_connected: self.tags_updated.disconnect() @@ -725,7 +693,11 @@ class PreviewPanel(QWidget): def write_container(self, index: int, field: BaseField, is_mixed: bool = False): """Update/Create data for a FieldContainer. - :param is_mixed: Relevant when multiple items are selected. If True, field is not present in all selected items + Args: + index(int): The container index. + field(BaseField): The type of field to write to. + is_mixed(bool): Relevant when multiple items are selected. + If True, field is not present in all selected items. """ # Remove 'Add Field' button from scroll_layout, to be re-added later. self.scroll_layout.takeAt(self.scroll_layout.count() - 1).widget() @@ -769,7 +741,6 @@ class PreviewPanel(QWidget): container.set_inner_widget(inner_container) - # inner_container.field = field inner_container.updated.connect( lambda: ( self.write_container(index, field), @@ -777,7 +748,6 @@ class PreviewPanel(QWidget): ) ) # NOTE: Tag Boxes have no Edit Button (But will when you can convert field types) - # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) container.set_remove_callback( lambda: self.remove_message_box( prompt=self.remove_field_prompt(field.type.name), @@ -893,8 +863,6 @@ class PreviewPanel(QWidget): inner_container = TextWidget(title, str(field.value)) container.set_inner_widget(inner_container) - # if type(item) == Entry: - # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) container.set_remove_callback( lambda: self.remove_message_box( prompt=self.remove_field_prompt(field.type.name), @@ -912,13 +880,10 @@ class PreviewPanel(QWidget): else: logger.warning("write_container - unknown field", field=field) container.set_title(field.type.name) - # container.set_editable(False) container.set_inline(False) title = f"{field.type.name} (Unknown Field Type)" inner_container = TextWidget(title, field.type.name) container.set_inner_widget(inner_container) - # if type(item) == Entry: - # container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) container.set_remove_callback( lambda: self.remove_message_box( prompt=self.remove_field_prompt(field.type.name), @@ -971,9 +936,7 @@ class PreviewPanel(QWidget): remove_mb.setText(prompt) remove_mb.setWindowTitle("Remove Field") remove_mb.setIcon(QMessageBox.Icon.Warning) - cancel_button = remove_mb.addButton( - "&Cancel", QMessageBox.ButtonRole.DestructiveRole - ) + cancel_button = remove_mb.addButton("&Cancel", QMessageBox.ButtonRole.DestructiveRole) remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole) # remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel) remove_mb.setDefaultButton(cancel_button) diff --git a/tagstudio/src/qt/widgets/progress.py b/tagstudio/src/qt/widgets/progress.py index ac14d87d..8adfb094 100644 --- a/tagstudio/src/qt/widgets/progress.py +++ b/tagstudio/src/qt/widgets/progress.py @@ -6,7 +6,7 @@ from typing import Optional from PySide6.QtCore import Qt -from PySide6.QtWidgets import QWidget, QVBoxLayout, QProgressDialog +from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget class ProgressWidget(QWidget): @@ -30,9 +30,7 @@ class ProgressWidget(QWidget): ) self.root.addWidget(self.pb) self.setFixedSize(432, 112) - self.setWindowFlags( - self.pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint - ) + self.setWindowFlags(self.pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint) self.setWindowTitle(window_title) self.setWindowModality(Qt.WindowModality.ApplicationModal) diff --git a/tagstudio/src/qt/widgets/tag.py b/tagstudio/src/qt/widgets/tag.py index 3cbb21f7..ae26b342 100644 --- a/tagstudio/src/qt/widgets/tag.py +++ b/tagstudio/src/qt/widgets/tag.py @@ -4,14 +4,13 @@ import math -from types import FunctionType from pathlib import Path +from types import FunctionType from PIL import Image -from PySide6.QtCore import Signal, Qt, QEvent -from PySide6.QtGui import QEnterEvent, QAction -from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton - +from PySide6.QtCore import QEvent, Qt, Signal +from PySide6.QtGui import QAction, QEnterEvent +from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget from src.core.library import Tag from src.core.palette import ColorType, get_tag_color @@ -67,64 +66,31 @@ class TagWidget(QWidget): self.inner_layout = QHBoxLayout() self.inner_layout.setObjectName("innerLayout") self.inner_layout.setContentsMargins(2, 2, 2, 2) - # self.inner_layout.setAlignment(Qt.AlignmentFlag.AlignRight) - # self.inner_container = QWidget() - # self.inner_container.setLayout(self.inner_layout) - # self.base_layout.addWidget(self.inner_container) self.bg_button.setLayout(self.inner_layout) self.bg_button.setMinimumSize(math.ceil(22 * 1.5), 22) - # self.bg_button.setStyleSheet( - # f'QPushButton {{' - # f'border: 2px solid #8f8f91;' - # f'border-radius: 6px;' - # f'background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 {ColorType.PRIMARY}, stop: 1 {ColorType.BORDER});' - # f'min-width: 80px;}}') - self.bg_button.setStyleSheet( - # f'background: {get_tag_color(ColorType.PRIMARY, tag.color)};' f"QPushButton{{" f"background: {get_tag_color(ColorType.PRIMARY, tag.color)};" - # f'background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {get_tag_color(ColorType.PRIMARY, tag.color)}, stop:1.0 {get_tag_color(ColorType.BORDER, tag.color)});' - # f"border-color:{get_tag_color(ColorType.PRIMARY, tag.color)};" f"color: {get_tag_color(ColorType.TEXT, tag.color)};" f"font-weight: 600;" f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};" f"border-radius: 6px;" f"border-style:solid;" f"border-width: {math.ceil(self.devicePixelRatio())}px;" - # f'border-top:2px solid {get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};' - # f'border-bottom:2px solid {get_tag_color(ColorType.BORDER, tag.color)};' - # f'border-left:2px solid {get_tag_color(ColorType.BORDER, tag.color)};' - # f'border-right:2px solid {get_tag_color(ColorType.BORDER, tag.color)};' - # f'padding-top: 0.5px;' f"padding-right: 4px;" f"padding-bottom: 1px;" f"padding-left: 4px;" f"font-size: 13px" f"}}" f"QPushButton::hover{{" - # f'background: {get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};' - # f'background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {get_tag_color(ColorType.PRIMARY, tag.color)}, stop:1.0 {get_tag_color(ColorType.BORDER, tag.color)});' - # f"border-color:{get_tag_color(ColorType.PRIMARY, tag.color)};" - # f"color: {get_tag_color(ColorType.TEXT, tag.color)};" f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};" f"}}" ) - # self.renderer = ThumbRenderer() - # self.renderer.updated.connect(lambda ts, i, s, ext: (self.update_thumb(ts, image=i), - # self.update_size( - # ts, size=s), - # self.set_extension(ext))) - - # self.bg_button.setLayout(self.base_layout) - self.base_layout.addWidget(self.bg_button) - # self.setMinimumSize(self.bg_button.size()) - # logging.info(tag.color) if has_remove: self.remove_button = QPushButton(self) self.remove_button.setFlat(True) @@ -133,28 +99,16 @@ class TagWidget(QWidget): self.remove_button.setStyleSheet( f"color: {get_tag_color(ColorType.PRIMARY, tag.color)};" f"background: {get_tag_color(ColorType.TEXT, tag.color)};" - # f"color: {'black' if color not in ['black', 'gray', 'dark gray', 'cool gray', 'warm gray', 'blue', 'purple', 'violet'] else 'white'};" - # f"border-color: {get_tag_color(ColorType.BORDER, tag.color)};" f"font-weight: 800;" - # f"border-color:{'black' if color not in [ - # 'black', 'gray', 'dark gray', - # 'cool gray', 'warm gray', 'blue', - # 'purple', 'violet'] else 'white'};" f"border-radius: 4px;" - # f'border-style:solid;' f"border-width:0;" - # f'padding-top: 1.5px;' - # f'padding-right: 4px;' f"padding-bottom: 4px;" - # f'padding-left: 4px;' f"font-size: 14px" ) self.remove_button.setMinimumSize(19, 19) self.remove_button.setMaximumSize(19, 19) - # self.remove_button.clicked.connect(on_remove_callback) self.remove_button.clicked.connect(self.on_remove.emit) - # self.inner_layout.addWidget(self.edit_button) if has_remove: self.inner_layout.addWidget(self.remove_button) self.inner_layout.addStretch(1) @@ -162,21 +116,16 @@ class TagWidget(QWidget): # NOTE: Do this if you don't want the tag to stretch, like in a search. # self.bg_button.setMaximumWidth(self.bg_button.sizeHint().width()) - # self.set_click(on_click_callback) self.bg_button.clicked.connect(self.on_click.emit) - # self.setMinimumSize(50,20) - - def enterEvent(self, event: QEnterEvent) -> None: + def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802 if self.has_remove: self.remove_button.setHidden(False) - # self.edit_button.setHidden(False) self.update() return super().enterEvent(event) - def leaveEvent(self, event: QEvent) -> None: + def leaveEvent(self, event: QEvent) -> None: # noqa: N802 if self.has_remove: self.remove_button.setHidden(True) - # self.edit_button.setHidden(True) self.update() return super().leaveEvent(event) diff --git a/tagstudio/src/qt/widgets/tag_box.py b/tagstudio/src/qt/widgets/tag_box.py index 567721fa..c24a3519 100644 --- a/tagstudio/src/qt/widgets/tag_box.py +++ b/tagstudio/src/qt/widgets/tag_box.py @@ -7,19 +7,18 @@ import math import typing import structlog -from PySide6.QtCore import Signal, Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import QPushButton - -from src.core.constants import TAG_FAVORITE, TAG_ARCHIVED +from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE from src.core.library import Entry, Tag from src.core.library.alchemy.enums import FilterState from src.core.library.alchemy.fields import TagBoxField from src.qt.flowlayout import FlowLayout -from src.qt.widgets.fields import FieldWidget -from src.qt.widgets.tag import TagWidget -from src.qt.widgets.panel import PanelModal from src.qt.modals.build_tag import BuildTagPanel from src.qt.modals.tag_search import TagSearchPanel +from src.qt.widgets.fields import FieldWidget +from src.qt.widgets.panel import PanelModal +from src.qt.widgets.tag import TagWidget if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -42,10 +41,12 @@ class TagBoxWidget(FieldWidget): assert isinstance(field, TagBoxField), f"field is {type(field)}" self.field = field - self.driver = driver # Used for creating tag click callbacks that search entries for that tag. + self.driver = ( + driver # Used for creating tag click callbacks that search entries for that tag. + ) self.setObjectName("tagBox") self.base_layout = FlowLayout() - self.base_layout.setGridEfficiency(False) + self.base_layout.enable_grid_optimizations(value=False) self.base_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(self.base_layout) @@ -94,7 +95,7 @@ class TagBoxWidget(FieldWidget): is_recycled = True for tag in tags: - tag_widget = TagWidget(tag, True, True) + tag_widget = TagWidget(tag, has_edit=True, has_remove=True) tag_widget.on_click.connect( lambda tag_id=tag.id: ( self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"), @@ -133,7 +134,6 @@ class TagBoxWidget(FieldWidget): done_callback=self.driver.preview_panel.update_widgets, has_save=True, ) - # self.edit_modal.widget.update_display_name.connect(lambda t: self.edit_modal.title_widget.setText(t)) # TODO - this was update_tag() self.edit_modal.saved.connect( lambda: self.driver.lib.update_tag( @@ -141,7 +141,6 @@ class TagBoxWidget(FieldWidget): subtag_ids=build_tag_panel.subtags, ) ) - # panel.tag_updated.connect(lambda tag: self.lib.update_tag(tag)) self.edit_modal.show() def add_tag_callback(self, tag_id: int): diff --git a/tagstudio/src/qt/widgets/text.py b/tagstudio/src/qt/widgets/text.py index 49c680c9..8238f4c1 100644 --- a/tagstudio/src/qt/widgets/text.py +++ b/tagstudio/src/qt/widgets/text.py @@ -21,9 +21,7 @@ class TextWidget(FieldWidget): # self.text_label.textFormat(Qt.TextFormat.RichText) self.text_label.setStyleSheet("font-size: 12px") self.text_label.setWordWrap(True) - self.text_label.setTextInteractionFlags( - Qt.TextInteractionFlag.TextSelectableByMouse - ) + self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self.base_layout.addWidget(self.text_label) self.set_text(text) diff --git a/tagstudio/src/qt/widgets/text_box_edit.py b/tagstudio/src/qt/widgets/text_box_edit.py index d35304f9..44cca3f4 100644 --- a/tagstudio/src/qt/widgets/text_box_edit.py +++ b/tagstudio/src/qt/widgets/text_box_edit.py @@ -3,8 +3,7 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtWidgets import QVBoxLayout, QPlainTextEdit - +from PySide6.QtWidgets import QPlainTextEdit, QVBoxLayout from src.qt.widgets.panel import PanelWidget diff --git a/tagstudio/src/qt/widgets/text_line_edit.py b/tagstudio/src/qt/widgets/text_line_edit.py index 7185f024..74b65c5b 100644 --- a/tagstudio/src/qt/widgets/text_line_edit.py +++ b/tagstudio/src/qt/widgets/text_line_edit.py @@ -3,8 +3,7 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio from typing import Callable -from PySide6.QtWidgets import QVBoxLayout, QLineEdit - +from PySide6.QtWidgets import QLineEdit, QVBoxLayout from src.qt.widgets.panel import PanelWidget diff --git a/tagstudio/src/qt/widgets/thumb_button.py b/tagstudio/src/qt/widgets/thumb_button.py index 179efaec..e56408b7 100644 --- a/tagstudio/src/qt/widgets/thumb_button.py +++ b/tagstudio/src/qt/widgets/thumb_button.py @@ -5,7 +5,7 @@ from PySide6 import QtCore from PySide6.QtCore import QEvent -from PySide6.QtGui import QEnterEvent, QPainter, QColor, QPen, QPainterPath, QPaintEvent +from PySide6.QtGui import QColor, QEnterEvent, QPainter, QPainterPath, QPaintEvent, QPen from PySide6.QtWidgets import QWidget from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper @@ -19,7 +19,7 @@ class ThumbButton(QPushButtonWrapper): # self.clicked.connect(lambda checked: self.set_selected(True)) - def paintEvent(self, event: QPaintEvent) -> None: + def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 super().paintEvent(event) if self.hovered or self.selected: painter = QPainter() @@ -47,9 +47,7 @@ class ThumbButton(QPushButtonWrapper): # painter.drawPath(path) if self.selected: - painter.setCompositionMode( - QPainter.CompositionMode.CompositionMode_HardLight - ) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_HardLight) color = QColor("#bb4ff0") color.setAlphaF(0.5) pen = QPen(color, width) @@ -57,29 +55,25 @@ class ThumbButton(QPushButtonWrapper): painter.fillPath(path, color) painter.drawPath(path) - painter.setCompositionMode( - QPainter.CompositionMode.CompositionMode_Source - ) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) color = QColor("#bb4ff0") if not self.hovered else QColor("#55bbf6") pen = QPen(color, width) painter.setPen(pen) painter.drawPath(path) elif self.hovered: - painter.setCompositionMode( - QPainter.CompositionMode.CompositionMode_Source - ) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) color = QColor("#55bbf6") pen = QPen(color, width) painter.setPen(pen) painter.drawPath(path) painter.end() - def enterEvent(self, event: QEnterEvent) -> None: + def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802 self.hovered = True self.repaint() return super().enterEvent(event) - def leaveEvent(self, event: QEvent) -> None: + def leaveEvent(self, event: QEvent) -> None: # noqa: N802 self.hovered = False self.repaint() return super().leaveEvent(event) diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index e0045902..ff9a99b0 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -8,30 +8,28 @@ from pathlib import Path import cv2 import rawpy -from pillow_heif import register_heif_opener, register_avif_opener +import structlog from PIL import ( Image, - UnidentifiedImageError, - ImageQt, ImageDraw, + ImageFile, ImageFont, ImageOps, - ImageFile, + ImageQt, + UnidentifiedImageError, ) from PIL.Image import DecompressionBombError -from PySide6.QtCore import QObject, Signal, QSize +from pillow_heif import register_avif_opener, register_heif_opener +from PySide6.QtCore import QObject, QSize, Signal from PySide6.QtGui import QPixmap - -from src.qt.helpers.gradient import four_corner_gradient_background from src.core.constants import ( - PLAINTEXT_TYPES, - VIDEO_TYPES, IMAGE_TYPES, + PLAINTEXT_TYPES, RAW_IMAGE_TYPES, + VIDEO_TYPES, ) -import structlog - from src.core.utils.encoding import detect_char_encoding +from src.qt.helpers.gradient import four_corner_gradient_background ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -91,12 +89,11 @@ class ThumbRenderer(QObject): filepath: str | Path, base_size: tuple[int, int], pixel_ratio: float, - is_loading=False, - gradient=False, - update_on_ratio_change=False, + is_loading: bool = False, + gradient: bool = False, + update_on_ratio_change: bool = False, ): """Internal renderer. Render an entry/element thumbnail for the GUI.""" - logger.debug("rendering thumbnail", path=filepath) image: Image.Image = None @@ -136,9 +133,7 @@ class ThumbRenderer(QObject): image = ImageOps.exif_transpose(image) except DecompressionBombError as e: - logger.error( - "Couldn't Render thumbnail", filepath=filepath, error=e - ) + logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) elif _filepath.suffix.lower() in RAW_IMAGE_TYPES: try: @@ -151,17 +146,13 @@ class ThumbRenderer(QObject): decoder_name="raw", ) except DecompressionBombError as e: - logger.error( - "Couldn't Render thumbnail", filepath=filepath, error=e - ) + logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) except ( rawpy._rawpy.LibRawIOError, rawpy._rawpy.LibRawFileUnsupportedError, ) as e: - logger.error( - "Couldn't Render thumbnail", filepath=filepath, error=e - ) + logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) # Videos ======================================================= elif _filepath.suffix.lower() in VIDEO_TYPES: @@ -232,8 +223,7 @@ class ThumbRenderer(QObject): resampling_method = ( Image.Resampling.NEAREST - if max(image.size[0], image.size[1]) - < max(base_size[0], base_size[1]) + if max(image.size[0], image.size[1]) < max(base_size[0], base_size[1]) else Image.Resampling.BILINEAR ) image = image.resize((new_x, new_y), resample=resampling_method) @@ -272,9 +262,7 @@ class ThumbRenderer(QObject): UnicodeDecodeError, ) as e: if e is not UnicodeDecodeError: - logger.error( - "Couldn't Render thumbnail", filepath=filepath, error=e - ) + logger.error("Couldn't Render thumbnail", filepath=filepath, error=e) if update_on_ratio_change: self.updated_ratio.emit(1) @@ -299,6 +287,4 @@ class ThumbRenderer(QObject): ) else: - self.updated.emit( - timestamp, QPixmap(), QSize(*base_size), _filepath.suffix.lower() - ) + self.updated.emit(timestamp, QPixmap(), QSize(*base_size), _filepath.suffix.lower()) diff --git a/tagstudio/src/qt/widgets/video_player.py b/tagstudio/src/qt/widgets/video_player.py index 9fc65604..0d5928f5 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -2,36 +2,34 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio import logging - import typing -from PySide6.QtCore import ( - Qt, - QSize, - QTimer, - QVariantAnimation, - QUrl, - QObject, - QEvent, - QRectF, -) -from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices -from PySide6.QtMultimediaWidgets import QGraphicsVideoItem -from PySide6.QtWidgets import QGraphicsView, QGraphicsScene -from PySide6.QtGui import ( - QPen, - QColor, - QBrush, - QResizeEvent, - QWheelEvent, - QAction, - QRegion, - QBitmap, -) -from PySide6.QtSvgWidgets import QSvgWidget -from src.qt.helpers.file_opener import FileOpenerHelper from PIL import Image, ImageDraw +from PySide6.QtCore import ( + QEvent, + QObject, + QRectF, + QSize, + Qt, + QTimer, + QUrl, + QVariantAnimation, +) +from PySide6.QtGui import ( + QAction, + QBitmap, + QBrush, + QColor, + QPen, + QRegion, + QResizeEvent, +) +from PySide6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer +from PySide6.QtMultimediaWidgets import QGraphicsVideoItem +from PySide6.QtSvgWidgets import QSvgWidget +from PySide6.QtWidgets import QGraphicsScene, QGraphicsView from src.core.enums import SettingItems +from src.qt.helpers.file_opener import FileOpenerHelper if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -49,11 +47,9 @@ class VideoPlayer(QGraphicsView): self.driver = driver self.resolution = QSize(1280, 720) self.animation = QVariantAnimation(self) - self.animation.valueChanged.connect( - lambda value: self.setTintTransparency(value) - ) + self.animation.valueChanged.connect(lambda value: self.set_tint_opacity(value)) self.hover_fix_timer = QTimer() - self.hover_fix_timer.timeout.connect(lambda: self.checkIfStillHovered()) + self.hover_fix_timer.timeout.connect(lambda: self.check_if_hovered()) self.hover_fix_timer.setSingleShot(True) self.content_visible = False self.filepath = None @@ -63,16 +59,14 @@ class VideoPlayer(QGraphicsView): self.setScene(QGraphicsScene(self)) self.player = QMediaPlayer(self) self.player.mediaStatusChanged.connect( - lambda: self.checkMediaStatus(self.player.mediaStatus()) + lambda: self.check_media_status(self.player.mediaStatus()) ) self.video_preview = VideoPreview() self.player.setVideoOutput(self.video_preview) self.video_preview.setAcceptHoverEvents(True) self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.RightButton) self.video_preview.installEventFilter(self) - self.player.setAudioOutput( - QAudioOutput(QMediaDevices().defaultAudioOutput(), self.player) - ) + self.player.setAudioOutput(QAudioOutput(QMediaDevices().defaultAudioOutput(), self.player)) self.player.audioOutput().setMuted(True) self.setAlignment(Qt.AlignmentFlag.AlignCenter) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) @@ -92,7 +86,7 @@ class VideoPlayer(QGraphicsView): # Set up the buttons. self.play_pause = QSvgWidget() - self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) + self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, on=True) self.play_pause.setMouseTracking(True) self.play_pause.installEventFilter(self) self.scene().addWidget(self.play_pause) @@ -104,7 +98,7 @@ class VideoPlayer(QGraphicsView): self.play_pause.hide() self.mute_button = QSvgWidget() - self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) + self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, on=True) self.mute_button.setMouseTracking(True) self.mute_button.installEventFilter(self) self.scene().addWidget(self.mute_button) @@ -121,9 +115,9 @@ class VideoPlayer(QGraphicsView): autoplay_action.setCheckable(True) self.addAction(autoplay_action) autoplay_action.setChecked( - bool(self.driver.settings.value(SettingItems.AUTOPLAY, True, type=bool)) + bool(self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool)) ) - autoplay_action.triggered.connect(lambda: self.toggleAutoplay()) + autoplay_action.triggered.connect(lambda: self.toggle_autoplay()) self.autoplay = autoplay_action open_file_action = QAction("Open file", self) @@ -137,11 +131,12 @@ class VideoPlayer(QGraphicsView): self.player.stop() super().close(*args, **kwargs) - def toggleAutoplay(self) -> None: + def toggle_autoplay(self) -> None: + """Toggle the autoplay state of the video.""" self.driver.settings.setValue(SettingItems.AUTOPLAY, self.autoplay.isChecked()) self.driver.settings.sync() - def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None: + def check_media_status(self, media_status: QMediaPlayer.MediaStatus) -> None: if media_status == QMediaPlayer.MediaStatus.EndOfMedia: # Switches current video to with video at filepath. # Reason for this is because Pyside6 can't handle setting a new source and freezes. @@ -155,10 +150,11 @@ class VideoPlayer(QGraphicsView): else: self.player.pause() self.opener.set_filepath(self.filepath) - self.keepControlsInPlace() - self.updateControls() + self.reposition_controls() + self.update_controls() - def updateControls(self) -> None: + def update_controls(self) -> None: + """Update the icons of the video player controls.""" if self.player.audioOutput().isMuted(): self.mute_button.load(self.driver.rm.volume_mute_icon) else: @@ -169,19 +165,16 @@ class VideoPlayer(QGraphicsView): else: self.play_pause.load(self.driver.rm.play_icon) - def wheelEvent(self, event: QWheelEvent) -> None: - return - - def eventFilter(self, obj: QObject, event: QEvent) -> bool: - # This chunk of code is for the video controls. + def eventFilter(self, obj: QObject, event: QEvent) -> bool: # noqa: N802 + """Manage events for the video player.""" if ( event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton # type: ignore ): if obj == self.play_pause and self.player.hasVideo(): - self.pauseToggle() + self.toggle_pause() elif obj == self.mute_button and self.player.hasAudio(): - self.muteToggle() + self.toggle_mute() elif obj == self.video_preview: if event.type() in ( @@ -192,8 +185,7 @@ class VideoPlayer(QGraphicsView): self.underMouse() self.hover_fix_timer.start(10) elif ( - event.type() - in (QEvent.Type.GraphicsSceneHoverLeave, QEvent.Type.HoverLeave) + event.type() in (QEvent.Type.GraphicsSceneHoverLeave, QEvent.Type.HoverLeave) and not self.video_preview.isUnderMouse() ): self.hover_fix_timer.stop() @@ -201,32 +193,37 @@ class VideoPlayer(QGraphicsView): return super().eventFilter(obj, event) - def checkIfStillHovered(self) -> None: - # I don't know why, but the HoverLeave event is not triggered sometimes - # and does not hide the controls. - # So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse - # is still in the video preview. + def check_if_hovered(self) -> None: + """Check if the mouse is still hovering over the video player.""" + # Sometimes the HoverLeave event does not trigger and is unable to hide the video controls. + # As a workaround, this is called by a QTimer every 10ms + # to check if the mouse is still in the video preview. if not self.video_preview.isUnderMouse(): self.releaseMouse() else: self.hover_fix_timer.start(10) - def setTintTransparency(self, value) -> None: - self.video_tint.setBrush(QBrush(QColor(0, 0, 0, value))) + def set_tint_opacity(self, opacity: int) -> None: + """Set the opacity of the video player's tint. - def underMouse(self) -> bool: + Args: + opacity(int): The opacity value, from 0-255. + """ + self.video_tint.setBrush(QBrush(QColor(0, 0, 0, opacity))) + + def underMouse(self) -> bool: # noqa: N802 self.animation.setStartValue(self.video_tint.brush().color().alpha()) self.animation.setEndValue(100) self.animation.setDuration(250) self.animation.start() self.play_pause.show() self.mute_button.show() - self.keepControlsInPlace() - self.updateControls() + self.reposition_controls() + self.update_controls() return super().underMouse() - def releaseMouse(self) -> None: + def releaseMouse(self) -> None: # noqa: N802 self.animation.setStartValue(self.video_tint.brush().color().alpha()) self.animation.setEndValue(0) self.animation.setDuration(500) @@ -236,12 +233,13 @@ class VideoPlayer(QGraphicsView): return super().releaseMouse() - def resetControlsToDefault(self) -> None: - # Resets the video controls to their default state. + def reset_controls(self) -> None: + """Reset the video controls to their default state.""" self.play_pause.load(self.driver.rm.pause_icon) self.mute_button.load(self.driver.rm.volume_mute_icon) - def pauseToggle(self) -> None: + def toggle_pause(self) -> None: + """Toggle the pause state of the video.""" if self.player.isPlaying(): self.player.pause() self.play_pause.load(self.driver.rm.play_icon) @@ -249,7 +247,8 @@ class VideoPlayer(QGraphicsView): self.player.play() self.play_pause.load(self.driver.rm.pause_icon) - def muteToggle(self) -> None: + def toggle_mute(self) -> None: + """Toggle the mute state of the video.""" if self.player.audioOutput().isMuted(): self.player.audioOutput().setMuted(False) self.mute_button.load(self.driver.rm.volume_icon) @@ -258,8 +257,10 @@ class VideoPlayer(QGraphicsView): self.mute_button.load(self.driver.rm.volume_mute_icon) def play(self, filepath: str, resolution: QSize) -> None: - # Sets the filepath and sends the current player position to the very end, - # so that the new video can be played. + """Set the filepath and send the current player position to the very end. + + This is used so that the new video can be played. + """ logging.info(f"Playing {filepath}") self.resolution = resolution self.filepath = filepath @@ -267,14 +268,18 @@ class VideoPlayer(QGraphicsView): self.player.setPosition(self.player.duration()) self.player.play() else: - self.checkMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia) + self.check_media_status(QMediaPlayer.MediaStatus.EndOfMedia) def stop(self) -> None: self.filepath = None self.player.stop() - def resizeVideo(self, new_size: QSize) -> None: - # Resizes the video preview to the new size. + def resize_video(self, new_size: QSize) -> None: + """Resize the video player. + + Args: + new_size(QSize): The new size of the video player to set. + """ self.video_preview.setSize(new_size) self.video_tint.setRect( 0, 0, self.video_preview.size().width(), self.video_preview.size().height() @@ -282,11 +287,12 @@ class VideoPlayer(QGraphicsView): contents = self.contentsRect() self.centerOn(self.video_preview) - self.roundCorners() + self.apply_rounded_corners() self.setSceneRect(0, 0, contents.width(), contents.height()) - self.keepControlsInPlace() + self.reposition_controls() - def roundCorners(self) -> None: + def apply_rounded_corners(self) -> None: + """Apply a rounded corner effect to the video player.""" width: int = int(max(self.contentsRect().size().width(), 0)) height: int = int(max(self.contentsRect().size().height(), 0)) mask = Image.new( @@ -306,8 +312,8 @@ class VideoPlayer(QGraphicsView): final_mask = mask.getchannel("A").toqpixmap() self.setMask(QRegion(QBitmap(final_mask))) - def keepControlsInPlace(self) -> None: - # Keeps the video controls in the places they should be. + def reposition_controls(self) -> None: + """Reposition video controls to their intended locations.""" self.play_pause.move( int(self.width() / 2 - self.play_pause.size().width() / 2), int(self.height() / 2 - self.play_pause.size().height() / 2), @@ -317,10 +323,10 @@ class VideoPlayer(QGraphicsView): int(self.height() - self.mute_button.size().height() - 10), ) - def resizeEvent(self, event: QResizeEvent) -> None: - # Keeps the video preview in the center of the screen. + def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802 + """Keep the video preview in the center of the screen.""" self.centerOn(self.video_preview) - self.resizeVideo( + self.resize_video( QSize( int(self.video_preview.size().width()), int(self.video_preview.size().height()), @@ -329,7 +335,7 @@ class VideoPlayer(QGraphicsView): class VideoPreview(QGraphicsVideoItem): - def boundingRect(self): + def boundingRect(self): # noqa: N802 return QRectF(0, 0, self.size().width(), self.size().height()) def paint(self, painter, option, widget=None) -> None: diff --git a/tagstudio/tag_studio.py b/tagstudio/tag_studio.py index a6b47b6b..ceceb077 100755 --- a/tagstudio/tag_studio.py +++ b/tagstudio/tag_studio.py @@ -5,13 +5,12 @@ """TagStudio launcher.""" -import structlog -import logging - -from src.qt.ts_qt import QtDriver import argparse +import logging import traceback +import structlog +from src.qt.ts_qt import QtDriver structlog.configure( wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), @@ -66,7 +65,7 @@ def main(): driver.start() except Exception: traceback.print_exc() - print(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...") + logging.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...") input() diff --git a/tagstudio/tests/conftest.py b/tagstudio/tests/conftest.py index 2c299739..a7bff9bf 100644 --- a/tagstudio/tests/conftest.py +++ b/tagstudio/tests/conftest.py @@ -1,7 +1,7 @@ -import sys import pathlib +import sys from tempfile import TemporaryDirectory -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest @@ -9,10 +9,10 @@ CWD = pathlib.Path(__file__).parent # this needs to be above `src` imports sys.path.insert(0, str(CWD.parent)) -from src.core.library import Library, Tag, Entry +from src.core.library import Entry, Library, Tag +from src.core.library import alchemy as backend from src.core.library.alchemy.enums import TagColor from src.core.library.alchemy.fields import TagBoxField, _FieldID -from src.core.library import alchemy as backend from src.qt.ts_qt import QtDriver diff --git a/tagstudio/tests/macros/test_folders_tags.py b/tagstudio/tests/macros/test_folders_tags.py index a5263c7a..669124b8 100644 --- a/tagstudio/tests/macros/test_folders_tags.py +++ b/tagstudio/tests/macros/test_folders_tags.py @@ -3,7 +3,5 @@ from src.qt.modals.folders_to_tags import folders_to_tags def test_folders_to_tags(library): folders_to_tags(library) - entry = [ - x for x in library.get_entries(with_joins=True) if "bar.md" in str(x.path) - ][0] + entry = [x for x in library.get_entries(with_joins=True) if "bar.md" in str(x.path)][0] assert {x.name for x in entry.tags} == {"two", "bar"} diff --git a/tagstudio/tests/macros/test_missing_files.py b/tagstudio/tests/macros/test_missing_files.py index f269d249..e90c0077 100644 --- a/tagstudio/tests/macros/test_missing_files.py +++ b/tagstudio/tests/macros/test_missing_files.py @@ -2,7 +2,6 @@ import pathlib from tempfile import TemporaryDirectory import pytest - from src.core.library import Library from src.core.library.alchemy.enums import FilterState from src.core.utils.missing_files import MissingRegistry diff --git a/tagstudio/tests/macros/test_refresh_dir.py b/tagstudio/tests/macros/test_refresh_dir.py index f5887cd9..e0b1e8b6 100644 --- a/tagstudio/tests/macros/test_refresh_dir.py +++ b/tagstudio/tests/macros/test_refresh_dir.py @@ -2,7 +2,6 @@ import pathlib from tempfile import TemporaryDirectory import pytest - from src.core.constants import LibraryPrefs from src.core.utils.refresh_dir import RefreshDirTracker diff --git a/tagstudio/tests/macros/test_sidecar.py b/tagstudio/tests/macros/test_sidecar.py index a1f1be35..700169f4 100644 --- a/tagstudio/tests/macros/test_sidecar.py +++ b/tagstudio/tests/macros/test_sidecar.py @@ -3,7 +3,6 @@ from pathlib import Path from tempfile import TemporaryDirectory import pytest - from src.core.enums import MacroID from src.core.library.alchemy.fields import _FieldID diff --git a/tagstudio/tests/qt/test_driver.py b/tagstudio/tests/qt/test_driver.py index ee7b3338..13a4cdee 100644 --- a/tagstudio/tests/qt/test_driver.py +++ b/tagstudio/tests/qt/test_driver.py @@ -1,7 +1,6 @@ from pathlib import Path from unittest.mock import Mock - from src.core.library import Entry from src.core.library.alchemy.enums import FilterState from src.core.library.json.library import ItemType @@ -49,22 +48,22 @@ def test_select_item_bridge(qt_driver, entry_min): assert len(qt_driver.item_thumbs) == 3 # select first item - qt_driver.select_item(0, False, False) + qt_driver.select_item(0, append=False, bridge=False) assert qt_driver.selected == [0] # add second item to selection - qt_driver.select_item(1, False, bridge=True) + qt_driver.select_item(1, append=False, bridge=True) assert qt_driver.selected == [0, 1] # add third item to selection - qt_driver.select_item(2, False, bridge=True) + qt_driver.select_item(2, append=False, bridge=True) assert qt_driver.selected == [0, 1, 2] # select third item only - qt_driver.select_item(2, False, bridge=False) + qt_driver.select_item(2, append=False, bridge=False) assert qt_driver.selected == [2] - qt_driver.select_item(0, False, bridge=True) + qt_driver.select_item(0, append=False, bridge=True) assert qt_driver.selected == [0, 1, 2] diff --git a/tagstudio/tests/qt/test_flow_widget.py b/tagstudio/tests/qt/test_flow_widget.py index ccfac874..4ec7119f 100644 --- a/tagstudio/tests/qt/test_flow_widget.py +++ b/tagstudio/tests/qt/test_flow_widget.py @@ -1,6 +1,5 @@ from PySide6.QtCore import QRect -from PySide6.QtWidgets import QWidget, QPushButton - +from PySide6.QtWidgets import QPushButton, QWidget from src.qt.flowlayout import FlowLayout @@ -10,9 +9,9 @@ def test_flow_layout_happy_path(qtbot): super().__init__() self.flow_layout = FlowLayout(self) - self.flow_layout.setGridEfficiency(True) + self.flow_layout.enable_grid_optimizations(value=True) self.flow_layout.addWidget(QPushButton("Short")) window = Window() assert window.flow_layout.count() - assert window.flow_layout._do_layout(QRect(0, 0, 0, 0), False) + assert window.flow_layout._do_layout(QRect(0, 0, 0, 0), test_only=False) diff --git a/tagstudio/tests/qt/test_item_thumb.py b/tagstudio/tests/qt/test_item_thumb.py index 7ed0c5da..09c527a0 100644 --- a/tagstudio/tests/qt/test_item_thumb.py +++ b/tagstudio/tests/qt/test_item_thumb.py @@ -1,7 +1,6 @@ import pytest - from src.core.library import ItemType -from src.qt.widgets.item_thumb import ItemThumb, BadgeType +from src.qt.widgets.item_thumb import BadgeType, ItemThumb @pytest.mark.parametrize("new_value", (True, False)) diff --git a/tagstudio/tests/qt/test_preview_panel.py b/tagstudio/tests/qt/test_preview_panel.py index 044fd8c6..3b612e32 100644 --- a/tagstudio/tests/qt/test_preview_panel.py +++ b/tagstudio/tests/qt/test_preview_panel.py @@ -2,10 +2,9 @@ from pathlib import Path from tempfile import TemporaryDirectory import pytest - from src.core.library import Entry from src.core.library.alchemy.enums import FieldTypeEnum -from src.core.library.alchemy.fields import _FieldID, TextField +from src.core.library.alchemy.fields import TextField, _FieldID from src.qt.widgets.preview_panel import PreviewPanel diff --git a/tagstudio/tests/qt/test_tag_widget.py b/tagstudio/tests/qt/test_tag_widget.py index e983841c..9d10691a 100644 --- a/tagstudio/tests/qt/test_tag_widget.py +++ b/tagstudio/tests/qt/test_tag_widget.py @@ -1,10 +1,9 @@ from unittest.mock import patch - from src.core.library.alchemy.fields import _FieldID +from src.qt.modals.build_tag import BuildTagPanel from src.qt.widgets.tag import TagWidget from src.qt.widgets.tag_box import TagBoxWidget -from src.qt.modals.build_tag import BuildTagPanel def test_tag_widget(qtbot, library, qt_driver): @@ -26,9 +25,7 @@ def test_tag_widget(qtbot, library, qt_driver): def test_tag_widget_add_existing_raises(library, qt_driver, entry_full): # Given - tag_field = [ - f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name - ][0] + tag_field = [f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name][0] assert len(entry_full.tags) == 1 tag = next(iter(entry_full.tags)) @@ -69,9 +66,7 @@ def test_tag_widget_remove(qtbot, qt_driver, library, entry_full): assert tag assert entry_full.tag_box_fields - tag_field = [ - f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name - ][0] + tag_field = [f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name][0] tag_widget = TagBoxWidget(tag_field, "title", qt_driver) tag_widget.driver.selected = [0] @@ -93,9 +88,7 @@ def test_tag_widget_edit(qtbot, qt_driver, library, entry_full): assert tag assert entry_full.tag_box_fields - tag_field = [ - f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name - ][0] + tag_field = [f for f in entry_full.tag_box_fields if f.type_key == _FieldID.TAGS.name][0] tag_box_widget = TagBoxWidget(tag_field, "title", qt_driver) tag_box_widget.driver.selected = [0] diff --git a/tagstudio/tests/test_filter_state.py b/tagstudio/tests/test_filter_state.py index 4add369b..f97f5f32 100644 --- a/tagstudio/tests/test_filter_state.py +++ b/tagstudio/tests/test_filter_state.py @@ -1,5 +1,4 @@ import pytest - from src.core.library.alchemy.enums import FilterState diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index 584eec5d..b61bef81 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -2,12 +2,10 @@ from pathlib import Path, PureWindowsPath from tempfile import TemporaryDirectory import pytest - from src.core.constants import LibraryPrefs -from src.core.library.alchemy import Entry -from src.core.library.alchemy import Library +from src.core.library.alchemy import Entry, Library from src.core.library.alchemy.enums import FilterState -from src.core.library.alchemy.fields import _FieldID, TextField +from src.core.library.alchemy.fields import TextField, _FieldID def test_library_bootstrap(): @@ -100,10 +98,7 @@ def test_get_entry(library, entry_min): def test_entries_count(library): - entries = [ - Entry(path=Path(f"{x}.txt"), folder=library.folder, fields=[]) - for x in range(10) - ] + entries = [Entry(path=Path(f"{x}.txt"), folder=library.folder, fields=[]) for x in range(10)] library.add_entries(entries) results = library.search_library( FilterState( @@ -379,12 +374,8 @@ def test_update_field_order(library, entry_full): title_field = entry_full.text_fields[0] # When add two more fields - library.add_entry_field_type( - entry_full.id, field_id=title_field.type_key, value="first" - ) - library.add_entry_field_type( - entry_full.id, field_id=title_field.type_key, value="second" - ) + library.add_entry_field_type(entry_full.id, field_id=title_field.type_key, value="first") + library.add_entry_field_type(entry_full.id, field_id=title_field.type_key, value="second") # remove the one on first position assert title_field.position == 0