From 3123dffd3775b0a8c408fd1e44212899e72d2122 Mon Sep 17 00:00:00 2001
From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Date: Tue, 31 Dec 2024 06:57:43 -0800
Subject: [PATCH] ui: show fields in preview panel
known issues:
- fields to not visually update after being edited until the entries are reloaded from the thumbnail grid (yes, the thumbnail grid)
- add field button currently non-functional
- surprise segfaults
---
tagstudio/src/qt/widgets/item_thumb.py | 1 +
.../qt/widgets/preview/field_containers.py | 1457 +++++++++--------
.../qt/widgets/preview/recent_libraries.py | 68 +-
tagstudio/src/qt/widgets/preview_panel.py | 23 +-
4 files changed, 815 insertions(+), 734 deletions(-)
diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py
index 1a74304b..31144349 100644
--- a/tagstudio/src/qt/widgets/item_thumb.py
+++ b/tagstudio/src/qt/widgets/item_thumb.py
@@ -524,6 +524,7 @@ class ItemThumb(FlowWidget):
if toggle_value:
self.lib.add_tags_to_entry(entry_id, tag_id)
else:
+ # TODO: Implement
self.lib.remove_tag_from_entry(entry_id, tag_id)
if self.driver.preview_panel.is_open:
diff --git a/tagstudio/src/qt/widgets/preview/field_containers.py b/tagstudio/src/qt/widgets/preview/field_containers.py
index c05950f7..5415f42a 100644
--- a/tagstudio/src/qt/widgets/preview/field_containers.py
+++ b/tagstudio/src/qt/widgets/preview/field_containers.py
@@ -2,15 +2,40 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
+import sys
import typing
+from collections.abc import Callable
+from datetime import datetime as dt
import structlog
-from PySide6.QtCore import Signal
+from PySide6.QtCore import Qt, Signal
+from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import (
+ QFrame,
+ QHBoxLayout,
+ QMessageBox,
+ QScrollArea,
+ QSizePolicy,
+ QVBoxLayout,
QWidget,
)
+from src.core.enums import Theme
+from src.core.library.alchemy.fields import (
+ BaseField,
+ DatetimeField,
+ FieldTypeEnum,
+ TextField,
+)
from src.core.library.alchemy.library import Library
from src.qt.translations import Translations
+from src.core.library.alchemy.models import Entry
+from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
+from src.qt.modals.add_field import AddFieldModal
+from src.qt.widgets.fields import FieldContainer
+from src.qt.widgets.panel import PanelModal
+from src.qt.widgets.text import TextWidget
+from src.qt.widgets.text_box_edit import EditTextBox
+from src.qt.widgets.text_line_edit import EditTextLine
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
@@ -26,695 +51,741 @@ class FieldContainers(QWidget):
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__()
-
-# self.is_connected = False
-# self.lib = library
-# self.driver: QtDriver = driver
-# self.initialized = False
-# self.is_open: bool = False
-# self.common_fields: list = []
-# self.mixed_fields: list = []
-# self.selected: list[int] = [] # New way of tracking items
-# self.containers: list[FieldContainer] = []
-
-# self.panel_bg_color = (
-# Theme.COLOR_BG_DARK.value
-# if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
-# else Theme.COLOR_BG_LIGHT.value
-# )
-
-# self.scroll_layout = QVBoxLayout()
-# self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
-# self.scroll_layout.setContentsMargins(6, 1, 6, 6)
-
-# scroll_container: QWidget = QWidget()
-# scroll_container.setObjectName("entryScrollContainer")
-# scroll_container.setLayout(self.scroll_layout)
-
-# info_section = QWidget()
-# info_layout = QVBoxLayout(info_section)
-# info_layout.setContentsMargins(0, 0, 0, 0)
-# info_layout.setSpacing(6)
-
-# self.scroll_area = QScrollArea()
-# self.scroll_area.setObjectName("entryScrollArea")
-# self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
-# self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
-# self.scroll_area.setWidgetResizable(True)
-# self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
-# self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
-# # NOTE: I would rather have this style applied to the scroll_area
-# # background and NOT the scroll container background, so that the
-# # rounded corners are maintained when scrolling. I was unable to
-# # find the right trick to only select that particular element.
-
-# self.scroll_area.setStyleSheet(
-# "QWidget#entryScrollContainer{"
-# f"background:{self.panel_bg_color};"
-# "border-radius:6px;"
-# "}"
-# )
-# self.scroll_area.setWidget(scroll_container)
-
-# self.afb_container = QWidget()
-# self.afb_layout = QVBoxLayout(self.afb_container)
-# self.afb_layout.setContentsMargins(0, 12, 0, 0)
-
-# self.add_field_button = QPushButtonWrapper()
-# self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
-# self.add_field_button.setMinimumSize(96, 28)
-# self.add_field_button.setMaximumSize(96, 28)
-# Translations.translate_qobject(self.add_field_button, "library.field.add")
-# self.afb_layout.addWidget(self.add_field_button)
-# self.add_field_modal = AddFieldModal(self.lib)
-# self.place_add_field_button()
-
-# def remove_field_prompt(self, name: str) -> str:
-# return Translations.translate_formatted("library.field.confirm_remove", name=name)
-
-# def place_add_field_button(self):
-# self.scroll_layout.addWidget(self.afb_container)
-# 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.add_field_modal.done.connect(
-# lambda f: (self.add_field_to_selected(f), self.update_widgets())
-# )
-# self.add_field_modal.is_connected = True
-# self.add_field_button.clicked.connect(self.add_field_modal.show)
-
-# def add_field_to_selected(self, field_list: list):
-# """Add list of entry fields to one or more selected items."""
-# logger.info("add_field_to_selected", selected=self.selected, fields=field_list)
-# for grid_idx in self.selected:
-# entry = self.driver.frame_content[grid_idx]
-# for field_item in field_list:
-# self.lib.add_entry_field_type(
-# entry.id,
-# field_id=field_item.data(Qt.ItemDataRole.UserRole),
-# )
-
-# def write_container(self, index: int, field: BaseField, is_mixed: bool = False):
-# """Update/Create data for a FieldContainer.
-
-# 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()
-# if len(self.containers) < (index + 1):
-# container = FieldContainer()
-# self.containers.append(container)
-# self.scroll_layout.addWidget(container)
-# else:
-# container = self.containers[index]
-
-# # if isinstance(field, TagBoxField):
-# # container.set_title(field.type.name)
-# # container.set_inline(False)
-# # title = f"{field.type.name} (Tag Box)"
-# #
-# # if not is_mixed:
-# # inner_container = container.get_inner_widget()
-# # if isinstance(inner_container, TagBoxWidget):
-# # inner_container.set_field(field)
-# # inner_container.set_tags(list(field.tags))
-# #
-# # try:
-# # inner_container.updated.disconnect()
-# # except RuntimeError:
-# # logger.error("Failed to disconnect inner_container.updated")
-# #
-# # else:
-# # inner_container = TagBoxWidget(
-# # field,
-# # title,
-# # self.driver,
-# # )
-# #
-# # container.set_inner_widget(inner_container)
-# #
-# # inner_container.updated.connect(
-# # lambda: (
-# # self.write_container(index, field),
-# # self.update_widgets(),
-# # )
-# # )
-# # # NOTE: Tag Boxes have no Edit Button
-# # (But will when you can convert field types)
-# # container.set_remove_callback(
-# # lambda: self.remove_message_box(
-# # prompt=self.remove_field_prompt(field.type.name),
-# # callback=lambda: (
-# # self.remove_field(field),
-# # self.update_selected_entry(self.driver),
-# # # reload entry and its fields
-# # self.update_widgets(),
-# # ),
-# # )
-# # )
-# # else:
-# # text = "Mixed Data"
-# # title = f"{field.type.name} (Wacky Tag Box)"
-# # inner_container = TextWidget(title, text)
-# # container.set_inner_widget(inner_container)
-# #
-# # self.tags_updated.emit()
-# # # self.dynamic_widgets.append(inner_container)
-# # elif field.type.type == FieldTypeEnum.TEXT_LINE:
-# if field.type.type == FieldTypeEnum.TEXT_LINE:
-# container.set_title(field.type.name)
-# container.set_inline(False)
-
-# # Normalize line endings in any text content.
-# if not is_mixed:
-# assert isinstance(field.value, (str, type(None)))
-# text = field.value or ""
-# else:
-# text = "Mixed Data"
-
-# title = f"{field.type.name} ({field.type.type.value})"
-# inner_container = TextWidget(title, text)
-# container.set_inner_widget(inner_container)
-# if not is_mixed:
-# modal = PanelModal(
-# EditTextLine(field.value),
-# title=title,
-# window_title=f"Edit {field.type.type.value}",
-# save_callback=(
-# lambda content: (
-# self.update_field(field, content),
-# self.update_widgets(),
-# )
-# ),
-# )
-# if "pytest" in sys.modules:
-# # for better testability
-# container.modal = modal # type: ignore
-
-# container.set_edit_callback(modal.show)
-# container.set_remove_callback(
-# lambda: self.remove_message_box(
-# prompt=self.remove_field_prompt(field.type.type.value),
-# callback=lambda: (
-# self.remove_field(field),
-# self.update_widgets(),
-# ),
-# )
-# )
-
-# elif field.type.type == FieldTypeEnum.TEXT_BOX:
-# container.set_title(field.type.name)
-# # container.set_editable(True)
-# container.set_inline(False)
-# # Normalize line endings in any text content.
-# if not is_mixed:
-# assert isinstance(field.value, (str, type(None)))
-# text = (field.value or "").replace("\r", "\n")
-# else:
-# text = "Mixed Data"
-# title = f"{field.type.name} (Text Box)"
-# inner_container = TextWidget(title, text)
-# container.set_inner_widget(inner_container)
-# if not is_mixed:
-# modal = PanelModal(
-# EditTextBox(field.value),
-# title=title,
-# window_title=f"Edit {field.type.name}",
-# save_callback=(
-# lambda content: (
-# self.update_field(field, content),
-# self.update_widgets(),
-# )
-# ),
-# )
-# container.set_edit_callback(modal.show)
-# container.set_remove_callback(
-# lambda: self.remove_message_box(
-# prompt=self.remove_field_prompt(field.type.name),
-# callback=lambda: (
-# self.remove_field(field),
-# self.update_widgets(),
-# ),
-# )
-# )
-
-# elif field.type.type == FieldTypeEnum.DATETIME:
-# if not is_mixed:
-# try:
-# container.set_title(field.type.name)
-# # container.set_editable(False)
-# container.set_inline(False)
-# # TODO: Localize this and/or add preferences.
-# date = dt.strptime(field.value, "%Y-%m-%d %H:%M:%S")
-# title = f"{field.type.name} (Date)"
-# inner_container = TextWidget(title, date.strftime("%D - %r"))
-# container.set_inner_widget(inner_container)
-# except Exception:
-# container.set_title(field.type.name)
-# # container.set_editable(False)
-# container.set_inline(False)
-# title = f"{field.type.name} (Date) (Unknown Format)"
-# inner_container = TextWidget(title, str(field.value))
-# container.set_inner_widget(inner_container)
-
-# container.set_remove_callback(
-# lambda: self.remove_message_box(
-# prompt=self.remove_field_prompt(field.type.name),
-# callback=lambda: (
-# self.remove_field(field),
-# self.update_widgets(),
-# ),
-# )
-# )
-# else:
-# text = "Mixed Data"
-# title = f"{field.type.name} (Wacky Date)"
-# inner_container = TextWidget(title, text)
-# container.set_inner_widget(inner_container)
-# else:
-# logger.warning("write_container - unknown field", field=field)
-# container.set_title(field.type.name)
-# 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)
-# container.set_remove_callback(
-# lambda: self.remove_message_box(
-# prompt=self.remove_field_prompt(field.type.name),
-# callback=lambda: (
-# self.remove_field(field),
-# self.update_widgets(),
-# ),
-# )
-# )
-
-# container.edit_button.setHidden(True)
-# container.setHidden(False)
-# self.place_add_field_button()
-
-# def remove_field(self, field: BaseField):
-# """Remove a field from all selected Entries."""
-# logger.info("removing field", field=field, selected=self.selected)
-# entry_ids = []
-
-# for grid_idx in self.selected:
-# entry = self.driver.frame_content[grid_idx]
-# entry_ids.append(entry.id)
-
-# self.lib.remove_entry_field(field, entry_ids)
-
-# # # if the field is meta tags, update the badges
-# # if field.type_key == _FieldID.TAGS_META.value:
-# # self.driver.update_badges(self.selected)
-
-# def update_field(self, field: BaseField, content: str) -> None:
-# """Update a field in all selected Entries, given a field object."""
-# assert isinstance(
-# field,
-# (TextField, DatetimeField), # , TagBoxField)
-# ), f"instance: {type(field)}"
-
-# entry_ids = []
-# for grid_idx in self.selected:
-# entry = self.driver.frame_content[grid_idx]
-# entry_ids.append(entry.id)
-
-# assert entry_ids, "No entries selected"
-# self.lib.update_entry_field(
-# entry_ids,
-# field,
-# content,
-# )
-
-# def remove_message_box(self, prompt: str, callback: Callable) -> None:
-# remove_mb = QMessageBox()
-# remove_mb.setText(prompt)
-# remove_mb.setWindowTitle("Remove Field")
-# remove_mb.setIcon(QMessageBox.Icon.Warning)
-# cancel_button = remove_mb.addButton(
-# Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole
-# )
-# remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole)
-# # remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel)
-# remove_mb.setDefaultButton(cancel_button)
-# remove_mb.setEscapeButton(cancel_button)
-# result = remove_mb.exec_()
-# # logging.info(result)
-# if result == 3: # TODO - what is this magic number?
-# callback()
-
-# def set_tags_updated_slot(self, slot: object):
-# """Replacement for tag_callback."""
-# if self.is_connected:
-# self.tags_updated.disconnect()
-
-# logger.info("[UPDATE CONTAINER] Setting tags updated slot")
-# self.tags_updated.connect(slot)
-# self.is_connected = True
-
-# def update_widgets(self):
-# """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
-# window_title = ""
-
-# # # update list of libraries
-# # self.fill_libs_widget(self.libs_layout)
-
-# if not self.driver.selected:
-# if self.selected or not self.initialized:
-# self.file_label.setText("No Items Selected")
-# self.file_label.set_file_path("")
-# self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
-
-# self.dimensions_label.setText("")
-# self.update_date_label()
-# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
-# self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
-
-# ratio = self.devicePixelRatio()
-# self.thumb_renderer.render(
-# time.time(),
-# "",
-# (512, 512),
-# ratio,
-# is_loading=True,
-# update_on_ratio_change=True,
-# )
-# if self.preview_img.is_connected:
-# self.preview_img.clicked.disconnect()
-# for c in self.containers:
-# c.setHidden(True)
-# self.preview_img.show()
-# self.preview_vid.stop()
-# self.preview_vid.hide()
-# self.media_player.hide()
-# self.media_player.stop()
-# self.preview_gif.hide()
-# self.selected = list(self.driver.selected)
-# self.add_field_button.setHidden(True)
-
-# # common code
-# self.initialized = True
-# self.setWindowTitle(window_title)
-# self.show()
-# return True
-
-# # reload entry and fill it into the grid again
-# # TODO - do this more granular
-# # TODO - Entry reload is maybe not necessary
-# for grid_idx in self.driver.selected:
-# entry = self.driver.frame_content[grid_idx]
-# results = self.lib.search_library(FilterState(id=entry.id))
-# logger.info(
-# "found item",
-# entries=len(results.items),
-# grid_idx=grid_idx,
-# lookup_id=entry.id,
-# )
-# self.driver.frame_content[grid_idx] = results[0]
-
-# if len(self.driver.selected) == 1:
-# # 1 Selected Entry
-# selected_idx = self.driver.selected[0]
-# item = self.driver.frame_content[selected_idx]
-
-# self.preview_img.show()
-# self.preview_vid.stop()
-# self.preview_vid.hide()
-# self.media_player.stop()
-# self.media_player.hide()
-# self.preview_gif.hide()
-
-# # 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.set_file_path(filepath)
-# ratio = self.devicePixelRatio()
-# self.thumb_renderer.render(
-# time.time(),
-# filepath,
-# (512, 512),
-# ratio,
-# update_on_ratio_change=True,
-# )
-# file_str: str = ""
-# separator: str = f"{os.path.sep}" # Gray
-# for i, part in enumerate(filepath.parts):
-# part_ = part.strip(os.path.sep)
-# if i != len(filepath.parts) - 1:
-# file_str += f"{"\u200b".join(part_)}{separator}"
-# else:
-# file_str += f"
{"\u200b".join(part_)}"
-# self.file_label.setText(file_str)
-# self.file_label.setCursor(Qt.CursorShape.PointingHandCursor)
-
-# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
-# self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor)
-
-# self.opener = FileOpenerHelper(filepath)
-# self.open_file_action.triggered.connect(self.opener.open_file)
-# self.open_explorer_action.triggered.connect(self.opener.open_explorer)
-
-# # TODO: Do this all somewhere else, this is just here temporarily.
-# ext: str = filepath.suffix.lower()
-# try:
-# if MediaCategories.is_ext_in_category(
-# ext, MediaCategories.IMAGE_ANIMATED_TYPES, mime_fallback=True
-# ):
-# if self.preview_gif.movie():
-# self.preview_gif.movie().stop()
-# self.gif_buffer.close()
-
-# image: Image.Image = Image.open(filepath)
-# anim_image: Image.Image = image
-# image_bytes_io: io.BytesIO = io.BytesIO()
-# anim_image.save(
-# image_bytes_io,
-# "GIF",
-# lossless=True,
-# save_all=True,
-# loop=0,
-# disposal=2,
-# )
-# image_bytes_io.seek(0)
-# ba: bytes = image_bytes_io.read()
-
-# self.gif_buffer.setData(ba)
-# movie = QMovie(self.gif_buffer, QByteArray())
-# self.preview_gif.setMovie(movie)
-# movie.start()
-
-# self.resizeEvent(
-# QResizeEvent(
-# QSize(image.width, image.height),
-# QSize(image.width, image.height),
-# )
-# )
-# self.preview_img.hide()
-# self.preview_vid.hide()
-# self.preview_gif.show()
-
-# image = None
-# if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RASTER_TYPES):
-# image = Image.open(str(filepath))
-# elif MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES):
-# try:
-# with rawpy.imread(str(filepath)) as raw:
-# rgb = raw.postprocess()
-# image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
-# except (
-# rawpy._rawpy.LibRawIOError,
-# rawpy._rawpy.LibRawFileUnsupportedError,
-# ):
-# pass
-# elif MediaCategories.is_ext_in_category(ext, MediaCategories.AUDIO_TYPES):
-# self.media_player.show()
-# self.media_player.play(filepath)
-# elif MediaCategories.is_ext_in_category(
-# ext, MediaCategories.VIDEO_TYPES
-# ) and is_readable_video(filepath):
-# video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG)
-# video.set(
-# cv2.CAP_PROP_POS_FRAMES,
-# (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
-# )
-# success, frame = video.read()
-# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
-# image = Image.fromarray(frame)
-# if success:
-# self.preview_img.hide()
-# self.preview_vid.play(filepath, QSize(image.width, image.height))
-# self.resizeEvent(
-# QResizeEvent(
-# QSize(image.width, image.height),
-# QSize(image.width, image.height),
-# )
-# )
-# self.preview_vid.show()
-
-# # Stats for specific file types are displayed here.
-# if image and (
-# MediaCategories.is_ext_in_category(
-# ext, MediaCategories.IMAGE_RASTER_TYPES, mime_fallback=True
-# )
-# or MediaCategories.is_ext_in_category(
-# ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
-# )
-# or MediaCategories.is_ext_in_category(
-# ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True
-# )
-# ):
-# self.dimensions_label.setText(
-# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
-# f"{image.width} x {image.height} px"
-# )
-# elif MediaCategories.is_ext_in_category(
-# ext, MediaCategories.FONT_TYPES, mime_fallback=True
-# ):
-# try:
-# font = ImageFont.truetype(filepath)
-# self.dimensions_label.setText(
-# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
-# f"{font.getname()[0]} ({font.getname()[1]}) "
-# )
-# except OSError:
-# self.dimensions_label.setText(
-# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
-# )
-# logger.info(
-# f"[PreviewPanel][ERROR] Couldn't read font file: {filepath}"
-# )
-# else:
-# self.dimensions_label.setText(f"{ext.upper()[1:]}")
-# self.dimensions_label.setText(
-# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
-# )
-# self.update_date_label(filepath)
-
-# if not filepath.is_file():
-# raise FileNotFoundError
-
-# except (FileNotFoundError, cv2.error) as e:
-# self.dimensions_label.setText(f"{ext.upper()[1:]}")
-# logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
-# self.update_date_label()
-# except (
-# UnidentifiedImageError,
-# DecompressionBombError,
-# ) as e:
-# self.dimensions_label.setText(
-# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
-# )
-# logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
-# self.update_date_label(filepath)
-
-# 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.is_connected = True
-
-# self.selected = self.driver.selected
-# logger.info(
-# "rendering item fields",
-# item=item.id,
-# fields=[x.type_key for x in item.fields],
-# )
-# for idx, field in enumerate(item.fields):
-# self.write_container(idx, field)
-
-# # Hide leftover containers
-# if len(self.containers) > len(item.fields):
-# for i, c in enumerate(self.containers):
-# if i > (len(item.fields) - 1):
-# c.setHidden(True)
-
-# self.add_field_button.setHidden(False)
-
-# # Multiple Selected Items
-# elif len(self.driver.selected) > 1:
-# self.preview_img.show()
-# self.preview_gif.hide()
-# self.preview_vid.stop()
-# self.preview_vid.hide()
-# self.media_player.stop()
-# self.media_player.hide()
-# self.update_date_label()
-# 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.set_file_path("")
-# self.dimensions_label.setText("")
-
-# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
-# self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
-
-# ratio = self.devicePixelRatio()
-# self.thumb_renderer.render(
-# time.time(),
-# "",
-# (512, 512),
-# ratio,
-# is_loading=True,
-# update_on_ratio_change=True,
-# )
-# if self.preview_img.is_connected:
-# self.preview_img.clicked.disconnect()
-# self.preview_img.is_connected = False
-
-# # fill shared fields from first item
-# first_item = self.driver.frame_content[self.driver.selected[0]]
-# common_fields = [f for f in first_item.fields]
-# mixed_fields = []
-
-# # iterate through other items
-# for grid_idx in self.driver.selected[1:]:
-# item = self.driver.frame_content[grid_idx]
-# item_field_types = {f.type_key for f in item.fields}
-# for f in common_fields[:]:
-# if f.type_key not in item_field_types:
-# common_fields.remove(f)
-# mixed_fields.append(f)
-
-# self.common_fields = common_fields
-# self.mixed_fields = sorted(mixed_fields, key=lambda x: x.type.position)
-
-# self.selected = list(self.driver.selected)
-# logger.info(
-# "update_widgets common_fields",
-# common_fields=self.common_fields,
-# )
-# for i, f in enumerate(self.common_fields):
-# self.write_container(i, f)
-
-# logger.info(
-# "update_widgets mixed_fields",
-# mixed_fields=self.mixed_fields,
-# start=len(self.common_fields),
-# )
-# for i, f in enumerate(self.mixed_fields, start=len(self.common_fields)):
-# self.write_container(i, f, is_mixed=True)
-
-# # Hide leftover containers
-# if len(self.containers) > len(self.common_fields) + len(self.mixed_fields):
-# for i, c in enumerate(self.containers):
-# if i > (len(self.common_fields) + len(self.mixed_fields) - 1):
-# c.setHidden(True)
-
-# self.add_field_button.setHidden(False)
-
-# self.initialized = True
-
-# self.setWindowTitle(window_title)
-# self.show()
-# return True
+ self.is_connected = False
+ self.lib = library
+ self.driver: QtDriver = driver
+ self.initialized = False
+ self.is_open: bool = False
+ self.common_fields: list = []
+ self.mixed_fields: list = []
+ self.selected: list[Entry] = []
+ self.containers: list[FieldContainer] = []
+
+ self.panel_bg_color = (
+ Theme.COLOR_BG_DARK.value
+ if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
+ else Theme.COLOR_BG_LIGHT.value
+ )
+
+ self.scroll_layout = QVBoxLayout()
+ self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+ self.scroll_layout.setContentsMargins(6, 1, 6, 6)
+
+ scroll_container: QWidget = QWidget()
+ scroll_container.setObjectName("entryScrollContainer")
+ scroll_container.setLayout(self.scroll_layout)
+
+ info_section = QWidget()
+ info_layout = QVBoxLayout(info_section)
+ info_layout.setContentsMargins(0, 0, 0, 0)
+ info_layout.setSpacing(6)
+
+ self.scroll_area = QScrollArea()
+ self.scroll_area.setObjectName("entryScrollArea")
+ self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
+ self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
+ self.scroll_area.setWidgetResizable(True)
+ self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
+ self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
+ # NOTE: I would rather have this style applied to the scroll_area
+ # background and NOT the scroll container background, so that the
+ # rounded corners are maintained when scrolling. I was unable to
+ # find the right trick to only select that particular element.
+
+ self.scroll_area.setStyleSheet(
+ "QWidget#entryScrollContainer{"
+ f"background:{self.panel_bg_color};"
+ "border-radius:6px;"
+ "}"
+ )
+ self.scroll_area.setWidget(scroll_container)
+
+ self.afb_container = QWidget()
+ self.afb_layout = QVBoxLayout(self.afb_container)
+ self.afb_layout.setContentsMargins(0, 12, 0, 0)
+
+ self.add_field_button = QPushButtonWrapper()
+ self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.add_field_button.setMinimumSize(96, 28)
+ self.add_field_button.setMaximumSize(96, 28)
+ Translations.translate_qobject(self.add_field_button, "library.field.add")
+ self.afb_layout.addWidget(self.add_field_button)
+ self.add_field_modal = AddFieldModal(self.lib)
+ self.place_add_field_button()
+
+ root_layout = QHBoxLayout(self)
+ root_layout.setContentsMargins(0, 0, 0, 0)
+ root_layout.addWidget(self.scroll_area)
+
+ def update_from_entry(self, entry: Entry):
+ """Update tags and fields from a single Entry source."""
+ self.selected = [entry]
+ logger.info(
+ "[Field Containers] Updating Selection",
+ entry=entry,
+ fields=entry.fields,
+ tags=entry.tags,
+ )
+
+ # # reload entry and fill it into the grid again
+ # # TODO - do this more granular
+ # # TODO - Entry reload is maybe not necessary
+ # for grid_idx in self.driver.selected:
+ # entry = self.driver.frame_content[grid_idx]
+ # results = self.lib.search_library(FilterState(id=entry.id))
+ # logger.info(
+ # "found item",
+ # entries=len(results.items),
+ # grid_idx=grid_idx,
+ # lookup_id=entry.id,
+ # )
+ # self.driver.frame_content[grid_idx] = results[0]
+
+ # for index in self.driver.selected:
+ # self.driver.frame_content[index] = self.lib.get_entry(self.selected[0].id)
+
+ for idx, field in enumerate(entry.fields):
+ self.write_container(idx, field, is_mixed=False)
+ if entry.tags:
+ # TODO: Display the tag categories
+ pass
+
+ # Hide leftover containers
+ if len(self.containers) > len(entry.fields):
+ for i, c in enumerate(self.containers):
+ if i > (len(entry.fields) - 1):
+ c.setHidden(True)
+
+ self.add_field_button.setHidden(False)
+
+ def remove_field_prompt(self, name: str) -> str:
+ return Translations.translate_formatted("library.field.confirm_remove", name=name)
+
+ def place_add_field_button(self):
+ self.scroll_layout.addWidget(self.afb_container)
+ 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.add_field_modal.done.connect(
+ # lambda f: (self.add_field_to_selected(f), self.update_widgets())
+ # )
+ self.add_field_modal.is_connected = True
+ self.add_field_button.clicked.connect(self.add_field_modal.show)
+
+ def add_field_to_selected(self, field_list: list):
+ """Add list of entry fields to one or more selected items."""
+ logger.info("add_field_to_selected", selected=self.selected, fields=field_list)
+ for grid_idx in self.selected:
+ entry = self.driver.frame_content[grid_idx]
+ for field_item in field_list:
+ self.lib.add_entry_field_type(
+ entry.id,
+ field_id=field_item.data(Qt.ItemDataRole.UserRole),
+ )
+
+ def write_container(self, index: int, field: BaseField, is_mixed: bool = False):
+ """Update/Create data for a FieldContainer.
+
+ 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()
+ if len(self.containers) < (index + 1):
+ container = FieldContainer()
+ self.containers.append(container)
+ self.scroll_layout.addWidget(container)
+ else:
+ container = self.containers[index]
+
+ # if isinstance(field, TagBoxField):
+ # container.set_title(field.type.name)
+ # container.set_inline(False)
+ # title = f"{field.type.name} (Tag Box)"
+ #
+ # if not is_mixed:
+ # inner_container = container.get_inner_widget()
+ # if isinstance(inner_container, TagBoxWidget):
+ # inner_container.set_field(field)
+ # inner_container.set_tags(list(field.tags))
+ #
+ # try:
+ # inner_container.updated.disconnect()
+ # except RuntimeError:
+ # logger.error("Failed to disconnect inner_container.updated")
+ #
+ # else:
+ # inner_container = TagBoxWidget(
+ # field,
+ # title,
+ # self.driver,
+ # )
+ #
+ # container.set_inner_widget(inner_container)
+ #
+ # inner_container.updated.connect(
+ # lambda: (
+ # self.write_container(index, field),
+ # self.update_widgets(),
+ # )
+ # )
+ # # NOTE: Tag Boxes have no Edit Button
+ # (But will when you can convert field types)
+ # container.set_remove_callback(
+ # lambda: self.remove_message_box(
+ # prompt=self.remove_field_prompt(field.type.name),
+ # callback=lambda: (
+ # self.remove_field(field),
+ # self.update_selected_entry(self.driver),
+ # # reload entry and its fields
+ # self.update_widgets(),
+ # ),
+ # )
+ # )
+ # else:
+ # text = "Mixed Data"
+ # title = f"{field.type.name} (Wacky Tag Box)"
+ # inner_container = TextWidget(title, text)
+ # container.set_inner_widget(inner_container)
+ #
+ # self.tags_updated.emit()
+ # # self.dynamic_widgets.append(inner_container)
+ # elif field.type.type == FieldTypeEnum.TEXT_LINE:
+ if field.type.type == FieldTypeEnum.TEXT_LINE:
+ container.set_title(field.type.name)
+ container.set_inline(False)
+
+ # Normalize line endings in any text content.
+ if not is_mixed:
+ assert isinstance(field.value, (str, type(None)))
+ text = field.value or ""
+ else:
+ text = "Mixed Data"
+
+ title = f"{field.type.name} ({field.type.type.value})"
+ inner_container = TextWidget(title, text)
+ container.set_inner_widget(inner_container)
+ if not is_mixed:
+ modal = PanelModal(
+ EditTextLine(field.value),
+ title=title,
+ window_title=f"Edit {field.type.type.value}",
+ save_callback=(
+ lambda content: (
+ self.update_field(field, content),
+ self.update_from_entry(self.selected[0]),
+ )
+ ),
+ )
+ if "pytest" in sys.modules:
+ # for better testability
+ container.modal = modal # type: ignore
+
+ container.set_edit_callback(modal.show)
+ container.set_remove_callback(
+ lambda: self.remove_message_box(
+ prompt=self.remove_field_prompt(field.type.type.value),
+ callback=lambda: (
+ self.remove_field(field),
+ self.update_from_entry(self.selected[0]),
+ ),
+ )
+ )
+
+ elif field.type.type == FieldTypeEnum.TEXT_BOX:
+ container.set_title(field.type.name)
+ # container.set_editable(True)
+ container.set_inline(False)
+ # Normalize line endings in any text content.
+ if not is_mixed:
+ assert isinstance(field.value, (str, type(None)))
+ text = (field.value or "").replace("\r", "\n")
+ else:
+ text = "Mixed Data"
+ title = f"{field.type.name} (Text Box)"
+ inner_container = TextWidget(title, text)
+ container.set_inner_widget(inner_container)
+ if not is_mixed:
+ modal = PanelModal(
+ EditTextBox(field.value),
+ title=title,
+ window_title=f"Edit {field.type.name}",
+ save_callback=(
+ lambda content: (
+ self.update_field(field, content),
+ self.update_from_entry(self.selected[0]),
+ )
+ ),
+ )
+ container.set_edit_callback(modal.show)
+ container.set_remove_callback(
+ lambda: self.remove_message_box(
+ prompt=self.remove_field_prompt(field.type.name),
+ callback=lambda: (
+ self.remove_field(field),
+ self.update_from_entry(self.selected[0]),
+ ),
+ )
+ )
+
+ elif field.type.type == FieldTypeEnum.DATETIME:
+ if not is_mixed:
+ try:
+ container.set_title(field.type.name)
+ # container.set_editable(False)
+ container.set_inline(False)
+ # TODO: Localize this and/or add preferences.
+ date = dt.strptime(field.value, "%Y-%m-%d %H:%M:%S")
+ title = f"{field.type.name} (Date)"
+ inner_container = TextWidget(title, date.strftime("%D - %r"))
+ container.set_inner_widget(inner_container)
+ except Exception:
+ container.set_title(field.type.name)
+ # container.set_editable(False)
+ container.set_inline(False)
+ title = f"{field.type.name} (Date) (Unknown Format)"
+ inner_container = TextWidget(title, str(field.value))
+ container.set_inner_widget(inner_container)
+
+ container.set_remove_callback(
+ lambda: self.remove_message_box(
+ prompt=self.remove_field_prompt(field.type.name),
+ callback=lambda: (
+ self.remove_field(field),
+ self.update_from_entry(self.selected[0]),
+ ),
+ )
+ )
+ else:
+ text = "Mixed Data"
+ title = f"{field.type.name} (Wacky Date)"
+ inner_container = TextWidget(title, text)
+ container.set_inner_widget(inner_container)
+ else:
+ logger.warning("write_container - unknown field", field=field)
+ container.set_title(field.type.name)
+ 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)
+ container.set_remove_callback(
+ lambda: self.remove_message_box(
+ prompt=self.remove_field_prompt(field.type.name),
+ callback=lambda: (
+ self.remove_field(field),
+ self.update_from_entry(self.selected[0]),
+ ),
+ )
+ )
+
+ container.edit_button.setHidden(True)
+ container.setHidden(False)
+ self.place_add_field_button()
+
+ def remove_field(self, field: BaseField):
+ """Remove a field from all selected Entries."""
+ logger.info("removing field", field=field, selected=self.selected)
+ entry_ids = []
+
+ for grid_idx in self.selected:
+ entry = self.driver.frame_content[grid_idx]
+ entry_ids.append(entry.id)
+
+ self.lib.remove_entry_field(field, entry_ids)
+
+ # # if the field is meta tags, update the badges
+ # if field.type_key == _FieldID.TAGS_META.value:
+ # self.driver.update_badges(self.selected)
+
+ def update_field(self, field: BaseField, content: str) -> None:
+ """Update a field in all selected Entries, given a field object."""
+ assert isinstance(
+ field,
+ (TextField, DatetimeField), # , TagBoxField)
+ ), f"instance: {type(field)}"
+
+ entry_ids = []
+ # for grid_idx in self.selected:
+ # entry = self.driver.frame_content[grid_idx]
+ # entry_ids.append(entry.id)
+ for entry in self.selected:
+ entry_ids.append(entry.id)
+
+ assert entry_ids, "No entries selected"
+ self.lib.update_entry_field(
+ entry_ids,
+ field,
+ content,
+ )
+
+ def remove_message_box(self, prompt: str, callback: Callable) -> None:
+ remove_mb = QMessageBox()
+ remove_mb.setText(prompt)
+ remove_mb.setWindowTitle("Remove Field")
+ remove_mb.setIcon(QMessageBox.Icon.Warning)
+ cancel_button = remove_mb.addButton(
+ Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole
+ )
+ remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole)
+ # remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel)
+ remove_mb.setDefaultButton(cancel_button)
+ remove_mb.setEscapeButton(cancel_button)
+ result = remove_mb.exec_()
+ # logging.info(result)
+ if result == 3: # TODO - what is this magic number?
+ callback()
+
+ def set_tags_updated_slot(self, slot: object):
+ """Replacement for tag_callback."""
+ if self.is_connected:
+ self.tags_updated.disconnect()
+
+ logger.info("[UPDATE CONTAINER] Setting tags updated slot")
+ self.tags_updated.connect(slot)
+ self.is_connected = True
+
+ # def update_widgets(self):
+ # """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
+ # # window_title = ""
+
+ # # # update list of libraries
+ # # self.fill_libs_widget(self.libs_layout)
+
+ # # if not self.driver.selected:
+ # # if self.selected or not self.initialized:
+ # # # self.file_label.setText("No Items Selected")
+ # # # self.file_label.set_file_path("")
+ # # # self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
+
+ # # # self.dimensions_label.setText("")
+ # # # self.update_date_label()
+ # # # self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
+ # # # self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
+
+ # # # ratio = self.devicePixelRatio()
+ # # # self.thumb_renderer.render(
+ # # # time.time(),
+ # # # "",
+ # # # (512, 512),
+ # # # ratio,
+ # # # is_loading=True,
+ # # # update_on_ratio_change=True,
+ # # # )
+ # # # if self.preview_img.is_connected:
+ # # # self.preview_img.clicked.disconnect()
+ # # # for c in self.containers:
+ # # # c.setHidden(True)
+ # # # self.preview_img.show()
+ # # # self.preview_vid.stop()
+ # # # self.preview_vid.hide()
+ # # # self.media_player.hide()
+ # # # self.media_player.stop()
+ # # # self.preview_gif.hide()
+ # # # self.selected = list(self.driver.selected)
+ # # self.add_field_button.setHidden(True)
+
+ # # # common code
+ # # self.initialized = True
+ # # self.setWindowTitle(window_title)
+ # # self.show()
+ # # return True
+
+ # # reload entry and fill it into the grid again
+ # # TODO - do this more granular
+ # # TODO - Entry reload is maybe not necessary
+ # for grid_idx in self.driver.selected:
+ # entry = self.driver.frame_content[grid_idx]
+ # results = self.lib.search_library(FilterState(id=entry.id))
+ # logger.info(
+ # "found item",
+ # entries=len(results.items),
+ # grid_idx=grid_idx,
+ # lookup_id=entry.id,
+ # )
+ # self.driver.frame_content[grid_idx] = results[0]
+
+ # if len(self.driver.selected) == 1:
+ # # 1 Selected Entry
+ # selected_idx = self.driver.selected[0]
+ # item = self.driver.frame_content[selected_idx]
+
+ # self.preview_img.show()
+ # self.preview_vid.stop()
+ # self.preview_vid.hide()
+ # self.media_player.stop()
+ # self.media_player.hide()
+ # self.preview_gif.hide()
+
+ # # 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.set_file_path(filepath)
+ # ratio = self.devicePixelRatio()
+ # self.thumb_renderer.render(
+ # time.time(),
+ # filepath,
+ # (512, 512),
+ # ratio,
+ # update_on_ratio_change=True,
+ # )
+ # file_str: str = ""
+ # separator: str = f"{os.path.sep}" # Gray
+ # for i, part in enumerate(filepath.parts):
+ # part_ = part.strip(os.path.sep)
+ # if i != len(filepath.parts) - 1:
+ # file_str += f"{"\u200b".join(part_)}{separator}"
+ # else:
+ # file_str += f"
{"\u200b".join(part_)}"
+ # self.file_label.setText(file_str)
+ # self.file_label.setCursor(Qt.CursorShape.PointingHandCursor)
+
+ # self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
+ # self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor)
+
+ # self.opener = FileOpenerHelper(filepath)
+ # self.open_file_action.triggered.connect(self.opener.open_file)
+ # self.open_explorer_action.triggered.connect(self.opener.open_explorer)
+
+ # # TODO: Do this all somewhere else, this is just here temporarily.
+ # ext: str = filepath.suffix.lower()
+ # try:
+ # if MediaCategories.is_ext_in_category(
+ # ext, MediaCategories.IMAGE_ANIMATED_TYPES, mime_fallback=True
+ # ):
+ # if self.preview_gif.movie():
+ # self.preview_gif.movie().stop()
+ # self.gif_buffer.close()
+
+ # image: Image.Image = Image.open(filepath)
+ # anim_image: Image.Image = image
+ # image_bytes_io: io.BytesIO = io.BytesIO()
+ # anim_image.save(
+ # image_bytes_io,
+ # "GIF",
+ # lossless=True,
+ # save_all=True,
+ # loop=0,
+ # disposal=2,
+ # )
+ # image_bytes_io.seek(0)
+ # ba: bytes = image_bytes_io.read()
+
+ # self.gif_buffer.setData(ba)
+ # movie = QMovie(self.gif_buffer, QByteArray())
+ # self.preview_gif.setMovie(movie)
+ # movie.start()
+
+ # self.resizeEvent(
+ # QResizeEvent(
+ # QSize(image.width, image.height),
+ # QSize(image.width, image.height),
+ # )
+ # )
+ # self.preview_img.hide()
+ # self.preview_vid.hide()
+ # self.preview_gif.show()
+
+ # image = None
+ # if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RASTER_TYPES): #noqa: E501
+ # image = Image.open(str(filepath))
+ # elif MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES):
+ # try:
+ # with rawpy.imread(str(filepath)) as raw:
+ # rgb = raw.postprocess()
+ # image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black") #noqa: E501
+ # except (
+ # rawpy._rawpy.LibRawIOError,
+ # rawpy._rawpy.LibRawFileUnsupportedError,
+ # ):
+ # pass
+ # elif MediaCategories.is_ext_in_category(ext, MediaCategories.AUDIO_TYPES):
+ # self.media_player.show()
+ # self.media_player.play(filepath)
+ # elif MediaCategories.is_ext_in_category(
+ # ext, MediaCategories.VIDEO_TYPES
+ # ) and is_readable_video(filepath):
+ # video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG)
+ # video.set(
+ # cv2.CAP_PROP_POS_FRAMES,
+ # (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
+ # )
+ # success, frame = video.read()
+ # frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+ # image = Image.fromarray(frame)
+ # if success:
+ # self.preview_img.hide()
+ # self.preview_vid.play(filepath, QSize(image.width, image.height))
+ # self.resizeEvent(
+ # QResizeEvent(
+ # QSize(image.width, image.height),
+ # QSize(image.width, image.height),
+ # )
+ # )
+ # self.preview_vid.show()
+
+ # # Stats for specific file types are displayed here.
+ # if image and (
+ # MediaCategories.is_ext_in_category(
+ # ext, MediaCategories.IMAGE_RASTER_TYPES, mime_fallback=True
+ # )
+ # or MediaCategories.is_ext_in_category(
+ # ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
+ # )
+ # or MediaCategories.is_ext_in_category(
+ # ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True
+ # )
+ # ):
+ # self.dimensions_label.setText(
+ # f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
+ # f"{image.width} x {image.height} px"
+ # )
+ # elif MediaCategories.is_ext_in_category(
+ # ext, MediaCategories.FONT_TYPES, mime_fallback=True
+ # ):
+ # try:
+ # font = ImageFont.truetype(filepath)
+ # self.dimensions_label.setText(
+ # f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
+ # f"{font.getname()[0]} ({font.getname()[1]}) "
+ # )
+ # except OSError:
+ # self.dimensions_label.setText(
+ # f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
+ # )
+ # logger.info(
+ # f"[PreviewPanel][ERROR] Couldn't read font file: {filepath}"
+ # )
+ # else:
+ # self.dimensions_label.setText(f"{ext.upper()[1:]}")
+ # self.dimensions_label.setText(
+ # f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
+ # )
+ # self.update_date_label(filepath)
+
+ # if not filepath.is_file():
+ # raise FileNotFoundError
+
+ # except (FileNotFoundError, cv2.error) as e:
+ # self.dimensions_label.setText(f"{ext.upper()[1:]}")
+ # logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
+ # self.update_date_label()
+ # except (
+ # UnidentifiedImageError,
+ # DecompressionBombError,
+ # ) as e:
+ # self.dimensions_label.setText(
+ # f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
+ # )
+ # logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
+ # self.update_date_label(filepath)
+
+ # if self.preview_img.is_connected:
+ # self.preview_img.clicked.disconnect()
+ # self.preview_img.clicked.connect(lambda checked=False, pth=filepath: open_file(pth)) #noqa: E501
+ # self.preview_img.is_connected = True
+
+ # self.selected = self.driver.selected
+ # logger.info(
+ # "rendering item fields",
+ # item=item.id,
+ # fields=[x.type_key for x in item.fields],
+ # )
+ # for idx, field in enumerate(item.fields):
+ # self.write_container(idx, field)
+
+ # # Hide leftover containers
+ # if len(self.containers) > len(item.fields):
+ # for i, c in enumerate(self.containers):
+ # if i > (len(item.fields) - 1):
+ # c.setHidden(True)
+
+ # self.add_field_button.setHidden(False)
+
+ # # Multiple Selected Items
+ # elif len(self.driver.selected) > 1:
+ # self.preview_img.show()
+ # self.preview_gif.hide()
+ # self.preview_vid.stop()
+ # self.preview_vid.hide()
+ # self.media_player.stop()
+ # self.media_player.hide()
+ # self.update_date_label()
+ # 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.set_file_path("")
+ # self.dimensions_label.setText("")
+
+ # self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
+ # self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
+
+ # ratio = self.devicePixelRatio()
+ # self.thumb_renderer.render(
+ # time.time(),
+ # "",
+ # (512, 512),
+ # ratio,
+ # is_loading=True,
+ # update_on_ratio_change=True,
+ # )
+ # if self.preview_img.is_connected:
+ # self.preview_img.clicked.disconnect()
+ # self.preview_img.is_connected = False
+
+ # # fill shared fields from first item
+ # first_item = self.driver.frame_content[self.driver.selected[0]]
+ # common_fields = [f for f in first_item.fields]
+ # mixed_fields = []
+
+ # # iterate through other items
+ # for grid_idx in self.driver.selected[1:]:
+ # item = self.driver.frame_content[grid_idx]
+ # item_field_types = {f.type_key for f in item.fields}
+ # for f in common_fields[:]:
+ # if f.type_key not in item_field_types:
+ # common_fields.remove(f)
+ # mixed_fields.append(f)
+
+ # self.common_fields = common_fields
+ # self.mixed_fields = sorted(mixed_fields, key=lambda x: x.type.position)
+
+ # self.selected = list(self.driver.selected)
+ # logger.info(
+ # "update_widgets common_fields",
+ # common_fields=self.common_fields,
+ # )
+ # for i, f in enumerate(self.common_fields):
+ # self.write_container(i, f)
+
+ # logger.info(
+ # "update_widgets mixed_fields",
+ # mixed_fields=self.mixed_fields,
+ # start=len(self.common_fields),
+ # )
+ # for i, f in enumerate(self.mixed_fields, start=len(self.common_fields)):
+ # self.write_container(i, f, is_mixed=True)
+
+ # # Hide leftover containers
+ # if len(self.containers) > len(self.common_fields) + len(self.mixed_fields):
+ # for i, c in enumerate(self.containers):
+ # if i > (len(self.common_fields) + len(self.mixed_fields) - 1):
+ # c.setHidden(True)
+
+ # self.add_field_button.setHidden(False)
+
+ # self.initialized = True
+
+ # self.setWindowTitle(window_title)
+ # self.show()
+ # return True
diff --git a/tagstudio/src/qt/widgets/preview/recent_libraries.py b/tagstudio/src/qt/widgets/preview/recent_libraries.py
index f47c2163..fd2fb2b2 100644
--- a/tagstudio/src/qt/widgets/preview/recent_libraries.py
+++ b/tagstudio/src/qt/widgets/preview/recent_libraries.py
@@ -23,46 +23,50 @@ class RecentLibraries(QWidget):
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__()
+ # # keep list of rendered libraries to avoid needless re-rendering
+ # self.render_libs: set = set()
+ # self.library = library
+ # self.driver = driver
+ # layout = QVBoxLayout()
-# # keep list of rendered libraries to avoid needless re-rendering
-# self.render_libs: set = set()
-# self.library = library
-# self.driver = driver
-# layout = QVBoxLayout()
+ # settings = driver.settings
+ # settings.beginGroup(SettingItems.LIBS_LIST)
+ # lib_items: dict[str, tuple[str, str]] = {}
+ # for item_tstamp in settings.allKeys():
+ # val = str(settings.value(item_tstamp, type=str))
+ # cut_val = val
+ # if len(val) > 45:
+ # cut_val = f"{val[0:10]} ... {val[-10:]}"
+ # lib_items[item_tstamp] = (val, cut_val)
-# settings = driver.settings
-# settings.beginGroup(SettingItems.LIBS_LIST)
-# lib_items: dict[str, tuple[str, str]] = {}
-# for item_tstamp in settings.allKeys():
-# val = str(settings.value(item_tstamp, type=str))
-# cut_val = val
-# if len(val) > 45:
-# cut_val = f"{val[0:10]} ... {val[-10:]}"
-# lib_items[item_tstamp] = (val, cut_val)
+ # settings.endGroup()
-# settings.endGroup()
+ # new_keys = set(lib_items.keys())
+ # if new_keys == self.render_libs:
+ # # no need to re-render
+ # return
-# new_keys = set(lib_items.keys())
-# if new_keys == self.render_libs:
-# # no need to re-render
-# return
+ # # sort lib_items by the key
+ # libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True)
-# # sort lib_items by the key
-# libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True)
+ # self.render_libs = new_keys
+ # self.setLayout(layout)
-# self.render_libs = new_keys
-# self.setLayout(layout)
+ # self._fill_libs_widget(libs_sorted, layout)
-# self._fill_libs_widget(libs_sorted, layout)
+ # 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)
+ # if child.widget() is not None:
+ # child.widget().deleteLater()
+ # elif child.layout() is not None:
+ # clear_layout(child.layout()) # type: ignore
-# 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)
-# if child.widget() is not None:
-# child.widget().deleteLater()
-# elif child.layout() is not None:
-# clear_layout(child.layout()) # type: ignore
# # remove any potential previous items
# clear_layout(layout)
diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py
index cb8a50f8..fc07b8cd 100644
--- a/tagstudio/src/qt/widgets/preview_panel.py
+++ b/tagstudio/src/qt/widgets/preview_panel.py
@@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
+import traceback
import typing
from pathlib import Path
@@ -64,8 +65,9 @@ class PreviewPanel(QWidget):
splitter.addWidget(self.thumb)
splitter.addWidget(self.thumb.media_player)
splitter.addWidget(self.file_attrs)
+ splitter.addWidget(self.fields)
# splitter.addWidget(self.libs_flow_container)
- splitter.setStretchFactor(1, 2)
+ splitter.setStretchFactor(3, 2)
root_layout = QHBoxLayout(self)
root_layout.setContentsMargins(0, 0, 0, 0)
@@ -91,7 +93,8 @@ class PreviewPanel(QWidget):
# self.file_attrs.update_blank()
self.file_attrs.update_stats()
self.file_attrs.update_date_label()
- pass
+
+ # One Item Selected
elif len(self.driver.selected) == 1:
entry: Entry = items[0]
filepath: Path = self.lib.library_dir / entry.path
@@ -99,19 +102,21 @@ class PreviewPanel(QWidget):
stats: dict = self.thumb.update_preview(filepath, ext)
logger.info("stats", stats=stats, ext=ext)
- self.file_attrs.update_stats(filepath, ext, stats)
- self.file_attrs.update_date_label(filepath)
- # TODO: Render regular single selection
- # TODO: Return known attributes from thumb, and give those to field_attrs
- # self.file_attrs.update_filename()
- pass
+ try:
+ self.file_attrs.update_stats(filepath, ext, stats)
+ self.file_attrs.update_date_label(filepath)
+ self.fields.update_from_entry(entry)
+ except Exception as e:
+ logger.error("[Preview Panel] Error updating selection", error=e)
+ traceback.print_exc()
+
# Multiple Selected Items
elif len(self.driver.selected) > 1:
# Render mixed selection
self.file_attrs.update_multi_selection(len(self.driver.selected))
self.file_attrs.update_date_label()
+ # self.fields.update_from_entries(items)
# self.file_attrs.update_selection_count()
- pass
# self.thumb.update_widgets()
# # self.file_attrs.update_widgets()