diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 2840d69b..1669844c 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -15,7 +15,6 @@ import re import sys import time import webbrowser -from collections.abc import Sequence from pathlib import Path from queue import Queue @@ -52,6 +51,8 @@ from PySide6.QtWidgets import ( QWidget, ) from src.core.constants import ( + TAG_ARCHIVED, + TAG_FAVORITE, VERSION, VERSION_BRANCH, ) @@ -89,6 +90,12 @@ from src.qt.widgets.preview_panel import PreviewPanel from src.qt.widgets.progress import ProgressWidget from src.qt.widgets.thumb_renderer import ThumbRenderer +BADGE_TAGS = { + BadgeType.FAVORITE: TAG_FAVORITE, + BadgeType.ARCHIVED: TAG_ARCHIVED, +} + + # SIGQUIT is not defined on Windows if sys.platform == "win32": from signal import SIGINT, SIGTERM, signal @@ -485,6 +492,17 @@ class QtDriver(DriverMixin, QObject): self.main_window.searchField.textChanged.connect(self.update_completions_list) self.preview_panel = PreviewPanel(self.lib, self) + self.preview_panel.fields.archived_updated.connect( + lambda hidden: self.update_badges( + {BadgeType.ARCHIVED: hidden}, origin_id=0, add_tags=False + ) + ) + self.preview_panel.fields.favorite_updated.connect( + lambda hidden: self.update_badges( + {BadgeType.FAVORITE: hidden}, origin_id=0, add_tags=False + ) + ) + splitter = self.main_window.splitter splitter.addWidget(self.preview_panel) @@ -919,6 +937,8 @@ class QtDriver(DriverMixin, QObject): page_index = max(0, min(page_index, self.pages_count - 1)) self.filter.page_index = page_index + # TODO: Re-allow selecting entries across multiple pages at once. + # This works fine with additive selection but becomes a nightmare with bridging. self.filter_items() def remove_grid_item(self, grid_idx: int): @@ -957,6 +977,7 @@ class QtDriver(DriverMixin, QObject): def select_item(self, item_id: int, append: bool, bridge: bool): """Select one or more items in the Thumbnail Grid.""" logger.info("[QtDriver] Selecting Items:", item_id=item_id, append=append, bridge=bridge) + if append: if item_id not in self.selected: self.selected.append(item_id) @@ -969,40 +990,45 @@ class QtDriver(DriverMixin, QObject): if it.item_id == item_id: it.thumb_button.set_selected(False) + # TODO: Allow bridge selecting across pages. elif bridge and self.selected: - contents = self.frame_content - last_index = self.frame_content.index(self.selected[-1]) - current_index = self.frame_content.index(item_id) - index_range: list = contents[ - min(last_index, current_index) : max(last_index, current_index) + 1 - ] - # Preserve bridge direction for correct appending order. - if last_index < current_index: - index_range.reverse() - for entry_id in index_range: - for it in self.item_thumbs: - if it.item_id == entry_id: - it.thumb_button.set_selected(True) - if entry_id not in self.selected: - self.selected.append(entry_id) + last_index = -1 + current_index = -1 + try: + contents = self.frame_content + last_index = self.frame_content.index(self.selected[-1]) + current_index = self.frame_content.index(item_id) + index_range: list = contents[ + min(last_index, current_index) : max(last_index, current_index) + 1 + ] + + # Preserve bridge direction for correct appending order. + if last_index < current_index: + index_range.reverse() + for entry_id in index_range: + for it in self.item_thumbs: + if it.item_id == entry_id: + it.thumb_button.set_selected(True) + if entry_id not in self.selected: + self.selected.append(entry_id) + except Exception as e: + # TODO: Allow bridge selecting across pages. + logger.error( + "[QtDriver] Previous selected item not on current page!", + error=e, + item_id=item_id, + current_index=current_index, + last_index=last_index, + ) else: self.selected.clear() self.selected.append(item_id) - for it in self.item_thumbs: - if it.item_id == item_id: - it.thumb_button.set_selected(True) - else: - it.thumb_button.set_selected(False) - - # NOTE: By using the preview panel's "set_tags_updated_slot" method, - # only the last of multiple identical item selections are connected. - # If attaching the slot to multiple duplicate selections is needed, - # just bypass the method and manually disconnect and connect the slots. - if len(self.selected) == 1: - for it in self.item_thumbs: - if it.item_id == item_id: - self.preview_panel.fields.set_tags_updated_slot(it.refresh_badge) + for it in self.item_thumbs: + if it.item_id in self.selected: + it.thumb_button.set_selected(True) + else: + it.thumb_button.set_selected(False) self.set_macro_menu_viability() self.preview_panel.update_widgets() @@ -1113,10 +1139,9 @@ class QtDriver(DriverMixin, QObject): continue if not entry: continue + item_thumb.set_mode(ItemType.ENTRY) item_thumb.set_item_id(entry.id) - - # TODO - show after item is rendered item_thumb.show() is_loading = True self.thumb_job_queue.put( @@ -1162,18 +1187,28 @@ class QtDriver(DriverMixin, QObject): ) # Restore Selected Borders - is_selected = (item_thumb.mode, item_thumb.item_id) in self.selected + is_selected = item_thumb.item_id in self.selected item_thumb.thumb_button.set_selected(is_selected) - def update_badges(self, item_ids: Sequence[int] = None): - if not item_ids: - # no items passed, update all items in grid - item_ids = range(min(len(self.item_thumbs), len(self.frame_content))) + def update_badges(self, badge_values: dict[BadgeType, bool], origin_id: int, add_tags=True): + """Update the tag badges for item_thumbs. + + Args: + badge_values(dict[BadgeType, bool]): The BadgeType and associated viability state. + origin_id(int): The ID of the item_thumb calling this method. If the ID is found as a + part of the current selection, or if the ID is 0, the the entire current selection + will be updated. Otherwise, only item_thumbs with that ID will be updated. + add_tags(bool): Flag determining if tags associated with the badges need to be added to + the items. Defaults to True. + """ + item_ids = self.selected if (not origin_id or origin_id in self.selected) else [origin_id] - item_ids_ = set(item_ids) for it in self.item_thumbs: - if it.item_id in item_ids_: - it.refresh_badge() + if it.item_id in item_ids: + for badge_type, value in badge_values.items(): + if add_tags: + it.toggle_item_tag(it.item_id, value, BADGE_TAGS[badge_type]) + it.assign_badge(badge_type, value) def filter_items(self, filter: FilterState | None = None) -> None: if not self.lib.library_dir: @@ -1189,13 +1224,9 @@ class QtDriver(DriverMixin, QObject): self.main_window.statusbar.repaint() # search the library - start_time = time.time() - results = self.lib.search_library(self.filter) - logger.info("items to render", count=len(results)) - end_time = time.time() # inform user about completed search diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index 5594eb5e..a3bdb293 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -7,6 +7,7 @@ from enum import Enum from functools import wraps from pathlib import Path from typing import TYPE_CHECKING +from warnings import catch_warnings import structlog from PIL import Image, ImageQt @@ -437,18 +438,10 @@ class ItemThumb(FlowWidget): def update_clickable(self, clickable: typing.Callable): """Updates attributes of a thumbnail element.""" - if self.thumb_button.is_connected: - self.thumb_button.pressed.disconnect() if clickable: + with catch_warnings(record=True): + self.thumb_button.pressed.disconnect() self.thumb_button.pressed.connect(clickable) - self.thumb_button.is_connected = True - - def refresh_badge(self, entry_id: int | None = None): - entry = self.lib.get_entry_full(self.item_id) - if not entry: - return - self.assign_badge(BadgeType.ARCHIVED, entry.is_archived) - self.assign_badge(BadgeType.FAVORITE, entry.is_favorite) def set_item_id(self, item_id: int): self.item_id = item_id @@ -489,20 +482,9 @@ class ItemThumb(FlowWidget): return toggle_value = self.badges[badge_type].isChecked() - self.badge_active[badge_type] = toggle_value - tag_id = BADGE_TAGS[badge_type] - - # check if current item is selected. if so, update all selected items - if self.item_id in self.driver.selected: - items_to_update = self.driver.selected - else: - items_to_update = [self.item_id] - - for item_id in items_to_update: - self.toggle_item_tag(item_id, toggle_value, tag_id) - - self.driver.update_badges(items_to_update) + badge_values: dict[BadgeType, bool] = {badge_type: toggle_value} + self.driver.update_badges(badge_values, self.item_id) def toggle_item_tag( self, diff --git a/tagstudio/src/qt/widgets/preview/field_containers.py b/tagstudio/src/qt/widgets/preview/field_containers.py index e2108b1d..bb8b24ed 100644 --- a/tagstudio/src/qt/widgets/preview/field_containers.py +++ b/tagstudio/src/qt/widgets/preview/field_containers.py @@ -20,6 +20,10 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) +from src.core.constants import ( + TAG_ARCHIVED, + TAG_FAVORITE, +) from src.core.enums import Theme from src.core.library.alchemy.fields import ( BaseField, @@ -46,7 +50,8 @@ logger = structlog.get_logger(__name__) class FieldContainers(QWidget): """The Preview Panel Widget.""" - tags_updated = Signal() + favorite_updated = Signal(bool) + archived_updated = Signal(bool) def __init__(self, library: Library, driver: "QtDriver"): super().__init__() @@ -104,16 +109,11 @@ class FieldContainers(QWidget): root_layout.setContentsMargins(0, 0, 0, 0) root_layout.addWidget(self.scroll_area) - def update_from_entry(self, entry: Entry): + def update_from_entry(self, entry_id: int, update_badges: bool = True): """Update tags and fields from a single Entry source.""" - logger.info( - "[FieldContainers] Updating Selection", - path=entry.path, - fields=entry.fields, - tags=entry.tags, - ) + logger.warning("[FieldContainers] Updating Selection", entry_id=entry_id) - self.cached_entries = [self.lib.get_entry_full(entry.id)] + self.cached_entries = [self.lib.get_entry_full(entry_id)] entry_ = self.cached_entries[0] container_len: int = len(entry_.fields) container_index = 0 @@ -127,7 +127,9 @@ class FieldContainers(QWidget): ) container_index += 1 container_len += 1 - self.tags_updated.emit() + if update_badges: + self.emit_badge_signals({t.id for t in entry_.tags}) + # Write field container(s) for index, field in enumerate(entry_.fields, start=container_index): self.write_container(index, field, is_mixed=False) @@ -224,7 +226,7 @@ class FieldContainers(QWidget): for key in empty: cats.pop(key, None) - logger.info("[FieldContainers] Tag Categories", cats=cats) + logger.info("[FieldContainers] Tag Categories", categories=cats) return cats def remove_field_prompt(self, name: str) -> str: @@ -247,11 +249,13 @@ class FieldContainers(QWidget): field_id=field_item.data(Qt.ItemDataRole.UserRole), ) - def add_tags_to_selected(self, tags: list[int]): + def add_tags_to_selected(self, tags: int | list[int]): """Add list of tags to one or more selected items. Uses the current driver selection, NOT the field containers cache. """ + if isinstance(tags, int): + tags = [tags] logger.info( "[FieldContainers][add_tags_to_selected]", selected=self.driver.selected, @@ -262,7 +266,7 @@ class FieldContainers(QWidget): entry_id, tag_ids=tags, ) - self.tags_updated.emit() + self.emit_badge_signals(tags, emit_on_absent=False) def write_container(self, index: int, field: BaseField, is_mixed: bool = False): """Update/Create data for a FieldContainer. @@ -304,7 +308,7 @@ class FieldContainers(QWidget): save_callback=( lambda content: ( self.update_field(field, content), - self.update_from_entry(self.cached_entries[0]), + self.update_from_entry(self.cached_entries[0].id), ) ), ) @@ -318,7 +322,7 @@ class FieldContainers(QWidget): prompt=self.remove_field_prompt(field.type.type.value), callback=lambda: ( self.remove_field(field), - self.update_from_entry(self.cached_entries[0]), + self.update_from_entry(self.cached_entries[0].id), ), ) ) @@ -343,7 +347,7 @@ class FieldContainers(QWidget): save_callback=( lambda content: ( self.update_field(field, content), - self.update_from_entry(self.cached_entries[0]), + self.update_from_entry(self.cached_entries[0].id), ) ), ) @@ -353,7 +357,7 @@ class FieldContainers(QWidget): prompt=self.remove_field_prompt(field.type.name), callback=lambda: ( self.remove_field(field), - self.update_from_entry(self.cached_entries[0]), + self.update_from_entry(self.cached_entries[0].id), ), ) ) @@ -381,7 +385,7 @@ class FieldContainers(QWidget): prompt=self.remove_field_prompt(field.type.name), callback=lambda: ( self.remove_field(field), - self.update_from_entry(self.cached_entries[0]), + self.update_from_entry(self.cached_entries[0].id), ), ) ) @@ -402,7 +406,7 @@ class FieldContainers(QWidget): prompt=self.remove_field_prompt(field.type.name), callback=lambda: ( self.remove_field(field), - self.update_from_entry(self.cached_entries[0]), + self.update_from_entry(self.cached_entries[0].id), ), ) ) @@ -449,7 +453,9 @@ class FieldContainers(QWidget): ) container.set_inner_widget(inner_widget) - inner_widget.updated.connect(lambda: (self.update_from_entry(self.cached_entries[0]))) + inner_widget.updated.connect( + lambda: (self.update_from_entry(self.cached_entries[0].id, update_badges=True)) + ) else: text = "Mixed Data" inner_widget = TextWidget("Mixed Tags", text) @@ -500,10 +506,15 @@ class FieldContainers(QWidget): if result == 3: # TODO - what is this magic number? callback() - def set_tags_updated_slot(self, slot: object): - """Replacement for tag_callback.""" - with catch_warnings(record=True): - self.tags_updated.disconnect() + def emit_badge_signals(self, tag_ids: list[int] | set[int], emit_on_absent: bool = True): + """Emit any connected signals for updating badge icons.""" + logger.info("[emit_badge_signals] Emitting", tag_ids=tag_ids, emit_on_absent=emit_on_absent) + if TAG_ARCHIVED in tag_ids: + self.archived_updated.emit(True) # noqa: FBT003 + elif emit_on_absent: + self.archived_updated.emit(False) # noqa: FBT003 - logger.info("[FieldContainers][set_tags_updated_slot] Setting tags updated slot") - self.tags_updated.connect(slot) + if TAG_FAVORITE in tag_ids: + self.favorite_updated.emit(True) # noqa: FBT003 + elif emit_on_absent: + self.favorite_updated.emit(False) # noqa: FBT003 diff --git a/tagstudio/src/qt/widgets/preview/file_attributes.py b/tagstudio/src/qt/widgets/preview/file_attributes.py index af493bd5..fa1a6c7e 100644 --- a/tagstudio/src/qt/widgets/preview/file_attributes.py +++ b/tagstudio/src/qt/widgets/preview/file_attributes.py @@ -12,7 +12,7 @@ from pathlib import Path import structlog from humanfriendly import format_size from PIL import ImageFont -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt from PySide6.QtGui import QGuiApplication from PySide6.QtWidgets import ( QLabel, @@ -33,8 +33,6 @@ logger = structlog.get_logger(__name__) class FileAttributes(QWidget): """The Preview Panel Widget.""" - tags_updated = Signal() - def __init__(self, library: Library, driver: "QtDriver"): super().__init__() root_layout = QVBoxLayout(self) diff --git a/tagstudio/src/qt/widgets/preview/preview_thumb.py b/tagstudio/src/qt/widgets/preview/preview_thumb.py index baff35ed..d73f81f3 100644 --- a/tagstudio/src/qt/widgets/preview/preview_thumb.py +++ b/tagstudio/src/qt/widgets/preview/preview_thumb.py @@ -11,7 +11,7 @@ import cv2 import rawpy import structlog from PIL import Image, UnidentifiedImageError -from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt, Signal +from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt from PySide6.QtGui import QAction, QMovie, QResizeEvent from PySide6.QtWidgets import ( QHBoxLayout, @@ -39,8 +39,6 @@ logger = structlog.get_logger(__name__) class PreviewThumb(QWidget): """The Preview Panel Widget.""" - tags_updated = Signal() - def __init__(self, library: Library, driver: "QtDriver"): super().__init__() diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 00ba259e..62cf0bd2 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -8,7 +8,7 @@ from pathlib import Path from warnings import catch_warnings import structlog -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QHBoxLayout, QPushButton, @@ -37,8 +37,6 @@ logger = structlog.get_logger(__name__) class PreviewPanel(QWidget): """The Preview Panel Widget.""" - tags_updated = Signal() - # TODO: There should be a global button theme somewhere. button_style = ( f"QPushButton{{" @@ -149,15 +147,16 @@ class PreviewPanel(QWidget): # One Item Selected elif len(self.driver.selected) == 1: entry: Entry = self.lib.get_entry(self.driver.selected[0]) + entry_id = self.driver.selected[0] filepath: Path = self.lib.library_dir / entry.path ext: str = filepath.suffix.lower() stats: dict = self.thumb.update_preview(filepath, ext) self.file_attrs.update_stats(filepath, ext, stats) self.file_attrs.update_date_label(filepath) - self.fields.update_from_entry(entry) - self.update_add_tag_button(entry) - self.update_add_field_button(entry) + self.fields.update_from_entry(entry_id) + self.update_add_tag_button(entry_id) + self.update_add_field_button(entry_id) self.add_tag_button.setEnabled(True) self.add_field_button.setEnabled(True) @@ -181,7 +180,7 @@ class PreviewPanel(QWidget): traceback.print_exc() return False - def update_add_field_button(self, entry: Entry | None = None): + def update_add_field_button(self, entry_id: int | None = None): with catch_warnings(record=True): self.add_field_modal.done.disconnect() self.add_field_button.clicked.disconnect() @@ -189,12 +188,12 @@ class PreviewPanel(QWidget): self.add_field_modal.done.connect( lambda f: ( self.fields.add_field_to_selected(f), - (self.fields.update_from_entry(entry) if entry else ()), + (self.fields.update_from_entry(entry_id) if entry_id else ()), ) ) self.add_field_button.clicked.connect(self.add_field_modal.show) - def update_add_tag_button(self, entry: Entry = None): + def update_add_tag_button(self, entry_id: int = None): with catch_warnings(record=True): self.add_tag_modal.widget.tag_chosen.disconnect() self.add_tag_button.clicked.disconnect() @@ -202,7 +201,7 @@ class PreviewPanel(QWidget): self.add_tag_modal.widget.tag_chosen.connect( lambda t: ( self.fields.add_tags_to_selected(t), - (self.fields.update_from_entry(entry) if entry else ()), + (self.fields.update_from_entry(entry_id) if entry_id else ()), ) ) diff --git a/tagstudio/src/qt/widgets/tag_box.py b/tagstudio/src/qt/widgets/tag_box.py index 7f576c60..82d664ec 100644 --- a/tagstudio/src/qt/widgets/tag_box.py +++ b/tagstudio/src/qt/widgets/tag_box.py @@ -7,7 +7,6 @@ import typing import structlog from PySide6.QtCore import Signal -from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE from src.core.library import Tag from src.core.library.alchemy.enums import FilterState from src.qt.flowlayout import FlowLayout @@ -98,9 +97,6 @@ class TagBoxWidget(FieldWidget): selected=self.driver.selected, ) - if tag_id in (TAG_FAVORITE, TAG_ARCHIVED): - self.driver.update_badges(self.driver.selected) - for entry_id in self.driver.selected: self.driver.lib.remove_tags_from_entry(entry_id, tag_id)