ui: optimize selection and badge updates

This commit is contained in:
Travis Abendshien
2025-01-08 10:14:38 -08:00
parent 5207f28e2b
commit c8bbfe5cb4
7 changed files with 128 additions and 113 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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 = "<i>Mixed Data</i>"
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

View File

@@ -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)

View File

@@ -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__()

View File

@@ -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 ()),
)
)

View File

@@ -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)